Compare commits

...

50 Commits

Author SHA1 Message Date
Laurenz
574952f578 A bit of docs 2025-01-30 17:47:52 +01:00
Laurenz
cd2a61c354 Refactor a bit more 2025-01-30 17:07:26 +01:00
Laurenz
9362c0e63e Fix docs link 2025-01-30 16:23:00 +01:00
Laurenz
3862102398 Refactor 2025-01-30 16:15:56 +01:00
PgBiel
283fcbb71c attempt 2: use datasource 2025-01-30 02:37:03 -03:00
PgBiel
477e1b22b3 merge pixmap and main 2025-01-30 02:37:03 -03:00
Andrew Voynov
9665eecdb6
Fixed typo in the new outline docs (#5772) 2025-01-28 10:08:12 +00:00
Laurenz
85d1778974
Support first-line-indent for every paragraph (#5768) 2025-01-27 13:15:20 +00:00
Laurenz
176b070c77
Fix space collapsing for explicit paragraphs (#5749) 2025-01-24 12:31:03 +00:00
Laurenz
26e65bfef5
Semantic paragraphs (#5746) 2025-01-24 12:11:26 +00:00
Laurenz
467968af07
Tweak HTML pretty printing (#5745) 2025-01-24 11:15:09 +00:00
Laurenz
cd044825fc
Handle boxes and blocks a bit better in HTML export (#5744)
Co-authored-by: Martin Haug <3874949+reknih@users.noreply.github.com>
2025-01-23 22:18:02 +00:00
Ian Wrzesinski
6fe1e20afb Update math TextElem layout to separate out SymbolElem 2025-01-23 16:30:29 -05:00
Ian Wrzesinski
7838da02ec Add SymbolElem to realization 2025-01-23 16:28:29 -05:00
Ian Wrzesinski
fecdc39846 Use SymbolElem in more places and add char cast for content 2025-01-23 16:28:29 -05:00
Ian Wrzesinski
c47b71b435 Basic SymbolElem addition 2025-01-23 16:28:29 -05:00
Ian Wrzesinski
0b8b7d0f23 Just add MathText SyntaxKind 2025-01-23 16:28:29 -05:00
Malo
2d33393df9
Add support for c2sc OpenType feature in smallcaps (#5655) 2025-01-23 18:24:35 +00:00
T0mstone
b7546bace7
Ignore shebang at start of file (#5702) 2025-01-23 13:05:12 +00:00
wznmickey
ce299d5832
Support syntactically directly nested list, enum, and term list (#5728)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-01-23 12:52:20 +00:00
SekoiaTree
58dbbd48fe
Add lcm as an operator in math mode (#5718)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-01-23 12:35:29 +00:00
Ian Wrzesinski
f7bd03dd76
Fix delimiter unparen syntax (#5739) 2025-01-23 12:27:38 +00:00
Michael Färber
b3fb6c2326
Support quotes in HTML output (#5673) 2025-01-23 12:21:34 +00:00
Michael Färber
e61cd6fb9e
Support start attribute for enum in HTML export (#5676) 2025-01-23 12:18:46 +00:00
Michael Färber
dda486a412
HTML tables (#5666) 2025-01-23 12:08:48 +00:00
Laurenz
52ee33a275
Rework outline (#5735) 2025-01-23 11:50:51 +00:00
Laurenz
1bd8ff0e0f
Methods on elements (#5733) 2025-01-23 10:16:04 +00:00
Laurenz
6fcc432284
Don't link items if container is already linked (#5732) 2025-01-22 13:24:14 +00:00
Laurenz
b45f574703
Move no-hyphenation style in link from show to show-set rule (#5731) 2025-01-22 12:58:57 +00:00
Laurenz
b90ad470d6
Remove redundant doc comments on standard arguments (#5725) 2025-01-21 11:10:06 +00:00
Eric Biedert
c22c47b9c9
Add font exception for NewCM Sans Math (#5682) 2025-01-16 13:40:57 +00:00
Ian Wrzesinski
63c4720ed2
Fix list indent when starting at an open bracket (#5677) 2025-01-16 13:40:29 +00:00
Laurenz
a4ac4e6562
Make typst-timing WASM-compatible (#5689) 2025-01-13 19:19:37 +00:00
Laurenz
6b9b78596a
Don't generate accessors for required fields (#5680) 2025-01-10 16:54:11 +00:00
Laurenz
9473aface1
Fix memory size of TextElem (#5674) 2025-01-09 13:00:18 +00:00
Max
be6629c7cb
Better math argument parsing (#5008) 2025-01-09 10:49:06 +00:00
Laurenz
e2b37fef33
Revamp data loading and deprecate decode functions (#5671) 2025-01-09 09:34:16 +00:00
Laurenz
dacd6acd5e
More flexible and efficient Bytes representation (#5670) 2025-01-08 10:57:56 +00:00
Niklas Eicker
0a374d2380
Embed files associated with the document as a whole (#5221)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-01-08 09:38:34 +00:00
Laurenz
265df6c29f
Remove closing slashes from img tags (#5665) 2025-01-07 13:13:15 +00:00
Malo
e09b55f00f
Allow adding and joining arguments (#5651) 2025-01-06 15:41:58 +00:00
Michael Färber
5c876535cc
Move CellGrid from typst-layout to typst-library (#5585) 2025-01-06 15:20:28 +00:00
bbb651 🇮🇱
ce7f680fd5
Avoid stripping url prefixes multiple times or multiple prefixes (#5659) 2025-01-06 15:13:17 +00:00
Laurenz
cb8d862a55
Terminate pretty-printed HTML with trailing newline (#5661) 2025-01-06 13:55:34 +00:00
Myriad-Dreamin
e8bbf3794f
Select items by import paths (#5518) 2025-01-06 13:31:42 +00:00
Andrew Voynov
ec1e8f9e8d
Added precise definition for "character" in the docs for str.split (#5616) 2025-01-06 13:15:11 +00:00
Johann Birnick
3a1503154f
Basic HTML heading test (#5619) 2025-01-06 13:13:53 +00:00
Malo
36508c66db
Fix French text in example (#5635) 2025-01-06 13:00:44 +00:00
Jie Wang
39eeb116a4
Fix typos in scripting reference (#5660) 2025-01-06 12:58:36 +00:00
Malo
a2f685483a
Improve repr for arguments (#5652) 2025-01-06 12:43:41 +00:00
295 changed files with 6400 additions and 3486 deletions

11
Cargo.lock generated
View File

@ -3094,6 +3094,7 @@ dependencies = [
"parking_lot", "parking_lot",
"serde", "serde",
"serde_json", "serde_json",
"web-sys",
] ]
[[package]] [[package]]
@ -3419,6 +3420,16 @@ dependencies = [
"indexmap-nostd", "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]] [[package]]
name = "weezl" name = "weezl"
version = "0.1.8" version = "0.1.8"

View File

@ -134,6 +134,7 @@ ureq = { version = "2", default-features = false, features = ["native-tls", "gzi
usvg = { version = "0.43", default-features = false, features = ["text"] } usvg = { version = "0.43", default-features = false, features = ["text"] }
walkdir = "2" walkdir = "2"
wasmi = "0.39.0" wasmi = "0.39.0"
web-sys = "0.3"
xmlparser = "0.13.5" xmlparser = "0.13.5"
xmlwriter = "0.1.0" xmlwriter = "0.1.0"
xmp-writer = "0.3" xmp-writer = "0.3"

View File

@ -5,19 +5,19 @@
<p align="center"> <p align="center">
<a href="https://typst.app/docs/"> <a href="https://typst.app/docs/">
<img alt="Documentation" src="https://img.shields.io/website?down_message=offline&label=docs&up_color=007aff&up_message=online&url=https%3A%2F%2Ftypst.app%2Fdocs" <img alt="Documentation" src="https://img.shields.io/website?down_message=offline&label=docs&up_color=007aff&up_message=online&url=https%3A%2F%2Ftypst.app%2Fdocs"
/></a> ></a>
<a href="https://typst.app/"> <a href="https://typst.app/">
<img alt="Typst App" src="https://img.shields.io/website?down_message=offline&label=typst.app&up_color=239dad&up_message=online&url=https%3A%2F%2Ftypst.app" <img alt="Typst App" src="https://img.shields.io/website?down_message=offline&label=typst.app&up_color=239dad&up_message=online&url=https%3A%2F%2Ftypst.app"
/></a> ></a>
<a href="https://discord.gg/2uDybryKPe"> <a href="https://discord.gg/2uDybryKPe">
<img alt="Discord Server" src="https://img.shields.io/discord/1054443721975922748?color=5865F2&label=discord&labelColor=555" <img alt="Discord Server" src="https://img.shields.io/discord/1054443721975922748?color=5865F2&label=discord&labelColor=555"
/></a> ></a>
<a href="https://github.com/typst/typst/blob/main/LICENSE"> <a href="https://github.com/typst/typst/blob/main/LICENSE">
<img alt="Apache-2 License" src="https://img.shields.io/badge/license-Apache%202-brightgreen" <img alt="Apache-2 License" src="https://img.shields.io/badge/license-Apache%202-brightgreen"
/></a> ></a>
<a href="https://typst.app/jobs/"> <a href="https://typst.app/jobs/">
<img alt="Jobs at Typst" src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Ftypst.app%2Fassets%2Fdata%2Fshields.json&query=%24.jobs.text&label=jobs&color=%23A561FF&cacheSeconds=1800" <img alt="Jobs at Typst" src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Ftypst.app%2Fassets%2Fdata%2Fshields.json&query=%24.jobs.text&label=jobs&color=%23A561FF&cacheSeconds=1800"
/></a> ></a>
</p> </p>
Typst is a new markup-based typesetting system that is designed to be as powerful Typst is a new markup-based typesetting system that is designed to be as powerful
@ -39,7 +39,7 @@ A [gentle introduction][tutorial] to Typst is available in our documentation.
However, if you want to see the power of Typst encapsulated in one image, here However, if you want to see the power of Typst encapsulated in one image, here
it is: it is:
<p align="center"> <p align="center">
<img alt="Example" width="900" src="https://user-images.githubusercontent.com/17899797/228031796-ced0e452-fcee-4ae9-92da-b9287764ff25.png"/> <img alt="Example" width="900" src="https://user-images.githubusercontent.com/17899797/228031796-ced0e452-fcee-4ae9-92da-b9287764ff25.png">
</p> </p>

View File

@ -473,6 +473,9 @@ pub enum PdfStandard {
/// PDF/A-2b. /// PDF/A-2b.
#[value(name = "a-2b")] #[value(name = "a-2b")]
A_2b, A_2b,
/// PDF/A-3b.
#[value(name = "a-3b")]
A_3b,
} }
display_possible_values!(PdfStandard); display_possible_values!(PdfStandard);

View File

@ -136,6 +136,7 @@ impl CompileConfig {
.map(|standard| match standard { .map(|standard| match standard {
PdfStandard::V_1_7 => typst_pdf::PdfStandard::V_1_7, PdfStandard::V_1_7 => typst_pdf::PdfStandard::V_1_7,
PdfStandard::A_2b => typst_pdf::PdfStandard::A_2b, PdfStandard::A_2b => typst_pdf::PdfStandard::A_2b,
PdfStandard::A_3b => typst_pdf::PdfStandard::A_3b,
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
PdfStandards::new(&list)? PdfStandards::new(&list)?

View File

@ -305,7 +305,7 @@ impl FileSlot {
) -> FileResult<Bytes> { ) -> FileResult<Bytes> {
self.file.get_or_init( self.file.get_or_init(
|| read(self.id, project_root, package_storage), || read(self.id, project_root, package_storage),
|data, _| Ok(data.into()), |data, _| Ok(Bytes::new(data)),
) )
} }
} }

View File

@ -7,12 +7,11 @@ use typst_library::diag::{
use typst_library::engine::{Engine, Sink, Traced}; use typst_library::engine::{Engine, Sink, Traced};
use typst_library::foundations::{ use typst_library::foundations::{
Arg, Args, Bytes, Capturer, Closure, Content, Context, Func, IntoValue, Arg, Args, Bytes, Capturer, Closure, Content, Context, Func, IntoValue,
NativeElement, Scope, Scopes, Value, NativeElement, Scope, Scopes, SymbolElem, Value,
}; };
use typst_library::introspection::Introspector; use typst_library::introspection::Introspector;
use typst_library::math::LrElem; use typst_library::math::LrElem;
use typst_library::routines::Routines; use typst_library::routines::Routines;
use typst_library::text::TextElem;
use typst_library::World; use typst_library::World;
use typst_syntax::ast::{self, AstNode, Ident}; use typst_syntax::ast::{self, AstNode, Ident};
use typst_syntax::{Span, Spanned, SyntaxNode}; use typst_syntax::{Span, Spanned, SyntaxNode};
@ -325,6 +324,13 @@ fn eval_field_call(
} else if let Some(callee) = target.ty().scope().get(&field) { } else if let Some(callee) = target.ty().scope().get(&field) {
args.insert(0, target_expr.span(), target); args.insert(0, target_expr.span(), target);
Ok(FieldCall::Normal(callee.clone(), args)) 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!( } else if matches!(
target, target,
Value::Symbol(_) | Value::Func(_) | Value::Type(_) | Value::Module(_) Value::Symbol(_) | Value::Func(_) | Value::Type(_) | Value::Module(_)
@ -341,8 +347,20 @@ fn eval_field_call(
/// Produce an error when we cannot call the field. /// Produce an error when we cannot call the field.
fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic { fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic {
let mut error = let mut error = match &target {
error!(field.span(), "type {} has no method `{}`", target.ty(), field.as_str()); 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 { match target {
Value::Dict(ref dict) if matches!(dict.get(&field), Ok(Value::Func(_))) => { Value::Dict(ref dict) if matches!(dict.get(&field), Ok(Value::Func(_))) => {
@ -360,6 +378,7 @@ fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic {
} }
_ => {} _ => {}
} }
error error
} }
@ -382,16 +401,16 @@ fn wrap_args_in_math(
let mut body = Content::empty(); let mut body = Content::empty();
for (i, arg) in args.all::<Content>()?.into_iter().enumerate() { for (i, arg) in args.all::<Content>()?.into_iter().enumerate() {
if i > 0 { if i > 0 {
body += TextElem::packed(','); body += SymbolElem::packed(',');
} }
body += arg; body += arg;
} }
if trailing_comma { if trailing_comma {
body += TextElem::packed(','); body += SymbolElem::packed(',');
} }
Ok(Value::Content( Ok(Value::Content(
callee.display().spanned(callee_span) callee.display().spanned(callee_span)
+ LrElem::new(TextElem::packed('(') + body + TextElem::packed(')')) + LrElem::new(SymbolElem::packed('(') + body + SymbolElem::packed(')'))
.pack() .pack()
.spanned(args.span), .spanned(args.span),
)) ))
@ -685,8 +704,7 @@ mod tests {
// Named-params. // Named-params.
test(s, "$ foo(bar: y) $", &["foo"]); test(s, "$ foo(bar: y) $", &["foo"]);
// This should be updated when we improve named-param parsing: test(s, "$ foo(x-y: 1, bar-z: 2) $", &["foo"]);
test(s, "$ foo(x-y: 1, bar-z: 2) $", &["bar", "foo"]);
// Field access in math. // Field access in math.
test(s, "$ foo.bar $", &["foo"]); test(s, "$ foo.bar $", &["foo"]);

View File

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

View File

@ -211,7 +211,7 @@ fn resolve_package(
// Evaluate the manifest. // Evaluate the manifest.
let manifest_id = FileId::new(Some(spec.clone()), VirtualPath::new("typst.toml")); let manifest_id = FileId::new(Some(spec.clone()), VirtualPath::new("typst.toml"));
let bytes = engine.world.file(manifest_id).at(span)?; let bytes = engine.world.file(manifest_id).at(span)?;
let string = std::str::from_utf8(&bytes).map_err(FileError::from).at(span)?; let string = bytes.as_str().map_err(FileError::from).at(span)?;
let manifest: PackageManifest = toml::from_str(string) let manifest: PackageManifest = toml::from_str(string)
.map_err(|err| eco_format!("package manifest is malformed ({})", err.message())) .map_err(|err| eco_format!("package manifest is malformed ({})", err.message()))
.at(span)?; .at(span)?;

View File

@ -1,11 +1,11 @@
use ecow::eco_format; use ecow::eco_format;
use typst_library::diag::{At, SourceResult}; 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::{ use typst_library::math::{
AlignPointElem, AttachElem, FracElem, LrElem, PrimesElem, RootElem, AlignPointElem, AttachElem, FracElem, LrElem, PrimesElem, RootElem,
}; };
use typst_library::text::TextElem; use typst_library::text::TextElem;
use typst_syntax::ast::{self, AstNode}; use typst_syntax::ast::{self, AstNode, MathTextKind};
use crate::{Eval, Vm}; 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<'_> { impl Eval for ast::MathIdent<'_> {
type Output = Value; type Output = Value;
@ -102,6 +113,7 @@ impl Eval for ast::MathRoot<'_> {
type Output = Content; type Output = Content;
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { 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 index = self.index().map(|i| TextElem::packed(eco_format!("{i}")));
let radicand = self.radicand().eval_display(vm)?; let radicand = self.radicand().eval_display(vm)?;
Ok(RootElem::new(radicand).with_index(index).pack()) 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::diag::{bail, At, SourceResult, StrResult};
use typst_library::foundations::Repr; 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_library::layout::Frame;
use typst_syntax::Span; use typst_syntax::Span;
@ -12,15 +12,19 @@ pub fn html(document: &HtmlDocument) -> SourceResult<String> {
w.buf.push_str("<!DOCTYPE html>"); w.buf.push_str("<!DOCTYPE html>");
write_indent(&mut w); write_indent(&mut w);
write_element(&mut w, &document.root)?; write_element(&mut w, &document.root)?;
if w.pretty {
w.buf.push('\n');
}
Ok(w.buf) Ok(w.buf)
} }
#[derive(Default)] #[derive(Default)]
struct Writer { struct Writer {
/// The output buffer.
buf: String, buf: String,
/// current indentation level /// The current indentation level
level: usize, level: usize,
/// pretty printing enabled? /// Whether pretty printing is enabled.
pretty: bool, pretty: bool,
} }
@ -85,26 +89,32 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
let pretty = w.pretty; let pretty = w.pretty;
if !element.children.is_empty() { 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; let mut indent = w.pretty;
w.level += 1; w.level += 1;
for c in &element.children { for c in &element.children {
let pretty_child = match c { let pretty_around = match c {
HtmlNode::Tag(_) => continue, HtmlNode::Tag(_) => continue,
HtmlNode::Element(element) => is_pretty(element), HtmlNode::Element(child) => w.pretty && wants_pretty_around(child.tag),
HtmlNode::Text(..) | HtmlNode::Frame(_) => false, 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_indent(w);
} }
write_node(w, c)?; write_node(w, c)?;
indent = pretty_child; indent = pretty_around;
} }
w.level -= 1; w.level -= 1;
write_indent(w) write_indent(w);
} }
w.pretty = pretty; w.pretty = pretty;
@ -115,9 +125,27 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
Ok(()) Ok(())
} }
/// Whether the element should be pretty-printed. /// Whether we are allowed to add an extra newline at the start and end of the
fn is_pretty(element: &HtmlElement) -> bool { /// element's contents.
tag::is_block_by_default(element.tag) || matches!(element.tag, tag::meta) ///
/// 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. /// Escape a character.

View File

@ -14,9 +14,9 @@ use typst_library::html::{
use typst_library::introspection::{ use typst_library::introspection::{
Introspector, Locator, LocatorLink, SplitLocator, TagElem, 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::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::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
use typst_library::World; use typst_library::World;
use typst_syntax::Span; use typst_syntax::Span;
@ -139,7 +139,9 @@ fn html_fragment_impl(
let arenas = Arenas::default(); let arenas = Arenas::default();
let children = (engine.routines.realize)( 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 engine,
&mut locator, &mut locator,
&arenas, &arenas,
@ -189,7 +191,8 @@ fn handle(
}; };
output.push(element.into()); output.push(element.into());
} else if let Some(elem) = child.to_packed::<ParElem>() { } 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( output.push(
HtmlElement::new(tag::p) HtmlElement::new(tag::p)
.with_children(children) .with_children(children)
@ -197,13 +200,34 @@ fn handle(
.into(), .into(),
); );
} else if let Some(elem) = child.to_packed::<BoxElem>() { } else if let Some(elem) = child.to_packed::<BoxElem>() {
// FIXME: Very incomplete and hacky, but makes boxes kind fulfill their // TODO: This is rather incomplete.
// purpose for now.
if let Some(body) = elem.body(styles) { if let Some(body) = elem.body(styles) {
let children = let children =
html_fragment(engine, body, locator.next(&elem.span()), styles)?; 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>() { } else if child.is::<SpaceElem>() {
output.push(HtmlNode::text(' ', child.span())); output.push(HtmlNode::text(' ', child.span()));
} else if let Some(elem) = child.to_packed::<TextElem>() { } else if let Some(elem) = child.to_packed::<TextElem>() {

View File

@ -817,19 +817,8 @@ fn param_value_completions<'a>(
) { ) {
if param.name == "font" { if param.name == "font" {
ctx.font_completions(); ctx.font_completions();
} else if param.name == "path" { } else if let Some(extensions) = path_completion(func, param) {
ctx.file_completions_with_extensions(match func.name() { ctx.file_completions_with_extensions(extensions);
Some("image") => &["png", "jpg", "jpeg", "gif", "svg", "svgz"],
Some("csv") => &["csv"],
Some("plugin") => &["wasm"],
Some("cbor") => &["cbor"],
Some("json") => &["json"],
Some("toml") => &["toml"],
Some("xml") => &["xml"],
Some("yaml") => &["yml", "yaml"],
Some("bibliography") => &["bib", "yml", "yaml"],
_ => &[],
});
} else if func.name() == Some("figure") && param.name == "body" { } else if func.name() == Some("figure") && param.name == "body" {
ctx.snippet_completion("image", "image(\"${}\"),", "An image in a figure."); ctx.snippet_completion("image", "image(\"${}\"),", "An image in a figure.");
ctx.snippet_completion("table", "table(\n ${}\n),", "A table in a figure."); ctx.snippet_completion("table", "table(\n ${}\n),", "A table in a figure.");
@ -838,6 +827,28 @@ fn param_value_completions<'a>(
ctx.cast_completions(&param.input); ctx.cast_completions(&param.input);
} }
/// Returns which file extensions to complete for the given parameter if any.
fn path_completion(func: &Func, param: &ParamInfo) -> Option<&'static [&'static str]> {
Some(match (func.name(), param.name) {
(Some("image"), "source") => &["png", "jpg", "jpeg", "gif", "svg", "svgz"],
(Some("csv"), "source") => &["csv"],
(Some("plugin"), "source") => &["wasm"],
(Some("cbor"), "source") => &["cbor"],
(Some("json"), "source") => &["json"],
(Some("toml"), "source") => &["toml"],
(Some("xml"), "source") => &["xml"],
(Some("yaml"), "source") => &["yml", "yaml"],
(Some("bibliography"), "sources") => &["bib", "yml", "yaml"],
(Some("bibliography"), "style") => &["csl"],
(Some("cite"), "style") => &["csl"],
(Some("raw"), "syntaxes") => &["sublime-syntax"],
(Some("raw"), "theme") => &["tmtheme"],
(Some("embed"), "path") => &[],
(None, "path") => &[],
_ => return None,
})
}
/// Resolve a callee expression to a global function. /// Resolve a callee expression to a global function.
fn resolve_global_callee<'a>( fn resolve_global_callee<'a>(
ctx: &CompletionContext<'a>, ctx: &CompletionContext<'a>,

View File

@ -89,15 +89,21 @@ pub fn named_items<T>(
// ``` // ```
Some(ast::Imports::Items(items)) => { Some(ast::Imports::Items(items)) => {
for item in items.iter() { for item in items.iter() {
let original = item.original_name();
let bound = item.bound_name(); let bound = item.bound_name();
let scope = source.and_then(|(value, _)| value.scope());
let span = scope
.and_then(|s| s.get_span(&original))
.unwrap_or(Span::detached())
.or(bound.span());
let value = scope.and_then(|s| s.get(&original)); let (span, value) = item.path().iter().fold(
(bound.span(), source.map(|(value, _)| value)),
|(span, value), path_ident| {
let scope = value.and_then(|v| v.scope());
let span = scope
.and_then(|s| s.get_span(&path_ident))
.unwrap_or(Span::detached())
.or(span);
let value = scope.and_then(|s| s.get(&path_ident));
(span, value)
},
);
if let Some(res) = if let Some(res) =
recv(NamedItem::Import(bound.get(), span, value)) recv(NamedItem::Import(bound.get(), span, value))
{ {
@ -269,16 +275,18 @@ mod tests {
use std::borrow::Borrow; use std::borrow::Borrow;
use ecow::EcoString; use ecow::EcoString;
use typst::foundations::Value;
use typst::syntax::{LinkedNode, Side}; use typst::syntax::{LinkedNode, Side};
use super::named_items; use super::named_items;
use crate::tests::{FilePos, WorldLike}; use crate::tests::{FilePos, TestWorld, WorldLike};
type Response = Vec<EcoString>; type Response = Vec<(EcoString, Option<Value>)>;
trait ResponseExt { trait ResponseExt {
fn must_include<'a>(&self, includes: impl IntoIterator<Item = &'a str>) -> &Self; fn must_include<'a>(&self, includes: impl IntoIterator<Item = &'a str>) -> &Self;
fn must_exclude<'a>(&self, excludes: impl IntoIterator<Item = &'a str>) -> &Self; fn must_exclude<'a>(&self, excludes: impl IntoIterator<Item = &'a str>) -> &Self;
fn must_include_value(&self, name_value: (&str, Option<&Value>)) -> &Self;
} }
impl ResponseExt for Response { impl ResponseExt for Response {
@ -286,7 +294,7 @@ mod tests {
fn must_include<'a>(&self, includes: impl IntoIterator<Item = &'a str>) -> &Self { fn must_include<'a>(&self, includes: impl IntoIterator<Item = &'a str>) -> &Self {
for item in includes { for item in includes {
assert!( assert!(
self.iter().any(|v| v == item), self.iter().any(|v| v.0 == item),
"{item:?} was not contained in {self:?}", "{item:?} was not contained in {self:?}",
); );
} }
@ -297,12 +305,21 @@ mod tests {
fn must_exclude<'a>(&self, excludes: impl IntoIterator<Item = &'a str>) -> &Self { fn must_exclude<'a>(&self, excludes: impl IntoIterator<Item = &'a str>) -> &Self {
for item in excludes { for item in excludes {
assert!( assert!(
!self.iter().any(|v| v == item), !self.iter().any(|v| v.0 == item),
"{item:?} was wrongly contained in {self:?}", "{item:?} was wrongly contained in {self:?}",
); );
} }
self self
} }
#[track_caller]
fn must_include_value(&self, name_value: (&str, Option<&Value>)) -> &Self {
assert!(
self.iter().any(|v| (v.0.as_str(), v.1.as_ref()) == name_value),
"{name_value:?} was not contained in {self:?}",
);
self
}
} }
#[track_caller] #[track_caller]
@ -314,7 +331,7 @@ mod tests {
let leaf = node.leaf_at(cursor, Side::After).unwrap(); let leaf = node.leaf_at(cursor, Side::After).unwrap();
let mut items = vec![]; let mut items = vec![];
named_items(world, leaf, |s| { named_items(world, leaf, |s| {
items.push(s.name().clone()); items.push((s.name().clone(), s.value().clone()));
None::<()> None::<()>
}); });
items items
@ -340,5 +357,10 @@ mod tests {
#[test] #[test]
fn test_named_items_import() { fn test_named_items_import() {
test("#import \"foo.typ\": a; #(a);", 2).must_include(["a"]); test("#import \"foo.typ\": a; #(a);", 2).must_include(["a"]);
let world = TestWorld::new("#import \"foo.typ\": a.b; #(b);")
.with_source("foo.typ", "#import \"a.typ\"")
.with_source("a.typ", "#let b = 1;");
test(&world, 2).must_include_value(("b", Some(&Value::Int(1))));
} }
} }

View File

@ -55,7 +55,7 @@ impl TestWorld {
pub fn with_asset_at(mut self, path: &str, filename: &str) -> Self { pub fn with_asset_at(mut self, path: &str, filename: &str) -> Self {
let id = FileId::new(None, VirtualPath::new(path)); let id = FileId::new(None, VirtualPath::new(path));
let data = typst_dev_assets::get_by_name(filename).unwrap(); let data = typst_dev_assets::get_by_name(filename).unwrap();
let bytes = Bytes::from_static(data); let bytes = Bytes::new(data);
Arc::make_mut(&mut self.files).assets.insert(id, bytes); Arc::make_mut(&mut self.files).assets.insert(id, bytes);
self self
} }
@ -152,7 +152,7 @@ impl Default for TestBase {
fn default() -> Self { fn default() -> Self {
let fonts: Vec<_> = typst_assets::fonts() let fonts: Vec<_> = typst_assets::fonts()
.chain(typst_dev_assets::fonts()) .chain(typst_dev_assets::fonts())
.flat_map(|data| Font::iter(Bytes::from_static(data))) .flat_map(|data| Font::iter(Bytes::new(data)))
.collect(); .collect();
Self { Self {

View File

@ -13,6 +13,7 @@ use std::path::{Path, PathBuf};
use std::sync::OnceLock; use std::sync::OnceLock;
use fontdb::{Database, Source}; use fontdb::{Database, Source};
use typst_library::foundations::Bytes;
use typst_library::text::{Font, FontBook, FontInfo}; use typst_library::text::{Font, FontBook, FontInfo};
use typst_timing::TimingScope; use typst_timing::TimingScope;
@ -52,9 +53,8 @@ impl FontSlot {
.as_ref() .as_ref()
.expect("`path` is not `None` if `font` is uninitialized"), .expect("`path` is not `None` if `font` is uninitialized"),
) )
.ok()? .ok()?;
.into(); Font::new(Bytes::new(data), self.index)
Font::new(data, self.index)
}) })
.clone() .clone()
} }
@ -196,7 +196,7 @@ impl FontSearcher {
#[cfg(feature = "embed-fonts")] #[cfg(feature = "embed-fonts")]
fn add_embedded(&mut self) { fn add_embedded(&mut self) {
for data in typst_assets::fonts() { for data in typst_assets::fonts() {
let buffer = typst_library::foundations::Bytes::from_static(data); let buffer = Bytes::new(data);
for (i, font) in Font::iter(buffer).enumerate() { for (i, font) in Font::iter(buffer).enumerate() {
self.book.push(font.info().clone()); self.book.push(font.info().clone());
self.fonts.push(FontSlot { self.fonts.push(FontSlot {

View File

@ -20,12 +20,16 @@ use typst_library::model::ParElem;
use typst_library::routines::{Pair, Routines}; use typst_library::routines::{Pair, Routines};
use typst_library::text::TextElem; use typst_library::text::TextElem;
use typst_library::World; 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 /// Collects all elements of the flow into prepared children. These are much
/// simpler to handle than the raw elements. /// simpler to handle than the raw elements.
#[typst_macros::time] #[typst_macros::time]
#[allow(clippy::too_many_arguments)]
pub fn collect<'a>( pub fn collect<'a>(
engine: &mut Engine, engine: &mut Engine,
bump: &'a Bump, bump: &'a Bump,
@ -33,6 +37,7 @@ pub fn collect<'a>(
locator: Locator<'a>, locator: Locator<'a>,
base: Size, base: Size,
expand: bool, expand: bool,
mode: FlowMode,
) -> SourceResult<Vec<Child<'a>>> { ) -> SourceResult<Vec<Child<'a>>> {
Collector { Collector {
engine, engine,
@ -42,9 +47,9 @@ pub fn collect<'a>(
base, base,
expand, expand,
output: Vec::with_capacity(children.len()), output: Vec::with_capacity(children.len()),
last_was_par: false, par_situation: ParSituation::First,
} }
.run() .run(mode)
} }
/// State for collection. /// State for collection.
@ -56,12 +61,20 @@ struct Collector<'a, 'x, 'y> {
expand: bool, expand: bool,
locator: SplitLocator<'a>, locator: SplitLocator<'a>,
output: Vec<Child<'a>>, output: Vec<Child<'a>>,
last_was_par: bool, par_situation: ParSituation,
} }
impl<'a> Collector<'a, '_, '_> { impl<'a> Collector<'a, '_, '_> {
/// Perform the collection. /// 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 { for &(child, styles) in self.children {
if let Some(elem) = child.to_packed::<TagElem>() { if let Some(elem) = child.to_packed::<TagElem>() {
self.output.push(Child::Tag(&elem.tag)); self.output.push(Child::Tag(&elem.tag));
@ -94,6 +107,42 @@ impl<'a> Collector<'a, '_, '_> {
Ok(self.output) 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. /// Collect vertical spacing into a relative or fractional child.
fn v(&mut self, elem: &'a Packed<VElem>, styles: StyleChain<'a>) { fn v(&mut self, elem: &'a Packed<VElem>, styles: StyleChain<'a>) {
self.output.push(match elem.amount { self.output.push(match elem.amount {
@ -109,24 +158,34 @@ impl<'a> Collector<'a, '_, '_> {
elem: &'a Packed<ParElem>, elem: &'a Packed<ParElem>,
styles: StyleChain<'a>, styles: StyleChain<'a>,
) -> SourceResult<()> { ) -> SourceResult<()> {
let align = AlignElem::alignment_in(styles).resolve(styles); let lines = crate::inline::layout_par(
let leading = ParElem::leading_in(styles); elem,
let spacing = ParElem::spacing_in(styles);
let costs = TextElem::costs_in(styles);
let lines = crate::layout_inline(
self.engine, self.engine,
&elem.children,
self.locator.next(&elem.span()), self.locator.next(&elem.span()),
styles, styles,
self.last_was_par,
self.base, self.base,
self.expand, self.expand,
self.par_situation,
)? )?
.into_frames(); .into_frames();
let spacing = ParElem::spacing_in(styles);
self.output.push(Child::Rel(spacing.into(), 4)); 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. // Determine whether to prevent widow and orphans.
let len = lines.len(); let len = lines.len();
let prevent_orphans = let prevent_orphans =
@ -165,11 +224,6 @@ impl<'a> Collector<'a, '_, '_> {
self.output self.output
.push(Child::Line(self.boxed(LineChild { frame, align, need }))); .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 /// 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.output.push(spacing(elem.below(styles)));
self.last_was_par = false; self.par_situation = ParSituation::Other;
} }
/// Collects a placed element into a [`PlacedChild`]. /// Collects a placed element into a [`PlacedChild`].
@ -377,8 +431,9 @@ fn layout_single_impl(
route: Route::extend(route), route: Route::extend(route),
}; };
layout_single_block(elem, &mut engine, locator, styles, region) layout_and_modify(styles, |styles| {
.map(|frame| frame.post_processed(styles)) layout_single_block(elem, &mut engine, locator, styles, region)
})
} }
/// A child that encapsulates a prepared breakable block. /// A child that encapsulates a prepared breakable block.
@ -473,11 +528,8 @@ fn layout_multi_impl(
route: Route::extend(route), route: Route::extend(route),
}; };
layout_multi_block(elem, &mut engine, locator, styles, regions).map(|mut fragment| { layout_and_modify(styles, |styles| {
for frame in &mut fragment { layout_multi_block(elem, &mut engine, locator, styles, regions)
frame.post_process(styles);
}
fragment
}) })
} }
@ -579,20 +631,23 @@ impl PlacedChild<'_> {
self.cell.get_or_init(base, |base| { self.cell.get_or_init(base, |base| {
let align = self.alignment.unwrap_or_else(|| Alignment::CENTER); let align = self.alignment.unwrap_or_else(|| Alignment::CENTER);
let aligned = AlignElem::set_alignment(align).wrap(); let aligned = AlignElem::set_alignment(align).wrap();
let styles = self.styles.chain(&aligned);
let mut frame = crate::layout_frame( let mut frame = layout_and_modify(styles, |styles| {
engine, crate::layout_frame(
&self.elem.body, engine,
self.locator.relayout(), &self.elem.body,
self.styles.chain(&aligned), self.locator.relayout(),
Region::new(base, Axes::splat(false)), styles,
)?; Region::new(base, Axes::splat(false)),
)
})?;
if self.float { if self.float {
frame.set_parent(self.elem.location().unwrap()); 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_syntax::Span;
use typst_utils::{NonZeroExt, Numeric}; 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 /// Composes the contents of a single page/region. A region can have multiple
/// columns/subregions. /// columns/subregions.
@ -356,7 +358,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
migratable: bool, migratable: bool,
) -> FlowResult<()> { ) -> FlowResult<()> {
// Footnotes are only supported at the root level. // Footnotes are only supported at the root level.
if !self.config.root { if self.config.mode != FlowMode::Root {
return Ok(()); return Ok(());
} }

View File

@ -25,7 +25,7 @@ use typst_library::layout::{
Regions, Rel, Size, Regions, Rel, Size,
}; };
use typst_library::model::{FootnoteElem, FootnoteEntry, LineNumberingScope, ParLine}; 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::text::TextElem;
use typst_library::World; use typst_library::World;
use typst_utils::{NonZeroExt, Numeric}; use typst_utils::{NonZeroExt, Numeric};
@ -140,9 +140,10 @@ fn layout_fragment_impl(
engine.route.check_layout_depth().at(content.span())?; engine.route.check_layout_depth().at(content.span())?;
let mut kind = FragmentKind::Block;
let arenas = Arenas::default(); let arenas = Arenas::default();
let children = (engine.routines.realize)( let children = (engine.routines.realize)(
RealizationKind::LayoutFragment, RealizationKind::LayoutFragment(&mut kind),
&mut engine, &mut engine,
&mut locator, &mut locator,
&arenas, &arenas,
@ -158,25 +159,46 @@ fn layout_fragment_impl(
regions, regions,
columns, columns,
column_gutter, 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. /// Lays out realized content into regions, potentially with columns.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub(crate) fn layout_flow( pub fn layout_flow<'a>(
engine: &mut Engine, engine: &mut Engine,
children: &[Pair], children: &[Pair<'a>],
locator: &mut SplitLocator, locator: &mut SplitLocator<'a>,
shared: StyleChain, shared: StyleChain<'a>,
mut regions: Regions, mut regions: Regions,
columns: NonZeroUsize, columns: NonZeroUsize,
column_gutter: Rel<Abs>, column_gutter: Rel<Abs>,
root: bool, mode: FlowMode,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
// Prepare configuration that is shared across the whole flow. // Prepare configuration that is shared across the whole flow.
let config = Config { let config = Config {
root, mode,
shared, shared,
columns: { columns: {
let mut count = columns.get(); let mut count = columns.get();
@ -195,7 +217,7 @@ pub(crate) fn layout_flow(
gap: FootnoteEntry::gap_in(shared), gap: FootnoteEntry::gap_in(shared),
expand: regions.expand.x, expand: regions.expand.x,
}, },
line_numbers: root.then(|| LineNumberConfig { line_numbers: (mode == FlowMode::Root).then(|| LineNumberConfig {
scope: ParLine::numbering_scope_in(shared), scope: ParLine::numbering_scope_in(shared),
default_clearance: { default_clearance: {
let width = if PageElem::flipped_in(shared) { let width = if PageElem::flipped_in(shared) {
@ -225,6 +247,7 @@ pub(crate) fn layout_flow(
locator.next(&()), locator.next(&()),
Size::new(config.columns.width, regions.full), Size::new(config.columns.width, regions.full),
regions.expand.x, regions.expand.x,
mode,
)?; )?;
let mut work = Work::new(&children); let mut work = Work::new(&children);
@ -318,7 +341,7 @@ impl<'a, 'b> Work<'a, 'b> {
struct Config<'x> { struct Config<'x> {
/// Whether this is the root flow, which can host footnotes and line /// Whether this is the root flow, which can host footnotes and line
/// numbers. /// numbers.
root: bool, mode: FlowMode,
/// The styles shared by the whole flow. This is used for footnotes and line /// The styles shared by the whole flow. This is used for footnotes and line
/// numbers. /// numbers.
shared: StyleChain<'x>, shared: StyleChain<'x>,

View File

@ -3,6 +3,7 @@ use std::fmt::Debug;
use typst_library::diag::{bail, SourceResult}; use typst_library::diag::{bail, SourceResult};
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::{Resolve, StyleChain}; use typst_library::foundations::{Resolve, StyleChain};
use typst_library::layout::grid::resolve::{Cell, CellGrid, LinePosition, Repeatable};
use typst_library::layout::{ use typst_library::layout::{
Abs, Axes, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, Region, Regions, Rel, Abs, Axes, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, Region, Regions, Rel,
Size, Sizing, Size, Sizing,
@ -13,8 +14,8 @@ use typst_syntax::Span;
use typst_utils::{MaybeReverseIter, Numeric}; use typst_utils::{MaybeReverseIter, Numeric};
use super::{ use super::{
generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, Cell, CellGrid, generate_line_segments, hline_stroke_at_column, layout_cell, vline_stroke_at_row,
LinePosition, LineSegment, Repeatable, Rowspan, UnbreakableRowGroup, LineSegment, Rowspan, UnbreakableRowGroup,
}; };
/// Performs grid layout. /// Performs grid layout.
@ -843,7 +844,8 @@ impl<'a> GridLayouter<'a> {
let size = Size::new(available, height); let size = Size::new(available, height);
let pod = Region::new(size, Axes::splat(false)); let pod = Region::new(size, Axes::splat(false));
let frame = cell.layout(engine, 0, self.styles, pod.into())?.into_frame(); let frame =
layout_cell(cell, engine, 0, self.styles, pod.into())?.into_frame();
resolved.set_max(frame.width() - already_covered_width); resolved.set_max(frame.width() - already_covered_width);
} }
@ -1086,7 +1088,7 @@ impl<'a> GridLayouter<'a> {
}; };
let frames = let frames =
cell.layout(engine, disambiguator, self.styles, pod)?.into_frames(); layout_cell(cell, engine, disambiguator, self.styles, pod)?.into_frames();
// Skip the first region if one cell in it is empty. Then, // Skip the first region if one cell in it is empty. Then,
// remeasure. // remeasure.
@ -1252,9 +1254,9 @@ impl<'a> GridLayouter<'a> {
// rows. // rows.
pod.full = self.regions.full; pod.full = self.regions.full;
} }
let frame = cell let frame =
.layout(engine, disambiguator, self.styles, pod)? layout_cell(cell, engine, disambiguator, self.styles, pod)?
.into_frame(); .into_frame();
let mut pos = pos; let mut pos = pos;
if self.is_rtl { if self.is_rtl {
// In the grid, cell colspans expand to the right, // In the grid, cell colspans expand to the right,
@ -1310,7 +1312,7 @@ impl<'a> GridLayouter<'a> {
// Push the layouted frames into the individual output frames. // Push the layouted frames into the individual output frames.
let fragment = let fragment =
cell.layout(engine, disambiguator, self.styles, pod)?; layout_cell(cell, engine, disambiguator, self.styles, pod)?;
for (output, frame) in outputs.iter_mut().zip(fragment) { for (output, frame) in outputs.iter_mut().zip(fragment) {
let mut pos = pos; let mut pos = pos;
if self.is_rtl { if self.is_rtl {

View File

@ -1,41 +1,11 @@
use std::num::NonZeroUsize;
use std::sync::Arc; use std::sync::Arc;
use typst_library::foundations::{AlternativeFold, Fold}; use typst_library::foundations::{AlternativeFold, Fold};
use typst_library::layout::grid::resolve::{CellGrid, Line, Repeatable};
use typst_library::layout::Abs; use typst_library::layout::Abs;
use typst_library::visualize::Stroke; use typst_library::visualize::Stroke;
use super::{CellGrid, LinePosition, Repeatable, RowPiece}; use super::RowPiece;
/// Represents an explicit grid line (horizontal or vertical) specified by the
/// user.
pub struct Line {
/// The index of the track after this line. This will be the index of the
/// row a horizontal line is above of, or of the column right after a
/// vertical line.
///
/// Must be within `0..=tracks.len()` (where `tracks` is either `grid.cols`
/// or `grid.rows`, ignoring gutter tracks, as appropriate).
pub index: usize,
/// The index of the track at which this line starts being drawn.
/// This is the first column a horizontal line appears in, or the first row
/// a vertical line appears in.
///
/// Must be within `0..tracks.len()` minus gutter tracks.
pub start: usize,
/// The index after the last track through which the line is drawn.
/// Thus, the line is drawn through tracks `start..end` (note that `end` is
/// exclusive).
///
/// Must be within `1..=tracks.len()` minus gutter tracks.
/// `None` indicates the line should go all the way to the end.
pub end: Option<NonZeroUsize>,
/// The line's stroke. This is `None` when the line is explicitly used to
/// override a previously specified line.
pub stroke: Option<Arc<Stroke<Abs>>>,
/// The line's position in relation to the track with its index.
pub position: LinePosition,
}
/// Indicates which priority a particular grid line segment should have, based /// Indicates which priority a particular grid line segment should have, based
/// on the highest priority configuration that defined the segment's stroke. /// on the highest priority configuration that defined the segment's stroke.
@ -588,13 +558,13 @@ pub fn hline_stroke_at_column(
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::num::NonZeroUsize;
use typst_library::foundations::Content; use typst_library::foundations::Content;
use typst_library::introspection::Locator; use typst_library::introspection::Locator;
use typst_library::layout::grid::resolve::{Cell, Entry, LinePosition};
use typst_library::layout::{Axes, Sides, Sizing}; use typst_library::layout::{Axes, Sides, Sizing};
use typst_utils::NonZeroExt; use typst_utils::NonZeroExt;
use super::super::cells::Entry;
use super::super::Cell;
use super::*; use super::*;
fn sample_cell() -> Cell<'static> { fn sample_cell() -> Cell<'static> {

View File

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

View File

@ -1,50 +1,11 @@
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::layout::grid::resolve::{Footer, Header, Repeatable};
use typst_library::layout::{Abs, Axes, Frame, Regions}; use typst_library::layout::{Abs, Axes, Frame, Regions};
use super::layouter::GridLayouter; use super::layouter::GridLayouter;
use super::rowspans::UnbreakableRowGroup; use super::rowspans::UnbreakableRowGroup;
/// A repeatable grid header. Starts at the first row.
pub struct Header {
/// The index after the last row included in this header.
pub end: usize,
}
/// A repeatable grid footer. Stops at the last row.
pub struct Footer {
/// The first row included in this footer.
pub start: usize,
}
/// A possibly repeatable grid object.
/// It still exists even when not repeatable, but must not have additional
/// considerations by grid layout, other than for consistency (such as making
/// a certain group of rows unbreakable).
pub enum Repeatable<T> {
Repeated(T),
NotRepeated(T),
}
impl<T> Repeatable<T> {
/// Gets the value inside this repeatable, regardless of whether
/// it repeats.
pub fn unwrap(&self) -> &T {
match self {
Self::Repeated(repeated) => repeated,
Self::NotRepeated(not_repeated) => not_repeated,
}
}
/// Returns `Some` if the value is repeated, `None` otherwise.
pub fn as_repeated(&self) -> Option<&T> {
match self {
Self::Repeated(repeated) => Some(repeated),
Self::NotRepeated(_) => None,
}
}
}
impl GridLayouter<'_> { impl GridLayouter<'_> {
/// Layouts the header's rows. /// Layouts the header's rows.
/// Skips regions as necessary. /// Skips regions as necessary.

View File

@ -1,12 +1,12 @@
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::Resolve; use typst_library::foundations::Resolve;
use typst_library::layout::grid::resolve::Repeatable;
use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing}; use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing};
use typst_utils::MaybeReverseIter; use typst_utils::MaybeReverseIter;
use super::layouter::{in_last_with_offset, points, Row, RowPiece}; use super::layouter::{in_last_with_offset, points, Row, RowPiece};
use super::repeated::Repeatable; use super::{layout_cell, Cell, GridLayouter};
use super::{Cell, GridLayouter};
/// All information needed to layout a single rowspan. /// All information needed to layout a single rowspan.
pub struct Rowspan { pub struct Rowspan {
@ -141,7 +141,7 @@ impl GridLayouter<'_> {
} }
// Push the layouted frames directly into the finished frames. // Push the layouted frames directly into the finished frames.
let fragment = cell.layout(engine, disambiguator, self.styles, pod)?; let fragment = layout_cell(cell, engine, disambiguator, self.styles, pod)?;
let (current_region, current_rrows) = current_region_data.unzip(); let (current_region, current_rrows) = current_region_data.unzip();
for ((i, finished), frame) in self for ((i, finished), frame) in self
.finished .finished

View File

@ -2,16 +2,16 @@ use std::ffi::OsStr;
use typst_library::diag::{bail, warning, At, SourceResult, StrResult}; use typst_library::diag::{bail, warning, At, SourceResult, StrResult};
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::{Packed, Smart, StyleChain}; use typst_library::foundations::{Bytes, Derived, Packed, Smart, StyleChain};
use typst_library::introspection::Locator; use typst_library::introspection::Locator;
use typst_library::layout::{ use typst_library::layout::{
Abs, Axes, FixedAlignment, Frame, FrameItem, Point, Region, Size, Abs, Axes, FixedAlignment, Frame, FrameItem, Point, Region, Size,
}; };
use typst_library::loading::Readable; use typst_library::loading::DataSource;
use typst_library::text::families; use typst_library::text::families;
use typst_library::visualize::{ use typst_library::visualize::{
Curve, Image, ImageElem, ImageFit, ImageFormat, ImageOptions, ImageSource, Curve, Image, ImageElem, ImageFit, ImageFormat, ImageKind, ImageSource, PixmapImage,
RasterFormat, VectorFormat, RasterFormat, RasterImage, SvgImage, VectorFormat,
}; };
/// Layout the image. /// Layout the image.
@ -27,48 +27,52 @@ pub fn layout_image(
// Take the format that was explicitly defined, or parse the extension, // Take the format that was explicitly defined, or parse the extension,
// or try to detect the format. // or try to detect the format.
let source = elem.source(); let Derived { source, derived: data } = &elem.source;
let format = match (elem.format(styles), source) { let format = match elem.format(styles) {
(Smart::Custom(v), _) => v, Smart::Custom(v) => v,
(Smart::Auto, ImageSource::Readable(data)) => { Smart::Auto => determine_format(source, data).at(span)?,
determine_format(elem.path().as_str(), data).at(span)?
}
(Smart::Auto, ImageSource::Pixmap(_)) => {
bail!(span, "pixmaps require an explicit image format to be given");
}
}; };
// Warn the user if the image contains a foreign object. Not perfect // Warn the user if the image contains a foreign object. Not perfect
// because the svg could also be encoded, but that's an edge case. // because the svg could also be encoded, but that's an edge case.
if let ImageSource::Readable(data) = source { if format == ImageFormat::Vector(VectorFormat::Svg) {
if format == ImageFormat::Vector(VectorFormat::Svg) { let has_foreign_object =
let has_foreign_object = data.as_str().is_ok_and(|s| s.contains("<foreignObject"));
data.as_str().is_some_and(|s| s.contains("<foreignObject"));
if has_foreign_object { if has_foreign_object {
engine.sink.warn(warning!( engine.sink.warn(warning!(
span, span,
"image contains foreign object"; "image contains foreign object";
hint: "SVG images with foreign objects might render incorrectly in typst"; hint: "SVG images with foreign objects might render incorrectly in typst";
hint: "see https://github.com/typst/typst/issues/1421 for more information" hint: "see https://github.com/typst/typst/issues/1421 for more information"
)); ));
}
} }
} }
// Construct the image itself. // Construct the image itself.
let image = Image::new( let kind = match (format, source) {
source.clone(), (ImageFormat::Pixmap(format), ImageSource::Pixmap(source)) => {
format, ImageKind::Pixmap(PixmapImage::new(source.clone(), format).at(span)?)
&ImageOptions { }
alt: elem.alt(styles), (ImageFormat::Raster(format), ImageSource::Data(_)) => {
world: Some(engine.world), ImageKind::Raster(RasterImage::new(data.clone(), format).at(span)?)
families: &families(styles).map(|f| f.as_str()).collect::<Vec<_>>(), }
flatten_text: elem.flatten_text(styles), (ImageFormat::Vector(VectorFormat::Svg), ImageSource::Data(_)) => ImageKind::Svg(
scaling: elem.scaling(styles), SvgImage::with_fonts(
}, data.clone(),
) engine.world,
.at(span)?; elem.flatten_text(styles),
&families(styles).map(|f| f.as_str()).collect::<Vec<_>>(),
)
.at(span)?,
),
(ImageFormat::Pixmap(_), _) => bail!(span, "source must be a pixmap"),
(ImageFormat::Raster(_) | ImageFormat::Vector(_), _) => {
bail!(span, "expected readable source for the given format (str or bytes)")
}
};
let image = Image::new(kind, elem.alt(styles), elem.scaling(styles));
// Determine the image's pixel aspect ratio. // Determine the image's pixel aspect ratio.
let pxw = image.width(); let pxw = image.width();
@ -130,25 +134,29 @@ pub fn layout_image(
Ok(frame) Ok(frame)
} }
/// Determine the image format based on path and data. /// Try to determine the image format based on the data.
fn determine_format(path: &str, data: &Readable) -> StrResult<ImageFormat> { fn determine_format(source: &ImageSource, data: &Bytes) -> StrResult<ImageFormat> {
let ext = std::path::Path::new(path) match source {
.extension() ImageSource::Data(DataSource::Path(path)) => {
.and_then(OsStr::to_str) let ext = std::path::Path::new(path.as_str())
.unwrap_or_default() .extension()
.to_lowercase(); .and_then(OsStr::to_str)
.unwrap_or_default()
.to_lowercase();
Ok(match ext.as_str() { match ext.as_str() {
"png" => ImageFormat::Raster(RasterFormat::Png), "png" => return Ok(ImageFormat::Raster(RasterFormat::Png)),
"jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg), "jpg" | "jpeg" => return Ok(ImageFormat::Raster(RasterFormat::Jpg)),
"gif" => ImageFormat::Raster(RasterFormat::Gif), "gif" => return Ok(ImageFormat::Raster(RasterFormat::Gif)),
"svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg), "svg" | "svgz" => return Ok(ImageFormat::Vector(VectorFormat::Svg)),
_ => match &data { _ => {}
Readable::Str(_) => ImageFormat::Vector(VectorFormat::Svg), }
Readable::Bytes(bytes) => match RasterFormat::detect(bytes) { }
Some(f) => ImageFormat::Raster(f), ImageSource::Data(DataSource::Bytes(_)) => {}
None => bail!("unknown image format"), ImageSource::Pixmap(_) => {
}, bail!("pixmaps require an explicit image format to be given")
}, }
}) }
Ok(ImageFormat::detect(data).ok_or("unknown image format")?)
} }

View File

@ -11,7 +11,7 @@ use typst_utils::Numeric;
use crate::flow::unbreakable_pod; use crate::flow::unbreakable_pod;
use crate::shapes::{clip_rect, fill_and_stroke}; 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())] #[typst_macros::time(name = "box", span = elem.span())]
pub fn layout_box( pub fn layout_box(
elem: &Packed<BoxElem>, 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::foundations::{Packed, Resolve};
use typst_library::introspection::{SplitLocator, Tag, TagElem}; use typst_library::introspection::{SplitLocator, Tag, TagElem};
use typst_library::layout::{ use typst_library::layout::{
Abs, AlignElem, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing, Abs, AlignElem, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing,
Spacing, Spacing,
}; };
use typst_library::model::{EnumElem, ListElem, TermsElem};
use typst_library::routines::Pair;
use typst_library::text::{ use typst_library::text::{
is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes, is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes,
SpaceElem, TextElem, SpaceElem, TextElem,
@ -13,9 +15,10 @@ use typst_syntax::Span;
use typst_utils::Numeric; use typst_utils::Numeric;
use super::*; use super::*;
use crate::modifiers::{layout_and_modify, FrameModifiers, FrameModify};
// The characters by which spacing, inline content and pins are replaced in the // 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 SPACING_REPLACE: &str = " "; // Space
const OBJ_REPLACE: &str = "\u{FFFC}"; // Object Replacement Character 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 LTR_ISOLATE: &str = "\u{2066}";
const POP_ISOLATE: &str = "\u{2069}"; const POP_ISOLATE: &str = "\u{2069}";
/// A prepared item in a paragraph layout. /// A prepared item in a inline layout.
#[derive(Debug)] #[derive(Debug)]
pub enum Item<'a> { pub enum Item<'a> {
/// A shaped text run with consistent style and direction. /// A shaped text run with consistent style and direction.
@ -36,7 +39,7 @@ pub enum Item<'a> {
/// Fractional spacing between other items. /// Fractional spacing between other items.
Fractional(Fr, Option<(&'a Packed<BoxElem>, Locator<'a>, StyleChain<'a>)>), Fractional(Fr, Option<(&'a Packed<BoxElem>, Locator<'a>, StyleChain<'a>)>),
/// Layouted inline-level content. /// Layouted inline-level content.
Frame(Frame, StyleChain<'a>), Frame(Frame),
/// A tag. /// A tag.
Tag(&'a Tag), Tag(&'a Tag),
/// An item that is invisible and needs to be skipped, e.g. a Unicode /// An item that is invisible and needs to be skipped, e.g. a Unicode
@ -67,7 +70,7 @@ impl<'a> Item<'a> {
match self { match self {
Self::Text(shaped) => shaped.text, Self::Text(shaped) => shaped.text,
Self::Absolute(_, _) | Self::Fractional(_, _) => SPACING_REPLACE, Self::Absolute(_, _) | Self::Fractional(_, _) => SPACING_REPLACE,
Self::Frame(_, _) => OBJ_REPLACE, Self::Frame(_) => OBJ_REPLACE,
Self::Tag(_) => "", Self::Tag(_) => "",
Self::Skip(s) => s, Self::Skip(s) => s,
} }
@ -83,7 +86,7 @@ impl<'a> Item<'a> {
match self { match self {
Self::Text(shaped) => shaped.width, Self::Text(shaped) => shaped.width,
Self::Absolute(v, _) => *v, Self::Absolute(v, _) => *v,
Self::Frame(frame, _) => frame.width(), Self::Frame(frame) => frame.width(),
Self::Fractional(_, _) | Self::Tag(_) => Abs::zero(), Self::Fractional(_, _) | Self::Tag(_) => Abs::zero(),
Self::Skip(_) => 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 /// Collects all text into one string and a collection of segments that
/// segments that correspond to pieces of that string. This also performs /// correspond to pieces of that string. This also performs string-level
/// string-level preprocessing like case transformations. /// preprocessing like case transformations.
#[typst_macros::time] #[typst_macros::time]
pub fn collect<'a>( pub fn collect<'a>(
children: &'a StyleVec, children: &[Pair<'a>],
engine: &mut Engine<'_>, engine: &mut Engine<'_>,
locator: &mut SplitLocator<'a>, locator: &mut SplitLocator<'a>,
styles: &'a StyleChain<'a>, styles: StyleChain<'a>,
region: Size, region: Size,
consecutive: bool, situation: Option<ParSituation>,
) -> SourceResult<(String, Vec<Segment<'a>>, SpanMapper)> { ) -> SourceResult<(String, Vec<Segment<'a>>, SpanMapper)> {
let mut collector = Collector::new(2 + children.len()); let mut collector = Collector::new(2 + children.len());
let mut quoter = SmartQuoter::new(); let mut quoter = SmartQuoter::new();
let outer_dir = TextElem::dir_in(*styles); let outer_dir = TextElem::dir_in(styles);
let first_line_indent = ParElem::first_line_indent_in(*styles);
if !first_line_indent.is_zero() if let Some(situation) = situation {
&& consecutive let first_line_indent = ParElem::first_line_indent_in(styles);
&& AlignElem::alignment_in(*styles).resolve(*styles).x == outer_dir.start().into() if !first_line_indent.amount.is_zero()
{ && match situation {
collector.push_item(Item::Absolute(first_line_indent.resolve(*styles), false)); // First-line indent for the first paragraph after a list bullet
collector.spans.push(1, Span::detached()); // 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); for &(child, styles) in children {
if !hang.is_zero() {
collector.push_item(Item::Absolute(-hang, false));
collector.spans.push(1, Span::detached());
}
for (child, styles) in children.iter(styles) {
let prev_len = collector.full.len(); let prev_len = collector.full.len();
if child.is::<SpaceElem>() { if child.is::<SpaceElem>() {
@ -161,9 +177,9 @@ pub fn collect<'a>(
} }
if let Some(case) = TextElem::case_in(styles) { if let Some(case) = TextElem::case_in(styles) {
full.push_str(&case.apply(elem.text())); full.push_str(&case.apply(&elem.text));
} else { } else {
full.push_str(elem.text()); full.push_str(&elem.text);
} }
if dir != outer_dir { if dir != outer_dir {
@ -172,13 +188,12 @@ pub fn collect<'a>(
} }
}); });
} else if let Some(elem) = child.to_packed::<HElem>() { } else if let Some(elem) = child.to_packed::<HElem>() {
let amount = elem.amount(); if elem.amount.is_zero() {
if amount.is_zero() {
continue; continue;
} }
collector.push_item(match amount { collector.push_item(match elem.amount {
Spacing::Fr(fr) => Item::Fractional(*fr, None), Spacing::Fr(fr) => Item::Fractional(fr, None),
Spacing::Rel(rel) => Item::Absolute( Spacing::Rel(rel) => Item::Absolute(
rel.resolve(styles).relative_to(region.x), rel.resolve(styles).relative_to(region.x),
elem.weak(styles), elem.weak(styles),
@ -211,8 +226,10 @@ pub fn collect<'a>(
InlineItem::Space(space, weak) => { InlineItem::Space(space, weak) => {
collector.push_item(Item::Absolute(space, weak)); collector.push_item(Item::Absolute(space, weak));
} }
InlineItem::Frame(frame) => { InlineItem::Frame(mut frame) => {
collector.push_item(Item::Frame(frame, styles)); frame.modify(&FrameModifiers::get_in(styles));
apply_baseline_shift(&mut frame, styles);
collector.push_item(Item::Frame(frame));
} }
} }
} }
@ -223,13 +240,22 @@ pub fn collect<'a>(
if let Sizing::Fr(v) = elem.width(styles) { if let Sizing::Fr(v) = elem.width(styles) {
collector.push_item(Item::Fractional(v, Some((elem, loc, styles)))); collector.push_item(Item::Fractional(v, Some((elem, loc, styles))));
} else { } else {
let frame = layout_box(elem, engine, loc, styles, region)?; let mut frame = layout_and_modify(styles, |styles| {
collector.push_item(Item::Frame(frame, 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>() { } else if let Some(elem) = child.to_packed::<TagElem>() {
collector.push_item(Item::Tag(&elem.tag)); collector.push_item(Item::Tag(&elem.tag));
} else { } 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; let len = collector.full.len() - prev_len;
@ -239,6 +265,16 @@ pub fn collect<'a>(
Ok((collector.full, collector.segments, collector.spans)) 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. /// Collects segments.
struct Collector<'a> { struct Collector<'a> {
full: String, full: String,

View File

@ -14,7 +14,7 @@ pub fn finalize(
expand: bool, expand: bool,
locator: &mut SplitLocator<'_>, locator: &mut SplitLocator<'_>,
) -> SourceResult<Fragment> { ) -> 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. // expand or there's fractional spacing, fit-to-width otherwise.
let width = if !region.x.is_finite() let width = if !region.x.is_finite()
|| (!expand && lines.iter().all(|line| line.fr().is_zero())) || (!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 typst_utils::Numeric;
use super::*; use super::*;
use crate::modifiers::layout_and_modify;
const SHY: char = '\u{ad}'; const SHY: char = '\u{ad}';
const HYPHEN: char = '-'; const HYPHEN: char = '-';
@ -17,12 +18,12 @@ const EN_DASH: char = '';
const EM_DASH: char = '—'; const EM_DASH: char = '—';
const LINE_SEPARATOR: char = '\u{2028}'; // We use LS to distinguish justified breaks. 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 /// A layouted line, consisting of a sequence of layouted inline items that are
/// are mostly borrowed from the preparation phase. This type enables you to /// mostly borrowed from the preparation phase. This type enables you to measure
/// measure the size of a line in a range before committing to building the /// the size of a line in a range before committing to building the line's
/// line's frame. /// 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 /// 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 /// line, respectively. But even those can partially reuse previous results when
/// the break index is safe-to-break per rustybuzz. /// the break index is safe-to-break per rustybuzz.
@ -93,7 +94,7 @@ impl Line<'_> {
pub fn has_negative_width_items(&self) -> bool { pub fn has_negative_width_items(&self) -> bool {
self.items.iter().any(|item| match item { self.items.iter().any(|item| match item {
Item::Absolute(amount, _) => *amount < Abs::zero(), Item::Absolute(amount, _) => *amount < Abs::zero(),
Item::Frame(frame, _) => frame.width() < Abs::zero(), Item::Frame(frame) => frame.width() < Abs::zero(),
_ => false, _ => 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. /// Commit to a line and build its frame.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn commit( pub fn commit(
@ -424,7 +430,7 @@ pub fn commit(
let mut offset = Abs::zero(); let mut offset = Abs::zero();
// We always build the line from left to right. In an LTR paragraph, we must // 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. // hanging indent arises naturally due to the line width.
if p.dir == Dir::LTR { if p.dir == Dir::LTR {
offset += p.hang; offset += p.hang;
@ -509,10 +515,11 @@ pub fn commit(
let amount = v.share(fr, remaining); let amount = v.share(fr, remaining);
if let Some((elem, loc, styles)) = elem { if let Some((elem, loc, styles)) = elem {
let region = Size::new(amount, full); let region = Size::new(amount, full);
let mut frame = let mut frame = layout_and_modify(*styles, |styles| {
layout_box(elem, engine, loc.relayout(), *styles, region)?; layout_box(elem, engine, loc.relayout(), styles, region)
frame.translate(Point::with_y(TextElem::baseline_in(*styles))); })?;
push(&mut offset, frame.post_processed(*styles)); apply_baseline_shift(&mut frame, *styles);
push(&mut offset, frame);
} else { } else {
offset += amount; offset += amount;
} }
@ -524,12 +531,10 @@ pub fn commit(
justification_ratio, justification_ratio,
extra_justification, extra_justification,
); );
push(&mut offset, frame.post_processed(shaped.styles)); push(&mut offset, frame);
} }
Item::Frame(frame, styles) => { Item::Frame(frame) => {
let mut frame = frame.clone(); push(&mut offset, frame.clone());
frame.translate(Point::with_y(TextElem::baseline_in(*styles)));
push(&mut offset, frame.post_processed(*styles));
} }
Item::Tag(tag) => { Item::Tag(tag) => {
let mut frame = Frame::soft(Size::zero()); 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>>); pub struct Items<'a>(Vec<ItemEntry<'a>>);
impl<'a> Items<'a> { impl<'a> Items<'a> {

View File

@ -17,7 +17,7 @@ use unicode_segmentation::UnicodeSegmentation;
use super::*; use super::*;
/// The cost of a line or paragraph layout. /// The cost of a line or inline layout.
type Cost = f64; type Cost = f64;
// Cost parameters. // Cost parameters.
@ -104,7 +104,7 @@ impl Breakpoint {
} }
} }
/// Breaks the paragraph into lines. /// Breaks the text into lines.
pub fn linebreak<'a>( pub fn linebreak<'a>(
engine: &Engine, engine: &Engine,
p: &'a Preparation<'a>, p: &'a Preparation<'a>,
@ -181,13 +181,12 @@ fn linebreak_simple<'a>(
/// lines with hyphens even more. /// lines with hyphens even more.
/// ///
/// To find the layout with the minimal total cost the algorithm uses dynamic /// To find the layout with the minimal total cost the algorithm uses dynamic
/// programming: For each possible breakpoint it determines the optimal /// programming: For each possible breakpoint, it determines the optimal layout
/// paragraph layout _up to that point_. It walks over all possible start points /// _up to that point_. It walks over all possible start points for a line
/// for a line ending at that point and finds the one for which the cost of the /// ending at that point and finds the one for which the cost of the line plus
/// line plus the cost of the optimal paragraph up to the start point (already /// the cost of the optimal layout up to the start point (already computed and
/// computed and stored in dynamic programming table) is minimal. The final /// stored in dynamic programming table) is minimal. The final result is simply
/// result is simply the layout determined for the last breakpoint at the end of /// the layout determined for the last breakpoint at the end of text.
/// text.
#[typst_macros::time] #[typst_macros::time]
fn linebreak_optimized<'a>( fn linebreak_optimized<'a>(
engine: &Engine, engine: &Engine,
@ -215,7 +214,7 @@ fn linebreak_optimized_bounded<'a>(
metrics: &CostMetrics, metrics: &CostMetrics,
upper_bound: Cost, upper_bound: Cost,
) -> Vec<Line<'a>> { ) -> 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> { struct Entry<'a> {
pred: usize, pred: usize,
total: Cost, total: Cost,
@ -321,7 +320,7 @@ fn linebreak_optimized_bounded<'a>(
// This should only happen if our bound was faulty. Which shouldn't happen! // This should only happen if our bound was faulty. Which shouldn't happen!
if table[idx].end != p.text.len() { if table[idx].end != p.text.len() {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
panic!("bounded paragraph layout is incomplete"); panic!("bounded inline layout is incomplete");
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
return linebreak_optimized_bounded(engine, p, width, metrics, Cost::INFINITY); 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 /// (which is costly) to determine costs, it determines approximate costs using
/// cumulative arrays. /// 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 /// the exact cost. This cost is an upper bound for proper optimized
/// linebreaking. We can use it to heavily prune the search space. /// linebreaking. We can use it to heavily prune the search space.
#[typst_macros::time] #[typst_macros::time]
@ -355,7 +354,7 @@ fn linebreak_optimized_approximate(
// Determine the cumulative estimation metrics. // Determine the cumulative estimation metrics.
let estimates = Estimates::compute(p); 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 { struct Entry {
pred: usize, pred: usize,
total: Cost, total: Cost,
@ -862,7 +861,7 @@ struct CostMetrics {
} }
impl CostMetrics { impl CostMetrics {
/// Compute shared metrics for paragraph optimization. /// Compute shared metrics for inline layout optimization.
fn compute(p: &Preparation) -> Self { fn compute(p: &Preparation) -> Self {
Self { Self {
// When justifying, we may stretch spaces below their natural width. // 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 comemo::{Track, Tracked, TrackedMut};
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::engine::{Engine, Route, Sink, Traced}; use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{StyleChain, StyleVec}; use typst_library::foundations::{Packed, StyleChain};
use typst_library::introspection::{Introspector, Locator, LocatorLink}; use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator};
use typst_library::layout::{Fragment, Size}; use typst_library::layout::{Fragment, Size};
use typst_library::model::ParElem; use typst_library::model::ParElem;
use typst_library::routines::Routines; use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
use typst_library::World; use typst_library::World;
use self::collect::{collect, Item, Segment, SpanMapper}; use self::collect::{collect, Item, Segment, SpanMapper};
use self::deco::decorate; use self::deco::decorate;
use self::finalize::finalize; 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::linebreak::{linebreak, Breakpoint};
use self::prepare::{prepare, Preparation}; use self::prepare::{prepare, Preparation};
use self::shaping::{ use self::shaping::{
@ -34,18 +34,18 @@ use self::shaping::{
/// Range of a substring of text. /// Range of a substring of text.
type Range = std::ops::Range<usize>; type Range = std::ops::Range<usize>;
/// Layouts content inline. /// Layouts the paragraph.
pub fn layout_inline( pub fn layout_par(
elem: &Packed<ParElem>,
engine: &mut Engine, engine: &mut Engine,
children: &StyleVec,
locator: Locator, locator: Locator,
styles: StyleChain, styles: StyleChain,
consecutive: bool,
region: Size, region: Size,
expand: bool, expand: bool,
situation: ParSituation,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
layout_inline_impl( layout_par_impl(
children, elem,
engine.routines, engine.routines,
engine.world, engine.world,
engine.introspector, engine.introspector,
@ -54,17 +54,17 @@ pub fn layout_inline(
engine.route.track(), engine.route.track(),
locator.track(), locator.track(),
styles, styles,
consecutive,
region, region,
expand, expand,
situation,
) )
} }
/// The internal, memoized implementation of `layout_inline`. /// The internal, memoized implementation of `layout_par`.
#[comemo::memoize] #[comemo::memoize]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn layout_inline_impl( fn layout_par_impl(
children: &StyleVec, elem: &Packed<ParElem>,
routines: &Routines, routines: &Routines,
world: Tracked<dyn World + '_>, world: Tracked<dyn World + '_>,
introspector: Tracked<Introspector>, introspector: Tracked<Introspector>,
@ -73,12 +73,12 @@ fn layout_inline_impl(
route: Tracked<Route>, route: Tracked<Route>,
locator: Tracked<Locator>, locator: Tracked<Locator>,
styles: StyleChain, styles: StyleChain,
consecutive: bool,
region: Size, region: Size,
expand: bool, expand: bool,
situation: ParSituation,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let link = LocatorLink::new(locator); let link = LocatorLink::new(locator);
let locator = Locator::link(&link); let mut locator = Locator::link(&link).split();
let mut engine = Engine { let mut engine = Engine {
routines, routines,
world, world,
@ -88,18 +88,63 @@ fn layout_inline_impl(
route: Route::extend(route), 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. // Collect all text into one string for BiDi analysis.
let (text, segments, spans) = 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. // Perform BiDi analysis and performs some preparation steps before we
let p = prepare(&mut engine, children, &text, segments, spans, styles)?; // proceed to line breaking.
let p = prepare(engine, children, &text, segments, spans, styles, par)?;
// Break the paragraph into lines. // Break the text into lines.
let lines = linebreak(&engine, &p, region.x - p.hang); let lines = linebreak(engine, &p, region.x - p.hang);
// Turn the selected lines into frames. // 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::foundations::{Resolve, Smart};
use typst_library::layout::{Abs, AlignElem, Dir, Em, FixedAlignment}; use typst_library::layout::{Abs, AlignElem, Dir, Em, FixedAlignment};
use typst_library::model::Linebreaks; use typst_library::model::Linebreaks;
use typst_library::routines::Pair;
use typst_library::text::{Costs, Lang, TextElem}; use typst_library::text::{Costs, Lang, TextElem};
use typst_utils::SliceExt;
use unicode_bidi::{BidiInfo, Level as BidiLevel}; use unicode_bidi::{BidiInfo, Level as BidiLevel};
use super::*; use super::*;
/// A paragraph representation in which children are already layouted and text /// A representation in which children are already layouted and text is already
/// is already preshaped. /// preshaped.
/// ///
/// In many cases, we can directly reuse these results when constructing a line. /// 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 /// Only when a line break falls onto a text index that is not safe-to-break per
/// rustybuzz, we have to reshape that portion. /// rustybuzz, we have to reshape that portion.
pub struct Preparation<'a> { pub struct Preparation<'a> {
/// The paragraph's full text. /// The full text.
pub text: &'a str, 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>>, pub bidi: Option<BidiInfo<'a>>,
/// Text runs, spacing and layouted elements. /// Text runs, spacing and layouted elements.
pub items: Vec<(Range, Item<'a>)>, pub items: Vec<(Range, Item<'a>)>,
@ -33,15 +36,15 @@ pub struct Preparation<'a> {
pub dir: Dir, pub dir: Dir,
/// The text language if it's the same for all children. /// The text language if it's the same for all children.
pub lang: Option<Lang>, pub lang: Option<Lang>,
/// The paragraph's resolved horizontal alignment. /// The resolved horizontal alignment.
pub align: FixedAlignment, pub align: FixedAlignment,
/// Whether to justify the paragraph. /// Whether to justify text.
pub justify: bool, pub justify: bool,
/// The paragraph's hanging indent. /// Hanging indent to apply.
pub hang: Abs, pub hang: Abs,
/// Whether to add spacing between CJK and Latin characters. /// Whether to add spacing between CJK and Latin characters.
pub cjk_latin_spacing: bool, pub cjk_latin_spacing: bool,
/// Whether font fallback is enabled for this paragraph. /// Whether font fallback is enabled.
pub fallback: bool, pub fallback: bool,
/// How to determine line breaks. /// How to determine line breaks.
pub linebreaks: Smart<Linebreaks>, 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 /// representation on which we can do line breaking without layouting each and
/// every line from scratch. /// every line from scratch.
#[typst_macros::time] #[typst_macros::time]
pub fn prepare<'a>( pub fn prepare<'a>(
engine: &mut Engine, engine: &mut Engine,
children: &'a StyleVec, children: &[Pair<'a>],
text: &'a str, text: &'a str,
segments: Vec<Segment<'a>>, segments: Vec<Segment<'a>>,
spans: SpanMapper, spans: SpanMapper,
styles: StyleChain<'a>, styles: StyleChain<'a>,
situation: Option<ParSituation>,
) -> SourceResult<Preparation<'a>> { ) -> SourceResult<Preparation<'a>> {
let dir = TextElem::dir_in(styles); let dir = TextElem::dir_in(styles);
let default_level = match dir { let default_level = match dir {
@ -125,19 +129,26 @@ pub fn prepare<'a>(
add_cjk_latin_spacing(&mut items); 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 { Ok(Preparation {
text, text,
bidi: is_bidi.then_some(bidi), bidi: is_bidi.then_some(bidi),
items, items,
indices, indices,
spans, spans,
hyphenate: children.shared_get(styles, TextElem::hyphenate_in), hyphenate: shared_get(children, styles, TextElem::hyphenate_in),
costs: TextElem::costs_in(styles), costs: TextElem::costs_in(styles),
dir, 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, align: AlignElem::alignment_in(styles).resolve(styles).x,
justify: ParElem::justify_in(styles), justify: ParElem::justify_in(styles),
hang: ParElem::hanging_indent_in(styles), hang,
cjk_latin_spacing, cjk_latin_spacing,
fallback: TextElem::fallback_in(styles), fallback: TextElem::fallback_in(styles),
linebreaks: ParElem::linebreaks_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 /// Add some spacing between Han characters and western characters. See
/// Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition /// Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition
/// in Horizontal Written Mode /// in Horizontal Written Mode

View File

@ -20,6 +20,7 @@ use unicode_bidi::{BidiInfo, Level as BidiLevel};
use unicode_script::{Script, UnicodeScript}; use unicode_script::{Script, UnicodeScript};
use super::{decorate, Item, Range, SpanMapper}; use super::{decorate, Item, Range, SpanMapper};
use crate::modifiers::{FrameModifiers, FrameModify};
/// The result of shaping text. /// The result of shaping text.
/// ///
@ -28,7 +29,7 @@ use super::{decorate, Item, Range, SpanMapper};
/// frame. /// frame.
#[derive(Clone)] #[derive(Clone)]
pub struct ShapedText<'a> { 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, pub base: usize,
/// The text that was shaped. /// The text that was shaped.
pub text: &'a str, pub text: &'a str,
@ -65,9 +66,9 @@ pub struct ShapedGlyph {
pub y_offset: Em, pub y_offset: Em,
/// The adjustability of the glyph. /// The adjustability of the glyph.
pub adjustability: Adjustability, pub adjustability: Adjustability,
/// The byte range of this glyph's cluster in the full paragraph. A cluster /// The byte range of this glyph's cluster in the full inline layout. A
/// is a sequence of one or multiple glyphs that cannot be separated and /// cluster is a sequence of one or multiple glyphs that cannot be separated
/// must always be treated as a union. /// and must always be treated as a union.
/// ///
/// The range values of the glyphs in a [`ShapedText`] should not overlap /// The range values of the glyphs in a [`ShapedText`] should not overlap
/// with each other, and they should be monotonically increasing (for /// with each other, and they should be monotonically increasing (for
@ -326,6 +327,7 @@ impl<'a> ShapedText<'a> {
offset += width; offset += width;
} }
frame.modify(&FrameModifiers::get_in(self.styles));
frame frame
} }
@ -403,7 +405,7 @@ impl<'a> ShapedText<'a> {
/// Reshape a range of the shaped text, reusing information from this /// Reshape a range of the shaped text, reusing information from this
/// shaping process if possible. /// 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> { 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]; 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()) { if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {

View File

@ -6,6 +6,7 @@ mod image;
mod inline; mod inline;
mod lists; mod lists;
mod math; mod math;
mod modifiers;
mod pad; mod pad;
mod pages; mod pages;
mod repeat; mod repeat;
@ -16,7 +17,6 @@ mod transforms;
pub use self::flow::{layout_columns, layout_fragment, layout_frame}; pub use self::flow::{layout_columns, layout_fragment, layout_frame};
pub use self::grid::{layout_grid, layout_table}; pub use self::grid::{layout_grid, layout_table};
pub use self::image::layout_image; 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::lists::{layout_enum, layout_list};
pub use self::math::{layout_equation_block, layout_equation_inline}; pub use self::math::{layout_equation_block, layout_equation_inline};
pub use self::pad::layout_pad; pub use self::pad::layout_pad;

View File

@ -4,11 +4,12 @@ use typst_library::diag::SourceResult;
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::{Content, Context, Depth, Packed, StyleChain}; use typst_library::foundations::{Content, Context, Depth, Packed, StyleChain};
use typst_library::introspection::Locator; 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::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 typst_library::text::TextElem;
use crate::grid::{Cell, CellGrid, GridLayouter}; use crate::grid::GridLayouter;
/// Layout the list. /// Layout the list.
#[typst_macros::time(span = elem.span())] #[typst_macros::time(span = elem.span())]
@ -21,8 +22,9 @@ pub fn layout_list(
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let indent = elem.indent(styles); let indent = elem.indent(styles);
let body_indent = elem.body_indent(styles); let body_indent = elem.body_indent(styles);
let tight = elem.tight(styles);
let gutter = elem.spacing(styles).unwrap_or_else(|| { let gutter = elem.spacing(styles).unwrap_or_else(|| {
if elem.tight(styles) { if tight {
ParElem::leading_in(styles).into() ParElem::leading_in(styles).into()
} else { } else {
ParElem::spacing_in(styles).into() ParElem::spacing_in(styles).into()
@ -39,12 +41,18 @@ pub fn layout_list(
let mut cells = vec![]; let mut cells = vec![];
let mut locator = locator.split(); let mut locator = locator.split();
for item in elem.children() { for item in &elem.children {
// Text in wide lists shall always turn into paragraphs.
let mut body = item.body.clone();
if !tight {
body += ParbreakElem::shared();
}
cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new(Content::empty(), locator.next(&())));
cells.push(Cell::new(marker.clone(), locator.next(&marker.span()))); cells.push(Cell::new(marker.clone(), locator.next(&marker.span())));
cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new(Content::empty(), locator.next(&())));
cells.push(Cell::new( 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()), locator.next(&item.body.span()),
)); ));
} }
@ -77,8 +85,9 @@ pub fn layout_enum(
let reversed = elem.reversed(styles); let reversed = elem.reversed(styles);
let indent = elem.indent(styles); let indent = elem.indent(styles);
let body_indent = elem.body_indent(styles); let body_indent = elem.body_indent(styles);
let tight = elem.tight(styles);
let gutter = elem.spacing(styles).unwrap_or_else(|| { let gutter = elem.spacing(styles).unwrap_or_else(|| {
if elem.tight(styles) { if tight {
ParElem::leading_in(styles).into() ParElem::leading_in(styles).into()
} else { } else {
ParElem::spacing_in(styles).into() ParElem::spacing_in(styles).into()
@ -100,7 +109,7 @@ pub fn layout_enum(
// relation to the item it refers to. // relation to the item it refers to.
let number_align = elem.number_align(styles); let number_align = elem.number_align(styles);
for item in elem.children() { for item in &elem.children {
number = item.number(styles).unwrap_or(number); number = item.number(styles).unwrap_or(number);
let context = Context::new(None, Some(styles)); let context = Context::new(None, Some(styles));
@ -123,11 +132,17 @@ pub fn layout_enum(
let resolved = let resolved =
resolved.aligned(number_align).styled(TextElem::set_overhang(false)); 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(Content::empty(), locator.next(&())));
cells.push(Cell::new(resolved, locator.next(&()))); cells.push(Cell::new(resolved, locator.next(&())));
cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new(Content::empty(), locator.next(&())));
cells.push(Cell::new( 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()), locator.next(&item.body.span()),
)); ));
number = number =

View File

@ -16,7 +16,7 @@ pub fn layout_accent(
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let cramped = style_cramped(); let cramped = style_cramped();
let mut base = ctx.layout_into_fragment(elem.base(), styles.chain(&cramped))?; let mut base = ctx.layout_into_fragment(&elem.base, styles.chain(&cramped))?;
// Try to replace a glyph with its dotless variant. // Try to replace a glyph with its dotless variant.
if let MathFragment::Glyph(glyph) = &mut base { if let MathFragment::Glyph(glyph) = &mut base {
@ -29,8 +29,8 @@ pub fn layout_accent(
let width = elem.size(styles).relative_to(base.width()); let width = elem.size(styles).relative_to(base.width());
let Accent(c) = elem.accent(); let Accent(c) = elem.accent;
let mut glyph = GlyphFragment::new(ctx, styles, *c, elem.span()); let mut glyph = GlyphFragment::new(ctx, styles, c, elem.span());
// Try to replace accent glyph with flattened variant. // Try to replace accent glyph with flattened variant.
let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height); let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height);

View File

@ -1,10 +1,9 @@
use typst_library::diag::SourceResult; 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::layout::{Abs, Axis, Corner, Frame, Point, Rel, Size};
use typst_library::math::{ use typst_library::math::{
AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem, AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem,
}; };
use typst_library::text::TextElem;
use typst_utils::OptionExt; use typst_utils::OptionExt;
use super::{ use super::{
@ -29,7 +28,7 @@ pub fn layout_attach(
let elem = merged.as_ref().unwrap_or(elem); let elem = merged.as_ref().unwrap_or(elem);
let stretch = stretch_size(styles, elem); let stretch = stretch_size(styles, elem);
let mut base = ctx.layout_into_fragment(elem.base(), styles)?; let mut base = ctx.layout_into_fragment(&elem.base, styles)?;
let sup_style = style_for_superscript(styles); let sup_style = style_for_superscript(styles);
let sup_style_chain = styles.chain(&sup_style); let sup_style_chain = styles.chain(&sup_style);
let tl = elem.tl(sup_style_chain); let tl = elem.tl(sup_style_chain);
@ -95,7 +94,7 @@ pub fn layout_primes(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
match *elem.count() { match elem.count {
count @ 1..=4 => { count @ 1..=4 => {
let c = match count { let c = match count {
1 => '', 1 => '',
@ -104,13 +103,14 @@ pub fn layout_primes(
4 => '⁗', 4 => '⁗',
_ => unreachable!(), _ => unreachable!(),
}; };
let f = ctx.layout_into_fragment(&TextElem::packed(c), styles)?; let f = ctx.layout_into_fragment(&SymbolElem::packed(c), styles)?;
ctx.push(f); ctx.push(f);
} }
count => { count => {
// Custom amount of primes // Custom amount of primes
let prime = let prime = ctx
ctx.layout_into_fragment(&TextElem::packed(''), styles)?.into_frame(); .layout_into_fragment(&SymbolElem::packed(''), styles)?
.into_frame();
let width = prime.width() * (count + 1) as f64 / 2.0; let width = prime.width() * (count + 1) as f64 / 2.0;
let mut frame = Frame::soft(Size::new(width, prime.height())); let mut frame = Frame::soft(Size::new(width, prime.height()));
frame.set_baseline(prime.ascent()); frame.set_baseline(prime.ascent());
@ -134,7 +134,7 @@ pub fn layout_scripts(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?; let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
fragment.set_limits(Limits::Never); fragment.set_limits(Limits::Never);
ctx.push(fragment); ctx.push(fragment);
Ok(()) Ok(())
@ -148,7 +148,7 @@ pub fn layout_limits(
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let limits = if elem.inline(styles) { Limits::Always } else { Limits::Display }; let limits = if elem.inline(styles) { Limits::Always } else { Limits::Display };
let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?; let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
fragment.set_limits(limits); fragment.set_limits(limits);
ctx.push(fragment); ctx.push(fragment);
Ok(()) Ok(())
@ -157,9 +157,9 @@ pub fn layout_limits(
/// Get the size to stretch the base to. /// Get the size to stretch the base to.
fn stretch_size(styles: StyleChain, elem: &Packed<AttachElem>) -> Option<Rel<Abs>> { fn stretch_size(styles: StyleChain, elem: &Packed<AttachElem>) -> Option<Rel<Abs>> {
// Extract from an EquationElem. // Extract from an EquationElem.
let mut base = elem.base(); let mut base = &elem.base;
while let Some(equation) = base.to_packed::<EquationElem>() { while let Some(equation) = base.to_packed::<EquationElem>() {
base = equation.body(); base = &equation.body;
} }
base.to_packed::<StretchElem>().map(|stretch| stretch.size(styles)) base.to_packed::<StretchElem>().map(|stretch| stretch.size(styles))

View File

@ -16,7 +16,7 @@ pub fn layout_cancel(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let body = ctx.layout_into_fragment(elem.body(), styles)?; let body = ctx.layout_into_fragment(&elem.body, styles)?;
// Preserve properties of body. // Preserve properties of body.
let body_class = body.class(); let body_class = body.class();

View File

@ -1,5 +1,5 @@
use typst_library::diag::SourceResult; 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::layout::{Em, Frame, FrameItem, Point, Size};
use typst_library::math::{BinomElem, FracElem}; use typst_library::math::{BinomElem, FracElem};
use typst_library::text::TextElem; use typst_library::text::TextElem;
@ -23,8 +23,8 @@ pub fn layout_frac(
layout_frac_like( layout_frac_like(
ctx, ctx,
styles, styles,
elem.num(), &elem.num,
std::slice::from_ref(elem.denom()), std::slice::from_ref(&elem.denom),
false, false,
elem.span(), elem.span(),
) )
@ -37,7 +37,7 @@ pub fn layout_binom(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
layout_frac_like(ctx, styles, elem.upper(), elem.lower(), true, elem.span()) layout_frac_like(ctx, styles, &elem.upper, &elem.lower, true, elem.span())
} }
/// Layout a fraction or binomial. /// Layout a fraction or binomial.
@ -80,7 +80,10 @@ fn layout_frac_like(
let denom = ctx.layout_into_frame( let denom = ctx.layout_into_frame(
&Content::sequence( &Content::sequence(
// Add a comma between each element. // 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), styles.chain(&denom_style),
)?; )?;

View File

@ -1,23 +1,22 @@
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use rustybuzz::Feature; use rustybuzz::Feature;
use smallvec::SmallVec;
use ttf_parser::gsub::{AlternateSubstitution, SingleSubstitution, SubstitutionSubtable}; use ttf_parser::gsub::{AlternateSubstitution, SingleSubstitution, SubstitutionSubtable};
use ttf_parser::opentype_layout::LayoutTable; use ttf_parser::opentype_layout::LayoutTable;
use ttf_parser::{GlyphId, Rect}; use ttf_parser::{GlyphId, Rect};
use typst_library::foundations::StyleChain; use typst_library::foundations::StyleChain;
use typst_library::introspection::Tag; use typst_library::introspection::Tag;
use typst_library::layout::{ 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::math::{EquationElem, MathSize};
use typst_library::model::{Destination, LinkElem};
use typst_library::text::{Font, Glyph, Lang, Region, TextElem, TextItem}; use typst_library::text::{Font, Glyph, Lang, Region, TextElem, TextItem};
use typst_library::visualize::Paint; use typst_library::visualize::Paint;
use typst_syntax::Span; use typst_syntax::Span;
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
use super::{stretch_glyph, MathContext, Scaled}; use super::{stretch_glyph, MathContext, Scaled};
use crate::modifiers::{FrameModifiers, FrameModify};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum MathFragment { pub enum MathFragment {
@ -245,8 +244,7 @@ pub struct GlyphFragment {
pub class: MathClass, pub class: MathClass,
pub math_size: MathSize, pub math_size: MathSize,
pub span: Span, pub span: Span,
pub dests: SmallVec<[Destination; 1]>, pub modifiers: FrameModifiers,
pub hidden: bool,
pub limits: Limits, pub limits: Limits,
pub extended_shape: bool, pub extended_shape: bool,
} }
@ -302,8 +300,7 @@ impl GlyphFragment {
accent_attach: Abs::zero(), accent_attach: Abs::zero(),
class, class,
span, span,
dests: LinkElem::dests_in(styles), modifiers: FrameModifiers::get_in(styles),
hidden: HideElem::hidden_in(styles),
extended_shape: false, extended_shape: false,
}; };
fragment.set_id(ctx, id); fragment.set_id(ctx, id);
@ -390,7 +387,7 @@ impl GlyphFragment {
let mut frame = Frame::soft(size); let mut frame = Frame::soft(size);
frame.set_baseline(self.ascent); frame.set_baseline(self.ascent);
frame.push(Point::with_y(self.ascent + self.shift), FrameItem::Text(item)); 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 frame
} }
@ -516,7 +513,7 @@ impl FrameFragment {
let base_ascent = frame.ascent(); let base_ascent = frame.ascent();
let accent_attach = frame.width() / 2.0; let accent_attach = frame.width() / 2.0;
Self { Self {
frame: frame.post_processed(styles), frame: frame.modified(&FrameModifiers::get_in(styles)),
font_size: TextElem::size_in(styles), font_size: TextElem::size_in(styles),
class: EquationElem::class_in(styles).unwrap_or(MathClass::Normal), class: EquationElem::class_in(styles).unwrap_or(MathClass::Normal),
math_size: EquationElem::size_in(styles), math_size: EquationElem::size_in(styles),

View File

@ -2,6 +2,7 @@ use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain}; use typst_library::foundations::{Packed, StyleChain};
use typst_library::layout::{Abs, Axis, Rel}; use typst_library::layout::{Abs, Axis, Rel};
use typst_library::math::{EquationElem, LrElem, MidElem}; use typst_library::math::{EquationElem, LrElem, MidElem};
use typst_utils::SliceExt;
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
use super::{stretch_fragment, MathContext, MathFragment, DELIM_SHORT_FALL}; use super::{stretch_fragment, MathContext, MathFragment, DELIM_SHORT_FALL};
@ -13,32 +14,23 @@ pub fn layout_lr(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let mut body = elem.body();
// Extract from an EquationElem. // Extract from an EquationElem.
let mut body = &elem.body;
if let Some(equation) = body.to_packed::<EquationElem>() { if let Some(equation) = body.to_packed::<EquationElem>() {
body = equation.body(); body = &equation.body;
} }
// Extract implicit LrElem. // Extract implicit LrElem.
if let Some(lr) = body.to_packed::<LrElem>() { if let Some(lr) = body.to_packed::<LrElem>() {
if lr.size(styles).is_one() { if lr.size(styles).is_one() {
body = lr.body(); body = &lr.body;
} }
} }
let mut fragments = ctx.layout_into_fragments(body, styles)?; let mut fragments = ctx.layout_into_fragments(body, styles)?;
// Ignore leading and trailing ignorant fragments. // Ignore leading and trailing ignorant fragments.
let start_idx = fragments let (start_idx, end_idx) = fragments.split_prefix_suffix(|f| f.is_ignorant());
.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 inner_fragments = &mut fragments[start_idx..end_idx]; let inner_fragments = &mut fragments[start_idx..end_idx];
let axis = scaled!(ctx, styles, axis_height); let axis = scaled!(ctx, styles, axis_height);
@ -100,7 +92,7 @@ pub fn layout_mid(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let mut fragments = ctx.layout_into_fragments(elem.body(), styles)?; let mut fragments = ctx.layout_into_fragments(&elem.body, styles)?;
for fragment in &mut fragments { for fragment in &mut fragments {
match fragment { match fragment {

View File

@ -27,7 +27,7 @@ pub fn layout_vec(
let frame = layout_vec_body( let frame = layout_vec_body(
ctx, ctx,
styles, styles,
elem.children(), &elem.children,
elem.align(styles), elem.align(styles),
elem.gap(styles), elem.gap(styles),
LeftRightAlternator::Right, LeftRightAlternator::Right,
@ -44,7 +44,7 @@ pub fn layout_mat(
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let augment = elem.augment(styles); let augment = elem.augment(styles);
let rows = elem.rows(); let rows = &elem.rows;
if let Some(aug) = &augment { if let Some(aug) = &augment {
for &offset in &aug.hline.0 { for &offset in &aug.hline.0 {
@ -58,7 +58,7 @@ pub fn layout_mat(
} }
} }
let ncols = elem.rows().first().map_or(0, |row| row.len()); let ncols = rows.first().map_or(0, |row| row.len());
for &offset in &aug.vline.0 { for &offset in &aug.vline.0 {
if offset == 0 || offset.unsigned_abs() >= ncols { if offset == 0 || offset.unsigned_abs() >= ncols {
@ -97,7 +97,7 @@ pub fn layout_cases(
let frame = layout_vec_body( let frame = layout_vec_body(
ctx, ctx,
styles, styles,
elem.children(), &elem.children,
FixedAlignment::Start, FixedAlignment::Start,
elem.gap(styles), elem.gap(styles),
LeftRightAlternator::None, LeftRightAlternator::None,

View File

@ -17,7 +17,9 @@ use rustybuzz::Feature;
use ttf_parser::Tag; use ttf_parser::Tag;
use typst_library::diag::{bail, SourceResult}; use typst_library::diag::{bail, SourceResult};
use typst_library::engine::Engine; 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::introspection::{Counter, Locator, SplitLocator, TagElem};
use typst_library::layout::{ use typst_library::layout::{
Abs, AlignElem, Axes, BlockElem, BoxElem, Em, FixedAlignment, Fragment, Frame, HElem, 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()) let counter = Counter::of(EquationElem::elem())
.display_at_loc(engine, elem.location().unwrap(), styles, numbering)? .display_at_loc(engine, elem.location().unwrap(), styles, numbering)?
.spanned(span); .spanned(span);
let number = let number = crate::layout_frame(engine, &counter, locator.next(&()), styles, pod)?;
(engine.routines.layout_frame)(engine, &counter, locator.next(&()), styles, pod)?;
static NUMBER_GUTTER: Em = Em::new(0.5); static NUMBER_GUTTER: Em = Em::new(0.5);
let full_number_width = number.width() + NUMBER_GUTTER.resolve(styles); let full_number_width = number.width() + NUMBER_GUTTER.resolve(styles);
@ -535,6 +536,8 @@ fn layout_realized(
layout_h(elem, ctx, styles)?; layout_h(elem, ctx, styles)?;
} else if let Some(elem) = elem.to_packed::<TextElem>() { } else if let Some(elem) = elem.to_packed::<TextElem>() {
self::text::layout_text(elem, ctx, styles)?; 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>() { } else if let Some(elem) = elem.to_packed::<BoxElem>() {
layout_box(elem, ctx, styles)?; layout_box(elem, ctx, styles)?;
} else if elem.is::<AlignPointElem>() { } else if elem.is::<AlignPointElem>() {
@ -615,7 +618,7 @@ fn layout_box(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let frame = (ctx.engine.routines.layout_box)( let frame = crate::inline::layout_box(
elem, elem,
ctx.engine, ctx.engine,
ctx.locator.next(&elem.span()), ctx.locator.next(&elem.span()),
@ -632,7 +635,7 @@ fn layout_h(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
if let Spacing::Rel(rel) = elem.amount() { if let Spacing::Rel(rel) = elem.amount {
if rel.rel.is_zero() { if rel.rel.is_zero() {
ctx.push(MathFragment::Spacing(rel.abs.resolve(styles), elem.weak(styles))); ctx.push(MathFragment::Spacing(rel.abs.resolve(styles), elem.weak(styles)));
} }
@ -647,11 +650,10 @@ fn layout_class(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let class = *elem.class(); let style = EquationElem::set_class(Some(elem.class)).wrap();
let style = EquationElem::set_class(Some(class)).wrap(); let mut fragment = ctx.layout_into_fragment(&elem.body, styles.chain(&style))?;
let mut fragment = ctx.layout_into_fragment(elem.body(), styles.chain(&style))?; fragment.set_class(elem.class);
fragment.set_class(class); fragment.set_limits(Limits::for_class(elem.class));
fragment.set_limits(Limits::for_class(class));
ctx.push(fragment); ctx.push(fragment);
Ok(()) Ok(())
} }
@ -663,7 +665,7 @@ fn layout_op(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let fragment = ctx.layout_into_fragment(elem.text(), styles)?; let fragment = ctx.layout_into_fragment(&elem.text, styles)?;
let italics = fragment.italics_correction(); let italics = fragment.italics_correction();
let accent_attach = fragment.accent_attach(); let accent_attach = fragment.accent_attach();
let text_like = fragment.is_text_like(); let text_like = fragment.is_text_like();
@ -689,7 +691,7 @@ fn layout_external(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
(ctx.engine.routines.layout_frame)( crate::layout_frame(
ctx.engine, ctx.engine,
content, content,
ctx.locator.next(&content.span()), ctx.locator.next(&content.span()),

View File

@ -18,7 +18,6 @@ pub fn layout_root(
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let index = elem.index(styles); let index = elem.index(styles);
let radicand = elem.radicand();
let span = elem.span(); let span = elem.span();
let gap = scaled!( let gap = scaled!(
@ -36,7 +35,7 @@ pub fn layout_root(
let radicand = { let radicand = {
let cramped = style_cramped(); let cramped = style_cramped();
let styles = styles.chain(&cramped); let styles = styles.chain(&cramped);
let run = ctx.layout_into_run(radicand, styles)?; let run = ctx.layout_into_run(&elem.radicand, styles)?;
let multiline = run.is_multiline(); let multiline = run.is_multiline();
let mut radicand = run.into_fragment(styles).into_frame(); let mut radicand = run.into_fragment(styles).into_frame();
if multiline { if multiline {

View File

@ -10,6 +10,7 @@ use super::{
delimiter_alignment, GlyphFragment, MathContext, MathFragment, Scaled, delimiter_alignment, GlyphFragment, MathContext, MathFragment, Scaled,
VariantFragment, VariantFragment,
}; };
use crate::modifiers::FrameModify;
/// Maximum number of times extenders can be repeated. /// Maximum number of times extenders can be repeated.
const MAX_REPEATS: usize = 1024; const MAX_REPEATS: usize = 1024;
@ -21,7 +22,7 @@ pub fn layout_stretch(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?; let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
stretch_fragment( stretch_fragment(
ctx, ctx,
styles, styles,
@ -265,7 +266,7 @@ fn assemble(
let mut frame = Frame::soft(size); let mut frame = Frame::soft(size);
let mut offset = Abs::zero(); let mut offset = Abs::zero();
frame.set_baseline(baseline); frame.set_baseline(baseline);
frame.post_process_raw(base.dests, base.hidden); frame.modify(&base.modifiers);
for (fragment, advance) in selected { for (fragment, advance) in selected {
let pos = match axis { let pos = match axis {

View File

@ -1,8 +1,8 @@
use std::f64::consts::SQRT_2; use std::f64::consts::SQRT_2;
use ecow::{eco_vec, EcoString}; use ecow::EcoString;
use typst_library::diag::SourceResult; 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::layout::{Abs, Size};
use typst_library::math::{EquationElem, MathSize, MathVariant}; use typst_library::math::{EquationElem, MathSize, MathVariant};
use typst_library::text::{ use typst_library::text::{
@ -20,56 +20,68 @@ pub fn layout_text(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let text = elem.text(); let text = &elem.text;
let span = elem.span(); let span = elem.span();
let mut chars = text.chars(); let fragment = if text.contains(is_newline) {
let math_size = EquationElem::size_in(styles); layout_text_lines(text.split(is_newline), span, ctx, styles)?
let mut dtls = ctx.dtls_table.is_some(); } else {
let fragment: MathFragment = if let Some(mut glyph) = chars layout_inline_text(text, span, ctx, styles)?
.next() };
.filter(|_| chars.next().is_none()) ctx.push(fragment);
.map(|c| dtls_char(c, &mut dtls)) Ok(())
.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);
}
match math_size { /// Layout multiple lines of text.
MathSize::Script => { fn layout_text_lines<'a>(
glyph.make_script_size(ctx); lines: impl Iterator<Item = &'a str>,
} span: Span,
MathSize::ScriptScript => { ctx: &mut MathContext,
glyph.make_script_script_size(ctx); 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 { /// Layout the given text string into a [`FrameFragment`] after styling all
let mut variant = if math_size == MathSize::Display { /// characters for the math font (without auto-italics).
let height = scaled!(ctx, styles, display_operator_min_height) fn layout_inline_text(
.max(SQRT_2 * glyph.height()); text: &str,
glyph.stretch_vertical(ctx, height, Abs::zero()) span: Span,
} else { ctx: &mut MathContext,
glyph.into_variant() styles: StyleChain,
}; ) -> SourceResult<FrameFragment> {
// TeXbook p 155. Large operators are always vertically centered on the axis. if text.chars().all(|c| c.is_ascii_digit() || c == '.') {
variant.center_on_axis(ctx); // Small optimization for numbers. Note that this lays out slightly
variant.into() // differently to normal text and is worth re-evaluating in the future.
} else {
glyph.into()
}
} else if text.chars().all(|c| c.is_ascii_digit() || c == '.') {
// Numbers aren't that difficult.
let mut fragments = vec![]; let mut fragments = vec![];
for c in text.chars() { let is_single = text.chars().count() == 1;
let c = styled_char(styles, c, false); for unstyled_c in text.chars() {
fragments.push(GlyphFragment::new(ctx, styles, c, span).into()); 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); 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 { } else {
let local = [ let local = [
TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds)), TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds)),
@ -77,64 +89,97 @@ pub fn layout_text(
] ]
.map(|p| p.wrap()); .map(|p| p.wrap());
// Anything else is handled by Typst's standard text layout.
let styles = styles.chain(&local); let styles = styles.chain(&local);
let text: EcoString = let styled_text: EcoString =
text.chars().map(|c| styled_char(styles, c, false)).collect(); text.chars().map(|c| styled_char(styles, c, false)).collect();
if text.contains(is_newline) {
let mut fragments = vec![]; let spaced = styled_text.graphemes(true).nth(1).is_some();
for (i, piece) in text.split(is_newline).enumerate() { let elem = TextElem::packed(styled_text).spanned(span);
if i != 0 {
fragments.push(MathFragment::Linebreak); // 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
if !piece.is_empty() { // it will overflow. So emulate an `hbox` instead and allow the
fragments.push(layout_complex_text(piece, ctx, span, styles)?.into()); // paragraph to extend as far as needed.
} let frame = crate::inline::layout_inline(
} ctx.engine,
let mut frame = MathRun::new(fragments).into_frame(styles); &[(&elem, styles)],
let axis = scaled!(ctx, styles, axis_height); &mut ctx.locator.next(&span).split(),
frame.set_baseline(frame.height() / 2.0 + axis); styles,
FrameFragment::new(styles, frame).into() Size::splat(Abs::inf()),
} else { false,
layout_complex_text(&text, ctx, span, styles)?.into() 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); ctx.push(fragment);
Ok(()) Ok(())
} }
/// Layout the given text string into a [`FrameFragment`]. /// Layout a [`GlyphFragment`].
fn layout_complex_text( fn layout_glyph(
text: &str, mut glyph: GlyphFragment,
dtls: bool,
ctx: &mut MathContext, ctx: &mut MathContext,
span: Span,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<FrameFragment> { ) -> MathFragment {
// There isn't a natural width for a paragraph in a math environment; if dtls {
// because it will be placed somewhere probably not at the left margin glyph.make_dotless_form(ctx);
// it will overflow. So emulate an `hbox` instead and allow the paragraph }
// to extend as far as needed. let math_size = EquationElem::size_in(styles);
let spaced = text.graphemes(true).nth(1).is_some(); match math_size {
let elem = TextElem::packed(text).spanned(span); MathSize::Script => glyph.make_script_size(ctx),
let frame = (ctx.engine.routines.layout_inline)( MathSize::ScriptScript => glyph.make_script_script_size(ctx),
ctx.engine, _ => {}
&StyleVec::wrap(eco_vec![elem]), }
ctx.locator.next(&span),
styles,
false,
Size::splat(Abs::inf()),
false,
)?
.into_frame();
Ok(FrameFragment::new(styles, frame) if glyph.class == MathClass::Large {
.with_class(MathClass::Alphabetic) let mut variant = if math_size == MathSize::Display {
.with_text_like(true) let height = scaled!(ctx, styles, display_operator_min_height)
.with_spaced(spaced)) .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://www.w3.org/TR/mathml-core/#new-text-transform-mappings>
/// <https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols> /// <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 /// The non-dotless version of a dotless character that can be used with the
/// OpenType feature. /// `dtls` OpenType feature.
pub fn dtls_char(c: char, dtls: &mut bool) -> char { pub fn try_dotless(c: char) -> Option<char> {
match (c, *dtls) { match c {
('ı', true) => 'i', 'ı' => Some('i'),
('ȷ', true) => 'j', 'ȷ' => Some('j'),
_ => { _ => None,
*dtls = false;
c
}
} }
} }

View File

@ -32,7 +32,7 @@ pub fn layout_underline(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
layout_underoverline(ctx, styles, elem.body(), elem.span(), Position::Under) layout_underoverline(ctx, styles, &elem.body, elem.span(), Position::Under)
} }
/// Lays out an [`OverlineElem`]. /// Lays out an [`OverlineElem`].
@ -42,7 +42,7 @@ pub fn layout_overline(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
layout_underoverline(ctx, styles, elem.body(), elem.span(), Position::Over) layout_underoverline(ctx, styles, &elem.body, elem.span(), Position::Over)
} }
/// Lays out an [`UnderbraceElem`]. /// Lays out an [`UnderbraceElem`].
@ -55,7 +55,7 @@ pub fn layout_underbrace(
layout_underoverspreader( layout_underoverspreader(
ctx, ctx,
styles, styles,
elem.body(), &elem.body,
&elem.annotation(styles), &elem.annotation(styles),
'⏟', '⏟',
BRACE_GAP, BRACE_GAP,
@ -74,7 +74,7 @@ pub fn layout_overbrace(
layout_underoverspreader( layout_underoverspreader(
ctx, ctx,
styles, styles,
elem.body(), &elem.body,
&elem.annotation(styles), &elem.annotation(styles),
'⏞', '⏞',
BRACE_GAP, BRACE_GAP,
@ -93,7 +93,7 @@ pub fn layout_underbracket(
layout_underoverspreader( layout_underoverspreader(
ctx, ctx,
styles, styles,
elem.body(), &elem.body,
&elem.annotation(styles), &elem.annotation(styles),
'⎵', '⎵',
BRACKET_GAP, BRACKET_GAP,
@ -112,7 +112,7 @@ pub fn layout_overbracket(
layout_underoverspreader( layout_underoverspreader(
ctx, ctx,
styles, styles,
elem.body(), &elem.body,
&elem.annotation(styles), &elem.annotation(styles),
'⎴', '⎴',
BRACKET_GAP, BRACKET_GAP,
@ -131,7 +131,7 @@ pub fn layout_underparen(
layout_underoverspreader( layout_underoverspreader(
ctx, ctx,
styles, styles,
elem.body(), &elem.body,
&elem.annotation(styles), &elem.annotation(styles),
'⏝', '⏝',
PAREN_GAP, PAREN_GAP,
@ -150,7 +150,7 @@ pub fn layout_overparen(
layout_underoverspreader( layout_underoverspreader(
ctx, ctx,
styles, styles,
elem.body(), &elem.body,
&elem.annotation(styles), &elem.annotation(styles),
'⏜', '⏜',
PAREN_GAP, PAREN_GAP,
@ -169,7 +169,7 @@ pub fn layout_undershell(
layout_underoverspreader( layout_underoverspreader(
ctx, ctx,
styles, styles,
elem.body(), &elem.body,
&elem.annotation(styles), &elem.annotation(styles),
'⏡', '⏡',
SHELL_GAP, SHELL_GAP,
@ -188,7 +188,7 @@ pub fn layout_overshell(
layout_underoverspreader( layout_underoverspreader(
ctx, ctx,
styles, styles,
elem.body(), &elem.body,
&elem.annotation(styles), &elem.annotation(styles),
'⏠', '⏠',
SHELL_GAP, SHELL_GAP,

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. /// things like tags and weak pagebreaks.
pub fn collect<'a>( pub fn collect<'a>(
mut children: &'a mut [Pair<'a>], mut children: &'a mut [Pair<'a>],
mut locator: SplitLocator<'a>, locator: &mut SplitLocator<'a>,
mut initial: StyleChain<'a>, mut initial: StyleChain<'a>,
) -> Vec<Item<'a>> { ) -> Vec<Item<'a>> {
// The collected page-level items. // The collected page-level items.

View File

@ -83,7 +83,7 @@ fn layout_document_impl(
styles, 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); let introspector = Introspector::paged(&pages);
Ok(PagedDocument { pages, info, introspector }) Ok(PagedDocument { pages, info, introspector })
@ -93,7 +93,7 @@ fn layout_document_impl(
fn layout_pages<'a>( fn layout_pages<'a>(
engine: &mut Engine, engine: &mut Engine,
children: &'a mut [Pair<'a>], children: &'a mut [Pair<'a>],
locator: SplitLocator<'a>, locator: &mut SplitLocator<'a>,
styles: StyleChain<'a>, styles: StyleChain<'a>,
) -> SourceResult<Vec<Page>> { ) -> SourceResult<Vec<Page>> {
// Slice up the children into logical parts. // Slice up the children into logical parts.

View File

@ -19,7 +19,7 @@ use typst_library::visualize::Paint;
use typst_library::World; use typst_library::World;
use typst_utils::Numeric; 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 /// 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 /// 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)), Regions::repeat(area, area.map(Abs::is_finite)),
PageElem::columns_in(styles), PageElem::columns_in(styles),
ColumnsElem::gutter_in(styles), ColumnsElem::gutter_in(styles),
true, FlowMode::Root,
)?; )?;
// Layouts a single marginal. // Layouts a single marginal.

View File

@ -62,7 +62,7 @@ pub fn layout_path(
axes.resolve(styles).zip_map(region.size, Rel::relative_to).to_point() axes.resolve(styles).zip_map(region.size, Rel::relative_to).to_point()
}; };
let vertices = elem.vertices(); let vertices = &elem.vertices;
let points: Vec<Point> = vertices.iter().map(|c| resolve(c.vertex())).collect(); let points: Vec<Point> = vertices.iter().map(|c| resolve(c.vertex())).collect();
let mut size = Size::zero(); let mut size = Size::zero();
@ -150,7 +150,7 @@ pub fn layout_curve(
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
let mut builder = CurveBuilder::new(region, styles); let mut builder = CurveBuilder::new(region, styles);
for item in elem.components() { for item in &elem.components {
match item { match item {
CurveComponent::Move(element) => { CurveComponent::Move(element) => {
let relative = element.relative(styles); let relative = element.relative(styles);
@ -399,7 +399,7 @@ pub fn layout_polygon(
region: Region, region: Region,
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
let points: Vec<Point> = elem let points: Vec<Point> = elem
.vertices() .vertices
.iter() .iter()
.map(|c| c.resolve(styles).zip_map(region.size, Rel::relative_to).to_point()) .map(|c| c.resolve(styles).zip_map(region.size, Rel::relative_to).to_point())
.collect(); .collect();

View File

@ -27,7 +27,7 @@ pub fn layout_stack(
let spacing = elem.spacing(styles); let spacing = elem.spacing(styles);
let mut deferred = None; let mut deferred = None;
for child in elem.children() { for child in &elem.children {
match child { match child {
StackChild::Spacing(kind) => { StackChild::Spacing(kind) => {
layouter.layout_spacing(*kind); layouter.layout_spacing(*kind);
@ -36,14 +36,14 @@ pub fn layout_stack(
StackChild::Block(block) => { StackChild::Block(block) => {
// Transparently handle `h`. // Transparently handle `h`.
if let (Axis::X, Some(h)) = (axis, block.to_packed::<HElem>()) { if let (Axis::X, Some(h)) = (axis, block.to_packed::<HElem>()) {
layouter.layout_spacing(*h.amount()); layouter.layout_spacing(h.amount);
deferred = None; deferred = None;
continue; continue;
} }
// Transparently handle `v`. // Transparently handle `v`.
if let (Axis::Y, Some(v)) = (axis, block.to_packed::<VElem>()) { if let (Axis::Y, Some(v)) = (axis, block.to_packed::<VElem>()) {
layouter.layout_spacing(*v.amount()); layouter.layout_spacing(v.amount);
deferred = None; deferred = None;
continue; continue;
} }

View File

@ -52,7 +52,7 @@ pub fn layout_rotate(
region, region,
size, size,
styles, styles,
elem.body(), &elem.body,
Transform::rotate(angle), Transform::rotate(angle),
align, align,
elem.reflow(styles), elem.reflow(styles),
@ -81,7 +81,7 @@ pub fn layout_scale(
region, region,
size, size,
styles, styles,
elem.body(), &elem.body,
Transform::scale(scale.x, scale.y), Transform::scale(scale.x, scale.y),
elem.origin(styles).resolve(styles), elem.origin(styles).resolve(styles),
elem.reflow(styles), elem.reflow(styles),
@ -169,7 +169,7 @@ pub fn layout_skew(
region, region,
size, size,
styles, styles,
elem.body(), &elem.body,
Transform::skew(ax, ay), Transform::skew(ax, ay),
align, align,
elem.reflow(styles), elem.reflow(styles),

View File

@ -1,4 +1,5 @@
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use std::ops::Add;
use ecow::{eco_format, eco_vec, EcoString, EcoVec}; use ecow::{eco_format, eco_vec, EcoString, EcoVec};
use typst_syntax::{Span, Spanned}; use typst_syntax::{Span, Spanned};
@ -304,8 +305,6 @@ impl Args {
/// ``` /// ```
#[func(constructor)] #[func(constructor)]
pub fn construct( pub fn construct(
/// The real arguments (the other argument is just for the docs).
/// The docs argument cannot be called `args`.
args: &mut Args, args: &mut Args,
/// The arguments to construct. /// The arguments to construct.
#[external] #[external]
@ -366,7 +365,7 @@ impl Debug for Args {
impl Repr for Args { impl Repr for Args {
fn repr(&self) -> EcoString { fn repr(&self) -> EcoString {
let pieces = self.items.iter().map(Arg::repr).collect::<Vec<_>>(); let pieces = self.items.iter().map(Arg::repr).collect::<Vec<_>>();
repr::pretty_array_like(&pieces, false).into() eco_format!("arguments{}", repr::pretty_array_like(&pieces, false))
} }
} }
@ -376,6 +375,21 @@ impl PartialEq for Args {
} }
} }
impl Add for Args {
type Output = Self;
fn add(mut self, rhs: Self) -> Self::Output {
self.items.retain(|item| {
!item.name.as_ref().is_some_and(|name| {
rhs.items.iter().any(|a| a.name.as_ref() == Some(name))
})
});
self.items.extend(rhs.items);
self.span = Span::detached();
self
}
}
/// An argument to a function call: `12` or `draw: false`. /// An argument to a function call: `12` or `draw: false`.
#[derive(Clone, Hash)] #[derive(Clone, Hash)]
#[allow(clippy::derived_hash_with_manual_eq)] #[allow(clippy::derived_hash_with_manual_eq)]

View File

@ -301,9 +301,7 @@ impl Array {
#[func] #[func]
pub fn find( pub fn find(
&self, &self,
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>, context: Tracked<Context>,
/// The function to apply to each item. Must return a boolean. /// The function to apply to each item. Must return a boolean.
searcher: Func, searcher: Func,
@ -325,9 +323,7 @@ impl Array {
#[func] #[func]
pub fn position( pub fn position(
&self, &self,
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>, context: Tracked<Context>,
/// The function to apply to each item. Must return a boolean. /// The function to apply to each item. Must return a boolean.
searcher: Func, searcher: Func,
@ -363,8 +359,6 @@ impl Array {
/// ``` /// ```
#[func] #[func]
pub fn range( 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, args: &mut Args,
/// The start of the range (inclusive). /// The start of the range (inclusive).
#[external] #[external]
@ -402,9 +396,7 @@ impl Array {
#[func] #[func]
pub fn filter( pub fn filter(
&self, &self,
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>, context: Tracked<Context>,
/// The function to apply to each item. Must return a boolean. /// The function to apply to each item. Must return a boolean.
test: Func, test: Func,
@ -427,9 +419,7 @@ impl Array {
#[func] #[func]
pub fn map( pub fn map(
self, self,
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>, context: Tracked<Context>,
/// The function to apply to each item. /// The function to apply to each item.
mapper: Func, mapper: Func,
@ -481,8 +471,6 @@ impl Array {
#[func] #[func]
pub fn zip( pub fn zip(
self, 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, args: &mut Args,
/// Whether all arrays have to have the same length. /// Whether all arrays have to have the same length.
/// For example, `{(1, 2).zip((1, 2, 3), exact: true)}` produces an /// For example, `{(1, 2).zip((1, 2, 3), exact: true)}` produces an
@ -569,9 +557,7 @@ impl Array {
#[func] #[func]
pub fn fold( pub fn fold(
self, self,
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>, context: Tracked<Context>,
/// The initial value to start with. /// The initial value to start with.
init: Value, init: Value,
@ -631,9 +617,7 @@ impl Array {
#[func] #[func]
pub fn any( pub fn any(
self, self,
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>, context: Tracked<Context>,
/// The function to apply to each item. Must return a boolean. /// The function to apply to each item. Must return a boolean.
test: Func, test: Func,
@ -651,9 +635,7 @@ impl Array {
#[func] #[func]
pub fn all( pub fn all(
self, self,
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>, context: Tracked<Context>,
/// The function to apply to each item. Must return a boolean. /// The function to apply to each item. Must return a boolean.
test: Func, test: Func,
@ -831,11 +813,8 @@ impl Array {
#[func] #[func]
pub fn sorted( pub fn sorted(
self, self,
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>, context: Tracked<Context>,
/// The callsite span.
span: Span, span: Span,
/// If given, applies this function to the elements in the array to /// If given, applies this function to the elements in the array to
/// determine the keys to sort by. /// determine the keys to sort by.
@ -881,9 +860,7 @@ impl Array {
#[func(title = "Deduplicate")] #[func(title = "Deduplicate")]
pub fn dedup( pub fn dedup(
self, self,
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>, context: Tracked<Context>,
/// If given, applies this function to the elements in the array to /// If given, applies this function to the elements in the array to
/// determine the keys to deduplicate by. /// determine the keys to deduplicate by.
@ -967,9 +944,7 @@ impl Array {
#[func] #[func]
pub fn reduce( pub fn reduce(
self, self,
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>, context: Tracked<Context>,
/// The reducing function. Must have two parameters: One for the /// The reducing function. Must have two parameters: One for the
/// accumulated value and one for an item. /// accumulated value and one for an item.
@ -1124,6 +1099,53 @@ impl<T: FromValue, const N: usize> FromValue for SmallVec<[T; N]> {
} }
} }
/// One element, or multiple provided as an array.
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct OneOrMultiple<T>(pub Vec<T>);
impl<T: Reflect> Reflect for OneOrMultiple<T> {
fn input() -> CastInfo {
T::input() + Array::input()
}
fn output() -> CastInfo {
T::output() + Array::output()
}
fn castable(value: &Value) -> bool {
Array::castable(value) || T::castable(value)
}
}
impl<T: IntoValue + Clone> IntoValue for OneOrMultiple<T> {
fn into_value(self) -> Value {
self.0.into_value()
}
}
impl<T: FromValue> FromValue for OneOrMultiple<T> {
fn from_value(value: Value) -> HintedStrResult<Self> {
if T::castable(&value) {
return Ok(Self(vec![T::from_value(value)?]));
}
if Array::castable(&value) {
return Ok(Self(
Array::from_value(value)?
.into_iter()
.map(|value| T::from_value(value))
.collect::<HintedStrResult<_>>()?,
));
}
Err(Self::error(&value))
}
}
impl<T> Default for OneOrMultiple<T> {
fn default() -> Self {
Self(vec![])
}
}
/// The error message when the array is empty. /// The error message when the array is empty.
#[cold] #[cold]
fn array_is_empty() -> EcoString { fn array_is_empty() -> EcoString {

View File

@ -1,6 +1,8 @@
use std::borrow::Cow; use std::any::Any;
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::ops::{Add, AddAssign, Deref}; use std::ops::{Add, AddAssign, Deref};
use std::str::Utf8Error;
use std::sync::Arc; use std::sync::Arc;
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
@ -39,28 +41,75 @@ use crate::foundations::{cast, func, scope, ty, Array, Reflect, Repr, Str, Value
/// #str(data.slice(1, 4)) /// #str(data.slice(1, 4))
/// ``` /// ```
#[ty(scope, cast)] #[ty(scope, cast)]
#[derive(Clone, Hash, Eq, PartialEq)] #[derive(Clone, Hash)]
pub struct Bytes(Arc<LazyHash<Cow<'static, [u8]>>>); #[allow(clippy::derived_hash_with_manual_eq)]
pub struct Bytes(Arc<LazyHash<dyn Bytelike>>);
impl Bytes { impl Bytes {
/// Create a buffer from a static byte slice. /// Create `Bytes` from anything byte-like.
pub fn from_static(slice: &'static [u8]) -> Self { ///
Self(Arc::new(LazyHash::new(Cow::Borrowed(slice)))) /// The `data` type will directly back this bytes object. This means you can
/// e.g. pass `&'static [u8]` or `[u8; 8]` and no extra vector will be
/// allocated.
///
/// If the type is `Vec<u8>` and the `Bytes` are unique (i.e. not cloned),
/// the vector will be reused when mutating to the `Bytes`.
///
/// If your source type is a string, prefer [`Bytes::from_string`] to
/// directly use the UTF-8 encoded string data without any copying.
pub fn new<T>(data: T) -> Self
where
T: AsRef<[u8]> + Send + Sync + 'static,
{
Self(Arc::new(LazyHash::new(data)))
}
/// Create `Bytes` from anything string-like, implicitly viewing the UTF-8
/// representation.
///
/// The `data` type will directly back this bytes object. This means you can
/// e.g. pass `String` or `EcoString` without any copying.
pub fn from_string<T>(data: T) -> Self
where
T: AsRef<str> + Send + Sync + 'static,
{
Self(Arc::new(LazyHash::new(StrWrapper(data))))
} }
/// Return `true` if the length is 0. /// Return `true` if the length is 0.
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.0.is_empty() self.as_slice().is_empty()
} }
/// Return a view into the buffer. /// Return a view into the bytes.
pub fn as_slice(&self) -> &[u8] { pub fn as_slice(&self) -> &[u8] {
self self
} }
/// Return a copy of the buffer as a vector. /// Try to view the bytes as an UTF-8 string.
///
/// If these bytes were created via `Bytes::from_string`, UTF-8 validation
/// is skipped.
pub fn as_str(&self) -> Result<&str, Utf8Error> {
self.inner().as_str()
}
/// Return a copy of the bytes as a vector.
pub fn to_vec(&self) -> Vec<u8> { pub fn to_vec(&self) -> Vec<u8> {
self.0.to_vec() self.as_slice().to_vec()
}
/// Try to turn the bytes into a `Str`.
///
/// - If these bytes were created via `Bytes::from_string::<Str>`, the
/// string is cloned directly.
/// - If these bytes were created via `Bytes::from_string`, but from a
/// different type of string, UTF-8 validation is still skipped.
pub fn to_str(&self) -> Result<Str, Utf8Error> {
match self.inner().as_any().downcast_ref::<Str>() {
Some(string) => Ok(string.clone()),
None => self.as_str().map(Into::into),
}
} }
/// Resolve an index or throw an out of bounds error. /// Resolve an index or throw an out of bounds error.
@ -72,12 +121,15 @@ impl Bytes {
/// ///
/// `index == len` is considered in bounds. /// `index == len` is considered in bounds.
fn locate_opt(&self, index: i64) -> Option<usize> { fn locate_opt(&self, index: i64) -> Option<usize> {
let len = self.as_slice().len();
let wrapped = let wrapped =
if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) }; if index >= 0 { Some(index) } else { (len as i64).checked_add(index) };
wrapped.and_then(|v| usize::try_from(v).ok()).filter(|&v| v <= len)
}
wrapped /// Access the inner `dyn Bytelike`.
.and_then(|v| usize::try_from(v).ok()) fn inner(&self) -> &dyn Bytelike {
.filter(|&v| v <= self.0.len()) &**self.0
} }
} }
@ -106,7 +158,7 @@ impl Bytes {
/// The length in bytes. /// The length in bytes.
#[func(title = "Length")] #[func(title = "Length")]
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.0.len() self.as_slice().len()
} }
/// Returns the byte at the specified index. Returns the default value if /// Returns the byte at the specified index. Returns the default value if
@ -122,13 +174,13 @@ impl Bytes {
default: Option<Value>, default: Option<Value>,
) -> StrResult<Value> { ) -> StrResult<Value> {
self.locate_opt(index) self.locate_opt(index)
.and_then(|i| self.0.get(i).map(|&b| Value::Int(b.into()))) .and_then(|i| self.as_slice().get(i).map(|&b| Value::Int(b.into())))
.or(default) .or(default)
.ok_or_else(|| out_of_bounds_no_default(index, self.len())) .ok_or_else(|| out_of_bounds_no_default(index, self.len()))
} }
/// Extracts a subslice of the bytes. Fails with an error if the start or end /// Extracts a subslice of the bytes. Fails with an error if the start or
/// index is out of bounds. /// end index is out of bounds.
#[func] #[func]
pub fn slice( pub fn slice(
&self, &self,
@ -148,9 +200,17 @@ impl Bytes {
if end.is_none() { if end.is_none() {
end = count.map(|c: i64| start + c); end = count.map(|c: i64| start + c);
} }
let start = self.locate(start)?; let start = self.locate(start)?;
let end = self.locate(end.unwrap_or(self.len() as i64))?.max(start); let end = self.locate(end.unwrap_or(self.len() as i64))?.max(start);
Ok(self.0[start..end].into()) let slice = &self.as_slice()[start..end];
// We could hold a view into the original bytes here instead of
// making a copy, but it's unclear when that's worth it. Java
// originally did that for strings, but went back on it because a
// very small view into a very large buffer would be a sort of
// memory leak.
Ok(Bytes::new(slice.to_vec()))
} }
} }
@ -170,7 +230,15 @@ impl Deref for Bytes {
type Target = [u8]; type Target = [u8];
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.0 self.inner().as_bytes()
}
}
impl Eq for Bytes {}
impl PartialEq for Bytes {
fn eq(&self, other: &Self) -> bool {
self.0.eq(&other.0)
} }
} }
@ -180,18 +248,6 @@ impl AsRef<[u8]> for Bytes {
} }
} }
impl From<&[u8]> for Bytes {
fn from(slice: &[u8]) -> Self {
Self(Arc::new(LazyHash::new(slice.to_vec().into())))
}
}
impl From<Vec<u8>> for Bytes {
fn from(vec: Vec<u8>) -> Self {
Self(Arc::new(LazyHash::new(vec.into())))
}
}
impl Add for Bytes { impl Add for Bytes {
type Output = Self; type Output = Self;
@ -207,10 +263,12 @@ impl AddAssign for Bytes {
// Nothing to do // Nothing to do
} else if self.is_empty() { } else if self.is_empty() {
*self = rhs; *self = rhs;
} else if Arc::strong_count(&self.0) == 1 && matches!(**self.0, Cow::Owned(_)) { } else if let Some(vec) = Arc::get_mut(&mut self.0)
Arc::make_mut(&mut self.0).to_mut().extend_from_slice(&rhs); .and_then(|unique| unique.as_any_mut().downcast_mut::<Vec<u8>>())
{
vec.extend_from_slice(&rhs);
} else { } else {
*self = Self::from([self.as_slice(), rhs.as_slice()].concat()); *self = Self::new([self.as_slice(), rhs.as_slice()].concat());
} }
} }
} }
@ -228,20 +286,79 @@ impl Serialize for Bytes {
} }
} }
/// Any type that can back a byte buffer.
trait Bytelike: Send + Sync {
fn as_bytes(&self) -> &[u8];
fn as_str(&self) -> Result<&str, Utf8Error>;
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
}
impl<T> Bytelike for T
where
T: AsRef<[u8]> + Send + Sync + 'static,
{
fn as_bytes(&self) -> &[u8] {
self.as_ref()
}
fn as_str(&self) -> Result<&str, Utf8Error> {
std::str::from_utf8(self.as_ref())
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl Hash for dyn Bytelike {
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_bytes().hash(state);
}
}
/// Makes string-like objects usable with `Bytes`.
struct StrWrapper<T>(T);
impl<T> Bytelike for StrWrapper<T>
where
T: AsRef<str> + Send + Sync + 'static,
{
fn as_bytes(&self) -> &[u8] {
self.0.as_ref().as_bytes()
}
fn as_str(&self) -> Result<&str, Utf8Error> {
Ok(self.0.as_ref())
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
/// A value that can be cast to bytes. /// A value that can be cast to bytes.
pub struct ToBytes(Bytes); pub struct ToBytes(Bytes);
cast! { cast! {
ToBytes, ToBytes,
v: Str => Self(v.as_bytes().into()), v: Str => Self(Bytes::from_string(v)),
v: Array => Self(v.iter() v: Array => Self(v.iter()
.map(|item| match item { .map(|item| match item {
Value::Int(byte @ 0..=255) => Ok(*byte as u8), Value::Int(byte @ 0..=255) => Ok(*byte as u8),
Value::Int(_) => bail!("number must be between 0 and 255"), Value::Int(_) => bail!("number must be between 0 and 255"),
value => Err(<u8 as Reflect>::error(value)), value => Err(<u8 as Reflect>::error(value)),
}) })
.collect::<Result<Vec<u8>, _>>()? .collect::<Result<Vec<u8>, _>>()
.into() .map(Bytes::new)?
), ),
v: Bytes => Self(v), v: Bytes => Self(v),
} }

View File

@ -97,7 +97,6 @@ cast! {
/// ``` /// ```
#[func(title = "Power")] #[func(title = "Power")]
pub fn pow( pub fn pow(
/// The callsite span.
span: Span, span: Span,
/// The base of the power. /// The base of the power.
/// ///
@ -159,7 +158,6 @@ pub fn pow(
/// ``` /// ```
#[func(title = "Exponential")] #[func(title = "Exponential")]
pub fn exp( pub fn exp(
/// The callsite span.
span: Span, span: Span,
/// The exponent of the power. /// The exponent of the power.
exponent: Spanned<Num>, exponent: Spanned<Num>,
@ -412,7 +410,6 @@ pub fn tanh(
/// ``` /// ```
#[func(title = "Logarithm")] #[func(title = "Logarithm")]
pub fn log( pub fn log(
/// The callsite span.
span: Span, span: Span,
/// The number whose logarithm to calculate. Must be strictly positive. /// The number whose logarithm to calculate. Must be strictly positive.
value: Spanned<Num>, value: Spanned<Num>,
@ -454,7 +451,6 @@ pub fn log(
/// ``` /// ```
#[func(title = "Natural Logarithm")] #[func(title = "Natural Logarithm")]
pub fn ln( pub fn ln(
/// The callsite span.
span: Span, span: Span,
/// The number whose logarithm to calculate. Must be strictly positive. /// The number whose logarithm to calculate. Must be strictly positive.
value: Spanned<Num>, value: Spanned<Num>,
@ -782,7 +778,6 @@ pub fn round(
/// ``` /// ```
#[func] #[func]
pub fn clamp( pub fn clamp(
/// The callsite span.
span: Span, span: Span,
/// The number to clamp. /// The number to clamp.
value: DecNum, value: DecNum,
@ -815,7 +810,6 @@ pub fn clamp(
/// ``` /// ```
#[func(title = "Minimum")] #[func(title = "Minimum")]
pub fn min( pub fn min(
/// The callsite span.
span: Span, span: Span,
/// The sequence of values from which to extract the minimum. /// The sequence of values from which to extract the minimum.
/// Must not be empty. /// Must not be empty.
@ -833,7 +827,6 @@ pub fn min(
/// ``` /// ```
#[func(title = "Maximum")] #[func(title = "Maximum")]
pub fn max( pub fn max(
/// The callsite span.
span: Span, span: Span,
/// The sequence of values from which to extract the maximum. /// The sequence of values from which to extract the maximum.
/// Must not be empty. /// Must not be empty.
@ -911,7 +904,6 @@ pub fn odd(
/// ``` /// ```
#[func(title = "Remainder")] #[func(title = "Remainder")]
pub fn rem( pub fn rem(
/// The span of the function call.
span: Span, span: Span,
/// The dividend of the remainder. /// The dividend of the remainder.
dividend: DecNum, dividend: DecNum,
@ -950,7 +942,6 @@ pub fn rem(
/// ``` /// ```
#[func(title = "Euclidean Division")] #[func(title = "Euclidean Division")]
pub fn div_euclid( pub fn div_euclid(
/// The callsite span.
span: Span, span: Span,
/// The dividend of the division. /// The dividend of the division.
dividend: DecNum, dividend: DecNum,
@ -994,7 +985,6 @@ pub fn div_euclid(
/// ``` /// ```
#[func(title = "Euclidean Remainder", keywords = ["modulo", "modulus"])] #[func(title = "Euclidean Remainder", keywords = ["modulo", "modulus"])]
pub fn rem_euclid( pub fn rem_euclid(
/// The callsite span.
span: Span, span: Span,
/// The dividend of the remainder. /// The dividend of the remainder.
dividend: DecNum, dividend: DecNum,
@ -1031,7 +1021,6 @@ pub fn rem_euclid(
/// ``` /// ```
#[func(title = "Quotient")] #[func(title = "Quotient")]
pub fn quo( pub fn quo(
/// The span of the function call.
span: Span, span: Span,
/// The dividend of the quotient. /// The dividend of the quotient.
dividend: DecNum, dividend: DecNum,

View File

@ -13,7 +13,9 @@ use typst_syntax::{Span, Spanned};
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
use crate::diag::{At, HintedStrResult, HintedString, SourceResult, StrResult}; use crate::diag::{At, HintedStrResult, HintedString, SourceResult, StrResult};
use crate::foundations::{array, repr, NativeElement, Packed, Repr, Str, Type, Value}; use crate::foundations::{
array, repr, Fold, NativeElement, Packed, Repr, Str, Type, Value,
};
/// Determine details of a type. /// Determine details of a type.
/// ///
@ -497,3 +499,58 @@ cast! {
/// An operator that can be both unary or binary like `+`. /// An operator that can be both unary or binary like `+`.
"vary" => MathClass::Vary, "vary" => MathClass::Vary,
} }
/// A type that contains a user-visible source portion and something that is
/// derived from it, but not user-visible.
///
/// An example usage would be `source` being a `DataSource` and `derived` a
/// TextMate theme parsed from it. With `Derived`, we can store both parts in
/// the `RawElem::theme` field and get automatic nice `Reflect` and `IntoValue`
/// impls.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Derived<S, D> {
/// The source portion.
pub source: S,
/// The derived portion.
pub derived: D,
}
impl<S, D> Derived<S, D> {
/// Create a new instance from the `source` and the `derived` data.
pub fn new(source: S, derived: D) -> Self {
Self { source, derived }
}
}
impl<S: Reflect, D> Reflect for Derived<S, D> {
fn input() -> CastInfo {
S::input()
}
fn output() -> CastInfo {
S::output()
}
fn castable(value: &Value) -> bool {
S::castable(value)
}
fn error(found: &Value) -> HintedString {
S::error(found)
}
}
impl<S: IntoValue, D> IntoValue for Derived<S, D> {
fn into_value(self) -> Value {
self.source.into_value()
}
}
impl<S: Fold, D: Fold> Fold for Derived<S, D> {
fn fold(self, outer: Self) -> Self {
Self {
source: self.source.fold(outer.source),
derived: self.derived.fold(outer.derived),
}
}
}

View File

@ -9,7 +9,6 @@ use std::sync::Arc;
use comemo::Tracked; use comemo::Tracked;
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
use smallvec::smallvec;
use typst_syntax::Span; use typst_syntax::Span;
use typst_utils::{fat, singleton, LazyHash, SmallBitSet}; use typst_utils::{fat, singleton, LazyHash, SmallBitSet};
@ -500,7 +499,7 @@ impl Content {
/// Link the content somewhere. /// Link the content somewhere.
pub fn linked(self, dest: Destination) -> Self { 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. /// Set alignments for this content.

View File

@ -318,7 +318,6 @@ impl Datetime {
/// ``` /// ```
#[func] #[func]
pub fn today( pub fn today(
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// An offset to apply to the current UTC date. If set to `{auto}`, the /// An offset to apply to the current UTC date. If set to `{auto}`, the
/// offset will be the local offset. /// offset will be the local offset.

View File

@ -163,18 +163,14 @@ impl f64 {
size: u32, size: u32,
) -> StrResult<Bytes> { ) -> StrResult<Bytes> {
Ok(match size { Ok(match size {
8 => match endian { 8 => Bytes::new(match endian {
Endianness::Little => self.to_le_bytes(), Endianness::Little => self.to_le_bytes(),
Endianness::Big => self.to_be_bytes(), Endianness::Big => self.to_be_bytes(),
} }),
.as_slice() 4 => Bytes::new(match endian {
.into(),
4 => match endian {
Endianness::Little => (self as f32).to_le_bytes(), Endianness::Little => (self as f32).to_le_bytes(),
Endianness::Big => (self as f32).to_be_bytes(), Endianness::Big => (self as f32).to_be_bytes(),
} }),
.as_slice()
.into(),
_ => bail!("size must be either 4 or 8"), _ => bail!("size must be either 4 or 8"),
}) })
} }

View File

@ -334,8 +334,6 @@ impl Func {
#[func] #[func]
pub fn with( pub fn with(
self, self,
/// The real arguments (the other argument is just for the docs).
/// The docs argument cannot be called `args`.
args: &mut Args, args: &mut Args,
/// The arguments to apply to the function. /// The arguments to apply to the function.
#[external] #[external]
@ -361,8 +359,6 @@ impl Func {
#[func] #[func]
pub fn where_( pub fn where_(
self, self,
/// The real arguments (the other argument is just for the docs).
/// The docs argument cannot be called `args`.
args: &mut Args, args: &mut Args,
/// The fields to filter for. /// The fields to filter for.
#[variadic] #[variadic]

View File

@ -1,6 +1,7 @@
use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize, ParseIntError}; use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize, ParseIntError};
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use smallvec::SmallVec;
use crate::diag::{bail, StrResult}; use crate::diag::{bail, StrResult};
use crate::foundations::{ use crate::foundations::{
@ -322,7 +323,7 @@ impl i64 {
Endianness::Little => self.to_le_bytes(), Endianness::Little => self.to_le_bytes(),
}; };
let mut buf = vec![0u8; size]; let mut buf = SmallVec::<[u8; 8]>::from_elem(0, size);
match endian { match endian {
Endianness::Big => { Endianness::Big => {
// Copy the bytes from the array to the buffer, starting from // Copy the bytes from the array to the buffer, starting from
@ -339,7 +340,7 @@ impl i64 {
} }
} }
Bytes::from(buf) Bytes::new(buf)
} }
} }

View File

@ -266,7 +266,6 @@ impl assert {
/// ``` /// ```
#[func(title = "Evaluate")] #[func(title = "Evaluate")]
pub fn eval( pub fn eval(
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// A string of Typst code to evaluate. /// A string of Typst code to evaluate.
source: Spanned<String>, source: Spanned<String>,

View File

@ -6,7 +6,9 @@ use ecow::eco_format;
use typst_utils::Numeric; use typst_utils::Numeric;
use crate::diag::{bail, HintedStrResult, StrResult}; 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::layout::{Alignment, Length, Rel};
use crate::text::TextElem; use crate::text::TextElem;
use crate::visualize::Stroke; use crate::visualize::Stroke;
@ -30,12 +32,13 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> {
(Symbol(a), Str(b)) => Str(format_str!("{a}{b}")), (Symbol(a), Str(b)) => Str(format_str!("{a}{b}")),
(Bytes(a), Bytes(b)) => Bytes(a + b), (Bytes(a), Bytes(b)) => Bytes(a + b),
(Content(a), Content(b)) => Content(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)), (Content(a), Str(b)) => Content(a + TextElem::packed(b)),
(Str(a), Content(b)) => Content(TextElem::packed(a) + 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), (Array(a), Array(b)) => Array(a + b),
(Dict(a), Dict(b)) => Dict(a + b), (Dict(a), Dict(b)) => Dict(a + b),
(Args(a), Args(b)) => Args(a + b),
(a, b) => mismatch!("cannot join {} with {}", a, b), (a, b) => mismatch!("cannot join {} with {}", a, b),
}) })
} }
@ -129,13 +132,14 @@ pub fn add(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
(Symbol(a), Str(b)) => Str(format_str!("{a}{b}")), (Symbol(a), Str(b)) => Str(format_str!("{a}{b}")),
(Bytes(a), Bytes(b)) => Bytes(a + b), (Bytes(a), Bytes(b)) => Bytes(a + b),
(Content(a), Content(b)) => Content(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)), (Content(a), Str(b)) => Content(a + TextElem::packed(b)),
(Str(a), Content(b)) => Content(TextElem::packed(a) + 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), (Array(a), Array(b)) => Array(a + b),
(Dict(a), Dict(b)) => Dict(a + b), (Dict(a), Dict(b)) => Dict(a + b),
(Args(a), Args(b)) => Args(a + b),
(Color(color), Length(thickness)) | (Length(thickness), Color(color)) => { (Color(color), Length(thickness)) | (Length(thickness), Color(color)) => {
Stroke::from_pair(color, thickness).into_value() Stroke::from_pair(color, thickness).into_value()

View File

@ -9,7 +9,7 @@ use wasmi::{AsContext, AsContextMut};
use crate::diag::{bail, At, SourceResult, StrResult}; use crate::diag::{bail, At, SourceResult, StrResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{func, repr, scope, ty, Bytes}; use crate::foundations::{func, repr, scope, ty, Bytes};
use crate::World; use crate::loading::{DataSource, Load};
/// A WebAssembly plugin. /// A WebAssembly plugin.
/// ///
@ -152,17 +152,14 @@ impl Plugin {
/// Creates a new plugin from a WebAssembly file. /// Creates a new plugin from a WebAssembly file.
#[func(constructor)] #[func(constructor)]
pub fn construct( pub fn construct(
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// Path to a WebAssembly file. /// A path to a WebAssembly file or raw WebAssembly bytes.
/// ///
/// For more details, see the [Paths section]($syntax/#paths). /// For more details about paths, see the [Paths section]($syntax/#paths).
path: Spanned<EcoString>, source: Spanned<DataSource>,
) -> SourceResult<Plugin> { ) -> SourceResult<Plugin> {
let Spanned { v: path, span } = path; let data = source.load(engine.world)?;
let id = span.resolve_path(&path).at(span)?; Plugin::new(data).at(source.span)
let data = engine.world.file(id).at(span)?;
Plugin::new(data).at(span)
} }
} }
@ -293,7 +290,7 @@ impl Plugin {
_ => bail!("plugin did not respect the protocol"), _ => bail!("plugin did not respect the protocol"),
}; };
Ok(output.into()) Ok(Bytes::new(output))
} }
/// An iterator over all the function names defined by the plugin. /// An iterator over all the function names defined by the plugin.

View File

@ -425,9 +425,7 @@ impl Str {
#[func] #[func]
pub fn replace( pub fn replace(
&self, &self,
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>, context: Tracked<Context>,
/// The pattern to search for. /// The pattern to search for.
pattern: StrPattern, pattern: StrPattern,
@ -577,9 +575,10 @@ impl Str {
/// of the resulting parts. /// of the resulting parts.
/// ///
/// When the empty string is used as a separator, it separates every /// When the empty string is used as a separator, it separates every
/// character in the string, along with the beginning and end of the /// character (i.e., Unicode code point) in the string, along with the
/// string. In practice, this means that the resulting list of parts /// beginning and end of the string. In practice, this means that the
/// will contain the empty string at the start and end of the list. /// resulting list of parts will contain the empty string at the start
/// and end of the list.
#[func] #[func]
pub fn split( pub fn split(
&self, &self,
@ -783,11 +782,7 @@ cast! {
v: f64 => Self::Str(repr::display_float(v).into()), v: f64 => Self::Str(repr::display_float(v).into()),
v: Decimal => Self::Str(format_str!("{}", v)), v: Decimal => Self::Str(format_str!("{}", v)),
v: Version => Self::Str(format_str!("{}", v)), v: Version => Self::Str(format_str!("{}", v)),
v: Bytes => Self::Str( v: Bytes => Self::Str(v.to_str().map_err(|_| "bytes are not valid utf-8")?),
std::str::from_utf8(&v)
.map_err(|_| "bytes are not valid utf-8")?
.into()
),
v: Label => Self::Str(v.resolve().as_str().into()), v: Label => Self::Str(v.resolve().as_str().into()),
v: Type => Self::Str(v.long_name().into()), v: Type => Self::Str(v.long_name().into()),
v: Str => Self::Str(v), v: Str => Self::Str(v),

View File

@ -12,7 +12,8 @@ use typst_utils::LazyHash;
use crate::diag::{SourceResult, Trace, Tracepoint}; use crate::diag::{SourceResult, Trace, Tracepoint};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, ty, Content, Context, Element, Func, NativeElement, Repr, Selector, cast, ty, Content, Context, Element, Func, NativeElement, OneOrMultiple, Repr,
Selector,
}; };
use crate::text::{FontFamily, FontList, TextElem}; use crate::text::{FontFamily, FontList, TextElem};
@ -775,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. /// A property that is resolved with other properties from the style chain.
pub trait Resolve { pub trait Resolve {
/// The type of the resolved output. /// The type of the resolved output.
@ -939,6 +839,13 @@ impl<T, const N: usize> Fold for SmallVec<[T; N]> {
} }
} }
impl<T> Fold for OneOrMultiple<T> {
fn fold(self, mut outer: Self) -> Self {
outer.0.extend(self.0);
outer
}
}
/// A variant of fold for foldable optional (`Option<T>`) values where an inner /// A variant of fold for foldable optional (`Option<T>`) values where an inner
/// `None` value isn't respected (contrary to `Option`'s usual `Fold` /// `None` value isn't respected (contrary to `Option`'s usual `Fold`
/// implementation, with which folding with an inner `None` always returns /// implementation, with which folding with an inner `None` always returns

View File

@ -9,7 +9,10 @@ use typst_syntax::{is_ident, Span, Spanned};
use typst_utils::hash128; use typst_utils::hash128;
use crate::diag::{bail, SourceResult, StrResult}; 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. /// A Unicode symbol.
/// ///
@ -187,7 +190,6 @@ impl Symbol {
/// ``` /// ```
#[func(constructor)] #[func(constructor)]
pub fn construct( pub fn construct(
/// The callsite span.
span: Span, span: Span,
/// The variants of the symbol. /// The variants of the symbol.
/// ///
@ -426,3 +428,31 @@ fn parts(modifiers: &str) -> impl Iterator<Item = &str> {
fn contained(modifiers: &str, m: &str) -> bool { fn contained(modifiers: &str, m: &str) -> bool {
parts(modifiers).any(|part| part == m) 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. /// Returns the current compilation target.
#[func(contextual)] #[func(contextual)]
pub fn target( pub fn target(context: Tracked<Context>) -> HintedStrResult<Target> {
/// The callsite context.
context: Tracked<Context>,
) -> HintedStrResult<Target> {
Ok(TargetElem::target_in(context.styles()?)) Ok(TargetElem::target_in(context.styles()?))
} }

View File

@ -16,7 +16,7 @@ use crate::foundations::{
fields, ops, repr, Args, Array, AutoValue, Bytes, CastInfo, Content, Datetime, fields, ops, repr, Args, Array, AutoValue, Bytes, CastInfo, Content, Datetime,
Decimal, Dict, Duration, Fold, FromValue, Func, IntoValue, Label, Module, Decimal, Dict, Duration, Fold, FromValue, Func, IntoValue, Label, Module,
NativeElement, NativeType, NoneValue, Plugin, Reflect, Repr, Resolve, Scope, Str, NativeElement, NativeType, NoneValue, Plugin, Reflect, Repr, Resolve, Scope, Str,
Styles, Symbol, Type, Version, Styles, Symbol, SymbolElem, Type, Version,
}; };
use crate::layout::{Abs, Angle, Em, Fr, Length, Ratio, Rel}; use crate::layout::{Abs, Angle, Em, Fr, Length, Ratio, Rel};
use crate::text::{RawContent, RawElem, TextElem}; use crate::text::{RawContent, RawElem, TextElem};
@ -209,7 +209,7 @@ impl Value {
Self::Decimal(v) => TextElem::packed(eco_format!("{v}")), Self::Decimal(v) => TextElem::packed(eco_format!("{v}")),
Self::Str(v) => TextElem::packed(v), Self::Str(v) => TextElem::packed(v),
Self::Version(v) => TextElem::packed(eco_format!("{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::Content(v) => v,
Self::Module(module) => module.content(), Self::Module(module) => module.content(),
_ => RawElem::new(RawContent::Text(self.repr())) _ => RawElem::new(RawContent::Text(self.repr()))
@ -459,15 +459,15 @@ impl<'de> Visitor<'de> for ValueVisitor {
} }
fn visit_bytes<E: Error>(self, v: &[u8]) -> Result<Self::Value, E> { fn visit_bytes<E: Error>(self, v: &[u8]) -> Result<Self::Value, E> {
Ok(Bytes::from(v).into_value()) Ok(Bytes::new(v.to_vec()).into_value())
} }
fn visit_borrowed_bytes<E: Error>(self, v: &'de [u8]) -> Result<Self::Value, E> { fn visit_borrowed_bytes<E: Error>(self, v: &'de [u8]) -> Result<Self::Value, E> {
Ok(Bytes::from(v).into_value()) Ok(Bytes::new(v.to_vec()).into_value())
} }
fn visit_byte_buf<E: Error>(self, v: Vec<u8>) -> Result<Self::Value, E> { fn visit_byte_buf<E: Error>(self, v: Vec<u8>) -> Result<Self::Value, E> {
Ok(Bytes::from(v).into_value()) Ok(Bytes::new(v).into_value())
} }
fn visit_none<E: Error>(self) -> Result<Self::Value, E> { fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
@ -656,7 +656,7 @@ primitive! { Duration: "duration", Duration }
primitive! { Content: "content", primitive! { Content: "content",
Content, Content,
None => Content::empty(), None => Content::empty(),
Symbol(v) => TextElem::packed(v.get()), Symbol(v) => SymbolElem::packed(v.get()),
Str(v) => TextElem::packed(v) Str(v) => TextElem::packed(v)
} }
primitive! { Styles: "styles", Styles } primitive! { Styles: "styles", Styles }

View File

@ -210,7 +210,10 @@ impl HtmlAttr {
/// Creates a compile-time constant `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] #[track_caller]
pub const fn constant(string: &'static str) -> Self { pub const fn constant(string: &'static str) -> Self {
if string.is_empty() { if string.is_empty() {
@ -472,17 +475,55 @@ pub mod tag {
wbr 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 /// Whether nodes with the tag have the CSS property `display: block` by
/// default. /// 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 { pub fn is_block_by_default(tag: HtmlTag) -> bool {
matches!( matches!(
tag, tag,
@ -569,42 +610,29 @@ pub mod tag {
) )
} }
/// Whether this is a void tag whose associated element may not have a /// Whether nodes with the tag have the CSS property `display: table(-.*)?`
/// children. /// by default.
pub fn is_void(tag: HtmlTag) -> bool { pub fn is_tabular_by_default(tag: HtmlTag) -> bool {
matches!( matches!(
tag, tag,
self::area self::table
| self::base | self::thead
| self::br | self::tbody
| self::tfoot
| self::tr
| self::th
| self::td
| self::caption
| self::col | self::col
| self::embed | self::colgroup
| 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)
}
} }
/// Predefined constants for HTML attributes. /// Predefined constants for HTML attributes.
/// ///
/// Note: These are very incomplete. /// Note: These are very incomplete.
#[allow(non_upper_case_globals)]
pub mod attr { pub mod attr {
use super::HtmlAttr; use super::HtmlAttr;
@ -619,13 +647,18 @@ pub mod attr {
attrs! { attrs! {
charset charset
cite
colspan
content content
href href
name name
value reversed
role role
rowspan
start
style
value
} }
#[allow(non_upper_case_globals)]
pub const aria_level: HtmlAttr = HtmlAttr::constant("aria-level"); pub const aria_level: HtmlAttr = HtmlAttr::constant("aria-level");
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -100,7 +100,7 @@ pub struct AlignElem {
impl Show for Packed<AlignElem> { impl Show for Packed<AlignElem> {
#[typst_macros::time(name = "align", span = self.span())] #[typst_macros::time(name = "align", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(self.body().clone().aligned(self.alignment(styles))) Ok(self.body.clone().aligned(self.alignment(styles)))
} }
} }

View File

@ -14,9 +14,9 @@ use crate::visualize::{Paint, Stroke};
/// An inline-level container that sizes content. /// An inline-level container that sizes content.
/// ///
/// All elements except inline math, text, and boxes are block-level and cannot /// 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 /// occur inside of a [paragraph]($par). The box function can be used to
/// elements into a paragraph. Boxes take the size of their contents by default /// integrate such elements into a paragraph. Boxes take the size of their
/// but can also be sized explicitly. /// contents by default but can also be sized explicitly.
/// ///
/// # Example /// # Example
/// ```example /// ```example
@ -166,7 +166,7 @@ impl Packed<InlineElem> {
styles: StyleChain, styles: StyleChain,
region: Size, region: Size,
) -> SourceResult<Vec<InlineItem>> { ) -> SourceResult<Vec<InlineItem>> {
self.body().call(engine, locator, styles, region) self.body.call(engine, locator, styles, region)
} }
} }
@ -184,6 +184,10 @@ pub enum InlineItem {
/// Such a container can be used to separate content, size it, and give it a /// Such a container can be used to separate content, size it, and give it a
/// background or border. /// 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 /// # Examples
/// With a block, you can give a background to content while still allowing it /// With a block, you can give a background to content while still allowing it
/// to break across multiple pages. /// to break across multiple pages.

View File

@ -4,16 +4,13 @@ use std::fmt::{self, Debug, Formatter};
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use std::sync::Arc; use std::sync::Arc;
use smallvec::SmallVec;
use typst_syntax::Span; use typst_syntax::Span;
use typst_utils::{LazyHash, Numeric}; 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::introspection::{Location, Tag};
use crate::layout::{ use crate::layout::{Abs, Axes, FixedAlignment, Length, Point, Size, Transform};
Abs, Axes, FixedAlignment, HideElem, Length, Point, Size, Transform, use crate::model::Destination;
};
use crate::model::{Destination, LinkElem};
use crate::text::TextItem; use crate::text::TextItem;
use crate::visualize::{Color, Curve, FixedStroke, Geometry, Image, Paint, Shape}; 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. /// Hide all content in the frame, but keep metadata.
pub fn hide(&mut self) { pub fn hide(&mut self) {
Arc::make_mut(&mut self.items).retain_mut(|(_, item)| match item { Arc::make_mut(&mut self.items).retain_mut(|(_, item)| match item {

View File

@ -1,3 +1,5 @@
pub mod resolve;
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use std::sync::Arc; use std::sync::Arc;
@ -747,7 +749,7 @@ cast! {
impl Show for Packed<GridCell> { impl Show for Packed<GridCell> {
fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
show_grid_cell(self.body().clone(), self.inset(styles), self.align(styles)) show_grid_cell(self.body.clone(), self.inset(styles), self.align(styles))
} }
} }

View File

@ -2,19 +2,463 @@ use std::num::NonZeroUsize;
use std::sync::Arc; use std::sync::Arc;
use ecow::eco_format; use ecow::eco_format;
use typst_library::diag::{bail, At, Hint, HintedStrResult, HintedString, SourceResult}; use typst_library::diag::{
bail, At, Hint, HintedStrResult, HintedString, SourceResult, Trace, Tracepoint,
};
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::{Content, Smart, StyleChain}; use typst_library::foundations::{Content, Fold, Packed, Smart, StyleChain};
use typst_library::introspection::Locator; use typst_library::introspection::Locator;
use typst_library::layout::{ use typst_library::layout::{
Abs, Alignment, Axes, Celled, Fragment, Length, Regions, Rel, ResolvedCelled, Sides, Abs, Alignment, Axes, Celled, GridCell, GridChild, GridElem, GridItem, Length,
Sizing, OuterHAlignment, OuterVAlignment, Rel, ResolvedCelled, Sides, Sizing,
}; };
use typst_library::model::{TableCell, TableChild, TableElem, TableItem};
use typst_library::text::TextElem;
use typst_library::visualize::{Paint, Stroke}; use typst_library::visualize::{Paint, Stroke};
use typst_library::Dir;
use typst_syntax::Span; use typst_syntax::Span;
use typst_utils::NonZeroExt; use typst_utils::NonZeroExt;
use super::{Footer, Header, Line, Repeatable}; /// Convert a grid to a cell grid.
#[typst_macros::time(span = elem.span())]
pub fn grid_to_cellgrid<'a>(
elem: &Packed<GridElem>,
engine: &mut Engine,
locator: Locator<'a>,
styles: StyleChain,
) -> SourceResult<CellGrid<'a>> {
let inset = elem.inset(styles);
let align = elem.align(styles);
let columns = elem.columns(styles);
let rows = elem.rows(styles);
let column_gutter = elem.column_gutter(styles);
let row_gutter = elem.row_gutter(styles);
let fill = elem.fill(styles);
let stroke = elem.stroke(styles);
let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
// Use trace to link back to the grid when a specific cell errors
let tracepoint = || Tracepoint::Call(Some(eco_format!("grid")));
let resolve_item = |item: &GridItem| grid_item_to_resolvable(item, styles);
let children = elem.children.iter().map(|child| match child {
GridChild::Header(header) => ResolvableGridChild::Header {
repeat: header.repeat(styles),
span: header.span(),
items: header.children.iter().map(resolve_item),
},
GridChild::Footer(footer) => ResolvableGridChild::Footer {
repeat: footer.repeat(styles),
span: footer.span(),
items: footer.children.iter().map(resolve_item),
},
GridChild::Item(item) => {
ResolvableGridChild::Item(grid_item_to_resolvable(item, styles))
}
});
CellGrid::resolve(
tracks,
gutter,
locator,
children,
fill,
align,
&inset,
&stroke,
engine,
styles,
elem.span(),
)
.trace(engine.world, tracepoint, elem.span())
}
/// Convert a table to a cell grid.
#[typst_macros::time(span = elem.span())]
pub fn table_to_cellgrid<'a>(
elem: &Packed<TableElem>,
engine: &mut Engine,
locator: Locator<'a>,
styles: StyleChain,
) -> SourceResult<CellGrid<'a>> {
let inset = elem.inset(styles);
let align = elem.align(styles);
let columns = elem.columns(styles);
let rows = elem.rows(styles);
let column_gutter = elem.column_gutter(styles);
let row_gutter = elem.row_gutter(styles);
let fill = elem.fill(styles);
let stroke = elem.stroke(styles);
let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
// Use trace to link back to the table when a specific cell errors
let tracepoint = || Tracepoint::Call(Some(eco_format!("table")));
let resolve_item = |item: &TableItem| table_item_to_resolvable(item, styles);
let children = elem.children.iter().map(|child| match child {
TableChild::Header(header) => ResolvableGridChild::Header {
repeat: header.repeat(styles),
span: header.span(),
items: header.children.iter().map(resolve_item),
},
TableChild::Footer(footer) => ResolvableGridChild::Footer {
repeat: footer.repeat(styles),
span: footer.span(),
items: footer.children.iter().map(resolve_item),
},
TableChild::Item(item) => {
ResolvableGridChild::Item(table_item_to_resolvable(item, styles))
}
});
CellGrid::resolve(
tracks,
gutter,
locator,
children,
fill,
align,
&inset,
&stroke,
engine,
styles,
elem.span(),
)
.trace(engine.world, tracepoint, elem.span())
}
fn grid_item_to_resolvable(
item: &GridItem,
styles: StyleChain,
) -> ResolvableGridItem<Packed<GridCell>> {
match item {
GridItem::HLine(hline) => ResolvableGridItem::HLine {
y: hline.y(styles),
start: hline.start(styles),
end: hline.end(styles),
stroke: hline.stroke(styles),
span: hline.span(),
position: match hline.position(styles) {
OuterVAlignment::Top => LinePosition::Before,
OuterVAlignment::Bottom => LinePosition::After,
},
},
GridItem::VLine(vline) => ResolvableGridItem::VLine {
x: vline.x(styles),
start: vline.start(styles),
end: vline.end(styles),
stroke: vline.stroke(styles),
span: vline.span(),
position: match vline.position(styles) {
OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => {
LinePosition::After
}
OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => {
LinePosition::Before
}
OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before,
OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After,
},
},
GridItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()),
}
}
fn table_item_to_resolvable(
item: &TableItem,
styles: StyleChain,
) -> ResolvableGridItem<Packed<TableCell>> {
match item {
TableItem::HLine(hline) => ResolvableGridItem::HLine {
y: hline.y(styles),
start: hline.start(styles),
end: hline.end(styles),
stroke: hline.stroke(styles),
span: hline.span(),
position: match hline.position(styles) {
OuterVAlignment::Top => LinePosition::Before,
OuterVAlignment::Bottom => LinePosition::After,
},
},
TableItem::VLine(vline) => ResolvableGridItem::VLine {
x: vline.x(styles),
start: vline.start(styles),
end: vline.end(styles),
stroke: vline.stroke(styles),
span: vline.span(),
position: match vline.position(styles) {
OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => {
LinePosition::After
}
OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => {
LinePosition::Before
}
OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before,
OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After,
},
},
TableItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()),
}
}
impl ResolvableCell for Packed<TableCell> {
fn resolve_cell<'a>(
mut self,
x: usize,
y: usize,
fill: &Option<Paint>,
align: Smart<Alignment>,
inset: Sides<Option<Rel<Length>>>,
stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>,
breakable: bool,
locator: Locator<'a>,
styles: StyleChain,
) -> Cell<'a> {
let cell = &mut *self;
let colspan = cell.colspan(styles);
let rowspan = cell.rowspan(styles);
let breakable = cell.breakable(styles).unwrap_or(breakable);
let fill = cell.fill(styles).unwrap_or_else(|| fill.clone());
let cell_stroke = cell.stroke(styles);
let stroke_overridden =
cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_))));
// Using a typical 'Sides' fold, an unspecified side loses to a
// specified side. Additionally, when both are specified, an inner
// None wins over the outer Some, and vice-versa. When both are
// specified and Some, fold occurs, which, remarkably, leads to an Arc
// clone.
//
// In the end, we flatten because, for layout purposes, an unspecified
// cell stroke is the same as specifying 'none', so we equate the two
// concepts.
let stroke = cell_stroke.fold(stroke).map(Option::flatten);
cell.push_x(Smart::Custom(x));
cell.push_y(Smart::Custom(y));
cell.push_fill(Smart::Custom(fill.clone()));
cell.push_align(match align {
Smart::Custom(align) => {
Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align)))
}
// Don't fold if the table is using outer alignment. Use the
// cell's alignment instead (which, in the end, will fold with
// the outer alignment when it is effectively displayed).
Smart::Auto => cell.align(styles),
});
cell.push_inset(Smart::Custom(
cell.inset(styles).map_or(inset, |inner| inner.fold(inset)),
));
cell.push_stroke(
// Here we convert the resolved stroke to a regular stroke, however
// with resolved units (that is, 'em' converted to absolute units).
// We also convert any stroke unspecified by both the cell and the
// outer stroke ('None' in the folded stroke) to 'none', that is,
// all sides are present in the resulting Sides object accessible
// by show rules on table cells.
stroke.as_ref().map(|side| {
Some(side.as_ref().map(|cell_stroke| {
Arc::new((**cell_stroke).clone().map(Length::from))
}))
}),
);
cell.push_breakable(Smart::Custom(breakable));
Cell {
body: self.pack(),
locator,
fill,
colspan,
rowspan,
stroke,
stroke_overridden,
breakable,
}
}
fn x(&self, styles: StyleChain) -> Smart<usize> {
(**self).x(styles)
}
fn y(&self, styles: StyleChain) -> Smart<usize> {
(**self).y(styles)
}
fn colspan(&self, styles: StyleChain) -> NonZeroUsize {
(**self).colspan(styles)
}
fn rowspan(&self, styles: StyleChain) -> NonZeroUsize {
(**self).rowspan(styles)
}
fn span(&self) -> Span {
Packed::span(self)
}
}
impl ResolvableCell for Packed<GridCell> {
fn resolve_cell<'a>(
mut self,
x: usize,
y: usize,
fill: &Option<Paint>,
align: Smart<Alignment>,
inset: Sides<Option<Rel<Length>>>,
stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>,
breakable: bool,
locator: Locator<'a>,
styles: StyleChain,
) -> Cell<'a> {
let cell = &mut *self;
let colspan = cell.colspan(styles);
let rowspan = cell.rowspan(styles);
let breakable = cell.breakable(styles).unwrap_or(breakable);
let fill = cell.fill(styles).unwrap_or_else(|| fill.clone());
let cell_stroke = cell.stroke(styles);
let stroke_overridden =
cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_))));
// Using a typical 'Sides' fold, an unspecified side loses to a
// specified side. Additionally, when both are specified, an inner
// None wins over the outer Some, and vice-versa. When both are
// specified and Some, fold occurs, which, remarkably, leads to an Arc
// clone.
//
// In the end, we flatten because, for layout purposes, an unspecified
// cell stroke is the same as specifying 'none', so we equate the two
// concepts.
let stroke = cell_stroke.fold(stroke).map(Option::flatten);
cell.push_x(Smart::Custom(x));
cell.push_y(Smart::Custom(y));
cell.push_fill(Smart::Custom(fill.clone()));
cell.push_align(match align {
Smart::Custom(align) => {
Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align)))
}
// Don't fold if the grid is using outer alignment. Use the
// cell's alignment instead (which, in the end, will fold with
// the outer alignment when it is effectively displayed).
Smart::Auto => cell.align(styles),
});
cell.push_inset(Smart::Custom(
cell.inset(styles).map_or(inset, |inner| inner.fold(inset)),
));
cell.push_stroke(
// Here we convert the resolved stroke to a regular stroke, however
// with resolved units (that is, 'em' converted to absolute units).
// We also convert any stroke unspecified by both the cell and the
// outer stroke ('None' in the folded stroke) to 'none', that is,
// all sides are present in the resulting Sides object accessible
// by show rules on grid cells.
stroke.as_ref().map(|side| {
Some(side.as_ref().map(|cell_stroke| {
Arc::new((**cell_stroke).clone().map(Length::from))
}))
}),
);
cell.push_breakable(Smart::Custom(breakable));
Cell {
body: self.pack(),
locator,
fill,
colspan,
rowspan,
stroke,
stroke_overridden,
breakable,
}
}
fn x(&self, styles: StyleChain) -> Smart<usize> {
(**self).x(styles)
}
fn y(&self, styles: StyleChain) -> Smart<usize> {
(**self).y(styles)
}
fn colspan(&self, styles: StyleChain) -> NonZeroUsize {
(**self).colspan(styles)
}
fn rowspan(&self, styles: StyleChain) -> NonZeroUsize {
(**self).rowspan(styles)
}
fn span(&self) -> Span {
Packed::span(self)
}
}
/// Represents an explicit grid line (horizontal or vertical) specified by the
/// user.
pub struct Line {
/// The index of the track after this line. This will be the index of the
/// row a horizontal line is above of, or of the column right after a
/// vertical line.
///
/// Must be within `0..=tracks.len()` (where `tracks` is either `grid.cols`
/// or `grid.rows`, ignoring gutter tracks, as appropriate).
pub index: usize,
/// The index of the track at which this line starts being drawn.
/// This is the first column a horizontal line appears in, or the first row
/// a vertical line appears in.
///
/// Must be within `0..tracks.len()` minus gutter tracks.
pub start: usize,
/// The index after the last track through which the line is drawn.
/// Thus, the line is drawn through tracks `start..end` (note that `end` is
/// exclusive).
///
/// Must be within `1..=tracks.len()` minus gutter tracks.
/// `None` indicates the line should go all the way to the end.
pub end: Option<NonZeroUsize>,
/// The line's stroke. This is `None` when the line is explicitly used to
/// override a previously specified line.
pub stroke: Option<Arc<Stroke<Abs>>>,
/// The line's position in relation to the track with its index.
pub position: LinePosition,
}
/// A repeatable grid header. Starts at the first row.
pub struct Header {
/// The index after the last row included in this header.
pub end: usize,
}
/// A repeatable grid footer. Stops at the last row.
pub struct Footer {
/// The first row included in this footer.
pub start: usize,
}
/// A possibly repeatable grid object.
/// It still exists even when not repeatable, but must not have additional
/// considerations by grid layout, other than for consistency (such as making
/// a certain group of rows unbreakable).
pub enum Repeatable<T> {
Repeated(T),
NotRepeated(T),
}
impl<T> Repeatable<T> {
/// Gets the value inside this repeatable, regardless of whether
/// it repeats.
pub fn unwrap(&self) -> &T {
match self {
Self::Repeated(repeated) => repeated,
Self::NotRepeated(not_repeated) => not_repeated,
}
}
/// Returns `Some` if the value is repeated, `None` otherwise.
pub fn as_repeated(&self) -> Option<&T> {
match self {
Self::Repeated(repeated) => Some(repeated),
Self::NotRepeated(_) => None,
}
}
}
/// Used for cell-like elements which are aware of their final properties in /// Used for cell-like elements which are aware of their final properties in
/// the table, and may have property overrides. /// the table, and may have property overrides.
@ -131,26 +575,6 @@ impl<'a> Cell<'a> {
breakable: true, breakable: true,
} }
} }
/// Layout the cell into the given regions.
///
/// The `disambiguator` indicates which instance of this cell this should be
/// layouted as. For normal cells, it is always `0`, but for headers and
/// footers, it indicates the index of the header/footer among all. See the
/// [`Locator`] docs for more details on the concepts behind this.
pub fn layout(
&self,
engine: &mut Engine,
disambiguator: usize,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let mut locator = self.locator.relayout();
if disambiguator > 0 {
locator = locator.split().next_inner(disambiguator as u128);
}
crate::layout_fragment(engine, &self.body, locator, styles, regions)
}
} }
/// Indicates whether the line should be drawn before or after the track with /// Indicates whether the line should be drawn before or after the track with
@ -178,7 +602,7 @@ pub enum Entry<'a> {
impl<'a> Entry<'a> { impl<'a> Entry<'a> {
/// Obtains the cell inside this entry, if this is not a merged cell. /// 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 { match self {
Self::Cell(cell) => Some(cell), Self::Cell(cell) => Some(cell),
Self::Merged { .. } => None, Self::Merged { .. } => None,

View File

@ -29,6 +29,6 @@ pub struct HideElem {
impl Show for Packed<HideElem> { impl Show for Packed<HideElem> {
#[typst_macros::time(name = "hide", span = self.span())] #[typst_macros::time(name = "hide", span = self.span())]
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(self.body().clone().styled(HideElem::set_hidden(true))) Ok(self.body.clone().styled(HideElem::set_hidden(true)))
} }
} }

View File

@ -54,7 +54,6 @@ use crate::layout::{BlockElem, Size};
/// corresponding page dimension is set to `{auto}`. /// corresponding page dimension is set to `{auto}`.
#[func] #[func]
pub fn layout( pub fn layout(
/// The call span of this function.
span: Span, span: Span,
/// A function to call with the outer container's size. Its return value is /// A function to call with the outer container's size. Its return value is
/// displayed in the document. /// displayed in the document.
@ -89,7 +88,7 @@ impl Show for Packed<LayoutElem> {
let loc = elem.location().unwrap(); let loc = elem.location().unwrap();
let context = Context::new(Some(loc), Some(styles)); let context = Context::new(Some(loc), Some(styles));
let result = elem let result = elem
.func() .func
.call( .call(
engine, engine,
context.track(), context.track(),

View File

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

View File

@ -12,7 +12,7 @@ mod em;
mod fr; mod fr;
mod fragment; mod fragment;
mod frame; mod frame;
mod grid; pub mod grid;
mod hide; mod hide;
#[path = "layout.rs"] #[path = "layout.rs"]
mod layout_; mod layout_;

View File

@ -10,7 +10,7 @@ use crate::layout::{BlockElem, Length};
/// Space may be inserted between the instances of the body parameter, so be /// Space may be inserted between the instances of the body parameter, so be
/// sure to adjust the [`justify`]($repeat.justify) parameter accordingly. /// 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. /// infinite content.
/// ///
/// # Example /// # Example

View File

@ -21,6 +21,7 @@ pub mod layout;
pub mod loading; pub mod loading;
pub mod math; pub mod math;
pub mod model; pub mod model;
pub mod pdf;
pub mod routines; pub mod routines;
pub mod symbols; pub mod symbols;
pub mod text; pub mod text;
@ -249,6 +250,7 @@ fn global(math: Module, inputs: Dict, features: &Features) -> Module {
self::introspection::define(&mut global); self::introspection::define(&mut global);
self::loading::define(&mut global); self::loading::define(&mut global);
self::symbols::define(&mut global); self::symbols::define(&mut global);
self::pdf::define(&mut global);
global.reset_category(); global.reset_category();
if features.is_enabled(Feature::Html) { if features.is_enabled(Feature::Html) {
global.define_module(self::html::module()); global.define_module(self::html::module());

View File

@ -1,10 +1,10 @@
use ecow::{eco_format, EcoString}; use ecow::eco_format;
use typst_syntax::Spanned; use typst_syntax::Spanned;
use crate::diag::{At, SourceResult}; use crate::diag::{At, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{func, scope, Bytes, Value}; use crate::foundations::{func, scope, Bytes, Value};
use crate::World; use crate::loading::{DataSource, Load};
/// Reads structured data from a CBOR file. /// Reads structured data from a CBOR file.
/// ///
@ -19,31 +19,31 @@ use crate::World;
/// floating point numbers, which may result in an approximative value. /// floating point numbers, which may result in an approximative value.
#[func(scope, title = "CBOR")] #[func(scope, title = "CBOR")]
pub fn cbor( pub fn cbor(
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// Path to a CBOR file. /// A path to a CBOR file or raw CBOR bytes.
/// ///
/// For more details, see the [Paths section]($syntax/#paths). /// For more details about paths, see the [Paths section]($syntax/#paths).
path: Spanned<EcoString>, source: Spanned<DataSource>,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let Spanned { v: path, span } = path; let data = source.load(engine.world)?;
let id = span.resolve_path(&path).at(span)?; ciborium::from_reader(data.as_slice())
let data = engine.world.file(id).at(span)?; .map_err(|err| eco_format!("failed to parse CBOR ({err})"))
cbor::decode(Spanned::new(data, span)) .at(source.span)
} }
#[scope] #[scope]
impl cbor { impl cbor {
/// Reads structured data from CBOR bytes. /// Reads structured data from CBOR bytes.
///
/// This function is deprecated. The [`cbor`] function now accepts bytes
/// directly.
#[func(title = "Decode CBOR")] #[func(title = "Decode CBOR")]
pub fn decode( pub fn decode(
/// cbor data. engine: &mut Engine,
/// CBOR data.
data: Spanned<Bytes>, data: Spanned<Bytes>,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let Spanned { v: data, span } = data; cbor(engine, data.map(DataSource::Bytes))
ciborium::from_reader(data.as_slice())
.map_err(|err| eco_format!("failed to parse CBOR ({err})"))
.at(span)
} }
/// Encode structured data into CBOR bytes. /// Encode structured data into CBOR bytes.
@ -55,7 +55,7 @@ impl cbor {
let Spanned { v: value, span } = value; let Spanned { v: value, span } = value;
let mut res = Vec::new(); let mut res = Vec::new();
ciborium::into_writer(&value, &mut res) ciborium::into_writer(&value, &mut res)
.map(|_| res.into()) .map(|_| Bytes::new(res))
.map_err(|err| eco_format!("failed to encode value as CBOR ({err})")) .map_err(|err| eco_format!("failed to encode value as CBOR ({err})"))
.at(span) .at(span)
} }

View File

@ -4,8 +4,7 @@ use typst_syntax::Spanned;
use crate::diag::{bail, At, SourceResult}; use crate::diag::{bail, At, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{cast, func, scope, Array, Dict, IntoValue, Type, Value}; use crate::foundations::{cast, func, scope, Array, Dict, IntoValue, Type, Value};
use crate::loading::Readable; use crate::loading::{DataSource, Load, Readable};
use crate::World;
/// Reads structured data from a CSV file. /// Reads structured data from a CSV file.
/// ///
@ -26,12 +25,11 @@ use crate::World;
/// ``` /// ```
#[func(scope, title = "CSV")] #[func(scope, title = "CSV")]
pub fn csv( pub fn csv(
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// Path to a CSV file. /// Path to a CSV file or raw CSV bytes.
/// ///
/// For more details, see the [Paths section]($syntax/#paths). /// For more details about paths, see the [Paths section]($syntax/#paths).
path: Spanned<EcoString>, source: Spanned<DataSource>,
/// The delimiter that separates columns in the CSV file. /// The delimiter that separates columns in the CSV file.
/// Must be a single ASCII character. /// Must be a single ASCII character.
#[named] #[named]
@ -48,17 +46,62 @@ pub fn csv(
#[default(RowType::Array)] #[default(RowType::Array)]
row_type: RowType, row_type: RowType,
) -> SourceResult<Array> { ) -> SourceResult<Array> {
let Spanned { v: path, span } = path; let data = source.load(engine.world)?;
let id = span.resolve_path(&path).at(span)?;
let data = engine.world.file(id).at(span)?; let mut builder = ::csv::ReaderBuilder::new();
self::csv::decode(Spanned::new(Readable::Bytes(data), span), delimiter, row_type) let has_headers = row_type == RowType::Dict;
builder.has_headers(has_headers);
builder.delimiter(delimiter.0 as u8);
// Counting lines from 1 by default.
let mut line_offset: usize = 1;
let mut reader = builder.from_reader(data.as_slice());
let mut headers: Option<::csv::StringRecord> = None;
if has_headers {
// Counting lines from 2 because we have a header.
line_offset += 1;
headers = Some(
reader
.headers()
.map_err(|err| format_csv_error(err, 1))
.at(source.span)?
.clone(),
);
}
let mut array = Array::new();
for (line, result) in reader.records().enumerate() {
// Original solution was to use line from error, but that is
// incorrect with `has_headers` set to `false`. See issue:
// https://github.com/BurntSushi/rust-csv/issues/184
let line = line + line_offset;
let row = result.map_err(|err| format_csv_error(err, line)).at(source.span)?;
let item = if let Some(headers) = &headers {
let mut dict = Dict::new();
for (field, value) in headers.iter().zip(&row) {
dict.insert(field.into(), value.into_value());
}
dict.into_value()
} else {
let sub = row.into_iter().map(|field| field.into_value()).collect();
Value::Array(sub)
};
array.push(item);
}
Ok(array)
} }
#[scope] #[scope]
impl csv { impl csv {
/// Reads structured data from a CSV string/bytes. /// Reads structured data from a CSV string/bytes.
///
/// This function is deprecated. The [`csv`] function now accepts bytes
/// directly.
#[func(title = "Decode CSV")] #[func(title = "Decode CSV")]
pub fn decode( pub fn decode(
engine: &mut Engine,
/// CSV data. /// CSV data.
data: Spanned<Readable>, data: Spanned<Readable>,
/// The delimiter that separates columns in the CSV file. /// The delimiter that separates columns in the CSV file.
@ -77,51 +120,7 @@ impl csv {
#[default(RowType::Array)] #[default(RowType::Array)]
row_type: RowType, row_type: RowType,
) -> SourceResult<Array> { ) -> SourceResult<Array> {
let Spanned { v: data, span } = data; csv(engine, data.map(Readable::into_source), delimiter, row_type)
let has_headers = row_type == RowType::Dict;
let mut builder = ::csv::ReaderBuilder::new();
builder.has_headers(has_headers);
builder.delimiter(delimiter.0 as u8);
// Counting lines from 1 by default.
let mut line_offset: usize = 1;
let mut reader = builder.from_reader(data.as_slice());
let mut headers: Option<::csv::StringRecord> = None;
if has_headers {
// Counting lines from 2 because we have a header.
line_offset += 1;
headers = Some(
reader
.headers()
.map_err(|err| format_csv_error(err, 1))
.at(span)?
.clone(),
);
}
let mut array = Array::new();
for (line, result) in reader.records().enumerate() {
// Original solution was to use line from error, but that is
// incorrect with `has_headers` set to `false`. See issue:
// https://github.com/BurntSushi/rust-csv/issues/184
let line = line + line_offset;
let row = result.map_err(|err| format_csv_error(err, line)).at(span)?;
let item = if let Some(headers) = &headers {
let mut dict = Dict::new();
for (field, value) in headers.iter().zip(&row) {
dict.insert(field.into(), value.into_value());
}
dict.into_value()
} else {
let sub = row.into_iter().map(|field| field.into_value()).collect();
Value::Array(sub)
};
array.push(item);
}
Ok(array)
} }
} }
@ -137,18 +136,10 @@ impl Default for Delimiter {
cast! { cast! {
Delimiter, Delimiter,
self => self.0.into_value(), self => self.0.into_value(),
v: EcoString => { c: char => if c.is_ascii() {
let mut chars = v.chars(); Self(c)
let first = chars.next().ok_or("delimiter must not be empty")?; } else {
if chars.next().is_some() { bail!("delimiter must be an ASCII character")
bail!("delimiter must be a single character");
}
if !first.is_ascii() {
bail!("delimiter must be an ASCII character");
}
Self(first)
}, },
} }

View File

@ -1,11 +1,10 @@
use ecow::{eco_format, EcoString}; use ecow::eco_format;
use typst_syntax::Spanned; use typst_syntax::Spanned;
use crate::diag::{At, SourceResult}; use crate::diag::{At, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{func, scope, Str, Value}; use crate::foundations::{func, scope, Str, Value};
use crate::loading::Readable; use crate::loading::{DataSource, Load, Readable};
use crate::World;
/// Reads structured data from a JSON file. /// Reads structured data from a JSON file.
/// ///
@ -51,31 +50,31 @@ use crate::World;
/// ``` /// ```
#[func(scope, title = "JSON")] #[func(scope, title = "JSON")]
pub fn json( pub fn json(
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// Path to a JSON file. /// Path to a JSON file or raw JSON bytes.
/// ///
/// For more details, see the [Paths section]($syntax/#paths). /// For more details about paths, see the [Paths section]($syntax/#paths).
path: Spanned<EcoString>, source: Spanned<DataSource>,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let Spanned { v: path, span } = path; let data = source.load(engine.world)?;
let id = span.resolve_path(&path).at(span)?; serde_json::from_slice(data.as_slice())
let data = engine.world.file(id).at(span)?; .map_err(|err| eco_format!("failed to parse JSON ({err})"))
json::decode(Spanned::new(Readable::Bytes(data), span)) .at(source.span)
} }
#[scope] #[scope]
impl json { impl json {
/// Reads structured data from a JSON string/bytes. /// Reads structured data from a JSON string/bytes.
///
/// This function is deprecated. The [`json`] function now accepts bytes
/// directly.
#[func(title = "Decode JSON")] #[func(title = "Decode JSON")]
pub fn decode( pub fn decode(
engine: &mut Engine,
/// JSON data. /// JSON data.
data: Spanned<Readable>, data: Spanned<Readable>,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let Spanned { v: data, span } = data; json(engine, data.map(Readable::into_source))
serde_json::from_slice(data.as_slice())
.map_err(|err| eco_format!("failed to parse JSON ({err})"))
.at(span)
} }
/// Encodes structured data into a JSON string. /// Encodes structured data into a JSON string.

View File

@ -15,6 +15,10 @@ mod xml_;
#[path = "yaml.rs"] #[path = "yaml.rs"]
mod yaml_; mod yaml_;
use comemo::Tracked;
use ecow::EcoString;
use typst_syntax::Spanned;
pub use self::cbor_::*; pub use self::cbor_::*;
pub use self::csv_::*; pub use self::csv_::*;
pub use self::json_::*; pub use self::json_::*;
@ -23,7 +27,10 @@ pub use self::toml_::*;
pub use self::xml_::*; pub use self::xml_::*;
pub use self::yaml_::*; pub use self::yaml_::*;
use crate::diag::{At, SourceResult};
use crate::foundations::OneOrMultiple;
use crate::foundations::{cast, category, Bytes, Category, Scope, Str}; use crate::foundations::{cast, category, Bytes, Category, Scope, Str};
use crate::World;
/// Data loading from external files. /// Data loading from external files.
/// ///
@ -44,6 +51,76 @@ pub(super) fn define(global: &mut Scope) {
global.define_func::<xml>(); global.define_func::<xml>();
} }
/// Something we can retrieve byte data from.
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum DataSource {
/// A path to a file.
Path(EcoString),
/// Raw bytes.
Bytes(Bytes),
}
cast! {
DataSource,
self => match self {
Self::Path(v) => v.into_value(),
Self::Bytes(v) => v.into_value(),
},
v: EcoString => Self::Path(v),
v: Bytes => Self::Bytes(v),
}
/// Loads data from a path or provided bytes.
pub trait Load {
/// Bytes or a list of bytes (if there are multiple sources).
type Output;
/// Load the bytes.
fn load(&self, world: Tracked<dyn World + '_>) -> SourceResult<Self::Output>;
}
impl Load for Spanned<DataSource> {
type Output = Bytes;
fn load(&self, world: Tracked<dyn World + '_>) -> SourceResult<Bytes> {
self.as_ref().load(world)
}
}
impl Load for Spanned<&DataSource> {
type Output = Bytes;
fn load(&self, world: Tracked<dyn World + '_>) -> SourceResult<Bytes> {
match &self.v {
DataSource::Path(path) => {
let file_id = self.span.resolve_path(path).at(self.span)?;
world.file(file_id).at(self.span)
}
DataSource::Bytes(bytes) => Ok(bytes.clone()),
}
}
}
impl Load for Spanned<OneOrMultiple<DataSource>> {
type Output = Vec<Bytes>;
fn load(&self, world: Tracked<dyn World + '_>) -> SourceResult<Vec<Bytes>> {
self.as_ref().load(world)
}
}
impl Load for Spanned<&OneOrMultiple<DataSource>> {
type Output = Vec<Bytes>;
fn load(&self, world: Tracked<dyn World + '_>) -> SourceResult<Vec<Bytes>> {
self.v
.0
.iter()
.map(|source| Spanned::new(source, self.span).load(world))
.collect()
}
}
/// A value that can be read from a file. /// A value that can be read from a file.
#[derive(Debug, Clone, PartialEq, Hash)] #[derive(Debug, Clone, PartialEq, Hash)]
pub enum Readable { pub enum Readable {
@ -54,18 +131,15 @@ pub enum Readable {
} }
impl Readable { impl Readable {
pub fn as_slice(&self) -> &[u8] { pub fn into_bytes(self) -> Bytes {
match self { match self {
Readable::Bytes(v) => v, Self::Bytes(v) => v,
Readable::Str(v) => v.as_bytes(), Self::Str(v) => Bytes::from_string(v),
} }
} }
pub fn as_str(&self) -> Option<&str> { pub fn into_source(self) -> DataSource {
match self { DataSource::Bytes(self.into_bytes())
Readable::Str(v) => Some(v.as_str()),
Readable::Bytes(v) => std::str::from_utf8(v).ok(),
}
} }
} }
@ -78,12 +152,3 @@ cast! {
v: Str => Self::Str(v), v: Str => Self::Str(v),
v: Bytes => Self::Bytes(v), v: Bytes => Self::Bytes(v),
} }
impl From<Readable> for Bytes {
fn from(value: Readable) -> Self {
match value {
Readable::Bytes(v) => v,
Readable::Str(v) => v.as_bytes().into(),
}
}
}

View File

@ -1,7 +1,7 @@
use ecow::EcoString; use ecow::EcoString;
use typst_syntax::Spanned; use typst_syntax::Spanned;
use crate::diag::{At, SourceResult}; use crate::diag::{At, FileError, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{func, Cast}; use crate::foundations::{func, Cast};
use crate::loading::Readable; use crate::loading::Readable;
@ -24,7 +24,6 @@ use crate::World;
/// ``` /// ```
#[func] #[func]
pub fn read( pub fn read(
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// Path to a file. /// Path to a file.
/// ///
@ -42,12 +41,9 @@ pub fn read(
let data = engine.world.file(id).at(span)?; let data = engine.world.file(id).at(span)?;
Ok(match encoding { Ok(match encoding {
None => Readable::Bytes(data), None => Readable::Bytes(data),
Some(Encoding::Utf8) => Readable::Str( Some(Encoding::Utf8) => {
std::str::from_utf8(&data) Readable::Str(data.to_str().map_err(FileError::from).at(span)?)
.map_err(|_| "file is not valid utf-8") }
.at(span)?
.into(),
),
}) })
} }

View File

@ -1,11 +1,10 @@
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use typst_syntax::{is_newline, Spanned}; use typst_syntax::{is_newline, Spanned};
use crate::diag::{At, SourceResult}; use crate::diag::{At, FileError, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{func, scope, Str, Value}; use crate::foundations::{func, scope, Str, Value};
use crate::loading::Readable; use crate::loading::{DataSource, Load, Readable};
use crate::World;
/// Reads structured data from a TOML file. /// Reads structured data from a TOML file.
/// ///
@ -29,34 +28,32 @@ use crate::World;
/// ``` /// ```
#[func(scope, title = "TOML")] #[func(scope, title = "TOML")]
pub fn toml( pub fn toml(
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// Path to a TOML file. /// A path to a TOML file or raw TOML bytes.
/// ///
/// For more details, see the [Paths section]($syntax/#paths). /// For more details about paths, see the [Paths section]($syntax/#paths).
path: Spanned<EcoString>, source: Spanned<DataSource>,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let Spanned { v: path, span } = path; let data = source.load(engine.world)?;
let id = span.resolve_path(&path).at(span)?; let raw = data.as_str().map_err(FileError::from).at(source.span)?;
let data = engine.world.file(id).at(span)?; ::toml::from_str(raw)
toml::decode(Spanned::new(Readable::Bytes(data), span)) .map_err(|err| format_toml_error(err, raw))
.at(source.span)
} }
#[scope] #[scope]
impl toml { impl toml {
/// Reads structured data from a TOML string/bytes. /// Reads structured data from a TOML string/bytes.
///
/// This function is deprecated. The [`toml`] function now accepts bytes
/// directly.
#[func(title = "Decode TOML")] #[func(title = "Decode TOML")]
pub fn decode( pub fn decode(
engine: &mut Engine,
/// TOML data. /// TOML data.
data: Spanned<Readable>, data: Spanned<Readable>,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let Spanned { v: data, span } = data; toml(engine, data.map(Readable::into_source))
let raw = std::str::from_utf8(data.as_slice())
.map_err(|_| "file is not valid utf-8")
.at(span)?;
::toml::from_str(raw)
.map_err(|err| format_toml_error(err, raw))
.at(span)
} }
/// Encodes structured data into a TOML string. /// Encodes structured data into a TOML string.

View File

@ -5,8 +5,7 @@ use typst_syntax::Spanned;
use crate::diag::{format_xml_like_error, At, FileError, SourceResult}; use crate::diag::{format_xml_like_error, At, FileError, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{dict, func, scope, Array, Dict, IntoValue, Str, Value}; use crate::foundations::{dict, func, scope, Array, Dict, IntoValue, Str, Value};
use crate::loading::Readable; use crate::loading::{DataSource, Load, Readable};
use crate::World;
/// Reads structured data from an XML file. /// Reads structured data from an XML file.
/// ///
@ -58,38 +57,36 @@ use crate::World;
/// ``` /// ```
#[func(scope, title = "XML")] #[func(scope, title = "XML")]
pub fn xml( pub fn xml(
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// Path to an XML file. /// A path to an XML file or raw XML bytes.
/// ///
/// For more details, see the [Paths section]($syntax/#paths). /// For more details about paths, see the [Paths section]($syntax/#paths).
path: Spanned<EcoString>, source: Spanned<DataSource>,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let Spanned { v: path, span } = path; let data = source.load(engine.world)?;
let id = span.resolve_path(&path).at(span)?; let text = data.as_str().map_err(FileError::from).at(source.span)?;
let data = engine.world.file(id).at(span)?; let document = roxmltree::Document::parse_with_options(
xml::decode(Spanned::new(Readable::Bytes(data), span)) text,
ParsingOptions { allow_dtd: true, ..Default::default() },
)
.map_err(format_xml_error)
.at(source.span)?;
Ok(convert_xml(document.root()))
} }
#[scope] #[scope]
impl xml { impl xml {
/// Reads structured data from an XML string/bytes. /// Reads structured data from an XML string/bytes.
///
/// This function is deprecated. The [`xml`] function now accepts bytes
/// directly.
#[func(title = "Decode XML")] #[func(title = "Decode XML")]
pub fn decode( pub fn decode(
engine: &mut Engine,
/// XML data. /// XML data.
data: Spanned<Readable>, data: Spanned<Readable>,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let Spanned { v: data, span } = data; xml(engine, data.map(Readable::into_source))
let text = std::str::from_utf8(data.as_slice())
.map_err(FileError::from)
.at(span)?;
let document = roxmltree::Document::parse_with_options(
text,
ParsingOptions { allow_dtd: true, ..Default::default() },
)
.map_err(format_xml_error)
.at(span)?;
Ok(convert_xml(document.root()))
} }
} }

View File

@ -1,11 +1,10 @@
use ecow::{eco_format, EcoString}; use ecow::eco_format;
use typst_syntax::Spanned; use typst_syntax::Spanned;
use crate::diag::{At, SourceResult}; use crate::diag::{At, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{func, scope, Str, Value}; use crate::foundations::{func, scope, Str, Value};
use crate::loading::Readable; use crate::loading::{DataSource, Load, Readable};
use crate::World;
/// Reads structured data from a YAML file. /// Reads structured data from a YAML file.
/// ///
@ -41,31 +40,31 @@ use crate::World;
/// ``` /// ```
#[func(scope, title = "YAML")] #[func(scope, title = "YAML")]
pub fn yaml( pub fn yaml(
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// Path to a YAML file. /// A path to a YAML file or raw YAML bytes.
/// ///
/// For more details, see the [Paths section]($syntax/#paths). /// For more details about paths, see the [Paths section]($syntax/#paths).
path: Spanned<EcoString>, source: Spanned<DataSource>,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let Spanned { v: path, span } = path; let data = source.load(engine.world)?;
let id = span.resolve_path(&path).at(span)?; serde_yaml::from_slice(data.as_slice())
let data = engine.world.file(id).at(span)?; .map_err(|err| eco_format!("failed to parse YAML ({err})"))
yaml::decode(Spanned::new(Readable::Bytes(data), span)) .at(source.span)
} }
#[scope] #[scope]
impl yaml { impl yaml {
/// Reads structured data from a YAML string/bytes. /// Reads structured data from a YAML string/bytes.
///
/// This function is deprecated. The [`yaml`] function now accepts bytes
/// directly.
#[func(title = "Decode YAML")] #[func(title = "Decode YAML")]
pub fn decode( pub fn decode(
engine: &mut Engine,
/// YAML data. /// YAML data.
data: Spanned<Readable>, data: Spanned<Readable>,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let Spanned { v: data, span } = data; yaml(engine, data.map(Readable::into_source))
serde_yaml::from_slice(data.as_slice())
.map_err(|err| eco_format!("failed to parse YAML ({err})"))
.at(span)
} }
/// Encode structured data into a YAML string. /// Encode structured data into a YAML string.

View File

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

View File

@ -47,9 +47,9 @@ impl Packed<AttachElem> {
/// base AttachElem where possible. /// base AttachElem where possible.
pub fn merge_base(&self) -> Option<Self> { pub fn merge_base(&self) -> Option<Self> {
// Extract from an EquationElem. // Extract from an EquationElem.
let mut base = self.base(); let mut base = &self.base;
while let Some(equation) = base.to_packed::<EquationElem>() { while let Some(equation) = base.to_packed::<EquationElem>() {
base = equation.body(); base = &equation.body;
} }
// Move attachments from elem into base where possible. // Move attachments from elem into base where possible.

View File

@ -20,7 +20,9 @@ use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem};
/// A mathematical equation. /// 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
/// ```example /// ```example
@ -229,35 +231,20 @@ impl Refable for Packed<EquationElem> {
} }
impl Outlinable for Packed<EquationElem> { impl Outlinable for Packed<EquationElem> {
fn outline( fn outlined(&self) -> bool {
&self, self.block(StyleChain::default()) && self.numbering().is_some()
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 prefix(&self, numbers: Content) -> Content {
let supplement = self.supplement();
if !supplement.is_empty() { if !supplement.is_empty() {
supplement += TextElem::packed("\u{a0}"); supplement + TextElem::packed('\u{a0}') + numbers
} else {
numbers
} }
}
let numbers = self.counter().display_at_loc( fn body(&self) -> Content {
engine, Content::empty()
self.location().unwrap(),
styles,
numbering,
)?;
Ok(Some(supplement + numbers))
} }
} }

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