Merge branch 'main' into rect_caps

This commit is contained in:
Wannes Malfait 2025-01-31 18:44:11 +01:00
commit 33a0fb43a5
No known key found for this signature in database
268 changed files with 5140 additions and 2368 deletions

19
Cargo.lock generated
View File

@ -1122,9 +1122,9 @@ checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed"
[[package]]
name = "image"
version = "0.25.2"
version = "0.25.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10"
checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b"
dependencies = [
"bytemuck",
"byteorder-lite",
@ -2766,7 +2766,7 @@ dependencies = [
[[package]]
name = "typst-dev-assets"
version = "0.12.0"
source = "git+https://github.com/typst/typst-dev-assets?rev=b07d156#b07d1560143d6883887358d30edb25cb12fcf5b9"
source = "git+https://github.com/typst/typst-dev-assets?rev=7f8999d#7f8999d19907cd6e1148b295efbc844921c0761c"
[[package]]
name = "typst-docs"
@ -3036,6 +3036,7 @@ dependencies = [
"comemo",
"ecow",
"flate2",
"image",
"ttf-parser",
"typst-library",
"typst-macros",
@ -3093,6 +3094,7 @@ dependencies = [
"parking_lot",
"serde",
"serde_json",
"web-sys",
]
[[package]]
@ -3104,6 +3106,7 @@ dependencies = [
"rayon",
"siphasher 1.0.1",
"thin-vec",
"unicode-math-class",
]
[[package]]
@ -3418,6 +3421,16 @@ dependencies = [
"indexmap-nostd",
]
[[package]]
name = "web-sys"
version = "0.3.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "weezl"
version = "0.1.8"

View File

@ -33,7 +33,7 @@ typst-syntax = { path = "crates/typst-syntax", version = "0.12.0" }
typst-timing = { path = "crates/typst-timing", version = "0.12.0" }
typst-utils = { path = "crates/typst-utils", version = "0.12.0" }
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "8cccef9" }
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "b07d156" }
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "7f8999d" }
arrayvec = "0.7.4"
az = "1.2"
base64 = "0.22"
@ -67,7 +67,7 @@ icu_provider_adapters = "1.4"
icu_provider_blob = "1.4"
icu_segmenter = { version = "1.4", features = ["serde"] }
if_chain = "1"
image = { version = "0.25.2", default-features = false, features = ["png", "jpeg", "gif"] }
image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif"] }
indexmap = { version = "2", features = ["serde"] }
kamadak-exif = "0.5"
kurbo = "0.11"
@ -134,6 +134,7 @@ ureq = { version = "2", default-features = false, features = ["native-tls", "gzi
usvg = { version = "0.43", default-features = false, features = ["text"] }
walkdir = "2"
wasmi = "0.39.0"
web-sys = "0.3"
xmlparser = "0.13.5"
xmlwriter = "0.1.0"
xmp-writer = "0.3"

View File

@ -6,13 +6,12 @@ use typst_library::diag::{
};
use typst_library::engine::{Engine, Sink, Traced};
use typst_library::foundations::{
Arg, Args, Bytes, Capturer, Closure, Content, Context, Func, IntoValue,
NativeElement, Scope, Scopes, Value,
Arg, Args, Capturer, Closure, Content, Context, Func, NativeElement, Scope, Scopes,
SymbolElem, Value,
};
use typst_library::introspection::Introspector;
use typst_library::math::LrElem;
use typst_library::routines::Routines;
use typst_library::text::TextElem;
use typst_library::World;
use typst_syntax::ast::{self, AstNode, Ident};
use typst_syntax::{Span, Spanned, SyntaxNode};
@ -316,15 +315,16 @@ fn eval_field_call(
(target, args)
};
if let Value::Plugin(plugin) = &target {
// Call plugins by converting args to bytes.
let bytes = args.all::<Bytes>()?;
args.finish()?;
let value = plugin.call(&field, bytes).at(span)?.into_value();
Ok(FieldCall::Resolved(value))
} else if let Some(callee) = target.ty().scope().get(&field) {
if let Some(callee) = target.ty().scope().get(&field) {
args.insert(0, target_expr.span(), target);
Ok(FieldCall::Normal(callee.clone(), args))
} else if let Value::Content(content) = &target {
if let Some(callee) = content.elem().scope().get(&field) {
args.insert(0, target_expr.span(), target);
Ok(FieldCall::Normal(callee.clone(), args))
} else {
bail!(missing_field_call_error(target, field))
}
} else if matches!(
target,
Value::Symbol(_) | Value::Func(_) | Value::Type(_) | Value::Module(_)
@ -341,8 +341,20 @@ fn eval_field_call(
/// Produce an error when we cannot call the field.
fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic {
let mut error =
error!(field.span(), "type {} has no method `{}`", target.ty(), field.as_str());
let mut error = match &target {
Value::Content(content) => error!(
field.span(),
"element {} has no method `{}`",
content.elem().name(),
field.as_str(),
),
_ => error!(
field.span(),
"type {} has no method `{}`",
target.ty(),
field.as_str()
),
};
match target {
Value::Dict(ref dict) if matches!(dict.get(&field), Ok(Value::Func(_))) => {
@ -360,6 +372,7 @@ fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic {
}
_ => {}
}
error
}
@ -382,16 +395,16 @@ fn wrap_args_in_math(
let mut body = Content::empty();
for (i, arg) in args.all::<Content>()?.into_iter().enumerate() {
if i > 0 {
body += TextElem::packed(',');
body += SymbolElem::packed(',');
}
body += arg;
}
if trailing_comma {
body += TextElem::packed(',');
body += SymbolElem::packed(',');
}
Ok(Value::Content(
callee.display().spanned(callee_span)
+ LrElem::new(TextElem::packed('(') + body + TextElem::packed(')'))
+ LrElem::new(SymbolElem::packed('(') + body + SymbolElem::packed(')'))
.pack()
.spanned(args.span),
))

View File

@ -99,6 +99,7 @@ impl Eval for ast::Expr<'_> {
Self::Term(v) => v.eval(vm).map(Value::Content),
Self::Equation(v) => v.eval(vm).map(Value::Content),
Self::Math(v) => v.eval(vm).map(Value::Content),
Self::MathText(v) => v.eval(vm).map(Value::Content),
Self::MathIdent(v) => v.eval(vm),
Self::MathShorthand(v) => v.eval(vm),
Self::MathAlignPoint(v) => v.eval(vm).map(Value::Content),

View File

@ -6,7 +6,7 @@ use typst_library::diag::{
use typst_library::engine::Engine;
use typst_library::foundations::{Content, Module, Value};
use typst_library::World;
use typst_syntax::ast::{self, AstNode};
use typst_syntax::ast::{self, AstNode, BareImportError};
use typst_syntax::package::{PackageManifest, PackageSpec};
use typst_syntax::{FileId, Span, VirtualPath};
@ -16,11 +16,11 @@ impl Eval for ast::ModuleImport<'_> {
type Output = Value;
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let source = self.source();
let source_span = source.span();
let mut source = source.eval(vm)?;
let new_name = self.new_name();
let imports = self.imports();
let source_expr = self.source();
let source_span = source_expr.span();
let mut source = source_expr.eval(vm)?;
let mut is_str = false;
match &source {
Value::Func(func) => {
@ -32,6 +32,7 @@ impl Eval for ast::ModuleImport<'_> {
Value::Module(_) => {}
Value::Str(path) => {
source = Value::Module(import(&mut vm.engine, path, source_span)?);
is_str = true;
}
v => {
bail!(
@ -42,9 +43,12 @@ impl Eval for ast::ModuleImport<'_> {
}
}
// Source itself is imported if there is no import list or a rename.
let bare_name = self.bare_name();
let new_name = self.new_name();
if let Some(new_name) = new_name {
if let ast::Expr::Ident(ident) = self.source() {
if ident.as_str() == new_name.as_str() {
if let Ok(source_name) = &bare_name {
if source_name == new_name.as_str() {
// Warn on `import x as x`
vm.engine.sink.warn(warning!(
new_name.span(),
@ -58,12 +62,33 @@ impl Eval for ast::ModuleImport<'_> {
}
let scope = source.scope().unwrap();
match imports {
match self.imports() {
None => {
// Only import here if there is no rename.
if new_name.is_none() {
let name: EcoString = source.name().unwrap().into();
vm.scopes.top.define(name, source);
match self.bare_name() {
// Bare dynamic string imports are not allowed.
Ok(name)
if !is_str || matches!(source_expr, ast::Expr::Str(_)) =>
{
if matches!(source_expr, ast::Expr::Ident(_)) {
vm.engine.sink.warn(warning!(
source_expr.span(),
"this import has no effect",
));
}
vm.scopes.top.define_spanned(name, source, source_span);
}
Ok(_) | Err(BareImportError::Dynamic) => bail!(
source_span, "dynamic import requires an explicit name";
hint: "you can name the import with `as`"
),
Err(BareImportError::PathInvalid) => bail!(
source_span, "module name would not be a valid identifier";
hint: "you can rename the import with `as`",
),
// Bad package spec would have failed the import already.
Err(BareImportError::PackageInvalid) => unreachable!(),
}
}
}
Some(ast::Imports::Wildcard) => {

View File

@ -1,11 +1,11 @@
use ecow::eco_format;
use typst_library::diag::{At, SourceResult};
use typst_library::foundations::{Content, NativeElement, Symbol, Value};
use typst_library::foundations::{Content, NativeElement, Symbol, SymbolElem, Value};
use typst_library::math::{
AlignPointElem, AttachElem, FracElem, LrElem, PrimesElem, RootElem,
};
use typst_library::text::TextElem;
use typst_syntax::ast::{self, AstNode};
use typst_syntax::ast::{self, AstNode, MathTextKind};
use crate::{Eval, Vm};
@ -20,6 +20,17 @@ impl Eval for ast::Math<'_> {
}
}
impl Eval for ast::MathText<'_> {
type Output = Content;
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
match self.get() {
MathTextKind::Character(c) => Ok(SymbolElem::packed(c)),
MathTextKind::Number(text) => Ok(TextElem::packed(text.clone())),
}
}
}
impl Eval for ast::MathIdent<'_> {
type Output = Value;
@ -102,6 +113,7 @@ impl Eval for ast::MathRoot<'_> {
type Output = Content;
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
// Use `TextElem` to match `MathTextKind::Number` above.
let index = self.index().map(|i| TextElem::packed(eco_format!("{i}")));
let radicand = self.radicand().eval_display(vm)?;
Ok(RootElem::new(radicand).with_index(index).pack())

View File

@ -2,7 +2,7 @@ use std::fmt::Write;
use typst_library::diag::{bail, At, SourceResult, StrResult};
use typst_library::foundations::Repr;
use typst_library::html::{charsets, tag, HtmlDocument, HtmlElement, HtmlNode};
use typst_library::html::{charsets, tag, HtmlDocument, HtmlElement, HtmlNode, HtmlTag};
use typst_library::layout::Frame;
use typst_syntax::Span;
@ -20,10 +20,11 @@ pub fn html(document: &HtmlDocument) -> SourceResult<String> {
#[derive(Default)]
struct Writer {
/// The output buffer.
buf: String,
/// current indentation level
/// The current indentation level
level: usize,
/// pretty printing enabled?
/// Whether pretty printing is enabled.
pretty: bool,
}
@ -88,26 +89,32 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
let pretty = w.pretty;
if !element.children.is_empty() {
w.pretty &= is_pretty(element);
let pretty_inside = allows_pretty_inside(element.tag)
&& element.children.iter().any(|node| match node {
HtmlNode::Element(child) => wants_pretty_around(child.tag),
_ => false,
});
w.pretty &= pretty_inside;
let mut indent = w.pretty;
w.level += 1;
for c in &element.children {
let pretty_child = match c {
let pretty_around = match c {
HtmlNode::Tag(_) => continue,
HtmlNode::Element(element) => is_pretty(element),
HtmlNode::Element(child) => w.pretty && wants_pretty_around(child.tag),
HtmlNode::Text(..) | HtmlNode::Frame(_) => false,
};
if core::mem::take(&mut indent) || pretty_child {
if core::mem::take(&mut indent) || pretty_around {
write_indent(w);
}
write_node(w, c)?;
indent = pretty_child;
indent = pretty_around;
}
w.level -= 1;
write_indent(w)
write_indent(w);
}
w.pretty = pretty;
@ -118,9 +125,27 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
Ok(())
}
/// Whether the element should be pretty-printed.
fn is_pretty(element: &HtmlElement) -> bool {
tag::is_block_by_default(element.tag) || matches!(element.tag, tag::meta)
/// Whether we are allowed to add an extra newline at the start and end of the
/// element's contents.
///
/// Technically, users can change CSS `display` properties such that the
/// insertion of whitespace may actually impact the visual output. For example,
/// <https://www.w3.org/TR/css-text-3/#example-af2745cd> shows how adding CSS
/// rules to `<p>` 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.

View File

@ -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;
@ -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::<ParElem>() {
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::<BoxElem>() {
// 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::<BlockElem>()
.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::<SpaceElem>() {
output.push(HtmlNode::text(' ', child.span()));
} else if let Some(elem) = child.to_packed::<TextElem>() {

View File

@ -452,16 +452,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,
})
}
}
_ => {}
}
}

View File

@ -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<T>(
if let Some(v) = node.cast::<ast::ModuleImport>() {
let imports = v.imports();
let source = node
.children()
.find(|child| child.is::<ast::Expr>())
.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::<Module>().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,7 +75,7 @@ pub fn named_items<T>(
// import "foo": *;
// ```
Some(ast::Imports::Wildcard) => {
if let Some(scope) = source.and_then(|(value, _)| value.scope()) {
if let Some(scope) = source_value.and_then(Value::scope) {
for (name, value, span) in scope.iter() {
let item = NamedItem::Import(name, span, Some(value));
if let Some(res) = recv(item) {
@ -92,7 +92,7 @@ pub fn named_items<T>(
let bound = item.bound_name();
let (span, value) = item.path().iter().fold(
(bound.span(), source.map(|(value, _)| value)),
(bound.span(), source_value),
|(span, value), path_ident| {
let scope = value.and_then(|v| v.scope());
let span = scope
@ -175,8 +175,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>),
}
@ -186,7 +186,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,
}
}
@ -194,7 +194,7 @@ impl<'a> NamedItem<'a> {
pub(crate) fn value(&self) -> Option<Value> {
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(),
}
}
@ -202,7 +202,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,
}
}
@ -356,7 +356,17 @@ 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\"")

View File

@ -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<Vec<Child<'a>>> {
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<Child<'a>>,
last_was_par: bool,
par_situation: ParSituation,
}
impl<'a> Collector<'a, '_, '_> {
/// Perform the collection.
fn run(mut self) -> SourceResult<Vec<Child<'a>>> {
fn run(self, mode: FlowMode) -> SourceResult<Vec<Child<'a>>> {
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<Vec<Child<'a>>> {
for &(child, styles) in self.children {
if let Some(elem) = child.to_packed::<TagElem>() {
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<Vec<Child<'a>>> {
// Extract leading and trailing tags.
let (start, end) = self.children.split_prefix_suffix(|(c, _)| c.is::<TagElem>());
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,
None,
)?
.into_frames();
for (c, _) in &self.children[..start] {
let elem = c.to_packed::<TagElem>().unwrap();
self.output.push(Child::Tag(&elem.tag));
}
self.lines(lines, styles);
for (c, _) in &self.children[end..] {
let elem = c.to_packed::<TagElem>().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<VElem>, styles: StyleChain<'a>) {
self.output.push(match elem.amount {
@ -109,24 +158,34 @@ impl<'a> Collector<'a, '_, '_> {
elem: &'a Packed<ParElem>,
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 = ParElem::spacing_in(styles);
self.output.push(Child::Rel(spacing.into(), 4));
self.lines(lines, 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<Frame>, styles: StyleChain<'a>) {
let align = AlignElem::alignment_in(styles).resolve(styles);
let leading = ParElem::leading_in(styles);
let costs = TextElem::costs_in(styles);
// Determine whether to prevent widow and orphans.
let len = lines.len();
let prevent_orphans =
@ -165,11 +224,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 +272,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 +431,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 +528,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 +631,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)
})
}

View File

@ -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(());
}

View File

@ -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,25 +159,46 @@ 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<FragmentKind> 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<Abs>,
root: bool,
mode: FlowMode,
) -> SourceResult<Fragment> {
// Prepare configuration that is shared across the whole flow.
let config = Config {
root,
mode,
shared,
columns: {
let mut count = columns.get();
@ -195,7 +217,7 @@ pub(crate) fn layout_flow(
gap: FootnoteEntry::gap_in(shared),
expand: regions.expand.x,
},
line_numbers: root.then(|| LineNumberConfig {
line_numbers: (mode == FlowMode::Root).then(|| LineNumberConfig {
scope: ParLine::numbering_scope_in(shared),
default_clearance: {
let width = if PageElem::flipped_in(shared) {
@ -225,6 +247,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);
@ -318,7 +341,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>,

View File

@ -10,7 +10,8 @@ use typst_library::layout::{
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.
@ -49,15 +50,27 @@ pub fn layout_image(
}
// Construct the image itself.
let image = Image::with_fonts(
data.clone(),
format,
elem.alt(styles),
engine.world,
&families(styles).map(|f| f.as_str()).collect::<Vec<_>>(),
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,
elem.flatten_text(styles),
&families(styles).map(|f| f.as_str()).collect::<Vec<_>>(),
)
.at(span)?,
),
};
let image = Image::new(kind, elem.alt(styles), elem.scaling(styles));
// Determine the image's pixel aspect ratio.
let pxw = image.width();
@ -129,10 +142,10 @@ fn determine_format(source: &DataSource, data: &Bytes) -> StrResult<ImageFormat>
.to_lowercase();
match ext.as_str() {
"png" => return Ok(ImageFormat::Raster(RasterFormat::Png)),
"jpg" | "jpeg" => return Ok(ImageFormat::Raster(RasterFormat::Jpg)),
"gif" => return Ok(ImageFormat::Raster(RasterFormat::Gif)),
"svg" | "svgz" => return Ok(ImageFormat::Vector(VectorFormat::Svg)),
"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()),
_ => {}
}
}

View File

@ -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<BoxElem>,

View File

@ -1,10 +1,12 @@
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,
};
use typst_library::model::{EnumElem, ListElem, TermsElem};
use typst_library::routines::Pair;
use typst_library::text::{
is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes,
SpaceElem, TextElem,
@ -13,9 +15,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 +29,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 +39,7 @@ pub enum Item<'a> {
/// Fractional spacing between other items.
Fractional(Fr, Option<(&'a Packed<BoxElem>, 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 +70,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 +86,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 +115,51 @@ 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>,
styles: StyleChain<'a>,
region: Size,
consecutive: bool,
situation: Option<ParSituation>,
) -> SourceResult<(String, Vec<Segment<'a>>, 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));
collector.spans.push(1, Span::detached());
let outer_dir = TextElem::dir_in(styles);
if let Some(situation) = situation {
let first_line_indent = ParElem::first_line_indent_in(styles);
if !first_line_indent.amount.is_zero()
&& match situation {
// First-line indent for the first paragraph after a list bullet
// just looks bad.
ParSituation::First => first_line_indent.all && !in_list(styles),
ParSituation::Consecutive => true,
ParSituation::Other => first_line_indent.all,
}
&& AlignElem::alignment_in(styles).resolve(styles).x
== outer_dir.start().into()
{
collector.push_item(Item::Absolute(
first_line_indent.amount.resolve(styles),
false,
));
collector.spans.push(1, Span::detached());
}
let hang = ParElem::hanging_indent_in(styles);
if !hang.is_zero() {
collector.push_item(Item::Absolute(-hang, false));
collector.spans.push(1, Span::detached());
}
}
let hang = ParElem::hanging_indent_in(*styles);
if !hang.is_zero() {
collector.push_item(Item::Absolute(-hang, 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::<SpaceElem>() {
@ -210,8 +226,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));
}
}
}
@ -222,13 +240,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::<TagElem>() {
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;
@ -238,6 +265,16 @@ pub fn collect<'a>(
Ok((collector.full, collector.segments, collector.spans))
}
/// Whether we have a list ancestor.
///
/// When we support some kind of more general ancestry mechanism, this can
/// become more elegant.
fn in_list(styles: StyleChain) -> bool {
ListElem::depth_in(styles).0 > 0
|| !EnumElem::parents_in(styles).is_empty()
|| TermsElem::within_in(styles)
}
/// Collects segments.
struct Collector<'a> {
full: String,

View File

@ -14,7 +14,7 @@ pub fn finalize(
expand: bool,
locator: &mut SplitLocator<'_>,
) -> SourceResult<Fragment> {
// 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()))

View File

@ -10,6 +10,7 @@ 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 +18,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 +94,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,
})
}
@ -409,6 +410,11 @@ fn should_repeat_hyphen(pred_line: &Line, text: &str) -> bool {
}
}
/// 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(
@ -424,7 +430,7 @@ pub fn commit(
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;
@ -509,10 +515,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 +531,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());
@ -626,7 +631,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<ItemEntry<'a>>);
impl<'a> Items<'a> {

View File

@ -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,7 +104,7 @@ impl Breakpoint {
}
}
/// Breaks the paragraph into lines.
/// Breaks the text into lines.
pub fn linebreak<'a>(
engine: &Engine,
p: &'a Preparation<'a>,
@ -181,13 +181,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 +214,7 @@ fn linebreak_optimized_bounded<'a>(
metrics: &CostMetrics,
upper_bound: Cost,
) -> Vec<Line<'a>> {
/// 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,
@ -321,7 +320,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 +341,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 +354,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,
@ -862,7 +861,7 @@ 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.

View File

@ -13,17 +13,17 @@ pub use self::box_::layout_box;
use comemo::{Track, Tracked, TrackedMut};
use typst_library::diag::SourceResult;
use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{StyleChain, StyleVec};
use typst_library::introspection::{Introspector, Locator, LocatorLink};
use typst_library::foundations::{Packed, StyleChain};
use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator};
use typst_library::layout::{Fragment, Size};
use typst_library::model::ParElem;
use typst_library::routines::Routines;
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
use typst_library::World;
use self::collect::{collect, Item, Segment, SpanMapper};
use self::deco::decorate;
use self::finalize::finalize;
use self::line::{commit, line, Line};
use self::line::{apply_baseline_shift, commit, line, Line};
use self::linebreak::{linebreak, Breakpoint};
use self::prepare::{prepare, Preparation};
use self::shaping::{
@ -34,18 +34,18 @@ use self::shaping::{
/// Range of a substring of text.
type Range = std::ops::Range<usize>;
/// Layouts content inline.
pub fn layout_inline(
/// Layouts the paragraph.
pub fn layout_par(
elem: &Packed<ParElem>,
engine: &mut Engine,
children: &StyleVec,
locator: Locator,
styles: StyleChain,
consecutive: bool,
region: Size,
expand: bool,
situation: ParSituation,
) -> SourceResult<Fragment> {
layout_inline_impl(
children,
layout_par_impl(
elem,
engine.routines,
engine.world,
engine.introspector,
@ -54,17 +54,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<ParElem>,
routines: &Routines,
world: Tracked<dyn World + '_>,
introspector: Tracked<Introspector>,
@ -73,12 +73,12 @@ fn layout_inline_impl(
route: Tracked<Route>,
locator: Tracked<Locator>,
styles: StyleChain,
consecutive: bool,
region: Size,
expand: bool,
situation: ParSituation,
) -> SourceResult<Fragment> {
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 +88,63 @@ 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(
&mut engine,
&children,
&mut locator,
styles,
region,
expand,
Some(situation),
)
}
/// Lays out realized content with inline layout.
#[allow(clippy::too_many_arguments)]
pub fn layout_inline<'a>(
engine: &mut Engine,
children: &[Pair<'a>],
locator: &mut SplitLocator<'a>,
styles: StyleChain<'a>,
region: Size,
expand: bool,
par: Option<ParSituation>,
) -> SourceResult<Fragment> {
// Collect all text into one string for BiDi analysis.
let (text, segments, spans) =
collect(children, &mut engine, &mut locator, &styles, region, consecutive)?;
collect(children, engine, locator, styles, region, par)?;
// 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, children, &text, segments, spans, styles, par)?;
// 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 - p.hang);
// Turn the selected lines into frames.
finalize(&mut engine, &p, &lines, styles, region, expand, &mut locator)
finalize(engine, &p, &lines, styles, region, expand, locator)
}
/// Distinguishes between a few different kinds of paragraphs.
///
/// In the form `Option<ParSituation>`, `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,
}

View File

@ -1,23 +1,26 @@
use typst_library::foundations::{Resolve, Smart};
use typst_library::layout::{Abs, AlignElem, Dir, Em, FixedAlignment};
use typst_library::model::Linebreaks;
use typst_library::routines::Pair;
use typst_library::text::{Costs, Lang, TextElem};
use typst_utils::SliceExt;
use 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.
/// 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<BidiInfo<'a>>,
/// Text runs, spacing and layouted elements.
pub items: Vec<(Range, Item<'a>)>,
@ -33,15 +36,15 @@ pub struct Preparation<'a> {
pub dir: Dir,
/// The text language if it's the same for all children.
pub lang: Option<Lang>,
/// The paragraph's resolved horizontal alignment.
/// The resolved horizontal alignment.
pub align: FixedAlignment,
/// Whether to justify the paragraph.
/// Whether to justify text.
pub justify: bool,
/// The paragraph's hanging indent.
/// Hanging indent to apply.
pub hang: Abs,
/// Whether to add spacing between CJK and Latin characters.
pub cjk_latin_spacing: bool,
/// Whether font fallback is enabled for this paragraph.
/// Whether font fallback is enabled.
pub fallback: bool,
/// How to determine line breaks.
pub linebreaks: Smart<Linebreaks>,
@ -71,17 +74,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,
children: &[Pair<'a>],
text: &'a str,
segments: Vec<Segment<'a>>,
spans: SpanMapper,
styles: StyleChain<'a>,
situation: Option<ParSituation>,
) -> SourceResult<Preparation<'a>> {
let dir = TextElem::dir_in(styles);
let default_level = match dir {
@ -125,19 +129,26 @@ pub fn prepare<'a>(
add_cjk_latin_spacing(&mut items);
}
// Only apply hanging indent to real paragraphs.
let hang = if situation.is_some() {
ParElem::hanging_indent_in(styles)
} else {
Abs::zero()
};
Ok(Preparation {
text,
bidi: is_bidi.then_some(bidi),
items,
indices,
spans,
hyphenate: children.shared_get(styles, TextElem::hyphenate_in),
hyphenate: shared_get(children, styles, TextElem::hyphenate_in),
costs: TextElem::costs_in(styles),
dir,
lang: children.shared_get(styles, TextElem::lang_in),
lang: shared_get(children, styles, TextElem::lang_in),
align: AlignElem::alignment_in(styles).resolve(styles).x,
justify: ParElem::justify_in(styles),
hang: ParElem::hanging_indent_in(styles),
hang,
cjk_latin_spacing,
fallback: TextElem::fallback_in(styles),
linebreaks: ParElem::linebreaks_in(styles),
@ -145,6 +156,19 @@ pub fn prepare<'a>(
})
}
/// Get a style property, but only if it is the same for all of the children.
fn shared_get<T: PartialEq>(
children: &[Pair],
styles: StyleChain<'_>,
getter: fn(StyleChain) -> T,
) -> Option<T> {
let value = getter(styles);
children
.group_by_key(|&(_, s)| s)
.all(|(s, _)| getter(s) == value)
.then_some(value)
}
/// Add some spacing between Han characters and western characters. See
/// Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition
/// in Horizontal Written Mode

View File

@ -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()) {

View File

@ -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;

View File

@ -6,7 +6,7 @@ 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::GridLayouter;
@ -22,8 +22,9 @@ pub fn layout_list(
) -> SourceResult<Fragment> {
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()
@ -41,11 +42,17 @@ pub fn layout_list(
let mut locator = locator.split();
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()),
));
}
@ -78,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()
@ -124,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 =

View File

@ -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::{
@ -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());

View File

@ -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;
@ -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),
)?;

View File

@ -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

View File

@ -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};
@ -29,15 +30,7 @@ pub fn layout_lr(
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);

View File

@ -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::<TextElem>() {
self::text::layout_text(elem, ctx, styles)?;
} else if let Some(elem) = elem.to_packed::<SymbolElem>() {
self::text::layout_symbol(elem, ctx, styles)?;
} else if let Some(elem) = elem.to_packed::<BoxElem>() {
layout_box(elem, ctx, styles)?;
} else if elem.is::<AlignPointElem>() {
@ -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()),
@ -688,7 +691,7 @@ fn layout_external(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<Frame> {
(ctx.engine.routines.layout_frame)(
crate::layout_frame(
ctx.engine,
content,
ctx.locator.next(&content.span()),

View File

@ -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;
@ -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 {

View File

@ -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::{
@ -22,54 +22,66 @@ pub fn layout_text(
) -> SourceResult<()> {
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<Item = &'a str>,
span: Span,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<FrameFragment> {
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<FrameFragment> {
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,97 @@ 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,
None,
)?
.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<SymbolElem>,
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<FrameFragment> {
// There isn't a natural width for a paragraph in a math environment;
// because it will be placed somewhere probably not at the left margin
// it will overflow. So emulate an `hbox` instead and allow the paragraph
// to extend as far as needed.
let spaced = text.graphemes(true).nth(1).is_some();
let elem = TextElem::packed(text).spanned(span);
let frame = (ctx.engine.routines.layout_inline)(
ctx.engine,
&StyleVec::wrap(eco_vec![elem]),
ctx.locator.next(&span),
styles,
false,
Size::splat(Abs::inf()),
false,
)?
.into_frame();
) -> 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.
///
/// <https://www.w3.org/TR/mathml-core/#new-text-transform-mappings>
/// <https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols>
@ -353,15 +398,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<char> {
match c {
'ı' => Some('i'),
'ȷ' => Some('j'),
_ => None,
}
}

View File

@ -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<Destination>,
/// 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<T, E> FrameModify for Result<T, E>
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<F, R>(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)
}

View File

@ -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<Item<'a>> {
// The collected page-level items.

View File

@ -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<Vec<Page>> {
// Slice up the children into logical parts.

View File

@ -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.

View File

@ -305,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]

View File

@ -301,9 +301,7 @@ impl Array {
#[func]
pub fn find(
&self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// 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<Context>,
/// 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<Context>,
/// 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<Context>,
/// 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<Context>,
/// 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<Context>,
/// 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<Context>,
/// 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<Context>,
/// The callsite span.
span: Span,
/// If given, applies this function to the elements in the array to
/// determine the keys to sort by.
@ -881,9 +860,7 @@ impl Array {
#[func(title = "Deduplicate")]
pub fn dedup(
self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// If given, applies this function to the elements in the array to
/// determine the keys to deduplicate by.
@ -967,9 +944,7 @@ impl Array {
#[func]
pub fn reduce(
self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// The reducing function. Must have two parameters: One for the
/// accumulated value and one for an item.

View File

@ -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<Num>,
@ -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<Num>,
@ -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<Num>,
@ -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,

View File

@ -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.

View File

@ -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.

View File

@ -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, 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<LazyHash<Closure>>),
/// A plugin WebAssembly function.
Plugin(Arc<PluginFunc>),
/// 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::<Content>())))
}
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,6 +249,7 @@ 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(),
}
}
@ -266,6 +275,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<A: IntoArgs>(
&self,
@ -307,6 +324,12 @@ impl Func {
context,
args,
),
Repr::Plugin(func) => {
let inputs = args.all::<Bytes>()?;
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 +357,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 +382,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]
@ -429,12 +448,30 @@ impl From<Repr> for Func {
}
}
impl From<&'static NativeFuncData> for Func {
fn from(data: &'static NativeFuncData) -> Self {
Repr::Native(Static(data)).into()
}
}
impl From<Element> for Func {
fn from(func: Element) -> Self {
Repr::Element(func).into()
}
}
impl From<Closure> for Func {
fn from(closure: Closure) -> Self {
Repr::Closure(Arc::new(LazyHash::new(closure))).into()
}
}
impl From<PluginFunc> 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 +507,6 @@ pub struct NativeFuncData {
pub returns: LazyLock<CastInfo>,
}
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 +560,6 @@ impl Closure {
}
}
impl From<Closure> for Func {
fn from(closure: Closure) -> Self {
Repr::Closure(Arc::new(LazyHash::new(closure))).into()
}
}
cast! {
Closure,
self => Value::Func(self.into()),

View File

@ -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::*;
@ -114,16 +115,16 @@ pub(super) fn define(global: &mut Scope, inputs: Dict, features: &Features) {
global.define_type::<Symbol>();
global.define_type::<Duration>();
global.define_type::<Version>();
global.define_type::<Plugin>();
global.define_func::<repr::repr>();
global.define_func::<panic>();
global.define_func::<assert>();
global.define_func::<eval>();
global.define_func::<plugin>();
if features.is_enabled(Feature::Html) {
global.define_func::<target>();
}
global.define_module(calc::module());
global.define_module(sys::module(inputs));
global.define("calc", calc::module());
global.define("sys", sys::module(inputs));
}
/// Fails with an error.
@ -266,7 +267,6 @@ impl assert {
/// ```
#[func(title = "Evaluate")]
pub fn eval(
/// The engine.
engine: &mut Engine,
/// A string of Typst code to evaluate.
source: Spanned<String>,

View File

@ -7,14 +7,20 @@ use typst_syntax::FileId;
use crate::diag::StrResult;
use crate::foundations::{repr, ty, Content, Scope, Value};
/// An evaluated module, either built-in or resulting from a file.
/// An module of definitions.
///
/// You can access definitions from the module using
/// [field access notation]($scripting/#fields) and interact with it using the
/// [import and include syntaxes]($scripting/#modules). Alternatively, it is
/// possible to convert a module to a dictionary, and therefore access its
/// contents dynamically, using the
/// [dictionary constructor]($dictionary/#constructor).
/// A module
/// - be built-in
/// - stem from a [file import]($scripting/#modules)
/// - stem from a [package import]($scripting/#packages) (and thus indirectly
/// its entrypoint file)
/// - result from a call to the [plugin]($plugin) function
///
/// You can access definitions from the module using [field access
/// notation]($scripting/#fields) and interact with it using the [import and
/// include syntaxes]($scripting/#modules). Alternatively, it is possible to
/// convert a module to a dictionary, and therefore access its contents
/// dynamically, using the [dictionary constructor]($dictionary/#constructor).
///
/// # Example
/// ```example
@ -32,7 +38,7 @@ use crate::foundations::{repr, ty, Content, Scope, Value};
#[allow(clippy::derived_hash_with_manual_eq)]
pub struct Module {
/// The module's name.
name: EcoString,
name: Option<EcoString>,
/// The reference-counted inner fields.
inner: Arc<Repr>,
}
@ -52,14 +58,22 @@ impl Module {
/// Create a new module.
pub fn new(name: impl Into<EcoString>, scope: Scope) -> Self {
Self {
name: name.into(),
name: Some(name.into()),
inner: Arc::new(Repr { scope, content: Content::empty(), file_id: None }),
}
}
/// Create a new anonymous module without a name.
pub fn anonymous(scope: Scope) -> Self {
Self {
name: None,
inner: Arc::new(Repr { scope, content: Content::empty(), file_id: None }),
}
}
/// Update the module's name.
pub fn with_name(mut self, name: impl Into<EcoString>) -> Self {
self.name = name.into();
self.name = Some(name.into());
self
}
@ -82,8 +96,8 @@ impl Module {
}
/// Get the module's name.
pub fn name(&self) -> &EcoString {
&self.name
pub fn name(&self) -> Option<&EcoString> {
self.name.as_ref()
}
/// Access the module's scope.
@ -105,8 +119,9 @@ impl Module {
/// Try to access a definition in the module.
pub fn field(&self, name: &str) -> StrResult<&Value> {
self.scope().get(name).ok_or_else(|| {
eco_format!("module `{}` does not contain `{name}`", self.name())
self.scope().get(name).ok_or_else(|| match &self.name {
Some(module) => eco_format!("module `{module}` does not contain `{name}`"),
None => eco_format!("module does not contain `{name}`"),
})
}
@ -131,7 +146,10 @@ impl Debug for Module {
impl repr::Repr for Module {
fn repr(&self) -> EcoString {
eco_format!("<module {}>", self.name())
match &self.name {
Some(module) => eco_format!("<module {module}>"),
None => "<module>".into(),
}
}
}

View File

@ -6,7 +6,9 @@ use ecow::eco_format;
use typst_utils::Numeric;
use crate::diag::{bail, HintedStrResult, StrResult};
use crate::foundations::{format_str, Datetime, IntoValue, Regex, Repr, Value};
use crate::foundations::{
format_str, Datetime, IntoValue, Regex, Repr, SymbolElem, Value,
};
use crate::layout::{Alignment, Length, Rel};
use crate::text::TextElem;
use crate::visualize::Stroke;
@ -30,10 +32,10 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> {
(Symbol(a), Str(b)) => Str(format_str!("{a}{b}")),
(Bytes(a), Bytes(b)) => Bytes(a + b),
(Content(a), Content(b)) => Content(a + b),
(Content(a), Symbol(b)) => Content(a + TextElem::packed(b.get())),
(Content(a), Symbol(b)) => Content(a + SymbolElem::packed(b.get())),
(Content(a), Str(b)) => Content(a + TextElem::packed(b)),
(Str(a), Content(b)) => Content(TextElem::packed(a) + b),
(Symbol(a), Content(b)) => Content(TextElem::packed(a.get()) + b),
(Symbol(a), Content(b)) => Content(SymbolElem::packed(a.get()) + b),
(Array(a), Array(b)) => Array(a + b),
(Dict(a), Dict(b)) => Dict(a + b),
(Args(a), Args(b)) => Args(a + b),
@ -130,10 +132,10 @@ pub fn add(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
(Symbol(a), Str(b)) => Str(format_str!("{a}{b}")),
(Bytes(a), Bytes(b)) => Bytes(a + b),
(Content(a), Content(b)) => Content(a + b),
(Content(a), Symbol(b)) => Content(a + TextElem::packed(b.get())),
(Content(a), Symbol(b)) => Content(a + SymbolElem::packed(b.get())),
(Content(a), Str(b)) => Content(a + TextElem::packed(b)),
(Str(a), Content(b)) => Content(TextElem::packed(a) + b),
(Symbol(a), Content(b)) => Content(TextElem::packed(a.get()) + b),
(Symbol(a), Content(b)) => Content(SymbolElem::packed(a.get()) + b),
(Array(a), Array(b)) => Array(a + b),
(Dict(a), Dict(b)) => Dict(a + b),
@ -445,7 +447,6 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool {
(Args(a), Args(b)) => a == b,
(Type(a), Type(b)) => a == b,
(Module(a), Module(b)) => a == b,
(Plugin(a), Plugin(b)) => a == b,
(Datetime(a), Datetime(b)) => a == b,
(Duration(a), Duration(b)) => a == b,
(Dyn(a), Dyn(b)) => a == b,

View File

@ -4,43 +4,27 @@ use std::sync::{Arc, Mutex};
use ecow::{eco_format, EcoString};
use typst_syntax::Spanned;
use wasmi::{AsContext, AsContextMut};
use wasmi::Memory;
use crate::diag::{bail, At, SourceResult, StrResult};
use crate::engine::Engine;
use crate::foundations::{func, repr, scope, ty, Bytes};
use crate::foundations::{cast, func, scope, Bytes, Func, Module, Scope, Value};
use crate::loading::{DataSource, Load};
/// A WebAssembly plugin.
/// Loads a WebAssembly module.
///
/// Typst is capable of interfacing with plugins compiled to WebAssembly. Plugin
/// functions may accept multiple [byte buffers]($bytes) as arguments and return
/// a single byte buffer. They should typically be wrapped in idiomatic Typst
/// functions that perform the necessary conversions between native Typst types
/// and bytes.
/// The resulting [module] will contain one Typst [function] for each function
/// export of the loaded WebAssembly module.
///
/// Plugins run in isolation from your system, which means that printing,
/// reading files, or anything like that will not be supported for security
/// reasons. To run as a plugin, a program needs to be compiled to a 32-bit
/// shared WebAssembly library. Many compilers will use the
/// [WASI ABI](https://wasi.dev/) by default or as their only option (e.g.
/// emscripten), which allows printing, reading files, etc. This ABI will not
/// directly work with Typst. You will either need to compile to a different
/// target or [stub all functions](https://github.com/astrale-sharp/wasm-minimal-protocol/tree/master/crates/wasi-stub).
/// Typst WebAssembly plugins need to follow a specific
/// [protocol]($plugin/#protocol). To run as a plugin, a program needs to be
/// compiled to a 32-bit shared WebAssembly library. Plugin functions may accept
/// multiple [byte buffers]($bytes) as arguments and return a single byte
/// buffer. They should typically be wrapped in idiomatic Typst functions that
/// perform the necessary conversions between native Typst types and bytes.
///
/// # Plugins and Packages
/// Plugins are distributed as packages. A package can make use of a plugin
/// simply by including a WebAssembly file and loading it. Because the
/// byte-based plugin interface is quite low-level, plugins are typically
/// exposed through wrapper functions, that also live in the same package.
///
/// # Purity
/// Plugin functions must be pure: Given the same arguments, they must always
/// return the same value. The reason for this is that Typst functions must be
/// pure (which is quite fundamental to the language design) and, since Typst
/// function can call plugin functions, this requirement is inherited. In
/// particular, if a plugin function is called twice with the same arguments,
/// Typst might cache the results and call your function only once.
/// For security reasons, plugins run in isolation from your system. This means
/// that printing, reading files, or similar things are not supported.
///
/// # Example
/// ```example
@ -55,6 +39,50 @@ use crate::loading::{DataSource, Load};
/// #concat("hello", "world")
/// ```
///
/// Since the plugin function returns a module, it can be used with import
/// syntax:
/// ```typ
/// #import plugin("hello.wasm"): concatenate
/// ```
///
/// # Purity
/// Plugin functions **must be pure:** A plugin function call most not have any
/// observable side effects on future plugin calls and given the same arguments,
/// it must always return the same value.
///
/// The reason for this is that Typst functions must be pure (which is quite
/// fundamental to the language design) and, since Typst function can call
/// plugin functions, this requirement is inherited. In particular, if a plugin
/// function is called twice with the same arguments, Typst might cache the
/// results and call your function only once. Moreover, Typst may run multiple
/// instances of your plugin in multiple threads, with no state shared between
/// them.
///
/// Typst does not enforce plugin function purity (for efficiency reasons), but
/// calling an impure function will lead to unpredictable and irreproducible
/// results and must be avoided.
///
/// That said, mutable operations _can be_ useful for plugins that require
/// costly runtime initialization. Due to the purity requirement, such
/// initialization cannot be performed through a normal function call. Instead,
/// Typst exposes a [plugin transition API]($plugin.transition), which executes
/// a function call and then creates a derived module with new functions which
/// will observe the side effects produced by the transition call. The original
/// plugin remains unaffected.
///
/// # Plugins and Packages
/// Any Typst code can make use of a plugin simply by including a WebAssembly
/// file and loading it. However, because the byte-based plugin interface is
/// quite low-level, plugins are typically exposed through a package containing
/// the plugin and idiomatic wrapper functions.
///
/// # WASI
/// Many compilers will use the [WASI ABI](https://wasi.dev/) by default or as
/// their only option (e.g. emscripten), which allows printing, reading files,
/// etc. This ABI will not directly work with Typst. You will either need to
/// compile to a different target or [stub all
/// functions](https://github.com/astrale-sharp/wasm-minimal-protocol/tree/master/crates/wasi-stub).
///
/// # Protocol
/// To be used as a plugin, a WebAssembly module must conform to the following
/// protocol:
@ -67,8 +95,8 @@ use crate::loading::{DataSource, Load};
/// lengths, so `usize/size_t` may be preferable), and return one 32-bit
/// integer.
///
/// - The function should first allocate a buffer `buf` of length
/// `a_1 + a_2 + ... + a_n`, and then call
/// - The function should first allocate a buffer `buf` of length `a_1 + a_2 +
/// ... + a_n`, and then call
/// `wasm_minimal_protocol_write_args_to_buffer(buf.ptr)`.
///
/// - The `a_1` first bytes of the buffer now constitute the first argument, the
@ -85,19 +113,21 @@ use crate::loading::{DataSource, Load};
/// then interpreted as an UTF-8 encoded error message.
///
/// ## Imports
/// Plugin modules need to import two functions that are provided by the runtime.
/// (Types and functions are described using WAT syntax.)
/// Plugin modules need to import two functions that are provided by the
/// runtime. (Types and functions are described using WAT syntax.)
///
/// - `(import "typst_env" "wasm_minimal_protocol_write_args_to_buffer" (func (param i32)))`
/// - `(import "typst_env" "wasm_minimal_protocol_write_args_to_buffer" (func
/// (param i32)))`
///
/// Writes the arguments for the current function into a plugin-allocated
/// buffer. When a plugin function is called, it
/// [receives the lengths](#exports) of its input buffers as arguments. It
/// should then allocate a buffer whose capacity is at least the sum of these
/// lengths. It should then call this function with a `ptr` to the buffer to
/// fill it with the arguments, one after another.
/// buffer. When a plugin function is called, it [receives the
/// lengths](#exports) of its input buffers as arguments. It should then
/// allocate a buffer whose capacity is at least the sum of these lengths. It
/// should then call this function with a `ptr` to the buffer to fill it with
/// the arguments, one after another.
///
/// - `(import "typst_env" "wasm_minimal_protocol_send_result_to_host" (func (param i32 i32)))`
/// - `(import "typst_env" "wasm_minimal_protocol_send_result_to_host" (func
/// (param i32 i32)))`
///
/// Sends the output of the current function to the host (Typst). The first
/// parameter shall be a pointer to a buffer (`ptr`), while the second is the
@ -106,73 +136,147 @@ use crate::loading::{DataSource, Load};
/// interpreted as an error message, it should be encoded as UTF-8.
///
/// # Resources
/// For more resources, check out the
/// [wasm-minimal-protocol repository](https://github.com/astrale-sharp/wasm-minimal-protocol).
/// It contains:
/// For more resources, check out the [wasm-minimal-protocol
/// repository](https://github.com/astrale-sharp/wasm-minimal-protocol). It
/// contains:
///
/// - A list of example plugin implementations and a test runner for these
/// examples
/// - Wrappers to help you write your plugin in Rust (Zig wrapper in
/// development)
/// - A stubber for WASI
#[ty(scope, cast)]
#[derive(Clone)]
pub struct Plugin(Arc<Repr>);
/// The internal representation of a plugin.
struct Repr {
/// The raw WebAssembly bytes.
bytes: Bytes,
/// The function defined by the WebAssembly module.
functions: Vec<(EcoString, wasmi::Func)>,
/// Owns all data associated with the WebAssembly module.
store: Mutex<Store>,
}
/// Owns all data associated with the WebAssembly module.
type Store = wasmi::Store<StoreData>;
/// If there was an error reading/writing memory, keep the offset + length to
/// display an error message.
struct MemoryError {
offset: u32,
length: u32,
write: bool,
}
/// The persistent store data used for communication between store and host.
#[derive(Default)]
struct StoreData {
args: Vec<Bytes>,
output: Vec<u8>,
memory_error: Option<MemoryError>,
#[func(scope)]
pub fn plugin(
engine: &mut Engine,
/// A path to a WebAssembly file or raw WebAssembly bytes.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
source: Spanned<DataSource>,
) -> SourceResult<Module> {
let data = source.load(engine.world)?;
Plugin::module(data).at(source.span)
}
#[scope]
impl Plugin {
/// Creates a new plugin from a WebAssembly file.
#[func(constructor)]
pub fn construct(
/// The engine.
engine: &mut Engine,
/// A path to a WebAssembly file or raw WebAssembly bytes.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
source: Spanned<DataSource>,
) -> SourceResult<Plugin> {
let data = source.load(engine.world)?;
Plugin::new(data).at(source.span)
impl plugin {
/// Calls a plugin function that has side effects and returns a new module
/// with plugin functions that are guaranteed to have observed the results
/// of the mutable call.
///
/// Note that calling an impure function through a normal function call
/// (without use of the transition API) is forbidden and leads to
/// unpredictable behaviour. Read the [section on purity]($plugin/#purity)
/// for more details.
///
/// In the example below, we load the plugin `hello-mut.wasm` which exports
/// two functions: The `get()` function retrieves a global array as a
/// string. The `add(value)` function adds a value to the global array.
///
/// We call `add` via the transition API. The call `mutated.get()` on the
/// derived module will observe the addition. Meanwhile the original module
/// remains untouched as demonstrated by the `base.get()` call.
///
/// _Note:_ Due to limitations in the internal WebAssembly implementation,
/// the transition API can only guarantee to reflect changes in the plugin's
/// memory, not in WebAssembly globals. If your plugin relies on changes to
/// globals being visible after transition, you might want to avoid use of
/// the transition API for now. We hope to lift this limitation in the
/// future.
///
/// ```typ
/// #let base = plugin("hello-mut.wasm")
/// #assert.eq(base.get(), "[]")
///
/// #let mutated = plugin.transition(base.add, "hello")
/// #assert.eq(base.get(), "[]")
/// #assert.eq(mutated.get(), "[hello]")
/// ```
#[func]
pub fn transition(
/// The plugin function to call.
func: PluginFunc,
/// The byte buffers to call the function with.
#[variadic]
arguments: Vec<Bytes>,
) -> StrResult<Module> {
func.transition(arguments)
}
}
/// A function loaded from a WebAssembly plugin.
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct PluginFunc {
/// The underlying plugin, shared by this and the other functions.
plugin: Arc<Plugin>,
/// The name of the plugin function.
name: EcoString,
}
impl PluginFunc {
/// The name of the plugin function.
pub fn name(&self) -> &str {
&self.name
}
/// Call the WebAssembly function with the given arguments.
#[comemo::memoize]
#[typst_macros::time(name = "call plugin")]
pub fn call(&self, args: Vec<Bytes>) -> StrResult<Bytes> {
self.plugin.call(&self.name, args)
}
/// Transition a plugin and turn the result into a module.
#[comemo::memoize]
#[typst_macros::time(name = "transition plugin")]
pub fn transition(&self, args: Vec<Bytes>) -> StrResult<Module> {
self.plugin.transition(&self.name, args).map(Plugin::into_module)
}
}
cast! {
PluginFunc,
self => Value::Func(self.into()),
v: Func => v.to_plugin().ok_or("expected plugin function")?.clone(),
}
/// A plugin with potentially multiple instances for multi-threaded
/// execution.
struct Plugin {
/// Shared by all variants of the plugin.
base: Arc<PluginBase>,
/// A pool of plugin instances.
///
/// When multiple plugin calls run concurrently due to multi-threading, we
/// create new instances whenever we run out of ones.
pool: Mutex<Vec<PluginInstance>>,
/// A snapshot that new instances should be restored to.
snapshot: Option<Snapshot>,
/// A combined hash that incorporates all function names and arguments used
/// in transitions of this plugin, such that this plugin has a deterministic
/// hash and equality check that can differentiate it from "siblings" (same
/// base, different transitions).
fingerprint: u128,
}
impl Plugin {
/// Create a new plugin from raw WebAssembly bytes.
/// Create a plugin and turn it into a module.
#[comemo::memoize]
#[typst_macros::time(name = "load plugin")]
pub fn new(bytes: Bytes) -> StrResult<Plugin> {
fn module(bytes: Bytes) -> StrResult<Module> {
Self::new(bytes).map(Self::into_module)
}
/// Create a new plugin from raw WebAssembly bytes.
fn new(bytes: Bytes) -> StrResult<Self> {
let engine = wasmi::Engine::default();
let module = wasmi::Module::new(&engine, bytes.as_slice())
.map_err(|err| format!("failed to load WebAssembly module ({err})"))?;
// Ensure that the plugin exports its memory.
if !matches!(module.get_export("memory"), Some(wasmi::ExternType::Memory(_))) {
bail!("plugin does not export its memory");
}
let mut linker = wasmi::Linker::new(&engine);
linker
.func_wrap(
@ -189,58 +293,174 @@ impl Plugin {
)
.unwrap();
let mut store = Store::new(&engine, StoreData::default());
let instance = linker
.instantiate(&mut store, &module)
let base = Arc::new(PluginBase { bytes, linker, module });
let instance = PluginInstance::new(&base, None)?;
Ok(Self {
base,
snapshot: None,
fingerprint: 0,
pool: Mutex::new(vec![instance]),
})
}
/// Execute a function with access to an instsance.
fn call(&self, func: &str, args: Vec<Bytes>) -> StrResult<Bytes> {
// Acquire an instance from the pool (potentially creating a new one).
let mut instance = self.acquire()?;
// Execute the call on an instance from the pool. If the call fails, we
// return early and _don't_ return the instance to the pool as it might
// be irrecoverably damaged.
let output = instance.call(func, args)?;
// Return the instance to the pool.
self.pool.lock().unwrap().push(instance);
Ok(output)
}
/// Call a mutable plugin function, producing a new mutable whose functions
/// are guaranteed to be able to observe the mutation.
fn transition(&self, func: &str, args: Vec<Bytes>) -> StrResult<Plugin> {
// Derive a new transition hash from the old one and the function and arguments.
let fingerprint = typst_utils::hash128(&(self.fingerprint, func, &args));
// Execute the mutable call on an instance.
let mut instance = self.acquire()?;
// Call the function. If the call fails, we return early and _don't_
// return the instance to the pool as it might be irrecoverably damaged.
instance.call(func, args)?;
// Snapshot the instance after the mutable call.
let snapshot = instance.snapshot();
// Create a new plugin and move (this is important!) the used instance
// into it, so that the old plugin won't observe the mutation. Also
// save the snapshot so that instances that are initialized for the
// transitioned plugin's pool observe the mutation.
Ok(Self {
base: self.base.clone(),
snapshot: Some(snapshot),
fingerprint,
pool: Mutex::new(vec![instance]),
})
}
/// Acquire an instance from the pool (or create a new one).
fn acquire(&self) -> StrResult<PluginInstance> {
// Don't use match to ensure that the lock is released before we create
// a new instance.
if let Some(instance) = self.pool.lock().unwrap().pop() {
return Ok(instance);
}
PluginInstance::new(&self.base, self.snapshot.as_ref())
}
/// Turn a plugin into a Typst module containing plugin functions.
fn into_module(self) -> Module {
let shared = Arc::new(self);
// Build a scope from the collected functions.
let mut scope = Scope::new();
for export in shared.base.module.exports() {
if matches!(export.ty(), wasmi::ExternType::Func(_)) {
let name = EcoString::from(export.name());
let func = PluginFunc { plugin: shared.clone(), name: name.clone() };
scope.define(name, Func::from(func));
}
}
Module::anonymous(scope)
}
}
impl Debug for Plugin {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad("Plugin(..)")
}
}
impl PartialEq for Plugin {
fn eq(&self, other: &Self) -> bool {
self.base.bytes == other.base.bytes && self.fingerprint == other.fingerprint
}
}
impl Hash for Plugin {
fn hash<H: Hasher>(&self, state: &mut H) {
self.base.bytes.hash(state);
self.fingerprint.hash(state);
}
}
/// Shared by all pooled & transitioned variants of the plugin.
struct PluginBase {
/// The raw WebAssembly bytes.
bytes: Bytes,
/// The compiled WebAssembly module.
module: wasmi::Module,
/// A linker used to create a `Store` for execution.
linker: wasmi::Linker<CallData>,
}
/// An single plugin instance for single-threaded execution.
struct PluginInstance {
/// The underlying wasmi instance.
instance: wasmi::Instance,
/// The execution store of this concrete plugin instance.
store: wasmi::Store<CallData>,
}
/// A snapshot of a plugin instance.
struct Snapshot {
/// The number of pages in the main memory.
mem_pages: u32,
/// The data in the main memory.
mem_data: Vec<u8>,
}
impl PluginInstance {
/// Create a new execution instance of a plugin, potentially restoring
/// a snapshot.
#[typst_macros::time(name = "create plugin instance")]
fn new(base: &PluginBase, snapshot: Option<&Snapshot>) -> StrResult<PluginInstance> {
let mut store = wasmi::Store::new(base.linker.engine(), CallData::default());
let instance = base
.linker
.instantiate(&mut store, &base.module)
.and_then(|pre_instance| pre_instance.start(&mut store))
.map_err(|e| eco_format!("{e}"))?;
// Ensure that the plugin exports its memory.
if !matches!(
instance.get_export(&store, "memory"),
Some(wasmi::Extern::Memory(_))
) {
bail!("plugin does not export its memory");
let mut instance = PluginInstance { instance, store };
if let Some(snapshot) = snapshot {
instance.restore(snapshot);
}
// Collect exported functions.
let functions = instance
.exports(&store)
.filter_map(|export| {
let name = export.name().into();
export.into_func().map(|func| (name, func))
})
.collect();
Ok(Plugin(Arc::new(Repr { bytes, functions, store: Mutex::new(store) })))
Ok(instance)
}
/// Call the plugin function with the given `name`.
#[comemo::memoize]
#[typst_macros::time(name = "call plugin")]
pub fn call(&self, name: &str, args: Vec<Bytes>) -> StrResult<Bytes> {
// Find the function with the given name.
let func = self
.0
.functions
.iter()
.find(|(v, _)| v == name)
.map(|&(_, func)| func)
.ok_or_else(|| {
eco_format!("plugin does not contain a function called {name}")
})?;
/// Call a plugin function with byte arguments.
fn call(&mut self, func: &str, args: Vec<Bytes>) -> StrResult<Bytes> {
let handle = self
.instance
.get_export(&self.store, func)
.unwrap()
.into_func()
.unwrap();
let ty = handle.ty(&self.store);
let mut store = self.0.store.lock().unwrap();
let ty = func.ty(store.as_context());
// Check function signature.
// Check function signature. Do this lazily only when a function is called
// because there might be exported functions like `_initialize` that don't
// match the schema.
if ty.params().iter().any(|&v| v != wasmi::core::ValType::I32) {
bail!(
"plugin function `{name}` has a parameter that is not a 32-bit integer"
"plugin function `{func}` has a parameter that is not a 32-bit integer"
);
}
if ty.results() != [wasmi::core::ValType::I32] {
bail!("plugin function `{name}` does not return exactly one 32-bit integer");
bail!("plugin function `{func}` does not return exactly one 32-bit integer");
}
// Check inputs.
@ -261,23 +481,26 @@ impl Plugin {
.collect::<Vec<_>>();
// Store the input data.
store.data_mut().args = args;
self.store.data_mut().args = args;
// Call the function.
let mut code = wasmi::Val::I32(-1);
func.call(store.as_context_mut(), &lengths, std::slice::from_mut(&mut code))
handle
.call(&mut self.store, &lengths, std::slice::from_mut(&mut code))
.map_err(|err| eco_format!("plugin panicked: {err}"))?;
if let Some(MemoryError { offset, length, write }) =
store.data_mut().memory_error.take()
self.store.data_mut().memory_error.take()
{
return Err(eco_format!(
"plugin tried to {kind} out of bounds: pointer {offset:#x} is out of bounds for {kind} of length {length}",
"plugin tried to {kind} out of bounds: \
pointer {offset:#x} is out of bounds for {kind} of length {length}",
kind = if write { "write" } else { "read" }
));
}
// Extract the returned data.
let output = std::mem::take(&mut store.data_mut().output);
let output = std::mem::take(&mut self.store.data_mut().output);
// Parse the functions return value.
match code {
@ -294,39 +517,63 @@ impl Plugin {
Ok(Bytes::new(output))
}
/// An iterator over all the function names defined by the plugin.
pub fn iter(&self) -> impl Iterator<Item = &EcoString> {
self.0.functions.as_slice().iter().map(|(func_name, _)| func_name)
/// Creates a snapshot of this instance from which another one can be
/// initialized.
#[typst_macros::time(name = "save snapshot")]
fn snapshot(&self) -> Snapshot {
let memory = self.memory();
let mem_pages = memory.size(&self.store);
let mem_data = memory.data(&self.store).to_vec();
Snapshot { mem_pages, mem_data }
}
/// Restores the instance to a snapshot.
#[typst_macros::time(name = "restore snapshot")]
fn restore(&mut self, snapshot: &Snapshot) {
let memory = self.memory();
let current_size = memory.size(&self.store);
if current_size < snapshot.mem_pages {
memory
.grow(&mut self.store, snapshot.mem_pages - current_size)
.unwrap();
}
memory.data_mut(&mut self.store)[..snapshot.mem_data.len()]
.copy_from_slice(&snapshot.mem_data);
}
/// Retrieves a handle to the plugin's main memory.
fn memory(&self) -> Memory {
self.instance
.get_export(&self.store, "memory")
.unwrap()
.into_memory()
.unwrap()
}
}
impl Debug for Plugin {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad("Plugin(..)")
}
/// The persistent store data used for communication between store and host.
#[derive(Default)]
struct CallData {
/// Arguments for a current call.
args: Vec<Bytes>,
/// The results of the current call.
output: Vec<u8>,
/// A memory error that occured during execution of the current call.
memory_error: Option<MemoryError>,
}
impl repr::Repr for Plugin {
fn repr(&self) -> EcoString {
"plugin(..)".into()
}
}
impl PartialEq for Plugin {
fn eq(&self, other: &Self) -> bool {
self.0.bytes == other.0.bytes
}
}
impl Hash for Plugin {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.bytes.hash(state);
}
/// If there was an error reading/writing memory, keep the offset + length to
/// display an error message.
struct MemoryError {
offset: u32,
length: u32,
write: bool,
}
/// Write the arguments to the plugin function into the plugin's memory.
fn wasm_minimal_protocol_write_args_to_buffer(
mut caller: wasmi::Caller<StoreData>,
mut caller: wasmi::Caller<CallData>,
ptr: u32,
) {
let memory = caller.get_export("memory").unwrap().into_memory().unwrap();
@ -347,7 +594,7 @@ fn wasm_minimal_protocol_write_args_to_buffer(
/// Extracts the output of the plugin function from the plugin's memory.
fn wasm_minimal_protocol_send_result_to_host(
mut caller: wasmi::Caller<StoreData>,
mut caller: wasmi::Caller<CallData>,
ptr: u32,
len: u32,
) {

View File

@ -12,8 +12,8 @@ use typst_utils::Static;
use crate::diag::{bail, HintedStrResult, HintedString, StrResult};
use crate::foundations::{
Element, Func, IntoValue, Module, NativeElement, NativeFunc, NativeFuncData,
NativeType, Type, Value,
Element, Func, IntoValue, NativeElement, NativeFunc, NativeFuncData, NativeType,
Type, Value,
};
use crate::Library;
@ -167,6 +167,14 @@ impl Scope {
Default::default()
}
/// Create a new scope with the given capacity.
pub fn with_capacity(capacity: usize) -> Self {
Self {
map: IndexMap::with_capacity(capacity),
..Default::default()
}
}
/// Create a new scope with duplication prevention.
pub fn deduplicating() -> Self {
Self { deduplicate: true, ..Default::default() }
@ -252,11 +260,6 @@ impl Scope {
self.define(data.name, Element::from(data));
}
/// Define a module.
pub fn define_module(&mut self, module: Module) {
self.define(module.name().clone(), module);
}
/// Try to access a variable immutably.
pub fn get(&self, var: &str) -> Option<&Value> {
self.map.get(var).map(Slot::read)

View File

@ -425,9 +425,7 @@ impl Str {
#[func]
pub fn replace(
&self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// The pattern to search for.
pattern: StrPattern,

View File

@ -776,107 +776,6 @@ impl<'a> Iterator for Links<'a> {
}
}
/// A sequence of elements with associated styles.
#[derive(Clone, PartialEq, Hash)]
pub struct StyleVec {
/// The elements themselves.
elements: EcoVec<Content>,
/// A run-length encoded list of style lists.
///
/// Each element is a (styles, count) pair. Any elements whose
/// style falls after the end of this list is considered to
/// have an empty style list.
styles: EcoVec<(Styles, usize)>,
}
impl StyleVec {
/// Create a style vector from an unstyled vector content.
pub fn wrap(elements: EcoVec<Content>) -> Self {
Self { elements, styles: EcoVec::new() }
}
/// Create a `StyleVec` from a list of content with style chains.
pub fn create<'a>(buf: &[(&'a Content, StyleChain<'a>)]) -> (Self, StyleChain<'a>) {
let trunk = StyleChain::trunk(buf.iter().map(|&(_, s)| s)).unwrap_or_default();
let depth = trunk.links().count();
let mut elements = EcoVec::with_capacity(buf.len());
let mut styles = EcoVec::<(Styles, usize)>::new();
let mut last: Option<(StyleChain<'a>, usize)> = None;
for &(element, chain) in buf {
elements.push(element.clone());
if let Some((prev, run)) = &mut last {
if chain == *prev {
*run += 1;
} else {
styles.push((prev.suffix(depth), *run));
last = Some((chain, 1));
}
} else {
last = Some((chain, 1));
}
}
if let Some((last, run)) = last {
let skippable = styles.is_empty() && last == trunk;
if !skippable {
styles.push((last.suffix(depth), run));
}
}
(StyleVec { elements, styles }, trunk)
}
/// Whether there are no elements.
pub fn is_empty(&self) -> bool {
self.elements.is_empty()
}
/// The number of elements.
pub fn len(&self) -> usize {
self.elements.len()
}
/// Iterate over the contained content and style chains.
pub fn iter<'a>(
&'a self,
outer: &'a StyleChain<'_>,
) -> impl Iterator<Item = (&'a Content, StyleChain<'a>)> {
static EMPTY: Styles = Styles::new();
self.elements
.iter()
.zip(
self.styles
.iter()
.flat_map(|(local, count)| std::iter::repeat(local).take(*count))
.chain(std::iter::repeat(&EMPTY)),
)
.map(|(element, local)| (element, outer.chain(local)))
}
/// Get a style property, but only if it is the same for all children of the
/// style vector.
pub fn shared_get<T: PartialEq>(
&self,
styles: StyleChain<'_>,
getter: fn(StyleChain) -> T,
) -> Option<T> {
let value = getter(styles);
self.styles
.iter()
.all(|(local, _)| getter(styles.chain(local)) == value)
.then_some(value)
}
}
impl Debug for StyleVec {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
f.debug_list().entries(&self.elements).finish()
}
}
/// A property that is resolved with other properties from the style chain.
pub trait Resolve {
/// The type of the resolved output.

View File

@ -9,7 +9,10 @@ use typst_syntax::{is_ident, Span, Spanned};
use typst_utils::hash128;
use crate::diag::{bail, SourceResult, StrResult};
use crate::foundations::{cast, func, scope, ty, Array, Func, NativeFunc, Repr as _};
use crate::foundations::{
cast, elem, func, scope, ty, Array, Content, Func, NativeElement, NativeFunc, Packed,
PlainText, Repr as _,
};
/// A Unicode symbol.
///
@ -187,7 +190,6 @@ impl Symbol {
/// ```
#[func(constructor)]
pub fn construct(
/// The callsite span.
span: Span,
/// The variants of the symbol.
///
@ -426,3 +428,31 @@ fn parts(modifiers: &str) -> impl Iterator<Item = &str> {
fn contained(modifiers: &str, m: &str) -> bool {
parts(modifiers).any(|part| part == m)
}
/// A single character.
#[elem(Repr, PlainText)]
pub struct SymbolElem {
/// The symbol's character.
#[required]
pub text: char, // This is called `text` for consistency with `TextElem`.
}
impl SymbolElem {
/// Create a new packed symbol element.
pub fn packed(text: impl Into<char>) -> Content {
Self::new(text.into()).pack()
}
}
impl PlainText for Packed<SymbolElem> {
fn plain_text(&self, text: &mut EcoString) {
text.push(self.text);
}
}
impl crate::foundations::Repr for SymbolElem {
/// Use a custom repr that matches normal content.
fn repr(&self) -> EcoString {
eco_format!("[{}]", self.text)
}
}

View File

@ -30,9 +30,6 @@ pub struct TargetElem {
/// Returns the current compilation target.
#[func(contextual)]
pub fn target(
/// The callsite context.
context: Tracked<Context>,
) -> HintedStrResult<Target> {
pub fn target(context: Tracked<Context>) -> HintedStrResult<Target> {
Ok(TargetElem::target_in(context.styles()?))
}

View File

@ -136,7 +136,7 @@ impl Repr for Type {
} else if *self == Type::of::<NoneValue>() {
"type(none)"
} else {
self.long_name()
self.short_name()
}
.into()
}

View File

@ -15,8 +15,8 @@ use crate::diag::{HintedStrResult, HintedString, StrResult};
use crate::foundations::{
fields, ops, repr, Args, Array, AutoValue, Bytes, CastInfo, Content, Datetime,
Decimal, Dict, Duration, Fold, FromValue, Func, IntoValue, Label, Module,
NativeElement, NativeType, NoneValue, Plugin, Reflect, Repr, Resolve, Scope, Str,
Styles, Symbol, Type, Version,
NativeElement, NativeType, NoneValue, Reflect, Repr, Resolve, Scope, Str, Styles,
Symbol, SymbolElem, Type, Version,
};
use crate::layout::{Abs, Angle, Em, Fr, Length, Ratio, Rel};
use crate::text::{RawContent, RawElem, TextElem};
@ -84,8 +84,6 @@ pub enum Value {
Type(Type),
/// A module.
Module(Module),
/// A WebAssembly plugin.
Plugin(Plugin),
/// A dynamic value.
Dyn(Dynamic),
}
@ -147,7 +145,6 @@ impl Value {
Self::Args(_) => Type::of::<Args>(),
Self::Type(_) => Type::of::<Type>(),
Self::Module(_) => Type::of::<Module>(),
Self::Plugin(_) => Type::of::<Plugin>(),
Self::Dyn(v) => v.ty(),
}
}
@ -181,16 +178,6 @@ impl Value {
}
}
/// The name, if this is a function, type, or module.
pub fn name(&self) -> Option<&str> {
match self {
Self::Func(func) => func.name(),
Self::Type(ty) => Some(ty.short_name()),
Self::Module(module) => Some(module.name()),
_ => None,
}
}
/// Try to extract documentation for the value.
pub fn docs(&self) -> Option<&'static str> {
match self {
@ -209,7 +196,7 @@ impl Value {
Self::Decimal(v) => TextElem::packed(eco_format!("{v}")),
Self::Str(v) => TextElem::packed(v),
Self::Version(v) => TextElem::packed(eco_format!("{v}")),
Self::Symbol(v) => TextElem::packed(v.get()),
Self::Symbol(v) => SymbolElem::packed(v.get()),
Self::Content(v) => v,
Self::Module(module) => module.content(),
_ => RawElem::new(RawContent::Text(self.repr()))
@ -261,7 +248,6 @@ impl Debug for Value {
Self::Args(v) => Debug::fmt(v, f),
Self::Type(v) => Debug::fmt(v, f),
Self::Module(v) => Debug::fmt(v, f),
Self::Plugin(v) => Debug::fmt(v, f),
Self::Dyn(v) => Debug::fmt(v, f),
}
}
@ -299,7 +285,6 @@ impl Repr for Value {
Self::Args(v) => v.repr(),
Self::Type(v) => v.repr(),
Self::Module(v) => v.repr(),
Self::Plugin(v) => v.repr(),
Self::Dyn(v) => v.repr(),
}
}
@ -350,7 +335,6 @@ impl Hash for Value {
Self::Args(v) => v.hash(state),
Self::Type(v) => v.hash(state),
Self::Module(v) => v.hash(state),
Self::Plugin(v) => v.hash(state),
Self::Dyn(v) => v.hash(state),
}
}
@ -656,7 +640,7 @@ primitive! { Duration: "duration", Duration }
primitive! { Content: "content",
Content,
None => Content::empty(),
Symbol(v) => TextElem::packed(v.get()),
Symbol(v) => SymbolElem::packed(v.get()),
Str(v) => TextElem::packed(v)
}
primitive! { Styles: "styles", Styles }
@ -671,7 +655,6 @@ primitive! {
primitive! { Args: "arguments", Args }
primitive! { Type: "type", Type }
primitive! { Module: "module", Module }
primitive! { Plugin: "plugin", Plugin }
impl<T: Reflect> Reflect for Arc<T> {
fn input() -> CastInfo {
@ -730,6 +713,11 @@ mod tests {
assert_eq!(value.into_value().repr(), exp);
}
#[test]
fn test_value_size() {
assert!(std::mem::size_of::<Value>() <= 32);
}
#[test]
fn test_value_debug() {
// Primitives.

View File

@ -210,7 +210,10 @@ impl HtmlAttr {
/// Creates a compile-time constant `HtmlAttr`.
///
/// Should only be used in const contexts because it can panic.
/// Must only be used in const contexts (in a constant definition or
/// explicit `const { .. }` block) because otherwise a panic for a malformed
/// attribute or not auto-internible constant will only be caught at
/// runtime.
#[track_caller]
pub const fn constant(string: &'static str) -> Self {
if string.is_empty() {
@ -472,17 +475,55 @@ pub mod tag {
wbr
}
/// Whether this is a void tag whose associated element may not have a
/// children.
pub fn is_void(tag: HtmlTag) -> bool {
matches!(
tag,
self::area
| self::base
| self::br
| self::col
| self::embed
| self::hr
| self::img
| self::input
| self::link
| self::meta
| self::param
| self::source
| self::track
| self::wbr
)
}
/// Whether this is a tag containing raw text.
pub fn is_raw(tag: HtmlTag) -> bool {
matches!(tag, self::script | self::style)
}
/// Whether this is a tag containing escapable raw text.
pub fn is_escapable_raw(tag: HtmlTag) -> bool {
matches!(tag, self::textarea | self::title)
}
/// Whether an element is considered metadata.
pub fn is_metadata(tag: HtmlTag) -> bool {
matches!(
tag,
self::base
| self::link
| self::meta
| self::noscript
| self::script
| self::style
| self::template
| self::title
)
}
/// Whether nodes with the tag have the CSS property `display: block` by
/// default.
///
/// If this is true, then pretty-printing can insert spaces around such
/// nodes and around the contents of such nodes.
///
/// However, when users change the properties of such tags via CSS, the
/// insertion of whitespace may actually impact the visual output; for
/// example, <https://www.w3.org/TR/css-text-3/#example-af2745cd> shows how
/// adding CSS rules to `<p>` can make it sensitive to whitespace. In such
/// cases, users should disable pretty-printing.
pub fn is_block_by_default(tag: HtmlTag) -> bool {
matches!(
tag,
@ -569,42 +610,29 @@ pub mod tag {
)
}
/// Whether this is a void tag whose associated element may not have a
/// children.
pub fn is_void(tag: HtmlTag) -> bool {
/// Whether nodes with the tag have the CSS property `display: table(-.*)?`
/// by default.
pub fn is_tabular_by_default(tag: HtmlTag) -> bool {
matches!(
tag,
self::area
| self::base
| self::br
self::table
| self::thead
| self::tbody
| self::tfoot
| self::tr
| self::th
| self::td
| self::caption
| self::col
| self::embed
| self::hr
| self::img
| self::input
| self::link
| self::meta
| self::param
| self::source
| self::track
| self::wbr
| self::colgroup
)
}
/// Whether this is a tag containing raw text.
pub fn is_raw(tag: HtmlTag) -> bool {
matches!(tag, self::script | self::style)
}
/// Whether this is a tag containing escapable raw text.
pub fn is_escapable_raw(tag: HtmlTag) -> bool {
matches!(tag, self::textarea | self::title)
}
}
/// Predefined constants for HTML attributes.
///
/// Note: These are very incomplete.
#[allow(non_upper_case_globals)]
pub mod attr {
use super::HtmlAttr;
@ -619,13 +647,18 @@ pub mod attr {
attrs! {
charset
cite
colspan
content
href
name
value
reversed
role
rowspan
start
style
value
}
#[allow(non_upper_case_globals)]
pub const aria_level: HtmlAttr = HtmlAttr::constant("aria-level");
}

View File

@ -428,11 +428,8 @@ impl Counter {
#[func(contextual)]
pub fn get(
&self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// The callsite span.
span: Span,
) -> SourceResult<CounterState> {
let loc = context.location().at(span)?;
@ -444,11 +441,8 @@ impl Counter {
#[func(contextual)]
pub fn display(
self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// The call span of the display.
span: Span,
/// A [numbering pattern or a function]($numbering), which specifies how
/// to display the counter. If given a function, that function receives
@ -482,11 +476,8 @@ impl Counter {
#[func(contextual)]
pub fn at(
&self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// The callsite span.
span: Span,
/// The place at which the counter's value should be retrieved.
selector: LocatableSelector,
@ -500,11 +491,8 @@ impl Counter {
#[func(contextual)]
pub fn final_(
&self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// The callsite span.
span: Span,
) -> SourceResult<CounterState> {
context.introspect().at(span)?;
@ -528,7 +516,6 @@ impl Counter {
#[func]
pub fn step(
self,
/// The call span of the update.
span: Span,
/// The depth at which to step the counter. Defaults to `{1}`.
#[named]
@ -545,7 +532,6 @@ impl Counter {
#[func]
pub fn update(
self,
/// The call span of the update.
span: Span,
/// If given an integer or array of integers, sets the counter to that
/// value. If given a function, that function receives the previous

View File

@ -44,9 +44,6 @@ use crate::introspection::Location;
/// ```
/// Refer to the [`selector`] type for more details on before/after selectors.
#[func(contextual)]
pub fn here(
/// The callsite context.
context: Tracked<Context>,
) -> HintedStrResult<Location> {
pub fn here(context: Tracked<Context>) -> HintedStrResult<Location> {
context.location()
}

View File

@ -24,9 +24,7 @@ use crate::introspection::Location;
/// ```
#[func(contextual)]
pub fn locate(
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// A selector that should match exactly one element. This element will be
/// located.

View File

@ -136,9 +136,7 @@ use crate::foundations::{func, Array, Context, LocatableSelector, Value};
/// ```
#[func(contextual)]
pub fn query(
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// Can be
/// - an element function like a `heading` or `figure`,

View File

@ -289,11 +289,8 @@ impl State {
#[func(contextual)]
pub fn get(
&self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// The callsite span.
span: Span,
) -> SourceResult<Value> {
let loc = context.location().at(span)?;
@ -309,11 +306,8 @@ impl State {
#[func(contextual)]
pub fn at(
&self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// The callsite span.
span: Span,
/// The place at which the state's value should be retrieved.
selector: LocatableSelector,
@ -326,11 +320,8 @@ impl State {
#[func(contextual)]
pub fn final_(
&self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// The callsite span.
span: Span,
) -> SourceResult<Value> {
context.introspect().at(span)?;
@ -349,7 +340,6 @@ impl State {
#[func]
pub fn update(
self,
/// The span of the `update` call.
span: Span,
/// If given a non function-value, sets the state to that value. If
/// given a function, that function receives the previous state and has

View File

@ -14,9 +14,9 @@ use crate::visualize::{Paint, Stroke};
/// An inline-level container that sizes content.
///
/// All elements except inline math, text, and boxes are block-level and cannot
/// occur inside of a paragraph. The box function can be used to integrate such
/// elements into a paragraph. Boxes take the size of their contents by default
/// but can also be sized explicitly.
/// occur inside of a [paragraph]($par). The box function can be used to
/// integrate such elements into a paragraph. Boxes take the size of their
/// contents by default but can also be sized explicitly.
///
/// # Example
/// ```example
@ -184,6 +184,10 @@ pub enum InlineItem {
/// Such a container can be used to separate content, size it, and give it a
/// background or border.
///
/// Blocks are also the primary way to control whether text becomes part of a
/// paragraph or not. See [the paragraph documentation]($par/#what-becomes-a-paragraph)
/// for more details.
///
/// # Examples
/// With a block, you can give a background to content while still allowing it
/// to break across multiple pages.

View File

@ -4,16 +4,13 @@ use std::fmt::{self, Debug, Formatter};
use std::num::NonZeroUsize;
use std::sync::Arc;
use smallvec::SmallVec;
use typst_syntax::Span;
use typst_utils::{LazyHash, Numeric};
use crate::foundations::{cast, dict, Dict, Label, StyleChain, Value};
use crate::foundations::{cast, dict, Dict, Label, Value};
use crate::introspection::{Location, Tag};
use crate::layout::{
Abs, Axes, FixedAlignment, HideElem, Length, Point, Size, Transform,
};
use crate::model::{Destination, LinkElem};
use crate::layout::{Abs, Axes, FixedAlignment, Length, Point, Size, Transform};
use crate::model::Destination;
use crate::text::TextItem;
use crate::visualize::{Color, Curve, FixedStroke, Geometry, Image, Paint, Shape};
@ -304,49 +301,6 @@ impl Frame {
}
}
/// Apply late-stage properties from the style chain to this frame. This
/// includes:
/// - `HideElem::hidden`
/// - `LinkElem::dests`
///
/// This must be called on all frames produced by elements
/// that manually handle styles (because their children can have varying
/// styles). This currently includes flow, par, and equation.
///
/// Other elements 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 (because they don't manage
/// styles).
pub fn post_processed(mut self, styles: StyleChain) -> Self {
self.post_process(styles);
self
}
/// Post process in place.
pub fn post_process(&mut self, styles: StyleChain) {
if !self.is_empty() {
self.post_process_raw(
LinkElem::dests_in(styles),
HideElem::hidden_in(styles),
);
}
}
/// Apply raw late-stage properties from the raw data.
pub fn post_process_raw(&mut self, dests: SmallVec<[Destination; 1]>, hide: bool) {
if !self.is_empty() {
let size = self.size;
self.push_multiple(
dests
.into_iter()
.map(|dest| (Point::zero(), FrameItem::Link(dest, size))),
);
if hide {
self.hide();
}
}
}
/// Hide all content in the frame, but keep metadata.
pub fn hide(&mut self) {
Arc::make_mut(&mut self.items).retain_mut(|(_, item)| match item {

View File

@ -602,7 +602,7 @@ pub enum Entry<'a> {
impl<'a> Entry<'a> {
/// Obtains the cell inside this entry, if this is not a merged cell.
fn as_cell(&self) -> Option<&Cell<'a>> {
pub fn as_cell(&self) -> Option<&Cell<'a>> {
match self {
Self::Cell(cell) => Some(cell),
Self::Merged { .. } => None,

View File

@ -54,7 +54,6 @@ use crate::layout::{BlockElem, Size};
/// corresponding page dimension is set to `{auto}`.
#[func]
pub fn layout(
/// The call span of this function.
span: Span,
/// A function to call with the outer container's size. Its return value is
/// displayed in the document.

View File

@ -43,11 +43,8 @@ use crate::layout::{Abs, Axes, Length, Region, Size};
/// `height`, both of type [`length`].
#[func(contextual)]
pub fn measure(
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// The callsite span.
span: Span,
/// The width available to layout the content.
///

View File

@ -10,7 +10,7 @@ use crate::layout::{BlockElem, Length};
/// Space may be inserted between the instances of the body parameter, so be
/// sure to adjust the [`justify`]($repeat.justify) parameter accordingly.
///
/// Errors if there no bounds on the available space, as it would create
/// Errors if there are no bounds on the available space, as it would create
/// infinite content.
///
/// # Example

View File

@ -244,7 +244,7 @@ fn global(math: Module, inputs: Dict, features: &Features) -> Module {
self::model::define(&mut global);
self::text::define(&mut global);
global.reset_category();
global.define_module(math);
global.define("math", math);
self::layout::define(&mut global);
self::visualize::define(&mut global);
self::introspection::define(&mut global);
@ -253,7 +253,7 @@ fn global(math: Module, inputs: Dict, features: &Features) -> Module {
self::pdf::define(&mut global);
global.reset_category();
if features.is_enabled(Feature::Html) {
global.define_module(self::html::module());
global.define("html", self::html::module());
}
prelude(&mut global);
Module::new("global", global)

View File

@ -19,7 +19,6 @@ use crate::loading::{DataSource, Load};
/// floating point numbers, which may result in an approximative value.
#[func(scope, title = "CBOR")]
pub fn cbor(
/// The engine.
engine: &mut Engine,
/// A path to a CBOR file or raw CBOR bytes.
///
@ -40,7 +39,6 @@ impl cbor {
/// directly.
#[func(title = "Decode CBOR")]
pub fn decode(
/// The engine.
engine: &mut Engine,
/// CBOR data.
data: Spanned<Bytes>,

View File

@ -25,7 +25,6 @@ use crate::loading::{DataSource, Load, Readable};
/// ```
#[func(scope, title = "CSV")]
pub fn csv(
/// The engine.
engine: &mut Engine,
/// Path to a CSV file or raw CSV bytes.
///
@ -102,7 +101,6 @@ impl csv {
/// directly.
#[func(title = "Decode CSV")]
pub fn decode(
/// The engine.
engine: &mut Engine,
/// CSV data.
data: Spanned<Readable>,
@ -138,18 +136,10 @@ impl Default for Delimiter {
cast! {
Delimiter,
self => self.0.into_value(),
v: EcoString => {
let mut chars = v.chars();
let first = chars.next().ok_or("delimiter must not be empty")?;
if chars.next().is_some() {
bail!("delimiter must be a single character");
}
if !first.is_ascii() {
bail!("delimiter must be an ASCII character");
}
Self(first)
c: char => if c.is_ascii() {
Self(c)
} else {
bail!("delimiter must be an ASCII character")
},
}

View File

@ -50,7 +50,6 @@ use crate::loading::{DataSource, Load, Readable};
/// ```
#[func(scope, title = "JSON")]
pub fn json(
/// The engine.
engine: &mut Engine,
/// Path to a JSON file or raw JSON bytes.
///
@ -71,7 +70,6 @@ impl json {
/// directly.
#[func(title = "Decode JSON")]
pub fn decode(
/// The engine.
engine: &mut Engine,
/// JSON data.
data: Spanned<Readable>,

View File

@ -24,7 +24,6 @@ use crate::World;
/// ```
#[func]
pub fn read(
/// The engine.
engine: &mut Engine,
/// Path to a file.
///

View File

@ -28,7 +28,6 @@ use crate::loading::{DataSource, Load, Readable};
/// ```
#[func(scope, title = "TOML")]
pub fn toml(
/// The engine.
engine: &mut Engine,
/// A path to a TOML file or raw TOML bytes.
///
@ -50,7 +49,6 @@ impl toml {
/// directly.
#[func(title = "Decode TOML")]
pub fn decode(
/// The engine.
engine: &mut Engine,
/// TOML data.
data: Spanned<Readable>,

View File

@ -57,7 +57,6 @@ use crate::loading::{DataSource, Load, Readable};
/// ```
#[func(scope, title = "XML")]
pub fn xml(
/// The engine.
engine: &mut Engine,
/// A path to an XML file or raw XML bytes.
///
@ -83,7 +82,6 @@ impl xml {
/// directly.
#[func(title = "Decode XML")]
pub fn decode(
/// The engine.
engine: &mut Engine,
/// XML data.
data: Spanned<Readable>,

View File

@ -40,7 +40,6 @@ use crate::loading::{DataSource, Load, Readable};
/// ```
#[func(scope, title = "YAML")]
pub fn yaml(
/// The engine.
engine: &mut Engine,
/// A path to a YAML file or raw YAML bytes.
///
@ -61,7 +60,6 @@ impl yaml {
/// directly.
#[func(title = "Decode YAML")]
pub fn decode(
/// The engine.
engine: &mut Engine,
/// YAML data.
data: Spanned<Readable>,

View File

@ -1,8 +1,7 @@
use crate::diag::bail;
use crate::foundations::{cast, elem, func, Content, NativeElement, Value};
use crate::foundations::{cast, elem, func, Content, NativeElement, SymbolElem};
use crate::layout::{Length, Rel};
use crate::math::Mathy;
use crate::text::TextElem;
/// Attaches an accent to a base.
///
@ -142,8 +141,8 @@ cast! {
Accent,
self => self.0.into_value(),
v: char => Self::new(v),
v: Content => match v.to_packed::<TextElem>() {
Some(elem) => Value::Str(elem.text.clone().into()).cast()?,
None => bail!("expected text"),
v: Content => match v.to_packed::<SymbolElem>() {
Some(elem) => Self::new(elem.text),
None => bail!("expected a symbol"),
},
}

View File

@ -20,7 +20,9 @@ use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem};
/// A mathematical equation.
///
/// Can be displayed inline with text or as a separate block.
/// Can be displayed inline with text or as a separate block. An equation
/// becomes block-level through the presence of at least one space after the
/// opening dollar sign and one space before the closing dollar sign.
///
/// # Example
/// ```example
@ -229,35 +231,20 @@ impl Refable for Packed<EquationElem> {
}
impl Outlinable for Packed<EquationElem> {
fn outline(
&self,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<Option<Content>> {
if !self.block(StyleChain::default()) {
return Ok(None);
}
let Some(numbering) = self.numbering() else {
return Ok(None);
};
// After synthesis, this should always be custom content.
let mut supplement = match (**self).supplement(StyleChain::default()) {
Smart::Custom(Some(Supplement::Content(content))) => content,
_ => Content::empty(),
};
fn outlined(&self) -> bool {
self.block(StyleChain::default()) && self.numbering().is_some()
}
fn prefix(&self, numbers: Content) -> Content {
let supplement = self.supplement();
if !supplement.is_empty() {
supplement += TextElem::packed("\u{a0}");
supplement + TextElem::packed('\u{a0}') + numbers
} else {
numbers
}
}
let numbers = self.counter().display_at_loc(
engine,
self.location().unwrap(),
styles,
numbering,
)?;
Ok(Some(supplement + numbers))
fn body(&self) -> Content {
Content::empty()
}
}

View File

@ -1,7 +1,6 @@
use crate::foundations::{elem, func, Content, NativeElement};
use crate::foundations::{elem, func, Content, NativeElement, SymbolElem};
use crate::layout::{Length, Rel};
use crate::math::Mathy;
use crate::text::TextElem;
/// Scales delimiters.
///
@ -19,7 +18,7 @@ pub struct LrElem {
#[parse(
let mut arguments = args.all::<Content>()?.into_iter();
let mut body = arguments.next().unwrap_or_default();
arguments.for_each(|arg| body += TextElem::packed(',') + arg);
arguments.for_each(|arg| body += SymbolElem::packed(',') + arg);
body
)]
pub body: Content,
@ -125,9 +124,9 @@ fn delimited(
) -> Content {
let span = body.span();
let mut elem = LrElem::new(Content::sequence([
TextElem::packed(left),
SymbolElem::packed(left),
body,
TextElem::packed(right),
SymbolElem::packed(right),
]));
// Push size only if size is provided
if let Some(size) = size {

View File

@ -1,6 +1,6 @@
use smallvec::{smallvec, SmallVec};
use typst_syntax::Spanned;
use typst_utils::Numeric;
use typst_utils::{default_math_class, Numeric};
use unicode_math_class::MathClass;
use crate::diag::{bail, At, HintedStrResult, StrResult};
@ -292,7 +292,7 @@ impl Delimiter {
pub fn char(c: char) -> StrResult<Self> {
if !matches!(
unicode_math_class::class(c),
default_math_class(c),
Some(MathClass::Opening | MathClass::Closing | MathClass::Fence),
) {
bail!("invalid delimiter: \"{}\"", c)
@ -311,7 +311,7 @@ impl Delimiter {
Some(']') => Self(Some('[')),
Some('{') => Self(Some('}')),
Some('}') => Self(Some('{')),
Some(c) => match unicode_math_class::class(c) {
Some(c) => match default_math_class(c) {
Some(MathClass::Opening) => Self(char::from_u32(c as u32 + 1)),
Some(MathClass::Closing) => Self(char::from_u32(c as u32 - 1)),
_ => Self(Some(c)),

View File

@ -1,6 +1,6 @@
use ecow::EcoString;
use crate::foundations::{elem, Content, NativeElement, Scope};
use crate::foundations::{elem, Content, NativeElement, Scope, SymbolElem};
use crate::layout::HElem;
use crate::math::{upright, Mathy, THIN};
use crate::text::TextElem;
@ -17,9 +17,9 @@ use crate::text::TextElem;
/// # Predefined Operators { #predefined }
/// Typst predefines the operators `arccos`, `arcsin`, `arctan`, `arg`, `cos`,
/// `cosh`, `cot`, `coth`, `csc`, `csch`, `ctg`, `deg`, `det`, `dim`, `exp`,
/// `gcd`, `hom`, `id`, `im`, `inf`, `ker`, `lg`, `lim`, `liminf`, `limsup`,
/// `ln`, `log`, `max`, `min`, `mod`, `Pr`, `sec`, `sech`, `sin`, `sinc`,
/// `sinh`, `sup`, `tan`, `tanh`, `tg` and `tr`.
/// `gcd`, `lcm`, `hom`, `id`, `im`, `inf`, `ker`, `lg`, `lim`, `liminf`,
/// `limsup`, `ln`, `log`, `max`, `min`, `mod`, `Pr`, `sec`, `sech`, `sin`,
/// `sinc`, `sinh`, `sup`, `tan`, `tanh`, `tg` and `tr`.
#[elem(title = "Text Operator", Mathy)]
pub struct OpElem {
/// The operator's text.
@ -38,6 +38,7 @@ macro_rules! ops {
let operator = EcoString::from(ops!(@name $name $(: $value)?));
math.define(
stringify!($name),
// Latex also uses their equivalent of `TextElem` here.
OpElem::new(TextElem::new(operator).into())
.with_limits(ops!(@limit $($tts)*))
.pack()
@ -46,7 +47,7 @@ macro_rules! ops {
let dif = |d| {
HElem::new(THIN.into()).with_weak(true).pack()
+ upright(TextElem::packed(d))
+ upright(SymbolElem::packed(d))
};
math.define("dif", dif('d'));
math.define("Dif", dif('D'));
@ -75,6 +76,7 @@ ops! {
dim,
exp,
gcd (limits),
lcm (limits),
hom,
id,
im,

View File

@ -10,7 +10,6 @@ use crate::math::Mathy;
/// ```
#[func(title = "Square Root")]
pub fn sqrt(
/// The call span of this function.
span: Span,
/// The expression to take the square root of.
radicand: Content,

View File

@ -17,7 +17,7 @@ use hayagriva::{
use indexmap::IndexMap;
use smallvec::{smallvec, SmallVec};
use typst_syntax::{Span, Spanned};
use typst_utils::{ManuallyHash, NonZeroExt, PicoStr};
use typst_utils::{Get, ManuallyHash, NonZeroExt, PicoStr};
use crate::diag::{bail, error, At, FileError, HintedStrResult, SourceResult, StrResult};
use crate::engine::Engine;
@ -29,7 +29,7 @@ use crate::foundations::{
use crate::introspection::{Introspector, Locatable, Location};
use crate::layout::{
BlockBody, BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem,
Sizing, TrackSizings, VElem,
Sides, Sizing, TrackSizings,
};
use crate::loading::{DataSource, Load};
use crate::model::{
@ -38,7 +38,8 @@ use crate::model::{
};
use crate::routines::{EvalMode, Routines};
use crate::text::{
FontStyle, Lang, LocalName, Region, SubElem, SuperElem, TextElem, WeightDelta,
FontStyle, Lang, LocalName, Region, Smallcaps, SubElem, SuperElem, TextElem,
WeightDelta,
};
use crate::World;
@ -205,19 +206,20 @@ impl Show for Packed<BibliographyElem> {
const COLUMN_GUTTER: Em = Em::new(0.65);
const INDENT: Em = Em::new(1.5);
let span = self.span();
let mut seq = vec![];
if let Some(title) = self.title(styles).unwrap_or_else(|| {
Some(TextElem::packed(Self::local_name_in(styles)).spanned(self.span()))
Some(TextElem::packed(Self::local_name_in(styles)).spanned(span))
}) {
seq.push(
HeadingElem::new(title)
.with_depth(NonZeroUsize::ONE)
.pack()
.spanned(self.span()),
.spanned(span),
);
}
let span = self.span();
let works = Works::generate(engine).at(span)?;
let references = works
.references
@ -225,10 +227,9 @@ impl Show for Packed<BibliographyElem> {
.ok_or("CSL style is not suitable for bibliographies")
.at(span)?;
let row_gutter = ParElem::spacing_in(styles);
let row_gutter_elem = VElem::new(row_gutter.into()).with_weak(true).pack();
if references.iter().any(|(prefix, _)| prefix.is_some()) {
let row_gutter = ParElem::spacing_in(styles);
let mut cells = vec![];
for (prefix, reference) in references {
cells.push(GridChild::Item(GridItem::Cell(
@ -245,23 +246,27 @@ impl Show for Packed<BibliographyElem> {
.with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()]))
.with_row_gutter(TrackSizings(smallvec![row_gutter.into()]))
.pack()
.spanned(self.span()),
.spanned(span),
);
} else {
for (i, (_, reference)) in references.iter().enumerate() {
if i > 0 {
seq.push(row_gutter_elem.clone());
}
seq.push(reference.clone());
for (_, reference) in references {
let realized = reference.clone();
let block = if works.hanging_indent {
let body = HElem::new((-INDENT).into()).pack() + realized;
let inset = Sides::default()
.with(TextElem::dir_in(styles).start(), Some(INDENT.into()));
BlockElem::new()
.with_body(Some(BlockBody::Content(body)))
.with_inset(inset)
} else {
BlockElem::new().with_body(Some(BlockBody::Content(realized)))
};
seq.push(block.pack().spanned(span));
}
}
let mut content = Content::sequence(seq);
if works.hanging_indent {
content = content.styled(ParElem::set_hanging_indent(INDENT.into()));
}
Ok(content)
Ok(Content::sequence(seq))
}
}
@ -1046,7 +1051,8 @@ fn apply_formatting(mut content: Content, format: &hayagriva::Formatting) -> Con
match format.font_variant {
citationberg::FontVariant::Normal => {}
citationberg::FontVariant::SmallCaps => {
content = content.styled(TextElem::set_smallcaps(true));
content =
content.styled(TextElem::set_smallcaps(Some(Smallcaps::Minuscules)));
}
}

View File

@ -9,9 +9,11 @@ use crate::foundations::{
cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain,
Styles, TargetElem,
};
use crate::html::{attr, tag, HtmlAttr, HtmlElem};
use crate::html::{attr, tag, HtmlElem};
use crate::layout::{Alignment, BlockElem, Em, HAlignment, Length, VAlignment, VElem};
use crate::model::{ListItemLike, ListLike, Numbering, NumberingPattern, ParElem};
use crate::model::{
ListItemLike, ListLike, Numbering, NumberingPattern, ParElem, ParbreakElem,
};
/// A numbered list.
///
@ -226,22 +228,29 @@ impl EnumElem {
impl Show for Packed<EnumElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let tight = self.tight(styles);
if TargetElem::target_in(styles).is_html() {
let mut elem = HtmlElem::new(tag::ol);
if self.reversed(styles) {
elem =
elem.with_attr(const { HtmlAttr::constant("reversed") }, "reversed");
elem = elem.with_attr(attr::reversed, "reversed");
}
return Ok(elem
.with_body(Some(Content::sequence(self.children.iter().map(|item| {
let mut li = HtmlElem::new(tag::li);
if let Some(nr) = item.number(styles) {
li = li.with_attr(attr::value, eco_format!("{nr}"));
}
li.with_body(Some(item.body.clone())).pack().spanned(item.span())
}))))
.pack()
.spanned(self.span()));
if let Some(n) = self.start(styles).custom() {
elem = elem.with_attr(attr::start, eco_format!("{n}"));
}
let body = Content::sequence(self.children.iter().map(|item| {
let mut li = HtmlElem::new(tag::li);
if let Some(nr) = item.number(styles) {
li = li.with_attr(attr::value, eco_format!("{nr}"));
}
// Text in wide enums shall always turn into paragraphs.
let mut body = item.body.clone();
if !tight {
body += ParbreakElem::shared();
}
li.with_body(Some(body)).pack().spanned(item.span())
}));
return Ok(elem.with_body(Some(body)).pack().spanned(self.span()));
}
let mut realized =
@ -249,7 +258,7 @@ impl Show for Packed<EnumElem> {
.pack()
.spanned(self.span());
if self.tight(styles) {
if tight {
let leading = ParElem::leading_in(styles);
let spacing =
VElem::new(leading.into()).with_weak(true).with_attach(true).pack();

View File

@ -19,7 +19,9 @@ use crate::layout::{
AlignElem, Alignment, BlockBody, BlockElem, Em, HAlignment, Length, OuterVAlignment,
PlaceElem, PlacementScope, VAlignment, VElem,
};
use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement};
use crate::model::{
Numbering, NumberingPattern, Outlinable, ParbreakElem, Refable, Supplement,
};
use crate::text::{Lang, Region, TextElem};
use crate::visualize::ImageElem;
@ -156,6 +158,7 @@ pub struct FigureElem {
pub scope: PlacementScope,
/// The figure's caption.
#[borrowed]
pub caption: Option<Packed<FigureCaption>>,
/// The kind of figure this is.
@ -305,7 +308,7 @@ impl Synthesize for Packed<FigureElem> {
));
// Fill the figure's caption.
let mut caption = elem.caption(styles);
let mut caption = elem.caption(styles).clone();
if let Some(caption) = &mut caption {
caption.synthesize(engine, styles)?;
caption.push_kind(kind.clone());
@ -327,11 +330,12 @@ impl Synthesize for Packed<FigureElem> {
impl Show for Packed<FigureElem> {
#[typst_macros::time(name = "figure", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let span = self.span();
let target = TargetElem::target_in(styles);
let mut realized = self.body.clone();
// Build the caption, if any.
if let Some(caption) = self.caption(styles) {
if let Some(caption) = self.caption(styles).clone() {
let (first, second) = match caption.position(styles) {
OuterVAlignment::Top => (caption.pack(), realized),
OuterVAlignment::Bottom => (realized, caption.pack()),
@ -340,24 +344,27 @@ impl Show for Packed<FigureElem> {
seq.push(first);
if !target.is_html() {
let v = VElem::new(self.gap(styles).into()).with_weak(true);
seq.push(v.pack().spanned(self.span()))
seq.push(v.pack().spanned(span))
}
seq.push(second);
realized = Content::sequence(seq)
}
// Ensure that the body is considered a paragraph.
realized += ParbreakElem::shared().clone().spanned(span);
if target.is_html() {
return Ok(HtmlElem::new(tag::figure)
.with_body(Some(realized))
.pack()
.spanned(self.span()));
.spanned(span));
}
// Wrap the contents in a block.
realized = BlockElem::new()
.with_body(Some(BlockBody::Content(realized)))
.pack()
.spanned(self.span());
.spanned(span);
// Wrap in a float.
if let Some(align) = self.placement(styles) {
@ -366,10 +373,10 @@ impl Show for Packed<FigureElem> {
.with_scope(self.scope(styles))
.with_float(true)
.pack()
.spanned(self.span());
.spanned(span);
} else if self.scope(styles) == PlacementScope::Parent {
bail!(
self.span(),
span,
"parent-scoped placement is only available for floating figures";
hint: "you can enable floating placement with `figure(placement: auto, ..)`"
);
@ -423,46 +430,26 @@ impl Refable for Packed<FigureElem> {
}
impl Outlinable for Packed<FigureElem> {
fn outline(
&self,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<Option<Content>> {
if !self.outlined(StyleChain::default()) {
return Ok(None);
fn outlined(&self) -> bool {
(**self).outlined(StyleChain::default())
&& (self.caption(StyleChain::default()).is_some()
|| self.numbering().is_some())
}
fn prefix(&self, numbers: Content) -> Content {
let supplement = self.supplement();
if !supplement.is_empty() {
supplement + TextElem::packed('\u{a0}') + numbers
} else {
numbers
}
}
let Some(caption) = self.caption(StyleChain::default()) else {
return Ok(None);
};
let mut realized = caption.body.clone();
if let (
Smart::Custom(Some(Supplement::Content(mut supplement))),
Some(Some(counter)),
Some(numbering),
) = (
(**self).supplement(StyleChain::default()).clone(),
(**self).counter(),
self.numbering(),
) {
let numbers = counter.display_at_loc(
engine,
self.location().unwrap(),
styles,
numbering,
)?;
if !supplement.is_empty() {
supplement += TextElem::packed('\u{a0}');
}
let separator = caption.get_separator(StyleChain::default());
realized = supplement + numbers + separator + caption.body.clone();
}
Ok(Some(realized))
fn body(&self) -> Content {
self.caption(StyleChain::default())
.as_ref()
.map(|caption| caption.body.clone())
.unwrap_or_default()
}
}
@ -623,14 +610,17 @@ impl Show for Packed<FigureCaption> {
realized = supplement + numbers + self.get_separator(styles) + realized;
}
if TargetElem::target_in(styles).is_html() {
return Ok(HtmlElem::new(tag::figcaption)
Ok(if TargetElem::target_in(styles).is_html() {
HtmlElem::new(tag::figcaption)
.with_body(Some(realized))
.pack()
.spanned(self.span()));
}
Ok(realized)
.spanned(self.span())
} else {
BlockElem::new()
.with_body(Some(BlockBody::Content(realized)))
.pack()
.spanned(self.span())
})
}
}

View File

@ -310,11 +310,9 @@ impl Show for Packed<FootnoteEntry> {
impl ShowSet for Packed<FootnoteEntry> {
fn show_set(&self, _: StyleChain) -> Styles {
let text_size = Em::new(0.85);
let leading = Em::new(0.5);
let mut out = Styles::new();
out.set(ParElem::set_leading(leading.into()));
out.set(TextElem::set_size(TextSize(text_size.into())));
out.set(ParElem::set_leading(Em::new(0.5).into()));
out.set(TextElem::set_size(TextSize(Em::new(0.85).into())));
out
}
}

View File

@ -1,7 +1,7 @@
use std::num::NonZeroUsize;
use ecow::eco_format;
use typst_utils::NonZeroExt;
use typst_utils::{Get, NonZeroExt};
use crate::diag::{warning, SourceResult};
use crate::engine::Engine;
@ -13,8 +13,8 @@ use crate::html::{attr, tag, HtmlElem};
use crate::introspection::{
Count, Counter, CounterUpdate, Locatable, Locator, LocatorLink,
};
use crate::layout::{Abs, Axes, BlockBody, BlockElem, Em, HElem, Length, Region};
use crate::model::{Numbering, Outlinable, ParElem, Refable, Supplement};
use crate::layout::{Abs, Axes, BlockBody, BlockElem, Em, HElem, Length, Region, Sides};
use crate::model::{Numbering, Outlinable, Refable, Supplement};
use crate::text::{FontWeight, LocalName, SpaceElem, TextElem, TextSize};
/// A section heading.
@ -264,10 +264,6 @@ impl Show for Packed<HeadingElem> {
realized = numbering + spacing + realized;
}
if indent != Abs::zero() && !html {
realized = realized.styled(ParElem::set_hanging_indent(indent.into()));
}
Ok(if html {
// HTML's h1 is closer to a title element. There should only be one.
// Meanwhile, a level 1 Typst heading is a section heading. For this
@ -294,8 +290,17 @@ impl Show for Packed<HeadingElem> {
HtmlElem::new(t).with_body(Some(realized)).pack().spanned(span)
}
} else {
let realized = BlockBody::Content(realized);
BlockElem::new().with_body(Some(realized)).pack().spanned(span)
let block = if indent != Abs::zero() {
let body = HElem::new((-indent).into()).pack() + realized;
let inset = Sides::default()
.with(TextElem::dir_in(styles).start(), Some(indent.into()));
BlockElem::new()
.with_body(Some(BlockBody::Content(body)))
.with_inset(inset)
} else {
BlockElem::new().with_body(Some(BlockBody::Content(realized)))
};
block.pack().spanned(span)
})
}
}
@ -351,32 +356,21 @@ impl Refable for Packed<HeadingElem> {
}
impl Outlinable for Packed<HeadingElem> {
fn outline(
&self,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<Option<Content>> {
if !self.outlined(StyleChain::default()) {
return Ok(None);
}
let mut content = self.body.clone();
if let Some(numbering) = (**self).numbering(StyleChain::default()).as_ref() {
let numbers = Counter::of(HeadingElem::elem()).display_at_loc(
engine,
self.location().unwrap(),
styles,
numbering,
)?;
content = numbers + SpaceElem::shared().clone() + content;
};
Ok(Some(content))
fn outlined(&self) -> bool {
(**self).outlined(StyleChain::default())
}
fn level(&self) -> NonZeroUsize {
(**self).resolve_level(StyleChain::default())
}
fn prefix(&self, numbers: Content) -> Content {
numbers
}
fn body(&self) -> Content {
self.body.clone()
}
}
impl LocalName for Packed<HeadingElem> {

View File

@ -1,13 +1,12 @@
use std::ops::Deref;
use ecow::{eco_format, EcoString};
use smallvec::SmallVec;
use crate::diag::{bail, warning, At, SourceResult, StrResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, Content, Label, NativeElement, Packed, Repr, Show, Smart, StyleChain,
TargetElem,
cast, elem, Content, Label, NativeElement, Packed, Repr, Show, ShowSet, Smart,
StyleChain, Styles, TargetElem,
};
use crate::html::{attr, tag, HtmlElem};
use crate::introspection::Location;
@ -16,7 +15,7 @@ use crate::text::{Hyphenate, TextElem};
/// Links to a URL or a location in the document.
///
/// By default, links are not styled any different from normal text. However,
/// By default, links do not look any different from normal text. However,
/// you can easily apply a style of your choice with a show rule.
///
/// # Example
@ -31,6 +30,11 @@ use crate::text::{Hyphenate, TextElem};
/// ]
/// ```
///
/// # Hyphenation
/// If you enable hyphenation or justification, by default, it will not apply to
/// links to prevent unwanted hyphenation in URLs. You can opt out of this
/// default via `{show link: set text(hyphenate: true)}`.
///
/// # Syntax
/// This function also has dedicated syntax: Text that starts with `http://` or
/// `https://` is automatically turned into a link.
@ -85,10 +89,10 @@ pub struct LinkElem {
})]
pub body: Content,
/// This style is set on the content contained in the `link` element.
/// A destination style that should be applied to elements.
#[internal]
#[ghost]
pub dests: SmallVec<[Destination; 1]>,
pub current: Option<Destination>,
}
impl LinkElem {
@ -119,20 +123,26 @@ impl Show for Packed<LinkElem> {
body
}
} else {
let linked = match &self.dest {
match &self.dest {
LinkTarget::Dest(dest) => body.linked(dest.clone()),
LinkTarget::Label(label) => {
let elem = engine.introspector.query_label(*label).at(self.span())?;
let dest = Destination::Location(elem.location().unwrap());
body.clone().linked(dest)
}
};
linked.styled(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false))))
}
})
}
}
impl ShowSet for Packed<LinkElem> {
fn show_set(&self, _: StyleChain) -> Styles {
let mut out = Styles::new();
out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false))));
out
}
}
fn body_from_url(url: &Url) -> Content {
let text = ["mailto:", "tel:"]
.into_iter()

View File

@ -8,7 +8,7 @@ use crate::foundations::{
};
use crate::html::{tag, HtmlElem};
use crate::layout::{BlockElem, Em, Length, VElem};
use crate::model::ParElem;
use crate::model::{ParElem, ParbreakElem};
use crate::text::TextElem;
/// A bullet list.
@ -141,11 +141,18 @@ impl ListElem {
impl Show for Packed<ListElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let tight = self.tight(styles);
if TargetElem::target_in(styles).is_html() {
return Ok(HtmlElem::new(tag::ul)
.with_body(Some(Content::sequence(self.children.iter().map(|item| {
// Text in wide lists shall always turn into paragraphs.
let mut body = item.body.clone();
if !tight {
body += ParbreakElem::shared();
}
HtmlElem::new(tag::li)
.with_body(Some(item.body.clone()))
.with_body(Some(body))
.pack()
.spanned(item.span())
}))))
@ -158,7 +165,7 @@ impl Show for Packed<ListElem> {
.pack()
.spanned(self.span());
if self.tight(styles) {
if tight {
let leading = ParElem::leading_in(styles);
let spacing =
VElem::new(leading.into()).with_weak(true).with_attach(true).pack();

View File

@ -53,9 +53,7 @@ use crate::text::Case;
/// ```
#[func]
pub fn numbering(
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// Defines how the numbering works.
///

View File

@ -1,50 +1,61 @@
use std::num::NonZeroUsize;
use std::str::FromStr;
use comemo::Track;
use comemo::{Track, Tracked};
use smallvec::SmallVec;
use typst_syntax::Span;
use typst_utils::NonZeroExt;
use typst_utils::{Get, NonZeroExt};
use crate::diag::{bail, At, SourceResult};
use crate::diag::{bail, error, At, HintedStrResult, SourceResult, StrResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, scope, select_where, Content, Context, Func, LocatableSelector,
NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles,
cast, elem, func, scope, select_where, Args, Construct, Content, Context, Func,
LocatableSelector, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain,
Styles,
};
use crate::introspection::{
Counter, CounterKey, Introspector, Locatable, Location, Locator, LocatorLink,
};
use crate::introspection::{Counter, CounterKey, Locatable};
use crate::layout::{
BoxElem, Dir, Em, Fr, HElem, HideElem, Length, Rel, RepeatElem, Spacing,
Abs, Axes, BlockBody, BlockElem, BoxElem, Dir, Em, Fr, HElem, Length, Region, Rel,
RepeatElem, Sides,
};
use crate::model::{
Destination, HeadingElem, NumberingPattern, ParElem, ParbreakElem, Refable,
};
use crate::text::{LinebreakElem, LocalName, SpaceElem, TextElem};
use crate::math::EquationElem;
use crate::model::{Destination, HeadingElem, NumberingPattern, ParElem, Refable};
use crate::text::{LocalName, SpaceElem, TextElem};
/// A table of contents, figures, or other elements.
///
/// This function generates a list of all occurrences of an element in the
/// document, up to a given depth. The element's numbering and page number will
/// be displayed in the outline alongside its title or caption. By default this
/// generates a table of contents.
/// document, up to a given [`depth`]($outline.depth). The element's numbering
/// and page number will be displayed in the outline alongside its title or
/// caption.
///
/// # Example
/// ```example
/// #set heading(numbering: "1.")
/// #outline()
///
/// = Introduction
/// #lorem(5)
///
/// = Prior work
/// = Methods
/// == Setup
/// #lorem(10)
/// ```
///
/// # Alternative outlines
/// In its default configuration, this function generates a table of contents.
/// By setting the `target` parameter, the outline can be used to generate a
/// list of other kinds of elements than headings. In the example below, we list
/// all figures containing images by setting `target` to `{figure.where(kind:
/// image)}`. We could have also set it to just `figure`, but then the list
/// would also include figures containing tables or other material. For more
/// details on the `where` selector, [see here]($function.where).
/// list of other kinds of elements than headings.
///
/// In the example below, we list all figures containing images by setting
/// `target` to `{figure.where(kind: image)}`. Just the same, we could have set
/// it to `{figure.where(kind: table)}` to generate a list of tables.
///
/// We could also set it to just `figure`, without using a [`where`]($function.where)
/// selector, but then the list would contain _all_ figures, be it ones
/// containing images, tables, or other material.
///
/// ```example
/// #outline(
@ -59,16 +70,89 @@ use crate::text::{LinebreakElem, LocalName, SpaceElem, TextElem};
/// ```
///
/// # Styling the outline
/// The outline element has several options for customization, such as its
/// `title` and `indent` parameters. If desired, however, it is possible to have
/// more control over the outline's look and style through the
/// [`outline.entry`]($outline.entry) element.
#[elem(scope, keywords = ["Table of Contents"], Show, ShowSet, LocalName)]
/// At the most basic level, you can style the outline by setting properties on
/// it and its entries. This way, you can customize the outline's
/// [title]($outline.title), how outline entries are
/// [indented]($outline.indent), and how the space between an entry's text and
/// its page number should be [filled]($outline.entry.fill).
///
/// Richer customization is possible through configuration of the outline's
/// [entries]($outline.entry). The outline generates one entry for each outlined
/// element.
///
/// ## Spacing the entries { #entry-spacing }
/// Outline entries are [blocks]($block), so you can adjust the spacing between
/// them with normal block-spacing rules:
///
/// ```example
/// #show outline.entry.where(
/// level: 1
/// ): set block(above: 1.2em)
///
/// #outline()
///
/// = About ACME Corp.
/// == History
/// === Origins
/// = Products
/// == ACME Tools
/// ```
///
/// ## Building an outline entry from its parts { #building-an-entry }
/// For full control, you can also write a transformational show rule on
/// `outline.entry`. However, the logic for properly formatting and indenting
/// outline entries is quite complex and the outline entry itself only contains
/// two fields: The level and the outlined element.
///
/// For this reason, various helper functions are provided. You can mix and
/// match these to compose an entry from just the parts you like.
///
/// The default show rule for an outline entry looks like this[^1]:
/// ```typ
/// #show outline.entry: it => link(
/// it.element.location(),
/// it.indented(it.prefix(), it.inner()),
/// )
/// ```
///
/// - The [`indented`]($outline.entry.indented) function takes an optional
/// prefix and inner content and automatically applies the proper indentation
/// to it, such that different entries align nicely and long headings wrap
/// properly.
///
/// - The [`prefix`]($outline.entry.prefix) function formats the element's
/// numbering (if any). It also appends a supplement for certain elements.
///
/// - The [`inner`]($outline.entry.inner) function combines the element's
/// [`body`]($outline.entry.body), the filler, and the
/// [`page` number]($outline.entry.page).
///
/// You can use these individual functions to format the outline entry in
/// different ways. Let's say, you'd like to fully remove the filler and page
/// numbers. To achieve this, you could write a show rule like this:
///
/// ```example
/// #show outline.entry: it => link(
/// it.element.location(),
/// // Keep just the body, dropping
/// // the fill and the page.
/// it.indented(it.prefix(), it.body()),
/// )
///
/// #outline()
///
/// = About ACME Corp.
/// == History
/// ```
///
/// [^1]: The outline of equations is the exception to this rule as it does not
/// have a body and thus does not use indented layout.
#[elem(scope, keywords = ["Table of Contents", "toc"], Show, ShowSet, LocalName, Locatable)]
pub struct OutlineElem {
/// The title of the outline.
///
/// - When set to `{auto}`, an appropriate title for the
/// [text language]($text.lang) will be used. This is the default.
/// [text language]($text.lang) will be used.
/// - When set to `{none}`, the outline will not have a title.
/// - A custom title can be set by passing content.
///
@ -79,8 +163,10 @@ pub struct OutlineElem {
/// The type of element to include in the outline.
///
/// To list figures containing a specific kind of element, like a table, you
/// can write `{figure.where(kind: table)}`.
/// To list figures containing a specific kind of element, like an image or
/// a table, you can specify the desired kind in a [`where`]($function.where)
/// selector. See the section on [alternative outlines]($outline/#alternative-outlines)
/// for more details.
///
/// ```example
/// #outline(
@ -97,7 +183,7 @@ pub struct OutlineElem {
/// caption: [Experiment results],
/// )
/// ```
#[default(LocatableSelector(select_where!(HeadingElem, Outlined => true)))]
#[default(LocatableSelector(HeadingElem::elem().select()))]
#[borrowed]
pub target: LocatableSelector,
@ -121,21 +207,22 @@ pub struct OutlineElem {
/// How to indent the outline's entries.
///
/// - `{none}`: No indent
/// - `{auto}`: Indents the numbering of the nested entry with the title of
/// its parent entry. This only has an effect if the entries are numbered
/// (e.g., via [heading numbering]($heading.numbering)).
/// - [Relative length]($relative): Indents the item by this length
/// multiplied by its nesting level. Specifying `{2em}`, for instance,
/// would indent top-level headings (not nested) by `{0em}`, second level
/// - `{auto}`: Indents the numbering/prefix of a nested entry with the
/// title of its parent entry. If the entries are not numbered (e.g., via
/// [heading numbering]($heading.numbering)), this instead simply inserts
/// a fixed amount of `{1.2em}` indent per level.
///
/// - [Relative length]($relative): Indents the entry by the specified
/// length per nesting level. Specifying `{2em}`, for instance, would
/// indent top-level headings by `{0em}` (not nested), second level
/// headings by `{2em}` (nested once), third-level headings by `{4em}`
/// (nested twice) and so on.
/// - [Function]($function): You can completely customize this setting with
/// a function. That function receives the nesting level as a parameter
/// (starting at 0 for top-level headings/elements) and can return a
/// relative length or content making up the indent. For example,
/// `{n => n * 2em}` would be equivalent to just specifying `{2em}`, while
/// `{n => [→ ] * n}` would indent with one arrow per nesting level.
///
/// - [Function]($function): You can further customize this setting with a
/// function. That function receives the nesting level as a parameter
/// (starting at 0 for top-level headings/elements) and should return a
/// (relative) length. For example, `{n => n * 2em}` would be equivalent
/// to just specifying `{2em}`.
///
/// ```example
/// #set heading(numbering: "1.a.")
@ -150,11 +237,6 @@ pub struct OutlineElem {
/// indent: 2em,
/// )
///
/// #outline(
/// title: [Contents (Function)],
/// indent: n => [→ ] * n,
/// )
///
/// = About ACME Corp.
/// == History
/// === Origins
@ -163,20 +245,7 @@ pub struct OutlineElem {
/// == Products
/// #lorem(10)
/// ```
#[default(None)]
#[borrowed]
pub indent: Option<Smart<OutlineIndent>>,
/// Content to fill the space between the title and the page number. Can be
/// set to `{none}` to disable filling.
///
/// ```example
/// #outline(fill: line(length: 100%))
///
/// = A New Beginning
/// ```
#[default(Some(RepeatElem::new(TextElem::packed(".")).pack()))]
pub fill: Option<Content>,
pub indent: Smart<OutlineIndent>,
}
#[scope]
@ -188,79 +257,51 @@ impl OutlineElem {
impl Show for Packed<OutlineElem> {
#[typst_macros::time(name = "outline", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let mut seq = vec![ParbreakElem::shared().clone()];
let span = self.span();
// Build the outline title.
let mut seq = vec![];
if let Some(title) = self.title(styles).unwrap_or_else(|| {
Some(TextElem::packed(Self::local_name_in(styles)).spanned(self.span()))
Some(TextElem::packed(Self::local_name_in(styles)).spanned(span))
}) {
seq.push(
HeadingElem::new(title)
.with_depth(NonZeroUsize::ONE)
.pack()
.spanned(self.span()),
.spanned(span),
);
}
let indent = self.indent(styles);
let depth = self.depth(styles).unwrap_or(NonZeroUsize::new(usize::MAX).unwrap());
let mut ancestors: Vec<&Content> = vec![];
let elems = engine.introspector.query(&self.target(styles).0);
let depth = self.depth(styles).unwrap_or(NonZeroUsize::MAX);
for elem in &elems {
let Some(entry) = OutlineEntry::from_outlinable(
engine,
self.span(),
elem.clone(),
self.fill(styles),
styles,
)?
else {
continue;
// Build the outline entries.
for elem in elems {
let Some(outlinable) = elem.with::<dyn Outlinable>() else {
bail!(span, "cannot outline {}", elem.func().name());
};
if depth < entry.level {
continue;
let level = outlinable.level();
if outlinable.outlined() && level <= depth {
let entry = OutlineEntry::new(level, elem);
seq.push(entry.pack().spanned(span));
}
// Deals with the ancestors of the current element.
// This is only applicable for elements with a hierarchy/level.
while ancestors
.last()
.and_then(|ancestor| ancestor.with::<dyn Outlinable>())
.is_some_and(|last| last.level() >= entry.level)
{
ancestors.pop();
}
OutlineIndent::apply(
indent,
engine,
&ancestors,
&mut seq,
styles,
self.span(),
)?;
// Add the overridable outline entry, followed by a line break.
seq.push(entry.pack().spanned(self.span()));
seq.push(LinebreakElem::shared().clone());
ancestors.push(elem);
}
seq.push(ParbreakElem::shared().clone());
Ok(Content::sequence(seq))
}
}
impl ShowSet for Packed<OutlineElem> {
fn show_set(&self, _: StyleChain) -> Styles {
fn show_set(&self, styles: StyleChain) -> Styles {
let mut out = Styles::new();
out.set(HeadingElem::set_outlined(false));
out.set(HeadingElem::set_numbering(None));
out.set(ParElem::set_first_line_indent(Em::new(0.0).into()));
out.set(ParElem::set_justify(false));
out.set(BlockElem::set_above(Smart::Custom(ParElem::leading_in(styles).into())));
// Makes the outline itself available to its entries. Should be
// superseded by a proper ancestry mechanism in the future.
out.set(OutlineEntry::set_parent(Some(self.clone())));
out
}
}
@ -269,93 +310,29 @@ impl LocalName for Packed<OutlineElem> {
const KEY: &'static str = "outline";
}
/// Marks an element as being able to be outlined. This is used to implement the
/// `#outline()` element.
pub trait Outlinable: Refable {
/// Produce an outline item for this element.
fn outline(
&self,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<Option<Content>>;
/// Returns the nesting level of this element.
fn level(&self) -> NonZeroUsize {
NonZeroUsize::ONE
}
}
/// Defines how an outline is indented.
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum OutlineIndent {
Rel(Rel<Length>),
/// Indents by the specified length per level.
Rel(Rel),
/// Resolve the indent for a specific level through the given function.
Func(Func),
}
impl OutlineIndent {
fn apply(
indent: &Option<Smart<Self>>,
/// Resolve the indent for an entry with the given level.
fn resolve(
&self,
engine: &mut Engine,
ancestors: &Vec<&Content>,
seq: &mut Vec<Content>,
styles: StyleChain,
context: Tracked<Context>,
level: NonZeroUsize,
span: Span,
) -> SourceResult<()> {
match indent {
// 'none' | 'false' => no indenting
None => {}
// 'auto' | 'true' => use numbering alignment for indenting
Some(Smart::Auto) => {
// Add hidden ancestors numberings to realize the indent.
let mut hidden = Content::empty();
for ancestor in ancestors {
let ancestor_outlinable = ancestor.with::<dyn Outlinable>().unwrap();
if let Some(numbering) = ancestor_outlinable.numbering() {
let numbers = ancestor_outlinable.counter().display_at_loc(
engine,
ancestor.location().unwrap(),
styles,
numbering,
)?;
hidden += numbers + SpaceElem::shared().clone();
};
}
if !ancestors.is_empty() {
seq.push(HideElem::new(hidden).pack().spanned(span));
seq.push(SpaceElem::shared().clone().spanned(span));
}
}
// Length => indent with some fixed spacing per level
Some(Smart::Custom(OutlineIndent::Rel(length))) => {
seq.push(
HElem::new(Spacing::Rel(*length))
.pack()
.spanned(span)
.repeat(ancestors.len()),
);
}
// Function => call function with the current depth and take
// the returned content
Some(Smart::Custom(OutlineIndent::Func(func))) => {
let depth = ancestors.len();
let LengthOrContent(content) = func
.call(engine, Context::new(None, Some(styles)).track(), [depth])?
.cast()
.at(span)?;
if !content.is_empty() {
seq.push(content);
}
}
};
Ok(())
) -> SourceResult<Rel> {
let depth = level.get() - 1;
match self {
Self::Rel(length) => Ok(*length * depth as f64),
Self::Func(func) => func.call(engine, context, [depth])?.cast().at(span),
}
}
}
@ -365,46 +342,33 @@ cast! {
Self::Rel(v) => v.into_value(),
Self::Func(v) => v.into_value()
},
v: Rel<Length> => OutlineIndent::Rel(v),
v: Func => OutlineIndent::Func(v),
v: Rel<Length> => Self::Rel(v),
v: Func => Self::Func(v),
}
struct LengthOrContent(Content);
/// Marks an element as being able to be outlined.
pub trait Outlinable: Refable {
/// Whether this element should be included in the outline.
fn outlined(&self) -> bool;
cast! {
LengthOrContent,
v: Rel<Length> => Self(HElem::new(Spacing::Rel(v)).pack()),
v: Content => Self(v),
/// The nesting level of this element.
fn level(&self) -> NonZeroUsize {
NonZeroUsize::ONE
}
/// Constructs the default prefix given the formatted numbering.
fn prefix(&self, numbers: Content) -> Content;
/// The body of the entry.
fn body(&self) -> Content;
}
/// Represents each entry line in an outline, including the reference to the
/// outlined element, its page number, and the filler content between both.
/// Represents an entry line in an outline.
///
/// This element is intended for use with show rules to control the appearance
/// of outlines. To customize an entry's line, you can build it from scratch by
/// accessing the `level`, `element`, `body`, `fill` and `page` fields on the
/// entry.
///
/// ```example
/// #set heading(numbering: "1.")
///
/// #show outline.entry.where(
/// level: 1
/// ): it => {
/// v(12pt, weak: true)
/// strong(it)
/// }
///
/// #outline(indent: auto)
///
/// = Introduction
/// = Background
/// == History
/// == State of the Art
/// = Analysis
/// == Setup
/// ```
#[elem(name = "entry", title = "Outline Entry", Show)]
/// With show-set and show rules on outline entries, you can richly customize
/// the outline's appearance. See the
/// [section on styling the outline]($outline/#styling-the-outline) for details.
#[elem(scope, name = "entry", title = "Outline Entry", Show)]
pub struct OutlineEntry {
/// The nesting level of this outline entry. Starts at `{1}` for top-level
/// entries.
@ -412,90 +376,206 @@ pub struct OutlineEntry {
pub level: NonZeroUsize,
/// The element this entry refers to. Its location will be available
/// through the [`location`]($content.location) method on content
/// through the [`location`]($content.location) method on the content
/// and can be [linked]($link) to.
#[required]
pub element: Content,
/// The content which is displayed in place of the referred element at its
/// entry in the outline. For a heading, this would be its number followed
/// by the heading's title, for example.
#[required]
pub body: Content,
/// The content used to fill the space between the element's outline and
/// its page number, as defined by the outline element this entry is
/// located in. When `{none}`, empty space is inserted in that gap instead.
/// Content to fill the space between the title and the page number. Can be
/// set to `{none}` to disable filling.
///
/// Note that, when using show rules to override outline entries, it is
/// recommended to wrap the filling content in a [`box`] with fractional
/// width. For example, `{box(width: 1fr, repeat[-])}` would show precisely
/// as many `-` characters as necessary to fill a particular gap.
#[required]
/// The `fill` will be placed into a fractionally sized box that spans the
/// space between the entry's body and the page number. When using show
/// rules to override outline entries, it is thus recommended to wrap the
/// fill in a [`box`] with fractional width, i.e.
/// `{box(width: 1fr, it.fill}`.
///
/// When using [`repeat`], the [`gap`]($repeat.gap) property can be useful
/// to tweak the visual weight of the fill.
///
/// ```example
/// #set outline.entry(fill: line(length: 100%))
/// #outline()
///
/// = A New Beginning
/// ```
#[borrowed]
#[default(Some(
RepeatElem::new(TextElem::packed("."))
.with_gap(Em::new(0.15).into())
.pack()
))]
pub fill: Option<Content>,
/// The page number of the element this entry links to, formatted with the
/// numbering set for the referenced page.
#[required]
pub page: Content,
}
impl OutlineEntry {
/// Generates an OutlineEntry from the given element, if possible (errors if
/// the element does not implement `Outlinable`). If the element should not
/// be outlined (e.g. heading with 'outlined: false'), does not generate an
/// entry instance (returns `Ok(None)`).
fn from_outlinable(
engine: &mut Engine,
span: Span,
elem: Content,
fill: Option<Content>,
styles: StyleChain,
) -> SourceResult<Option<Self>> {
let Some(outlinable) = elem.with::<dyn Outlinable>() else {
bail!(span, "cannot outline {}", elem.func().name());
};
let Some(body) = outlinable.outline(engine, styles)? else {
return Ok(None);
};
let location = elem.location().unwrap();
let page_numbering = engine
.introspector
.page_numbering(location)
.cloned()
.unwrap_or_else(|| NumberingPattern::from_str("1").unwrap().into());
let page = Counter::new(CounterKey::Page).display_at_loc(
engine,
location,
styles,
&page_numbering,
)?;
Ok(Some(Self::new(outlinable.level(), elem, body, fill, page)))
}
/// Lets outline entries access the outline they are part of. This is a bit
/// of a hack and should be superseded by a proper ancestry mechanism.
#[ghost]
#[internal]
pub parent: Option<Packed<OutlineElem>>,
}
impl Show for Packed<OutlineEntry> {
#[typst_macros::time(name = "outline.entry", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let mut seq = vec![];
let elem = &self.element;
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let span = self.span();
let context = Context::new(None, Some(styles));
let context = context.track();
// In case a user constructs an outline entry with an arbitrary element.
let Some(location) = elem.location() else {
if elem.can::<dyn Locatable>() && elem.can::<dyn Outlinable>() {
bail!(
self.span(), "{} must have a location", elem.func().name();
hint: "try using a query or a show rule to customize the outline.entry instead",
)
} else {
bail!(self.span(), "cannot outline {}", elem.func().name())
let prefix = self.prefix(engine, context, span)?;
let inner = self.inner(engine, context, span)?;
let block = if self.element.is::<EquationElem>() {
let body = prefix.unwrap_or_default() + inner;
BlockElem::new()
.with_body(Some(BlockBody::Content(body)))
.pack()
.spanned(span)
} else {
self.indented(engine, context, span, prefix, inner, Em::new(0.5).into())?
};
let loc = self.element_location().at(span)?;
Ok(block.linked(Destination::Location(loc)))
}
}
#[scope]
impl OutlineEntry {
/// A helper function for producing an indented entry layout: Lays out a
/// prefix and the rest of the entry in an indent-aware way.
///
/// If the parent outline's [`indent`]($outline.indent) is `{auto}`, the
/// inner content of all entries at level `N` is aligned with the prefix of
/// all entries at level `N + 1`, leaving at least `gap` space between the
/// prefix and inner parts. Furthermore, the `inner` contents of all entries
/// at the same level are aligned.
///
/// If the outline's indent is a fixed value or a function, the prefixes are
/// indented, but the inner contents are simply inset from the prefix by the
/// specified `gap`, rather than aligning outline-wide.
#[func(contextual)]
pub fn indented(
&self,
engine: &mut Engine,
context: Tracked<Context>,
span: Span,
/// The `prefix` is aligned with the `inner` content of entries that
/// have level one less.
///
/// In the default show rule, this is just `it.prefix()`, but it can be
/// freely customized.
prefix: Option<Content>,
/// The formatted inner content of the entry.
///
/// In the default show rule, this is just `it.inner()`, but it can be
/// freely customized.
inner: Content,
/// The gap between the prefix and the inner content.
#[named]
#[default(Em::new(0.5).into())]
gap: Length,
) -> SourceResult<Content> {
let styles = context.styles().at(span)?;
let outline = Self::parent_in(styles)
.ok_or("must be called within the context of an outline")
.at(span)?;
let outline_loc = outline.location().unwrap();
let prefix_width = prefix
.as_ref()
.map(|prefix| measure_prefix(engine, prefix, outline_loc, styles))
.transpose()?;
let prefix_inset = prefix_width.map(|w| w + gap.resolve(styles));
let indent = outline.indent(styles);
let (base_indent, hanging_indent) = match &indent {
Smart::Auto => compute_auto_indents(
engine.introspector,
outline_loc,
styles,
self.level,
prefix_inset,
),
Smart::Custom(amount) => {
let base = amount.resolve(engine, context, self.level, span)?;
(base, prefix_inset)
}
};
let body = if let (
Some(prefix),
Some(prefix_width),
Some(prefix_inset),
Some(hanging_indent),
) = (prefix, prefix_width, prefix_inset, hanging_indent)
{
// Save information about our prefix that other outline entries
// can query for (within `compute_auto_indent`) to align
// themselves).
let mut seq = Vec::with_capacity(5);
if indent.is_auto() {
seq.push(PrefixInfo::new(outline_loc, self.level, prefix_inset).pack());
}
// Dedent the prefix by the amount of hanging indent and then skip
// ahead so that the inner contents are aligned.
seq.extend([
HElem::new((-hanging_indent).into()).pack(),
prefix,
HElem::new((hanging_indent - prefix_width).into()).pack(),
inner,
]);
Content::sequence(seq)
} else {
inner
};
let inset = Sides::default().with(
TextElem::dir_in(styles).start(),
Some(base_indent + Rel::from(hanging_indent.unwrap_or_default())),
);
Ok(BlockElem::new()
.with_inset(inset)
.with_body(Some(BlockBody::Content(body)))
.pack()
.spanned(span))
}
/// Formats the element's numbering (if any).
///
/// This also appends the element's supplement in case of figures or
/// equations. For instance, it would output `1.1` for a heading, but
/// `Figure 1` for a figure, as is usual for outlines.
#[func(contextual)]
pub fn prefix(
&self,
engine: &mut Engine,
context: Tracked<Context>,
span: Span,
) -> SourceResult<Option<Content>> {
let outlinable = self.outlinable().at(span)?;
let Some(numbering) = outlinable.numbering() else { return Ok(None) };
let loc = self.element_location().at(span)?;
let styles = context.styles().at(span)?;
let numbers =
outlinable.counter().display_at_loc(engine, loc, styles, numbering)?;
Ok(Some(outlinable.prefix(numbers)))
}
/// Creates the default inner content of the entry.
///
/// This includes the body, the fill, and page number.
#[func(contextual)]
pub fn inner(
&self,
engine: &mut Engine,
context: Tracked<Context>,
span: Span,
) -> SourceResult<Content> {
let styles = context.styles().at(span)?;
let mut seq = vec![];
// Isolate the entry body in RTL because the page number is typically
// LTR. I'm not sure whether LTR should conceptually also be isolated,
// but in any case we don't do it for now because the text shaping
@ -511,32 +591,174 @@ impl Show for Packed<OutlineEntry> {
seq.push(TextElem::packed("\u{202B}"));
}
seq.push(self.body.clone().linked(Destination::Location(location)));
seq.push(self.body().at(span)?);
if rtl {
// "Pop Directional Formatting"
seq.push(TextElem::packed("\u{202C}"));
}
// Add filler symbols between the section name and page number.
if let Some(filler) = &self.fill {
// Add the filler between the section name and page number.
if let Some(filler) = self.fill(styles) {
seq.push(SpaceElem::shared().clone());
seq.push(
BoxElem::new()
.with_body(Some(filler.clone()))
.with_width(Fr::one().into())
.pack()
.spanned(self.span()),
.spanned(span),
);
seq.push(SpaceElem::shared().clone());
} else {
seq.push(HElem::new(Fr::one().into()).pack().spanned(self.span()));
seq.push(HElem::new(Fr::one().into()).pack().spanned(span));
}
// Add the page number.
let page = self.page.clone().linked(Destination::Location(location));
seq.push(page);
// Add the page number. The word joiner in front ensures that the page
// number doesn't stand alone in its line.
seq.push(TextElem::packed("\u{2060}"));
seq.push(self.page(engine, context, span)?);
Ok(Content::sequence(seq))
}
/// The content which is displayed in place of the referred element at its
/// entry in the outline. For a heading, this is its
/// [`body`]($heading.body), for a figure a caption, and for equations it is
/// empty.
#[func]
pub fn body(&self) -> StrResult<Content> {
Ok(self.outlinable()?.body())
}
/// The page number of this entry's element, formatted with the numbering
/// set for the referenced page.
#[func(contextual)]
pub fn page(
&self,
engine: &mut Engine,
context: Tracked<Context>,
span: Span,
) -> SourceResult<Content> {
let loc = self.element_location().at(span)?;
let styles = context.styles().at(span)?;
let numbering = engine
.introspector
.page_numbering(loc)
.cloned()
.unwrap_or_else(|| NumberingPattern::from_str("1").unwrap().into());
Counter::new(CounterKey::Page).display_at_loc(engine, loc, styles, &numbering)
}
}
impl OutlineEntry {
fn outlinable(&self) -> StrResult<&dyn Outlinable> {
self.element
.with::<dyn Outlinable>()
.ok_or_else(|| error!("cannot outline {}", self.element.func().name()))
}
fn element_location(&self) -> HintedStrResult<Location> {
let elem = &self.element;
elem.location().ok_or_else(|| {
if elem.can::<dyn Locatable>() && elem.can::<dyn Outlinable>() {
error!(
"{} must have a location", elem.func().name();
hint: "try using a show rule to customize the outline.entry instead",
)
} else {
error!("cannot outline {}", elem.func().name())
}
})
}
}
cast! {
OutlineEntry,
v: Content => v.unpack::<Self>().map_err(|_| "expected outline entry")?
}
/// Measures the width of a prefix.
fn measure_prefix(
engine: &mut Engine,
prefix: &Content,
loc: Location,
styles: StyleChain,
) -> SourceResult<Abs> {
let pod = Region::new(Axes::splat(Abs::inf()), Axes::splat(false));
let link = LocatorLink::measure(loc);
Ok((engine.routines.layout_frame)(engine, prefix, Locator::link(&link), styles, pod)?
.width())
}
/// Compute the base indent and hanging indent for an auto-indented outline
/// entry of the given level, with the given prefix inset.
fn compute_auto_indents(
introspector: Tracked<Introspector>,
outline_loc: Location,
styles: StyleChain,
level: NonZeroUsize,
prefix_inset: Option<Abs>,
) -> (Rel, Option<Abs>) {
let indents = query_prefix_widths(introspector, outline_loc);
let fallback = Em::new(1.2).resolve(styles);
let get = |i: usize| indents.get(i).copied().flatten().unwrap_or(fallback);
let last = level.get() - 1;
let base: Abs = (0..last).map(get).sum();
let hang = prefix_inset.map(|p| p.max(get(last)));
(base.into(), hang)
}
/// Determines the maximum prefix inset (prefix width + gap) at each outline
/// level, for the outline with the given `loc`. Levels for which there is no
/// information available yield `None`.
#[comemo::memoize]
fn query_prefix_widths(
introspector: Tracked<Introspector>,
outline_loc: Location,
) -> SmallVec<[Option<Abs>; 4]> {
let mut widths = SmallVec::<[Option<Abs>; 4]>::new();
let elems = introspector.query(&select_where!(PrefixInfo, Key => outline_loc));
for elem in &elems {
let info = elem.to_packed::<PrefixInfo>().unwrap();
let level = info.level.get();
if widths.len() < level {
widths.resize(level, None);
}
widths[level - 1].get_or_insert(info.inset).set_max(info.inset);
}
widths
}
/// Helper type for introspection-based prefix alignment.
#[elem(Construct, Locatable, Show)]
struct PrefixInfo {
/// The location of the outline this prefix is part of. This is used to
/// scope prefix computations to a specific outline.
#[required]
key: Location,
/// The level of this prefix's entry.
#[required]
#[internal]
level: NonZeroUsize,
/// The width of the prefix, including the gap.
#[required]
#[internal]
inset: Abs,
}
impl Construct for PrefixInfo {
fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
bail!(args.span, "cannot be constructed manually");
}
}
impl Show for Packed<PrefixInfo> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(Content::empty())
}
}

View File

@ -1,22 +1,78 @@
use std::fmt::{self, Debug, Formatter};
use typst_utils::singleton;
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
elem, scope, Args, Cast, Construct, Content, NativeElement, Packed, Set, Smart,
StyleVec, Unlabellable,
cast, dict, elem, scope, Args, Cast, Construct, Content, Dict, NativeElement, Packed,
Smart, Unlabellable, Value,
};
use crate::introspection::{Count, CounterUpdate, Locatable};
use crate::layout::{Em, HAlignment, Length, OuterHAlignment};
use crate::model::Numbering;
/// Arranges text, spacing and inline-level elements into a paragraph.
/// A logical subdivison of textual content.
///
/// Although this function is primarily used in set rules to affect paragraph
/// properties, it can also be used to explicitly render its argument onto a
/// paragraph of its own.
/// Typst automatically collects _inline-level_ elements into paragraphs.
/// Inline-level elements include [text], [horizontal spacing]($h),
/// [boxes]($box), and [inline equations]($math.equation).
///
/// To separate paragraphs, use a blank line (or an explicit [`parbreak`]).
/// Paragraphs are also automatically interrupted by any block-level element
/// (like [`block`], [`place`], or anything that shows itself as one of these).
///
/// The `par` element is primarily used in set rules to affect paragraph
/// properties, but it can also be used to explicitly display its argument as a
/// paragraph of its own. Then, the paragraph's body may not contain any
/// block-level content.
///
/// # Boxes and blocks
/// As explained above, usually paragraphs only contain inline-level content.
/// However, you can integrate any kind of block-level content into a paragraph
/// by wrapping it in a [`box`].
///
/// Conversely, you can separate inline-level content from a paragraph by
/// wrapping it in a [`block`]. In this case, it will not become part of any
/// paragraph at all. Read the following section for an explanation of why that
/// matters and how it differs from just adding paragraph breaks around the
/// content.
///
/// # What becomes a paragraph?
/// When you add inline-level content to your document, Typst will automatically
/// wrap it in paragraphs. However, a typical document also contains some text
/// that is not semantically part of a paragraph, for example in a heading or
/// caption.
///
/// The rules for when Typst wraps inline-level content in a paragraph are as
/// follows:
///
/// - All text at the root of a document is wrapped in paragraphs.
///
/// - Text in a container (like a `block`) is only wrapped in a paragraph if the
/// container holds any block-level content. If all of the contents are
/// inline-level, no paragraph is created.
///
/// In the laid-out document, it's not immediately visible whether text became
/// part of a paragraph. However, it is still important for various reasons:
///
/// - Certain paragraph styling like `first-line-indent` will only apply to
/// proper paragraphs, not any text. Similarly, `par` show rules of course
/// only trigger on paragraphs.
///
/// - A proper distinction between paragraphs and other text helps people who
/// rely on assistive technologies (such as screen readers) navigate and
/// understand the document properly. Currently, this only applies to HTML
/// export since Typst does not yet output accessible PDFs, but support for
/// this is planned for the near future.
///
/// - HTML export will generate a `<p>` tag only for paragraphs.
///
/// When creating custom reusable components, you can and should take charge
/// over whether Typst creates paragraphs. By wrapping text in a [`block`]
/// instead of just adding paragraph breaks around it, you can force the absence
/// of a paragraph. Conversely, by adding a [`parbreak`] after some content in a
/// container, you can force it to become a paragraph even if it's just one
/// word. This is, for example, what [non-`tight`]($list.tight) lists do to
/// force their items to become paragraphs.
///
/// # Example
/// ```example
@ -37,7 +93,7 @@ use crate::model::Numbering;
/// let $a$ be the smallest of the
/// three integers. Then, we ...
/// ```
#[elem(scope, title = "Paragraph", Debug, Construct)]
#[elem(scope, title = "Paragraph")]
pub struct ParElem {
/// The spacing between lines.
///
@ -53,7 +109,6 @@ pub struct ParElem {
/// distribution of the top- and bottom-edge values affects the bounds of
/// the first and last line.
#[resolve]
#[ghost]
#[default(Em::new(0.65).into())]
pub leading: Length,
@ -68,7 +123,6 @@ pub struct ParElem {
/// takes precedence over the paragraph spacing. Headings, for instance,
/// reduce the spacing below them by default for a better look.
#[resolve]
#[ghost]
#[default(Em::new(1.2).into())]
pub spacing: Length,
@ -81,7 +135,6 @@ pub struct ParElem {
/// Note that the current [alignment]($align.alignment) still has an effect
/// on the placement of the last line except if it ends with a
/// [justified line break]($linebreak.justify).
#[ghost]
#[default(false)]
pub justify: bool,
@ -106,35 +159,66 @@ pub struct ParElem {
/// challenging to break in a visually
/// pleasing way.
/// ```
#[ghost]
pub linebreaks: Smart<Linebreaks>,
/// The indent the first line of a paragraph should have.
///
/// Only the first line of a consecutive paragraph will be indented (not
/// the first one in a block or on the page).
/// By default, only the first line of a consecutive paragraph will be
/// indented (not the first one in the document or container, and not
/// paragraphs immediately following other block-level elements).
///
/// If you want to indent all paragraphs instead, you can pass a dictionary
/// containing the `amount` of indent as a length and the pair
/// `{all: true}`. When `all` is omitted from the dictionary, it defaults to
/// `{false}`.
///
/// By typographic convention, paragraph breaks are indicated either by some
/// space between paragraphs or by indented first lines. Consider reducing
/// the [paragraph spacing]($block.spacing) to the [`leading`]($par.leading)
/// when using this property (e.g. using `[#set par(spacing: 0.65em)]`).
#[ghost]
pub first_line_indent: Length,
/// space between paragraphs or by indented first lines. Consider
/// - reducing the [paragraph `spacing`]($par.spacing) to the
/// [`leading`]($par.leading) using `{set par(spacing: 0.65em)}`
/// - increasing the [block `spacing`]($block.spacing) (which inherits the
/// paragraph spacing by default) to the original paragraph spacing using
/// `{set block(spacing: 1.2em)}`
///
/// ```example
/// #set block(spacing: 1.2em)
/// #set par(
/// first-line-indent: 1.5em,
/// spacing: 0.65em,
/// )
///
/// The first paragraph is not affected
/// by the indent.
///
/// But the second paragraph is.
///
/// #line(length: 100%)
///
/// #set par(first-line-indent: (
/// amount: 1.5em,
/// all: true,
/// ))
///
/// Now all paragraphs are affected
/// by the first line indent.
///
/// Even the first one.
/// ```
pub first_line_indent: FirstLineIndent,
/// The indent all but the first line of a paragraph should have.
#[ghost]
/// The indent that all but the first line of a paragraph should have.
///
/// ```example
/// #set par(hanging-indent: 1em)
///
/// #lorem(15)
/// ```
#[resolve]
pub hanging_indent: Length,
/// The contents of the paragraph.
#[external]
#[required]
pub body: Content,
/// The paragraph's children.
#[internal]
#[variadic]
pub children: StyleVec,
}
#[scope]
@ -143,28 +227,6 @@ impl ParElem {
type ParLine;
}
impl Construct for ParElem {
fn construct(engine: &mut Engine, args: &mut Args) -> SourceResult<Content> {
// The paragraph constructor is special: It doesn't create a paragraph
// element. Instead, it just ensures that the passed content lives in a
// separate paragraph and styles it.
let styles = Self::set(engine, args)?;
let body = args.expect::<Content>("body")?;
Ok(Content::sequence([
ParbreakElem::shared().clone(),
body.styled_with_map(styles),
ParbreakElem::shared().clone(),
]))
}
}
impl Debug for ParElem {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "Par ")?;
self.children.fmt(f)
}
}
/// How to determine line breaks in a paragraph.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum Linebreaks {
@ -177,6 +239,36 @@ pub enum Linebreaks {
Optimized,
}
/// Configuration for first line indent.
#[derive(Debug, Default, Copy, Clone, PartialEq, Hash)]
pub struct FirstLineIndent {
/// The amount of indent.
pub amount: Length,
/// Whether to indent all paragraphs, not just consecutive ones.
pub all: bool,
}
cast! {
FirstLineIndent,
self => Value::Dict(self.into()),
amount: Length => Self { amount, all: false },
mut dict: Dict => {
let amount = dict.take("amount")?.cast()?;
let all = dict.take("all").ok().map(|v| v.cast()).transpose()?.unwrap_or(false);
dict.finish(&["amount", "all"])?;
Self { amount, all }
},
}
impl From<FirstLineIndent> for Dict {
fn from(indent: FirstLineIndent) -> Self {
dict! {
"amount" => indent.amount,
"all" => indent.all,
}
}
}
/// A paragraph break.
///
/// This starts a new paragraph. Especially useful when used within code like

View File

@ -2,13 +2,14 @@ use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{
cast, elem, Content, Depth, Label, NativeElement, Packed, Show, ShowSet, Smart,
StyleChain, Styles,
StyleChain, Styles, TargetElem,
};
use crate::html::{attr, tag, HtmlElem};
use crate::introspection::Locatable;
use crate::layout::{
Alignment, BlockBody, BlockElem, Em, HElem, PadElem, Spacing, VElem,
};
use crate::model::{CitationForm, CiteElem};
use crate::model::{CitationForm, CiteElem, Destination, LinkElem, LinkTarget};
use crate::text::{SmartQuoteElem, SmartQuotes, SpaceElem, TextElem};
/// Displays a quote alongside an optional attribution.
@ -158,6 +159,7 @@ impl Show for Packed<QuoteElem> {
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let mut realized = self.body.clone();
let block = self.block(styles);
let html = TargetElem::target_in(styles).is_html();
if self.quotes(styles) == Smart::Custom(true) || !block {
let quotes = SmartQuotes::get(
@ -171,50 +173,69 @@ impl Show for Packed<QuoteElem> {
let Depth(depth) = QuoteElem::depth_in(styles);
let double = depth % 2 == 0;
// Add zero-width weak spacing to make the quotes "sticky".
let hole = HElem::hole().pack();
if !html {
// Add zero-width weak spacing to make the quotes "sticky".
let hole = HElem::hole().pack();
realized = Content::sequence([hole.clone(), realized, hole]);
}
realized = Content::sequence([
TextElem::packed(quotes.open(double)),
hole.clone(),
realized,
hole,
TextElem::packed(quotes.close(double)),
])
.styled(QuoteElem::set_depth(Depth(1)));
}
let attribution = self.attribution(styles);
if block {
realized = BlockElem::new()
.with_body(Some(BlockBody::Content(realized)))
.pack()
.spanned(self.span());
if let Some(attribution) = self.attribution(styles).as_ref() {
let mut seq = vec![TextElem::packed('—'), SpaceElem::shared().clone()];
match attribution {
Attribution::Content(content) => {
seq.push(content.clone());
}
Attribution::Label(label) => {
seq.push(
CiteElem::new(*label)
.with_form(Some(CitationForm::Prose))
.pack()
.spanned(self.span()),
);
realized = if html {
let mut elem = HtmlElem::new(tag::blockquote).with_body(Some(realized));
if let Some(Attribution::Content(attribution)) = attribution {
if let Some(link) = attribution.to_packed::<LinkElem>() {
if let LinkTarget::Dest(Destination::Url(url)) = &link.dest {
elem = elem.with_attr(attr::cite, url.clone().into_inner());
}
}
}
elem.pack()
} else {
BlockElem::new().with_body(Some(BlockBody::Content(realized))).pack()
}
.spanned(self.span());
// Use v(0.9em, weak: true) bring the attribution closer to the
// quote.
let gap = Spacing::Rel(Em::new(0.9).into());
let v = VElem::new(gap).with_weak(true).pack();
realized += v + Content::sequence(seq).aligned(Alignment::END);
if let Some(attribution) = attribution.as_ref() {
let attribution = match attribution {
Attribution::Content(content) => content.clone(),
Attribution::Label(label) => CiteElem::new(*label)
.with_form(Some(CitationForm::Prose))
.pack()
.spanned(self.span()),
};
let attribution = Content::sequence([
TextElem::packed('—'),
SpaceElem::shared().clone(),
attribution,
]);
if html {
realized += attribution;
} else {
// Bring the attribution a bit closer to the quote.
let gap = Spacing::Rel(Em::new(0.9).into());
let v = VElem::new(gap).with_weak(true).pack();
realized += v;
realized += BlockElem::new()
.with_body(Some(BlockBody::Content(attribution)))
.pack()
.aligned(Alignment::END);
}
}
realized = PadElem::new(realized).pack();
} else if let Some(Attribution::Label(label)) = self.attribution(styles) {
if !html {
realized = PadElem::new(realized).pack();
}
} else if let Some(Attribution::Label(label)) = attribution {
realized += SpaceElem::shared().clone()
+ CiteElem::new(*label).pack().spanned(self.span());
}

View File

@ -7,7 +7,11 @@ use crate::diag::{bail, HintedStrResult, HintedString, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, scope, Content, NativeElement, Packed, Show, Smart, StyleChain,
TargetElem,
};
use crate::html::{attr, tag, HtmlAttrs, HtmlElem, HtmlTag};
use crate::introspection::Locator;
use crate::layout::grid::resolve::{table_to_cellgrid, Cell, CellGrid, Entry};
use crate::layout::{
show_grid_cell, Abs, Alignment, BlockElem, Celled, GridCell, GridFooter, GridHLine,
GridHeader, GridVLine, Length, OuterHAlignment, OuterVAlignment, Rel, Sides,
@ -258,11 +262,65 @@ impl TableElem {
type TableFooter;
}
fn show_cell_html(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content {
let cell = cell.body.clone();
let Some(cell) = cell.to_packed::<TableCell>() else { return cell };
let mut attrs = HtmlAttrs::default();
let span = |n: NonZeroUsize| (n != NonZeroUsize::MIN).then(|| n.to_string());
if let Some(colspan) = span(cell.colspan(styles)) {
attrs.push(attr::colspan, colspan);
}
if let Some(rowspan) = span(cell.rowspan(styles)) {
attrs.push(attr::rowspan, rowspan);
}
HtmlElem::new(tag)
.with_body(Some(cell.body.clone()))
.with_attrs(attrs)
.pack()
.spanned(cell.span())
}
fn show_cellgrid_html(grid: CellGrid, styles: StyleChain) -> Content {
let elem = |tag, body| HtmlElem::new(tag).with_body(Some(body)).pack();
let mut rows: Vec<_> = grid.entries.chunks(grid.cols.len()).collect();
let tr = |tag, row: &[Entry]| {
let row = row
.iter()
.flat_map(|entry| entry.as_cell())
.map(|cell| show_cell_html(tag, cell, styles));
elem(tag::tr, Content::sequence(row))
};
let footer = grid.footer.map(|ft| {
let rows = rows.drain(ft.unwrap().start..);
elem(tag::tfoot, Content::sequence(rows.map(|row| tr(tag::td, row))))
});
let header = grid.header.map(|hd| {
let rows = rows.drain(..hd.unwrap().end);
elem(tag::thead, Content::sequence(rows.map(|row| tr(tag::th, row))))
});
let mut body = Content::sequence(rows.into_iter().map(|row| tr(tag::td, row)));
if header.is_some() || footer.is_some() {
body = elem(tag::tbody, body);
}
let content = header.into_iter().chain(core::iter::once(body)).chain(footer);
elem(tag::table, Content::sequence(content))
}
impl Show for Packed<TableElem> {
fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_table)
.pack()
.spanned(self.span()))
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(if TargetElem::target_in(styles).is_html() {
// TODO: This is a hack, it is not clear whether the locator is actually used by HTML.
// How can we find out whether locator is actually used?
let locator = Locator::root();
show_cellgrid_html(table_to_cellgrid(self, engine, locator, styles)?, styles)
} else {
BlockElem::multi_layouter(self.clone(), engine.routines.layout_table).pack()
}
.spanned(self.span()))
}
}

View File

@ -1,4 +1,4 @@
use typst_utils::Numeric;
use typst_utils::{Get, Numeric};
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
@ -7,8 +7,8 @@ use crate::foundations::{
Styles, TargetElem,
};
use crate::html::{tag, HtmlElem};
use crate::layout::{Dir, Em, HElem, Length, Sides, StackChild, StackElem, VElem};
use crate::model::{ListItemLike, ListLike, ParElem};
use crate::layout::{Em, HElem, Length, Sides, StackChild, StackElem, VElem};
use crate::model::{ListItemLike, ListLike, ParElem, ParbreakElem};
use crate::text::TextElem;
/// A list of terms and their descriptions.
@ -105,6 +105,11 @@ pub struct TermsElem {
/// ```
#[variadic]
pub children: Vec<Packed<TermItem>>,
/// Whether we are currently within a term list.
#[internal]
#[ghost]
pub within: bool,
}
#[scope]
@ -116,17 +121,25 @@ impl TermsElem {
impl Show for Packed<TermsElem> {
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let span = self.span();
let tight = self.tight(styles);
if TargetElem::target_in(styles).is_html() {
return Ok(HtmlElem::new(tag::dl)
.with_body(Some(Content::sequence(self.children.iter().flat_map(
|item| {
// Text in wide term lists shall always turn into paragraphs.
let mut description = item.description.clone();
if !tight {
description += ParbreakElem::shared();
}
[
HtmlElem::new(tag::dt)
.with_body(Some(item.term.clone()))
.pack()
.spanned(item.term.span()),
HtmlElem::new(tag::dd)
.with_body(Some(item.description.clone()))
.with_body(Some(description))
.pack()
.spanned(item.description.span()),
]
@ -139,7 +152,7 @@ impl Show for Packed<TermsElem> {
let indent = self.indent(styles);
let hanging_indent = self.hanging_indent(styles);
let gutter = self.spacing(styles).unwrap_or_else(|| {
if self.tight(styles) {
if tight {
ParElem::leading_in(styles).into()
} else {
ParElem::spacing_in(styles).into()
@ -157,23 +170,25 @@ impl Show for Packed<TermsElem> {
seq.push(child.term.clone().strong());
seq.push((*separator).clone());
seq.push(child.description.clone());
// Text in wide term lists shall always turn into paragraphs.
if !tight {
seq.push(ParbreakElem::shared().clone());
}
children.push(StackChild::Block(Content::sequence(seq)));
}
let mut padding = Sides::default();
if TextElem::dir_in(styles) == Dir::LTR {
padding.left = pad.into();
} else {
padding.right = pad.into();
}
let padding = Sides::default().with(TextElem::dir_in(styles).start(), pad.into());
let mut realized = StackElem::new(children)
.with_spacing(Some(gutter.into()))
.pack()
.spanned(span)
.padded(padding);
.padded(padding)
.styled(TermsElem::set_within(true));
if self.tight(styles) {
if tight {
let leading = ParElem::leading_in(styles);
let spacing = VElem::new(leading.into())
.with_weak(true)

View File

@ -13,7 +13,7 @@ pub static PDF: Category;
/// Hook up the `pdf` module.
pub(super) fn define(global: &mut Scope) {
global.category(PDF);
global.define_module(module());
global.define("pdf", module());
}
/// Hook up all `pdf` definitions.

View File

@ -10,8 +10,7 @@ use typst_utils::LazyHash;
use crate::diag::SourceResult;
use crate::engine::{Engine, Route, Sink, Traced};
use crate::foundations::{
Args, Cast, Closure, Content, Context, Func, Packed, Scope, StyleChain, StyleVec,
Styles, Value,
Args, Cast, Closure, Content, Context, Func, Packed, Scope, StyleChain, Styles, Value,
};
use crate::introspection::{Introspector, Locator, SplitLocator};
use crate::layout::{
@ -104,26 +103,6 @@ routines! {
region: Region,
) -> SourceResult<Frame>
/// Lays out inline content.
fn layout_inline(
engine: &mut Engine,
children: &StyleVec,
locator: Locator,
styles: StyleChain,
consecutive: bool,
region: Size,
expand: bool,
) -> SourceResult<Fragment>
/// Lays out a [`BoxElem`].
fn layout_box(
elem: &Packed<BoxElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
region: Size,
) -> SourceResult<Frame>
/// Lays out a [`ListElem`].
fn layout_list(
elem: &Packed<ListElem>,
@ -348,17 +327,62 @@ pub enum RealizationKind<'a> {
/// This the root realization for layout. Requires a mutable reference
/// to document metadata that will be filled from `set document` rules.
LayoutDocument(&'a mut DocumentInfo),
/// A nested realization in a container (e.g. a `block`).
LayoutFragment,
/// A nested realization in a container (e.g. a `block`). Requires a mutable
/// reference to an enum that will be set to `FragmentKind::Inline` if the
/// fragment's content was fully inline.
LayoutFragment(&'a mut FragmentKind),
/// A nested realization in a paragraph (i.e. a `par`)
LayoutPar,
/// This the root realization for HTML. Requires a mutable reference
/// to document metadata that will be filled from `set document` rules.
HtmlDocument(&'a mut DocumentInfo),
/// A nested realization in a container (e.g. a `block`).
HtmlFragment,
/// A nested realization in a container (e.g. a `block`). Requires a mutable
/// reference to an enum that will be set to `FragmentKind::Inline` if the
/// fragment's content was fully inline.
HtmlFragment(&'a mut FragmentKind),
/// A realization within math.
Math,
}
impl RealizationKind<'_> {
/// It this a realization for HTML export?
pub fn is_html(&self) -> bool {
matches!(self, Self::HtmlDocument(_) | Self::HtmlFragment(_))
}
/// It this a realization for a container?
pub fn is_fragment(&self) -> bool {
matches!(self, Self::LayoutFragment(_) | Self::HtmlFragment(_))
}
/// If this is a document-level realization, accesses the document info.
pub fn as_document_mut(&mut self) -> Option<&mut DocumentInfo> {
match self {
Self::LayoutDocument(info) | Self::HtmlDocument(info) => Some(*info),
_ => None,
}
}
/// If this is a container-level realization, accesses the fragment kind.
pub fn as_fragment_mut(&mut self) -> Option<&mut FragmentKind> {
match self {
Self::LayoutFragment(kind) | Self::HtmlFragment(kind) => Some(*kind),
_ => None,
}
}
}
/// The kind of fragment output that realization produced.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum FragmentKind {
/// The fragment's contents were fully inline, and as a result, the output
/// elements are too.
Inline,
/// The fragment contained non-inline content, so inline content was forced
/// into paragraphs, and as a result, the output elements are not inline.
Block,
}
/// Temporary storage arenas for lifetime extension during realization.
///
/// Must be kept live while the content returned from realization is processed.

View File

@ -10,7 +10,9 @@ use xmlwriter::XmlWriter;
use crate::foundations::Bytes;
use crate::layout::{Abs, Frame, FrameItem, Point, Size};
use crate::text::{Font, Glyph};
use crate::visualize::{FixedStroke, Geometry, Image, RasterFormat, VectorFormat};
use crate::visualize::{
ExchangeFormat, FixedStroke, Geometry, Image, RasterImage, SvgImage,
};
/// Whether this glyph should be rendered via simple outlining instead of via
/// `glyph_frame`.
@ -102,12 +104,8 @@ fn draw_raster_glyph(
upem: Abs,
raster_image: ttf_parser::RasterGlyphImage,
) -> Option<()> {
let image = Image::new(
Bytes::new(raster_image.data.to_vec()),
RasterFormat::Png.into(),
None,
)
.ok()?;
let data = Bytes::new(raster_image.data.to_vec());
let image = Image::plain(RasterImage::plain(data, ExchangeFormat::Png).ok()?);
// Apple Color emoji doesn't provide offset information (or at least
// not in a way ttf-parser understands), so we artificially shift their
@ -178,9 +176,8 @@ fn draw_colr_glyph(
ttf.paint_color_glyph(glyph_id, 0, RgbaColor::new(0, 0, 0, 255), &mut glyph_painter)?;
svg.end_element();
let data = svg.end_document().into_bytes();
let image = Image::new(Bytes::new(data), VectorFormat::Svg.into(), None).ok()?;
let data = Bytes::from_string(svg.end_document());
let image = Image::plain(SvgImage::new(data).ok()?);
let y_shift = Abs::pt(upem.to_pt() - y_max);
let position = Point::new(Abs::pt(x_min), y_shift);
@ -255,9 +252,8 @@ fn draw_svg_glyph(
ty = -top,
);
let image =
Image::new(Bytes::new(wrapper_svg.into_bytes()), VectorFormat::Svg.into(), None)
.ok()?;
let data = Bytes::from_string(wrapper_svg);
let image = Image::plain(SvgImage::new(data).ok()?);
let position = Point::new(Abs::pt(left), Abs::pt(top) + upem);
let size = Size::new(Abs::pt(width), Abs::pt(height));

View File

@ -228,6 +228,8 @@ static EXCEPTION_MAP: phf::Map<&'static str, Exception> = phf::phf_map! {
.style(FontStyle::Oblique),
"NewCMSans10-Regular" => Exception::new()
.family("New Computer Modern Sans"),
"NewCMSansMath-Regular" => Exception::new()
.family("New Computer Modern Sans Math"),
"NewCMUncial08-Bold" => Exception::new()
.family("New Computer Modern Uncial 08"),
"NewCMUncial08-Book" => Exception::new()

View File

@ -755,11 +755,10 @@ pub struct TextElem {
#[ghost]
pub case: Option<Case>,
/// Whether small capital glyphs should be used. ("smcp")
/// Whether small capital glyphs should be used. ("smcp", "c2sc")
#[internal]
#[default(false)]
#[ghost]
pub smallcaps: bool,
pub smallcaps: Option<Smallcaps>,
}
impl TextElem {
@ -1249,8 +1248,11 @@ pub fn features(styles: StyleChain) -> Vec<Feature> {
}
// Features that are off by default in Harfbuzz are only added if enabled.
if TextElem::smallcaps_in(styles) {
if let Some(sc) = TextElem::smallcaps_in(styles) {
feat(b"smcp", 1);
if sc == Smallcaps::All {
feat(b"c2sc", 1);
}
}
if TextElem::alternates_in(styles) {

View File

@ -475,6 +475,7 @@ impl ShowSet for Packed<RawElem> {
out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false))));
out.set(TextElem::set_size(TextSize(Em::new(0.8).into())));
out.set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")])));
out.set(TextElem::set_cjk_latin_spacing(Smart::Custom(None)));
if self.block(styles) {
out.set(ParElem::set_justify(false));
}

View File

@ -12,11 +12,11 @@ use crate::text::TextElem;
/// ```
///
/// # Smallcaps fonts
/// By default, this enables the OpenType `smcp` feature for the font. Not all
/// fonts support this feature. Sometimes smallcaps are part of a dedicated
/// font. This is, for example, the case for the _Latin Modern_ family of fonts.
/// In those cases, you can use a show-set rule to customize the appearance of
/// the text in smallcaps:
/// By default, this uses the `smcp` and `c2sc` OpenType features on the font.
/// Not all fonts support these features. Sometimes, smallcaps are part of a
/// dedicated font. This is, for example, the case for the _Latin Modern_ family
/// of fonts. In those cases, you can use a show-set rule to customize the
/// appearance of the text in smallcaps:
///
/// ```typ
/// #show smallcaps: set text(font: "Latin Modern Roman Caps")
@ -45,6 +45,17 @@ use crate::text::TextElem;
/// ```
#[elem(title = "Small Capitals", Show)]
pub struct SmallcapsElem {
/// Whether to turn uppercase letters into small capitals as well.
///
/// Unless overridden by a show rule, this enables the `c2sc` OpenType
/// feature.
///
/// ```example
/// #smallcaps(all: true)[UNICEF] is an
/// agency of #smallcaps(all: true)[UN].
/// ```
#[default(false)]
pub all: bool,
/// The content to display in small capitals.
#[required]
pub body: Content,
@ -52,7 +63,17 @@ pub struct SmallcapsElem {
impl Show for Packed<SmallcapsElem> {
#[typst_macros::time(name = "smallcaps", span = self.span())]
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(self.body.clone().styled(TextElem::set_smallcaps(true)))
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let sc = if self.all(styles) { Smallcaps::All } else { Smallcaps::Minuscules };
Ok(self.body.clone().styled(TextElem::set_smallcaps(Some(sc))))
}
}
/// What becomes small capitals.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Smallcaps {
/// Minuscules become small capitals.
Minuscules,
/// All letters become small capitals.
All,
}

View File

@ -248,8 +248,6 @@ impl Color {
/// ```
#[func]
pub fn luma(
/// 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 lightness component.
#[external]
@ -300,8 +298,6 @@ impl Color {
/// ```
#[func]
pub fn oklab(
/// 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 lightness component.
#[external]
@ -358,8 +354,6 @@ impl Color {
/// ```
#[func]
pub fn oklch(
/// 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 lightness component.
#[external]
@ -420,8 +414,6 @@ impl Color {
/// ```
#[func(title = "Linear RGB")]
pub fn linear_rgb(
/// 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 red component.
#[external]
@ -477,8 +469,6 @@ impl Color {
/// ```
#[func(title = "RGB")]
pub fn rgb(
/// 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 red component.
#[external]
@ -555,8 +545,6 @@ impl Color {
/// ```
#[func(title = "CMYK")]
pub fn cmyk(
/// 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 cyan component.
#[external]
@ -614,8 +602,6 @@ impl Color {
/// ```
#[func(title = "HSL")]
pub fn hsl(
/// 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 hue angle.
#[external]
@ -673,8 +659,6 @@ impl Color {
/// ```
#[func(title = "HSV")]
pub fn hsv(
/// 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 hue angle.
#[external]
@ -898,7 +882,6 @@ impl Color {
#[func]
pub fn saturate(
self,
/// The call span
span: Span,
/// The factor to saturate the color by.
factor: Ratio,
@ -924,7 +907,6 @@ impl Color {
#[func]
pub fn desaturate(
self,
/// The call span
span: Span,
/// The factor to desaturate the color by.
factor: Ratio,
@ -1001,7 +983,6 @@ impl Color {
#[func]
pub fn rotate(
self,
/// The call span
span: Span,
/// The angle to rotate the hue by.
angle: Angle,

View File

@ -200,9 +200,7 @@ impl Gradient {
/// ```
#[func(title = "Linear Gradient")]
pub fn linear(
/// The args of this function.
args: &mut Args,
/// The call site of this function.
span: Span,
/// The color [stops](#stops) of the gradient.
#[variadic]
@ -292,7 +290,6 @@ impl Gradient {
/// ```
#[func]
fn radial(
/// The call site of this function.
span: Span,
/// The color [stops](#stops) of the gradient.
#[variadic]
@ -407,7 +404,6 @@ impl Gradient {
/// ```
#[func]
pub fn conic(
/// The call site of this function.
span: Span,
/// The color [stops](#stops) of the gradient.
#[variadic]

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