diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index dfa836d18..33c5343c5 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -37,8 +37,8 @@ Below are some signs of a good PR:
- Adds/changes as little code and as few interfaces as possible. Should changes
to larger-scale abstractions be necessary, these should be discussed
throughout the implementation process.
-- Adds tests if appropriate (with reference images for visual tests). See the
- [testing] readme for more details.
+- Adds tests if appropriate (with reference output for visual/HTML tests). See
+ the [testing] readme for more details.
- Contains documentation comments on all new Rust types.
- Comes with brief documentation for all new Typst definitions
(elements/functions), ideally with a concise example that fits into ~5-10
diff --git a/Cargo.lock b/Cargo.lock
index be5117da2..8aa7c0ec1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3076,6 +3076,7 @@ dependencies = [
"typst",
"typst-assets",
"typst-dev-assets",
+ "typst-html",
"typst-library",
"typst-pdf",
"typst-render",
@@ -3092,6 +3093,7 @@ dependencies = [
"parking_lot",
"serde",
"serde_json",
+ "web-sys",
]
[[package]]
@@ -3417,6 +3419,16 @@ dependencies = [
"indexmap-nostd",
]
+[[package]]
+name = "web-sys"
+version = "0.3.70"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
[[package]]
name = "weezl"
version = "0.1.8"
diff --git a/Cargo.toml b/Cargo.toml
index b4f704f80..1be7816a7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -134,6 +134,7 @@ ureq = { version = "2", default-features = false, features = ["native-tls", "gzi
usvg = { version = "0.43", default-features = false, features = ["text"] }
walkdir = "2"
wasmi = "0.39.0"
+web-sys = "0.3"
xmlparser = "0.13.5"
xmlwriter = "0.1.0"
xmp-writer = "0.3"
diff --git a/README.md b/README.md
index 5d5c4798a..a5d20d2e6 100644
--- a/README.md
+++ b/README.md
@@ -5,19 +5,19 @@
+ >
+ >
+ >
+ >
+ >
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
it is:
-
+
diff --git a/crates/typst-cli/src/args.rs b/crates/typst-cli/src/args.rs
index 83c4c8f9e..d6855d100 100644
--- a/crates/typst-cli/src/args.rs
+++ b/crates/typst-cli/src/args.rs
@@ -473,6 +473,9 @@ pub enum PdfStandard {
/// PDF/A-2b.
#[value(name = "a-2b")]
A_2b,
+ /// PDF/A-3b.
+ #[value(name = "a-3b")]
+ A_3b,
}
display_possible_values!(PdfStandard);
diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs
index adeef0f2d..515a777a2 100644
--- a/crates/typst-cli/src/compile.rs
+++ b/crates/typst-cli/src/compile.rs
@@ -136,6 +136,7 @@ impl CompileConfig {
.map(|standard| match standard {
PdfStandard::V_1_7 => typst_pdf::PdfStandard::V_1_7,
PdfStandard::A_2b => typst_pdf::PdfStandard::A_2b,
+ PdfStandard::A_3b => typst_pdf::PdfStandard::A_3b,
})
.collect::>();
PdfStandards::new(&list)?
diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs
index af6cf228f..12e80d273 100644
--- a/crates/typst-cli/src/world.rs
+++ b/crates/typst-cli/src/world.rs
@@ -305,7 +305,7 @@ impl FileSlot {
) -> FileResult {
self.file.get_or_init(
|| read(self.id, project_root, package_storage),
- |data, _| Ok(data.into()),
+ |data, _| Ok(Bytes::new(data)),
)
}
}
diff --git a/crates/typst-eval/src/call.rs b/crates/typst-eval/src/call.rs
index fc934cef5..0a9e1c486 100644
--- a/crates/typst-eval/src/call.rs
+++ b/crates/typst-eval/src/call.rs
@@ -685,8 +685,7 @@ mod tests {
// Named-params.
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) $", &["bar", "foo"]);
+ test(s, "$ foo(x-y: 1, bar-z: 2) $", &["foo"]);
// Field access in math.
test(s, "$ foo.bar $", &["foo"]);
diff --git a/crates/typst-eval/src/import.rs b/crates/typst-eval/src/import.rs
index 5b67c0608..2060d25f1 100644
--- a/crates/typst-eval/src/import.rs
+++ b/crates/typst-eval/src/import.rs
@@ -211,7 +211,7 @@ fn resolve_package(
// Evaluate the manifest.
let manifest_id = FileId::new(Some(spec.clone()), VirtualPath::new("typst.toml"));
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)
.map_err(|err| eco_format!("package manifest is malformed ({})", err.message()))
.at(span)?;
diff --git a/crates/typst-html/src/encode.rs b/crates/typst-html/src/encode.rs
index b87b0e1d6..62146f867 100644
--- a/crates/typst-html/src/encode.rs
+++ b/crates/typst-html/src/encode.rs
@@ -12,6 +12,9 @@ pub fn html(document: &HtmlDocument) -> SourceResult {
w.buf.push_str("");
write_indent(&mut w);
write_element(&mut w, &document.root)?;
+ if w.pretty {
+ w.buf.push('\n');
+ }
Ok(w.buf)
}
diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs
index 5c2b500a0..0f8abddb7 100644
--- a/crates/typst-ide/src/complete.rs
+++ b/crates/typst-ide/src/complete.rs
@@ -521,11 +521,13 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool {
if_chain! {
if ctx.leaf.kind() == SyntaxKind::Ident;
if let Some(parent) = ctx.leaf.parent();
- if parent.kind() == SyntaxKind::ImportItems;
+ if parent.kind() == SyntaxKind::ImportItemPath;
if let Some(grand) = parent.parent();
- if let Some(ast::Expr::Import(import)) = grand.get().cast();
+ if grand.kind() == SyntaxKind::ImportItems;
+ if let Some(great) = grand.parent();
+ if let Some(ast::Expr::Import(import)) = great.get().cast();
if let Some(ast::Imports::Items(items)) = import.imports();
- if let Some(source) = grand.children().find(|child| child.is::());
+ if let Some(source) = great.children().find(|child| child.is::());
then {
ctx.from = ctx.leaf.offset();
import_item_completions(ctx, items, &source);
@@ -815,19 +817,8 @@ fn param_value_completions<'a>(
) {
if param.name == "font" {
ctx.font_completions();
- } else if param.name == "path" {
- ctx.file_completions_with_extensions(match func.name() {
- Some("image") => &["png", "jpg", "jpeg", "gif", "svg", "svgz"],
- Some("csv") => &["csv"],
- Some("plugin") => &["wasm"],
- Some("cbor") => &["cbor"],
- Some("json") => &["json"],
- Some("toml") => &["toml"],
- Some("xml") => &["xml"],
- Some("yaml") => &["yml", "yaml"],
- Some("bibliography") => &["bib", "yml", "yaml"],
- _ => &[],
- });
+ } else if let Some(extensions) = path_completion(func, param) {
+ ctx.file_completions_with_extensions(extensions);
} else if func.name() == Some("figure") && param.name == "body" {
ctx.snippet_completion("image", "image(\"${}\"),", "An image in a figure.");
ctx.snippet_completion("table", "table(\n ${}\n),", "A table in a figure.");
@@ -836,6 +827,28 @@ fn param_value_completions<'a>(
ctx.cast_completions(¶m.input);
}
+/// Returns which file extensions to complete for the given parameter if any.
+fn path_completion(func: &Func, param: &ParamInfo) -> Option<&'static [&'static str]> {
+ Some(match (func.name(), param.name) {
+ (Some("image"), "source") => &["png", "jpg", "jpeg", "gif", "svg", "svgz"],
+ (Some("csv"), "source") => &["csv"],
+ (Some("plugin"), "source") => &["wasm"],
+ (Some("cbor"), "source") => &["cbor"],
+ (Some("json"), "source") => &["json"],
+ (Some("toml"), "source") => &["toml"],
+ (Some("xml"), "source") => &["xml"],
+ (Some("yaml"), "source") => &["yml", "yaml"],
+ (Some("bibliography"), "sources") => &["bib", "yml", "yaml"],
+ (Some("bibliography"), "style") => &["csl"],
+ (Some("cite"), "style") => &["csl"],
+ (Some("raw"), "syntaxes") => &["sublime-syntax"],
+ (Some("raw"), "theme") => &["tmtheme"],
+ (Some("embed"), "path") => &[],
+ (None, "path") => &[],
+ _ => return None,
+ })
+}
+
/// Resolve a callee expression to a global function.
fn resolve_global_callee<'a>(
ctx: &CompletionContext<'a>,
@@ -1504,14 +1517,13 @@ impl BracketMode {
#[cfg(test)]
mod tests {
+ use std::borrow::Borrow;
use std::collections::BTreeSet;
use typst::layout::PagedDocument;
- use typst::syntax::{FileId, Source, VirtualPath};
- use typst::World;
use super::{autocomplete, Completion};
- use crate::tests::{SourceExt, TestWorld};
+ use crate::tests::{FilePos, TestWorld, WorldLike};
/// Quote a string.
macro_rules! q {
@@ -1583,60 +1595,50 @@ mod tests {
}
#[track_caller]
- fn test(text: &str, cursor: isize) -> Response {
- let world = TestWorld::new(text);
- test_with_world(&world, cursor)
+ fn test(world: impl WorldLike, pos: impl FilePos) -> Response {
+ let world = world.acquire();
+ let world = world.borrow();
+ let doc = typst::compile(world).output.ok();
+ test_with_doc(world, pos, doc.as_ref())
}
#[track_caller]
- fn test_with_world(world: &TestWorld, cursor: isize) -> Response {
- let doc = typst::compile(&world).output.ok();
- test_full(world, &world.main, doc.as_ref(), cursor)
- }
-
- #[track_caller]
- fn test_with_path(world: &TestWorld, path: &str, cursor: isize) -> Response {
- let doc = typst::compile(&world).output.ok();
- let id = FileId::new(None, VirtualPath::new(path));
- let source = world.source(id).unwrap();
- test_full(world, &source, doc.as_ref(), cursor)
- }
-
- #[track_caller]
- fn test_full(
- world: &TestWorld,
- source: &Source,
+ fn test_with_doc(
+ world: impl WorldLike,
+ pos: impl FilePos,
doc: Option<&PagedDocument>,
- cursor: isize,
) -> Response {
- autocomplete(world, doc, source, source.cursor(cursor), true)
+ let world = world.acquire();
+ let world = world.borrow();
+ let (source, cursor) = pos.resolve(world);
+ autocomplete(world, doc, &source, cursor, true)
}
#[test]
fn test_autocomplete_hash_expr() {
- test("#i", 2).must_include(["int", "if conditional"]);
+ test("#i", -1).must_include(["int", "if conditional"]);
}
#[test]
fn test_autocomplete_array_method() {
- test("#().", 4).must_include(["insert", "remove", "len", "all"]);
- test("#{ let x = (1, 2, 3); x. }", -2).must_include(["at", "push", "pop"]);
+ test("#().", -1).must_include(["insert", "remove", "len", "all"]);
+ test("#{ let x = (1, 2, 3); x. }", -3).must_include(["at", "push", "pop"]);
}
/// Test that extra space before '.' is handled correctly.
#[test]
fn test_autocomplete_whitespace() {
- test("#() .", 5).must_exclude(["insert", "remove", "len", "all"]);
- test("#{() .}", 6).must_include(["insert", "remove", "len", "all"]);
- test("#() .a", 6).must_exclude(["insert", "remove", "len", "all"]);
- test("#{() .a}", 7).must_include(["at", "any", "all"]);
+ test("#() .", -1).must_exclude(["insert", "remove", "len", "all"]);
+ test("#{() .}", -2).must_include(["insert", "remove", "len", "all"]);
+ test("#() .a", -1).must_exclude(["insert", "remove", "len", "all"]);
+ test("#{() .a}", -2).must_include(["at", "any", "all"]);
}
/// Test that the `before_window` doesn't slice into invalid byte
/// boundaries.
#[test]
fn test_autocomplete_before_window_char_boundary() {
- test("ππ #text(font: \"\")", -2);
+ test("ππ #text(font: \"\")", -3);
}
/// Ensure that autocompletion for `#cite(|)` completes bibligraphy labels,
@@ -1653,7 +1655,7 @@ mod tests {
let end = world.main.len_bytes();
world.main.edit(end..end, " #cite()");
- test_full(&world, &world.main, doc.as_ref(), -1)
+ test_with_doc(&world, -2, doc.as_ref())
.must_include(["netwok", "glacier-melt", "supplement"])
.must_exclude(["bib"]);
}
@@ -1677,13 +1679,13 @@ mod tests {
#[test]
fn test_autocomplete_positional_param() {
// No string given yet.
- test("#numbering()", -1).must_include(["string", "integer"]);
+ test("#numbering()", -2).must_include(["string", "integer"]);
// String is already given.
- test("#numbering(\"foo\", )", -1)
+ test("#numbering(\"foo\", )", -2)
.must_include(["integer"])
.must_exclude(["string"]);
// Integer is already given, but numbering is variadic.
- test("#numbering(\"foo\", 1, )", -1)
+ test("#numbering(\"foo\", 1, )", -2)
.must_include(["integer"])
.must_exclude(["string"]);
}
@@ -1698,14 +1700,14 @@ mod tests {
"#let clrs = (a: red, b: blue); #let nums = (a: 1, b: 2)",
);
- test_with_world(&world, -1)
+ test(&world, -2)
.must_include(["clrs", "aqua"])
.must_exclude(["nums", "a", "b"]);
}
#[test]
fn test_autocomplete_packages() {
- test("#import \"@\"", -1).must_include([q!("@preview/example:0.1.0")]);
+ test("#import \"@\"", -2).must_include([q!("@preview/example:0.1.0")]);
}
#[test]
@@ -1719,28 +1721,41 @@ mod tests {
.with_asset_at("assets/rhino.png", "rhino.png")
.with_asset_at("data/example.csv", "example.csv");
- test_with_path(&world, "main.typ", -1)
+ test(&world, -2)
.must_include([q!("content/a.typ"), q!("content/b.typ"), q!("utils.typ")])
.must_exclude([q!("assets/tiger.jpg")]);
- test_with_path(&world, "content/c.typ", -1)
+ test(&world, ("content/c.typ", -2))
.must_include([q!("../main.typ"), q!("a.typ"), q!("b.typ")])
.must_exclude([q!("c.typ")]);
- test_with_path(&world, "content/a.typ", -1)
+ test(&world, ("content/a.typ", -2))
.must_include([q!("../assets/tiger.jpg"), q!("../assets/rhino.png")])
.must_exclude([q!("../data/example.csv"), q!("b.typ")]);
- test_with_path(&world, "content/b.typ", -2)
- .must_include([q!("../data/example.csv")]);
+ test(&world, ("content/b.typ", -3)).must_include([q!("../data/example.csv")]);
}
#[test]
fn test_autocomplete_figure_snippets() {
- test("#figure()", -1)
+ test("#figure()", -2)
.must_apply("image", "image(\"${}\"),")
.must_apply("table", "table(\n ${}\n),");
- test("#figure(cap)", -1).must_apply("caption", "caption: [${}]");
+ test("#figure(cap)", -2).must_apply("caption", "caption: [${}]");
+ }
+
+ #[test]
+ fn test_autocomplete_import_items() {
+ let world = TestWorld::new("#import \"other.typ\": ")
+ .with_source("second.typ", "#import \"other.typ\": th")
+ .with_source("other.typ", "#let this = 1; #let that = 2");
+
+ test(&world, ("main.typ", 21))
+ .must_include(["*", "this", "that"])
+ .must_exclude(["figure"]);
+ test(&world, ("second.typ", 23))
+ .must_include(["this", "that"])
+ .must_exclude(["*", "figure"]);
}
}
diff --git a/crates/typst-ide/src/definition.rs b/crates/typst-ide/src/definition.rs
index c789430a2..31fb9e34e 100644
--- a/crates/typst-ide/src/definition.rs
+++ b/crates/typst-ide/src/definition.rs
@@ -86,6 +86,7 @@ pub fn definition(
#[cfg(test)]
mod tests {
+ use std::borrow::Borrow;
use std::ops::Range;
use typst::foundations::{IntoValue, NativeElement};
@@ -93,7 +94,7 @@ mod tests {
use typst::WorldExt;
use super::{definition, Definition};
- use crate::tests::{SourceExt, TestWorld};
+ use crate::tests::{FilePos, TestWorld, WorldLike};
type Response = (TestWorld, Option);
@@ -132,23 +133,19 @@ mod tests {
}
#[track_caller]
- fn test(text: &str, cursor: isize, side: Side) -> Response {
- let world = TestWorld::new(text);
- test_with_world(world, cursor, side)
- }
-
- #[track_caller]
- fn test_with_world(world: TestWorld, cursor: isize, side: Side) -> Response {
- let doc = typst::compile(&world).output.ok();
- let source = &world.main;
- let def = definition(&world, doc.as_ref(), source, source.cursor(cursor), side);
- (world, def)
+ fn test(world: impl WorldLike, pos: impl FilePos, side: Side) -> Response {
+ let world = world.acquire();
+ let world = world.borrow();
+ let doc = typst::compile(world).output.ok();
+ let (source, cursor) = pos.resolve(world);
+ let def = definition(world, doc.as_ref(), &source, cursor, side);
+ (world.clone(), def)
}
#[test]
fn test_definition_let() {
- test("#let x; #x", 9, Side::After).must_be_at("main.typ", 5..6);
- test("#let x() = {}; #x", 16, Side::After).must_be_at("main.typ", 5..6);
+ test("#let x; #x", -2, Side::After).must_be_at("main.typ", 5..6);
+ test("#let x() = {}; #x", -2, Side::After).must_be_at("main.typ", 5..6);
}
#[test]
@@ -158,33 +155,33 @@ mod tests {
// The span is at the args here because that's what the function value's
// span is. Not ideal, but also not too big of a big deal.
- test_with_world(world, -1, Side::Before).must_be_at("other.typ", 8..11);
+ test(&world, -2, Side::Before).must_be_at("other.typ", 8..11);
}
#[test]
fn test_definition_cross_file() {
let world = TestWorld::new("#import \"other.typ\": x; #x")
.with_source("other.typ", "#let x = 1");
- test_with_world(world, -1, Side::After).must_be_at("other.typ", 5..6);
+ test(&world, -2, Side::After).must_be_at("other.typ", 5..6);
}
#[test]
fn test_definition_import() {
let world = TestWorld::new("#import \"other.typ\" as o: x")
.with_source("other.typ", "#let x = 1");
- test_with_world(world, 14, Side::Before).must_be_at("other.typ", 0..0);
+ test(&world, 14, Side::Before).must_be_at("other.typ", 0..0);
}
#[test]
fn test_definition_include() {
let world = TestWorld::new("#include \"other.typ\"")
.with_source("other.typ", "Hello there");
- test_with_world(world, 14, Side::Before).must_be_at("other.typ", 0..0);
+ test(&world, 14, Side::Before).must_be_at("other.typ", 0..0);
}
#[test]
fn test_definition_ref() {
- test("#figure[] See @hi", 21, Side::After).must_be_at("main.typ", 1..9);
+ test("#figure[] See @hi", -2, Side::After).must_be_at("main.typ", 1..9);
}
#[test]
diff --git a/crates/typst-ide/src/jump.rs b/crates/typst-ide/src/jump.rs
index ba62b0ab9..ed74df226 100644
--- a/crates/typst-ide/src/jump.rs
+++ b/crates/typst-ide/src/jump.rs
@@ -182,12 +182,13 @@ mod tests {
//! ))
//! ```
+ use std::borrow::Borrow;
use std::num::NonZeroUsize;
use typst::layout::{Abs, Point, Position};
use super::{jump_from_click, jump_from_cursor, Jump};
- use crate::tests::TestWorld;
+ use crate::tests::{FilePos, TestWorld, WorldLike};
fn point(x: f64, y: f64) -> Point {
Point::new(Abs::pt(x), Abs::pt(y))
@@ -211,10 +212,11 @@ mod tests {
}
#[track_caller]
- fn test_click(text: &str, click: Point, expected: Option) {
- let world = TestWorld::new(text);
- let doc = typst::compile(&world).output.unwrap();
- let jump = jump_from_click(&world, &doc, &doc.pages[0].frame, click);
+ fn test_click(world: impl WorldLike, click: Point, expected: Option) {
+ let world = world.acquire();
+ let world = world.borrow();
+ let doc = typst::compile(world).output.unwrap();
+ let jump = jump_from_click(world, &doc, &doc.pages[0].frame, click);
if let (Some(Jump::Position(pos)), Some(Jump::Position(expected))) =
(&jump, &expected)
{
@@ -227,10 +229,12 @@ mod tests {
}
#[track_caller]
- fn test_cursor(text: &str, cursor: usize, expected: Option) {
- let world = TestWorld::new(text);
- let doc = typst::compile(&world).output.unwrap();
- let pos = jump_from_cursor(&doc, &world.main, cursor);
+ fn test_cursor(world: impl WorldLike, pos: impl FilePos, expected: Option) {
+ let world = world.acquire();
+ let world = world.borrow();
+ let doc = typst::compile(world).output.unwrap();
+ let (source, cursor) = pos.resolve(world);
+ let pos = jump_from_cursor(&doc, &source, cursor);
assert_eq!(!pos.is_empty(), expected.is_some());
if let (Some(pos), Some(expected)) = (pos.first(), expected) {
assert_eq!(pos.page, expected.page);
diff --git a/crates/typst-ide/src/matchers.rs b/crates/typst-ide/src/matchers.rs
index 18262f701..b92cbf557 100644
--- a/crates/typst-ide/src/matchers.rs
+++ b/crates/typst-ide/src/matchers.rs
@@ -89,15 +89,21 @@ pub fn named_items(
// ```
Some(ast::Imports::Items(items)) => {
for item in items.iter() {
- let original = item.original_name();
let bound = item.bound_name();
- let scope = source.and_then(|(value, _)| value.scope());
- let span = scope
- .and_then(|s| s.get_span(&original))
- .unwrap_or(Span::detached())
- .or(bound.span());
- let 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) =
recv(NamedItem::Import(bound.get(), span, value))
{
@@ -266,53 +272,95 @@ pub enum DerefTarget<'a> {
#[cfg(test)]
mod tests {
+ use std::borrow::Borrow;
+
+ use ecow::EcoString;
+ use typst::foundations::Value;
use typst::syntax::{LinkedNode, Side};
- use crate::{named_items, tests::TestWorld};
+ use super::named_items;
+ use crate::tests::{FilePos, TestWorld, WorldLike};
+
+ type Response = Vec<(EcoString, Option)>;
+
+ trait ResponseExt {
+ fn must_include<'a>(&self, includes: impl IntoIterator) -> &Self;
+ fn must_exclude<'a>(&self, excludes: impl IntoIterator) -> &Self;
+ fn must_include_value(&self, name_value: (&str, Option<&Value>)) -> &Self;
+ }
+
+ impl ResponseExt for Response {
+ #[track_caller]
+ fn must_include<'a>(&self, includes: impl IntoIterator) -> &Self {
+ for item in includes {
+ assert!(
+ self.iter().any(|v| v.0 == item),
+ "{item:?} was not contained in {self:?}",
+ );
+ }
+ self
+ }
+
+ #[track_caller]
+ fn must_exclude<'a>(&self, excludes: impl IntoIterator) -> &Self {
+ for item in excludes {
+ assert!(
+ !self.iter().any(|v| v.0 == item),
+ "{item:?} was wrongly contained in {self:?}",
+ );
+ }
+ self
+ }
+
+ #[track_caller]
+ fn must_include_value(&self, name_value: (&str, Option<&Value>)) -> &Self {
+ assert!(
+ self.iter().any(|v| (v.0.as_str(), v.1.as_ref()) == name_value),
+ "{name_value:?} was not contained in {self:?}",
+ );
+ self
+ }
+ }
#[track_caller]
- fn has_named_items(text: &str, cursor: usize, containing: &str) -> bool {
- let world = TestWorld::new(text);
-
- let src = world.main.clone();
- let node = LinkedNode::new(src.root());
+ fn test(world: impl WorldLike, pos: impl FilePos) -> Response {
+ let world = world.acquire();
+ let world = world.borrow();
+ let (source, cursor) = pos.resolve(world);
+ let node = LinkedNode::new(source.root());
let leaf = node.leaf_at(cursor, Side::After).unwrap();
-
- let res = named_items(&world, leaf, |s| {
- if containing == s.name() {
- return Some(true);
- }
-
- None
+ let mut items = vec![];
+ named_items(world, leaf, |s| {
+ items.push((s.name().clone(), s.value().clone()));
+ None::<()>
});
-
- res.unwrap_or_default()
+ items
}
#[test]
- fn test_simple_named_items() {
- // Has named items
- assert!(has_named_items(r#"#let a = 1;#let b = 2;"#, 8, "a"));
- assert!(has_named_items(r#"#let a = 1;#let b = 2;"#, 15, "a"));
-
- // Doesn't have named items
- assert!(!has_named_items(r#"#let a = 1;#let b = 2;"#, 8, "b"));
+ fn test_named_items_simple() {
+ let s = "#let a = 1;#let b = 2;";
+ test(s, 8).must_include(["a"]).must_exclude(["b"]);
+ test(s, 15).must_include(["b"]);
}
#[test]
- fn test_param_named_items() {
- // Has named items
- assert!(has_named_items(r#"#let f(a) = 1;#let b = 2;"#, 12, "a"));
- assert!(has_named_items(r#"#let f(a: b) = 1;#let b = 2;"#, 15, "a"));
+ fn test_named_items_param() {
+ let pos = "#let f(a) = 1;#let b = 2;";
+ test(pos, 12).must_include(["a"]);
+ test(pos, 19).must_include(["b", "f"]).must_exclude(["a"]);
- // Doesn't have named items
- assert!(!has_named_items(r#"#let f(a) = 1;#let b = 2;"#, 19, "a"));
- assert!(!has_named_items(r#"#let f(a: b) = 1;#let b = 2;"#, 15, "b"));
+ let named = "#let f(a: b) = 1;#let b = 2;";
+ test(named, 15).must_include(["a", "f"]).must_exclude(["b"]);
}
#[test]
- fn test_import_named_items() {
- // Cannot test much.
- assert!(has_named_items(r#"#import "foo.typ": a; #(a);"#, 24, "a"));
+ fn test_named_items_import() {
+ 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))));
}
}
diff --git a/crates/typst-ide/src/tests.rs b/crates/typst-ide/src/tests.rs
index 5a73fa375..6678ab841 100644
--- a/crates/typst-ide/src/tests.rs
+++ b/crates/typst-ide/src/tests.rs
@@ -1,4 +1,6 @@
+use std::borrow::Borrow;
use std::collections::HashMap;
+use std::sync::Arc;
use ecow::EcoString;
use typst::diag::{FileError, FileResult};
@@ -13,10 +15,10 @@ use typst::{Library, World};
use crate::IdeWorld;
/// A world for IDE testing.
+#[derive(Clone)]
pub struct TestWorld {
pub main: Source,
- assets: HashMap,
- sources: HashMap,
+ files: Arc,
base: &'static TestBase,
}
@@ -29,8 +31,7 @@ impl TestWorld {
let main = Source::new(Self::main_id(), text.into());
Self {
main,
- assets: HashMap::new(),
- sources: HashMap::new(),
+ files: Arc::new(TestFiles::default()),
base: singleton!(TestBase, TestBase::default()),
}
}
@@ -39,7 +40,7 @@ impl TestWorld {
pub fn with_source(mut self, path: &str, text: &str) -> Self {
let id = FileId::new(None, VirtualPath::new(path));
let source = Source::new(id, text.into());
- self.sources.insert(id, source);
+ Arc::make_mut(&mut self.files).sources.insert(id, source);
self
}
@@ -54,8 +55,8 @@ impl TestWorld {
pub fn with_asset_at(mut self, path: &str, filename: &str) -> Self {
let id = FileId::new(None, VirtualPath::new(path));
let data = typst_dev_assets::get_by_name(filename).unwrap();
- let bytes = Bytes::from_static(data);
- self.assets.insert(id, bytes);
+ let bytes = Bytes::new(data);
+ Arc::make_mut(&mut self.files).assets.insert(id, bytes);
self
}
@@ -81,7 +82,7 @@ impl World for TestWorld {
fn source(&self, id: FileId) -> FileResult {
if id == self.main.id() {
Ok(self.main.clone())
- } else if let Some(source) = self.sources.get(&id) {
+ } else if let Some(source) = self.files.sources.get(&id) {
Ok(source.clone())
} else {
Err(FileError::NotFound(id.vpath().as_rootless_path().into()))
@@ -89,7 +90,7 @@ impl World for TestWorld {
}
fn file(&self, id: FileId) -> FileResult {
- match self.assets.get(&id) {
+ match self.files.assets.get(&id) {
Some(bytes) => Ok(bytes.clone()),
None => Err(FileError::NotFound(id.vpath().as_rootless_path().into())),
}
@@ -111,8 +112,8 @@ impl IdeWorld for TestWorld {
fn files(&self) -> Vec {
std::iter::once(self.main.id())
- .chain(self.sources.keys().copied())
- .chain(self.assets.keys().copied())
+ .chain(self.files.sources.keys().copied())
+ .chain(self.files.assets.keys().copied())
.collect()
}
@@ -133,20 +134,11 @@ impl IdeWorld for TestWorld {
}
}
-/// Extra methods for [`Source`].
-pub trait SourceExt {
- /// Negative cursors index from the back.
- fn cursor(&self, cursor: isize) -> usize;
-}
-
-impl SourceExt for Source {
- fn cursor(&self, cursor: isize) -> usize {
- if cursor < 0 {
- self.len_bytes().checked_add_signed(cursor).unwrap()
- } else {
- cursor as usize
- }
- }
+/// Test-specific files.
+#[derive(Default, Clone)]
+struct TestFiles {
+ assets: HashMap,
+ sources: HashMap,
}
/// Shared foundation of all test worlds.
@@ -160,7 +152,7 @@ impl Default for TestBase {
fn default() -> Self {
let fonts: Vec<_> = typst_assets::fonts()
.chain(typst_dev_assets::fonts())
- .flat_map(|data| Font::iter(Bytes::from_static(data)))
+ .flat_map(|data| Font::iter(Bytes::new(data)))
.collect();
Self {
@@ -186,3 +178,58 @@ fn library() -> Library {
lib.styles.set(TextElem::set_size(TextSize(Abs::pt(10.0).into())));
lib
}
+
+/// The input to a test: Either just a string or a full `TestWorld`.
+pub trait WorldLike {
+ type World: Borrow;
+
+ fn acquire(self) -> Self::World;
+}
+
+impl<'a> WorldLike for &'a TestWorld {
+ type World = &'a TestWorld;
+
+ fn acquire(self) -> Self::World {
+ self
+ }
+}
+
+impl WorldLike for &str {
+ type World = TestWorld;
+
+ fn acquire(self) -> Self::World {
+ TestWorld::new(self)
+ }
+}
+
+/// Specifies a position in a file for a test.
+pub trait FilePos {
+ fn resolve(self, world: &TestWorld) -> (Source, usize);
+}
+
+impl FilePos for isize {
+ #[track_caller]
+ fn resolve(self, world: &TestWorld) -> (Source, usize) {
+ (world.main.clone(), cursor(&world.main, self))
+ }
+}
+
+impl FilePos for (&str, isize) {
+ #[track_caller]
+ fn resolve(self, world: &TestWorld) -> (Source, usize) {
+ let id = FileId::new(None, VirtualPath::new(self.0));
+ let source = world.source(id).unwrap();
+ let cursor = cursor(&source, self.1);
+ (source, cursor)
+ }
+}
+
+/// Resolve a signed index (negative from the back) to a unsigned index.
+#[track_caller]
+fn cursor(source: &Source, cursor: isize) -> usize {
+ if cursor < 0 {
+ source.len_bytes().checked_add_signed(cursor + 1).unwrap()
+ } else {
+ cursor as usize
+ }
+}
diff --git a/crates/typst-ide/src/tooltip.rs b/crates/typst-ide/src/tooltip.rs
index 4eaaeda1f..99ae0620b 100644
--- a/crates/typst-ide/src/tooltip.rs
+++ b/crates/typst-ide/src/tooltip.rs
@@ -274,10 +274,12 @@ fn font_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option {
#[cfg(test)]
mod tests {
+ use std::borrow::Borrow;
+
use typst::syntax::Side;
use super::{tooltip, Tooltip};
- use crate::tests::{SourceExt, TestWorld};
+ use crate::tests::{FilePos, TestWorld, WorldLike};
type Response = Option;
@@ -308,21 +310,17 @@ mod tests {
}
#[track_caller]
- fn test(text: &str, cursor: isize, side: Side) -> Response {
- let world = TestWorld::new(text);
- test_with_world(&world, cursor, side)
- }
-
- #[track_caller]
- fn test_with_world(world: &TestWorld, cursor: isize, side: Side) -> Response {
- let source = &world.main;
- let doc = typst::compile(&world).output.ok();
- tooltip(world, doc.as_ref(), source, source.cursor(cursor), side)
+ fn test(world: impl WorldLike, pos: impl FilePos, side: Side) -> Response {
+ let world = world.acquire();
+ let world = world.borrow();
+ let (source, cursor) = pos.resolve(world);
+ let doc = typst::compile(world).output.ok();
+ tooltip(world, doc.as_ref(), &source, cursor, side)
}
#[test]
fn test_tooltip() {
- test("#let x = 1 + 2", 14, Side::After).must_be_none();
+ test("#let x = 1 + 2", -1, Side::After).must_be_none();
test("#let x = 1 + 2", 5, Side::After).must_be_code("3");
test("#let x = 1 + 2", 6, Side::Before).must_be_code("3");
test("#let x = 1 + 2", 6, Side::Before).must_be_code("3");
@@ -330,7 +328,7 @@ mod tests {
#[test]
fn test_tooltip_empty_contextual() {
- test("#{context}", 10, Side::Before).must_be_code("context()");
+ test("#{context}", -1, Side::Before).must_be_code("context()");
}
#[test]
@@ -358,8 +356,7 @@ mod tests {
fn test_tooltip_star_import() {
let world = TestWorld::new("#import \"other.typ\": *")
.with_source("other.typ", "#let (a, b, c) = (1, 2, 3)");
- test_with_world(&world, 21, Side::Before).must_be_none();
- test_with_world(&world, 21, Side::After)
- .must_be_text("This star imports `a`, `b`, and `c`");
+ test(&world, -2, Side::Before).must_be_none();
+ test(&world, -2, Side::After).must_be_text("This star imports `a`, `b`, and `c`");
}
}
diff --git a/crates/typst-kit/src/fonts.rs b/crates/typst-kit/src/fonts.rs
index 83e13fd8f..c15d739ec 100644
--- a/crates/typst-kit/src/fonts.rs
+++ b/crates/typst-kit/src/fonts.rs
@@ -13,6 +13,7 @@ use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use fontdb::{Database, Source};
+use typst_library::foundations::Bytes;
use typst_library::text::{Font, FontBook, FontInfo};
use typst_timing::TimingScope;
@@ -52,9 +53,8 @@ impl FontSlot {
.as_ref()
.expect("`path` is not `None` if `font` is uninitialized"),
)
- .ok()?
- .into();
- Font::new(data, self.index)
+ .ok()?;
+ Font::new(Bytes::new(data), self.index)
})
.clone()
}
@@ -196,7 +196,7 @@ impl FontSearcher {
#[cfg(feature = "embed-fonts")]
fn add_embedded(&mut self) {
for data in typst_assets::fonts() {
- let buffer = typst_library::foundations::Bytes::from_static(data);
+ let buffer = Bytes::new(data);
for (i, font) in Font::iter(buffer).enumerate() {
self.book.push(font.info().clone());
self.fonts.push(FontSlot {
diff --git a/crates/typst-layout/src/flow/block.rs b/crates/typst-layout/src/flow/block.rs
index 71eacc1ce..6c2c3923d 100644
--- a/crates/typst-layout/src/flow/block.rs
+++ b/crates/typst-layout/src/flow/block.rs
@@ -364,6 +364,12 @@ fn breakable_pod<'a>(
/// Distribute a fixed height spread over existing regions into a new first
/// height and a new backlog.
+///
+/// Note that, if the given height fits within the first region, no backlog is
+/// generated and the first region's height shrinks to fit exactly the given
+/// height. In particular, negative and zero heights always fit in any region,
+/// so such heights are always directly returned as the new first region
+/// height.
fn distribute<'a>(
height: Abs,
mut regions: Regions,
@@ -371,7 +377,19 @@ fn distribute<'a>(
) -> (Abs, &'a mut [Abs]) {
// Build new region heights from old regions.
let mut remaining = height;
+
+ // Negative and zero heights always fit, so just keep them.
+ // No backlog is generated.
+ if remaining <= Abs::zero() {
+ buf.push(remaining);
+ return (buf[0], &mut buf[1..]);
+ }
+
loop {
+ // This clamp is safe (min <= max), as 'remaining' won't be negative
+ // due to the initial check above (on the first iteration) and due to
+ // stopping on 'remaining.approx_empty()' below (for the second
+ // iteration onwards).
let limited = regions.size.y.clamp(Abs::zero(), remaining);
buf.push(limited);
remaining -= limited;
diff --git a/crates/typst-layout/src/flow/compose.rs b/crates/typst-layout/src/flow/compose.rs
index 326456752..3cf66f9e3 100644
--- a/crates/typst-layout/src/flow/compose.rs
+++ b/crates/typst-layout/src/flow/compose.rs
@@ -15,7 +15,7 @@ use typst_library::model::{
FootnoteElem, FootnoteEntry, LineNumberingScope, Numbering, ParLineMarker,
};
use typst_syntax::Span;
-use typst_utils::NonZeroExt;
+use typst_utils::{NonZeroExt, Numeric};
use super::{distribute, Config, FlowResult, LineNumberConfig, PlacedChild, Stop, Work};
@@ -374,7 +374,11 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
let mut relayout = false;
let mut regions = *regions;
- let mut migratable = migratable && !breakable && regions.may_progress();
+
+ // The first footnote's origin frame should be migratable if the region
+ // may progress (already checked by the footnote function) and if the
+ // origin frame isn't breakable (checked here).
+ let mut migratable = migratable && !breakable;
for (y, elem) in notes {
// The amount of space used by the in-flow content that contains the
@@ -464,11 +468,35 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
// If the first frame is empty, then none of its content fit. If
// possible, we then migrate the origin frame to the next region to
// uphold the footnote invariant (that marker and entry are on the same
- // page). If not, we just queue the footnote for the next page.
+ // page). If not, we just queue the footnote for the next page, but
+ // only if that would actually make a difference (that is, if the
+ // footnote isn't alone in the page after not fitting in any previous
+ // pages, as it probably won't ever fit then).
+ //
+ // Note that a non-zero flow need also indicates that queueing would
+ // make a difference, because the flow need is subtracted from the
+ // available height in the entry's pod even if what caused that need
+ // wasn't considered for the input `regions`. For example, floats just
+ // pass the `regions` they received along to their footnotes, which
+ // don't take into account the space occupied by the floats themselves,
+ // but they do indicate their footnotes have a non-zero flow need, so
+ // queueing them can matter as, in the following pages, the flow need
+ // will be set to zero and the footnote will be alone in the page.
+ // Then, `may_progress()` will also be false (this time, correctly) and
+ // the footnote is laid out, as queueing wouldn't improve the lack of
+ // space anymore and would result in an infinite loop.
+ //
+ // However, it is worth noting that migration does take into account
+ // the original region, before inserting what prompted the flow need.
+ // Logically, if moving the original frame can't improve the lack of
+ // space, then migration should be inhibited. The space occupied by the
+ // original frame is not relevant for that check. Therefore,
+ // `regions.may_progress()` must still be checked separately for
+ // migration, regardless of the presence of flow need.
if first.is_empty() && exist_non_empty_frame {
- if migratable {
+ if migratable && regions.may_progress() {
return Err(Stop::Finish(false));
- } else {
+ } else if regions.may_progress() || !flow_need.is_zero() {
self.footnote_queue.push(elem);
return Ok(());
}
diff --git a/crates/typst-layout/src/flow/mod.rs b/crates/typst-layout/src/flow/mod.rs
index df716b338..2f0ec39a9 100644
--- a/crates/typst-layout/src/flow/mod.rs
+++ b/crates/typst-layout/src/flow/mod.rs
@@ -203,8 +203,14 @@ pub(crate) fn layout_flow(
} else {
PageElem::width_in(shared)
};
- (0.026 * width.unwrap_or_default())
- .clamp(Em::new(0.75).resolve(shared), Em::new(2.5).resolve(shared))
+
+ // Clamp below is safe (min <= max): if the font size is
+ // negative, we set min = max = 0; otherwise,
+ // `0.75 * size <= 2.5 * size` for zero and positive sizes.
+ (0.026 * width.unwrap_or_default()).clamp(
+ Em::new(0.75).resolve(shared).max(Abs::zero()),
+ Em::new(2.5).resolve(shared).max(Abs::zero()),
+ )
},
}),
};
@@ -354,6 +360,16 @@ struct LineNumberConfig {
/// Where line numbers are reset.
scope: LineNumberingScope,
/// The default clearance for `auto`.
+ ///
+ /// This value should be relative to the page's width, such that the
+ /// clearance between line numbers and text is small when the page is,
+ /// itself, small. However, that could cause the clearance to be too small
+ /// or too large when considering the current text size; in particular, a
+ /// larger text size would require more clearance to be able to tell line
+ /// numbers apart from text, whereas a smaller text size requires less
+ /// clearance so they aren't way too far apart. Therefore, the default
+ /// value is a percentage of the page width clamped between `0.75em` and
+ /// `2.5em`.
default_clearance: Abs,
}
diff --git a/crates/typst-layout/src/grid/layouter.rs b/crates/typst-layout/src/grid/layouter.rs
index 7c94617dc..1f9cf6796 100644
--- a/crates/typst-layout/src/grid/layouter.rs
+++ b/crates/typst-layout/src/grid/layouter.rs
@@ -3,6 +3,7 @@ use std::fmt::Debug;
use typst_library::diag::{bail, SourceResult};
use typst_library::engine::Engine;
use typst_library::foundations::{Resolve, StyleChain};
+use typst_library::layout::grid::resolve::{Cell, CellGrid, LinePosition, Repeatable};
use typst_library::layout::{
Abs, Axes, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, Region, Regions, Rel,
Size, Sizing,
@@ -13,8 +14,8 @@ use typst_syntax::Span;
use typst_utils::{MaybeReverseIter, Numeric};
use super::{
- generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, Cell, CellGrid,
- LinePosition, LineSegment, Repeatable, Rowspan, UnbreakableRowGroup,
+ generate_line_segments, hline_stroke_at_column, layout_cell, vline_stroke_at_row,
+ LineSegment, Rowspan, UnbreakableRowGroup,
};
/// Performs grid layout.
@@ -843,7 +844,8 @@ impl<'a> GridLayouter<'a> {
let size = Size::new(available, height);
let pod = Region::new(size, Axes::splat(false));
- let frame = cell.layout(engine, 0, self.styles, pod.into())?.into_frame();
+ let frame =
+ layout_cell(cell, engine, 0, self.styles, pod.into())?.into_frame();
resolved.set_max(frame.width() - already_covered_width);
}
@@ -1086,7 +1088,7 @@ impl<'a> GridLayouter<'a> {
};
let frames =
- cell.layout(engine, disambiguator, self.styles, pod)?.into_frames();
+ layout_cell(cell, engine, disambiguator, self.styles, pod)?.into_frames();
// Skip the first region if one cell in it is empty. Then,
// remeasure.
@@ -1252,9 +1254,9 @@ impl<'a> GridLayouter<'a> {
// rows.
pod.full = self.regions.full;
}
- let frame = cell
- .layout(engine, disambiguator, self.styles, pod)?
- .into_frame();
+ let frame =
+ layout_cell(cell, engine, disambiguator, self.styles, pod)?
+ .into_frame();
let mut pos = pos;
if self.is_rtl {
// In the grid, cell colspans expand to the right,
@@ -1310,7 +1312,7 @@ impl<'a> GridLayouter<'a> {
// Push the layouted frames into the individual output frames.
let fragment =
- cell.layout(engine, disambiguator, self.styles, pod)?;
+ layout_cell(cell, engine, disambiguator, self.styles, pod)?;
for (output, frame) in outputs.iter_mut().zip(fragment) {
let mut pos = pos;
if self.is_rtl {
diff --git a/crates/typst-layout/src/grid/lines.rs b/crates/typst-layout/src/grid/lines.rs
index 3e89612a1..1227953d1 100644
--- a/crates/typst-layout/src/grid/lines.rs
+++ b/crates/typst-layout/src/grid/lines.rs
@@ -1,41 +1,11 @@
-use std::num::NonZeroUsize;
use std::sync::Arc;
use typst_library::foundations::{AlternativeFold, Fold};
+use typst_library::layout::grid::resolve::{CellGrid, Line, Repeatable};
use typst_library::layout::Abs;
use typst_library::visualize::Stroke;
-use super::{CellGrid, LinePosition, Repeatable, RowPiece};
-
-/// Represents an explicit grid line (horizontal or vertical) specified by the
-/// user.
-pub struct Line {
- /// The index of the track after this line. This will be the index of the
- /// row a horizontal line is above of, or of the column right after a
- /// vertical line.
- ///
- /// Must be within `0..=tracks.len()` (where `tracks` is either `grid.cols`
- /// or `grid.rows`, ignoring gutter tracks, as appropriate).
- pub index: usize,
- /// The index of the track at which this line starts being drawn.
- /// This is the first column a horizontal line appears in, or the first row
- /// a vertical line appears in.
- ///
- /// Must be within `0..tracks.len()` minus gutter tracks.
- pub start: usize,
- /// The index after the last track through which the line is drawn.
- /// Thus, the line is drawn through tracks `start..end` (note that `end` is
- /// exclusive).
- ///
- /// Must be within `1..=tracks.len()` minus gutter tracks.
- /// `None` indicates the line should go all the way to the end.
- pub end: Option,
- /// The line's stroke. This is `None` when the line is explicitly used to
- /// override a previously specified line.
- pub stroke: Option>>,
- /// The line's position in relation to the track with its index.
- pub position: LinePosition,
-}
+use super::RowPiece;
/// Indicates which priority a particular grid line segment should have, based
/// on the highest priority configuration that defined the segment's stroke.
@@ -588,13 +558,13 @@ pub fn hline_stroke_at_column(
#[cfg(test)]
mod test {
+ use std::num::NonZeroUsize;
use typst_library::foundations::Content;
use typst_library::introspection::Locator;
+ use typst_library::layout::grid::resolve::{Cell, Entry, LinePosition};
use typst_library::layout::{Axes, Sides, Sizing};
use typst_utils::NonZeroExt;
- use super::super::cells::Entry;
- use super::super::Cell;
use super::*;
fn sample_cell() -> Cell<'static> {
diff --git a/crates/typst-layout/src/grid/mod.rs b/crates/typst-layout/src/grid/mod.rs
index 769bef8c5..1b4380f0a 100644
--- a/crates/typst-layout/src/grid/mod.rs
+++ b/crates/typst-layout/src/grid/mod.rs
@@ -1,40 +1,44 @@
-mod cells;
mod layouter;
mod lines;
mod repeated;
mod rowspans;
-pub use self::cells::{Cell, CellGrid};
pub use self::layouter::GridLayouter;
-use std::num::NonZeroUsize;
-use std::sync::Arc;
-
-use ecow::eco_format;
-use typst_library::diag::{SourceResult, Trace, Tracepoint};
+use typst_library::diag::SourceResult;
use typst_library::engine::Engine;
-use typst_library::foundations::{Fold, Packed, Smart, StyleChain};
+use typst_library::foundations::{Packed, StyleChain};
use typst_library::introspection::Locator;
-use typst_library::layout::{
- Abs, Alignment, Axes, Dir, Fragment, GridCell, GridChild, GridElem, GridItem, Length,
- OuterHAlignment, OuterVAlignment, Regions, Rel, Sides,
-};
-use typst_library::model::{TableCell, TableChild, TableElem, TableItem};
-use typst_library::text::TextElem;
-use typst_library::visualize::{Paint, Stroke};
-use typst_syntax::Span;
+use typst_library::layout::grid::resolve::{grid_to_cellgrid, table_to_cellgrid, Cell};
+use typst_library::layout::{Fragment, GridElem, Regions};
+use typst_library::model::TableElem;
-use self::cells::{
- LinePosition, ResolvableCell, ResolvableGridChild, ResolvableGridItem,
-};
use self::layouter::RowPiece;
use self::lines::{
- generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, Line,
- LineSegment,
+ generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, LineSegment,
};
-use self::repeated::{Footer, Header, Repeatable};
use self::rowspans::{Rowspan, UnbreakableRowGroup};
+/// Layout the cell into the given regions.
+///
+/// The `disambiguator` indicates which instance of this cell this should be
+/// layouted as. For normal cells, it is always `0`, but for headers and
+/// footers, it indicates the index of the header/footer among all. See the
+/// [`Locator`] docs for more details on the concepts behind this.
+pub fn layout_cell(
+ cell: &Cell,
+ engine: &mut Engine,
+ disambiguator: usize,
+ styles: StyleChain,
+ regions: Regions,
+) -> SourceResult {
+ let mut locator = cell.locator.relayout();
+ if disambiguator > 0 {
+ locator = locator.split().next_inner(disambiguator as u128);
+ }
+ crate::layout_fragment(engine, &cell.body, locator, styles, regions)
+}
+
/// Layout the grid.
#[typst_macros::time(span = elem.span())]
pub fn layout_grid(
@@ -44,54 +48,8 @@ pub fn layout_grid(
styles: StyleChain,
regions: Regions,
) -> SourceResult {
- let inset = elem.inset(styles);
- let align = elem.align(styles);
- let columns = elem.columns(styles);
- let rows = elem.rows(styles);
- let column_gutter = elem.column_gutter(styles);
- let row_gutter = elem.row_gutter(styles);
- let fill = elem.fill(styles);
- let stroke = elem.stroke(styles);
-
- let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
- let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
- // Use trace to link back to the grid when a specific cell errors
- let tracepoint = || Tracepoint::Call(Some(eco_format!("grid")));
- let resolve_item = |item: &GridItem| grid_item_to_resolvable(item, styles);
- let children = elem.children().iter().map(|child| match child {
- GridChild::Header(header) => ResolvableGridChild::Header {
- repeat: header.repeat(styles),
- span: header.span(),
- items: header.children().iter().map(resolve_item),
- },
- GridChild::Footer(footer) => ResolvableGridChild::Footer {
- repeat: footer.repeat(styles),
- span: footer.span(),
- items: footer.children().iter().map(resolve_item),
- },
- GridChild::Item(item) => {
- ResolvableGridChild::Item(grid_item_to_resolvable(item, styles))
- }
- });
- let grid = CellGrid::resolve(
- tracks,
- gutter,
- locator,
- children,
- fill,
- align,
- &inset,
- &stroke,
- engine,
- styles,
- elem.span(),
- )
- .trace(engine.world, tracepoint, elem.span())?;
-
- let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
-
- // Measure the columns and layout the grid row-by-row.
- layouter.layout(engine)
+ let grid = grid_to_cellgrid(elem, engine, locator, styles)?;
+ GridLayouter::new(&grid, regions, styles, elem.span()).layout(engine)
}
/// Layout the table.
@@ -103,314 +61,6 @@ pub fn layout_table(
styles: StyleChain,
regions: Regions,
) -> SourceResult {
- let inset = elem.inset(styles);
- let align = elem.align(styles);
- let columns = elem.columns(styles);
- let rows = elem.rows(styles);
- let column_gutter = elem.column_gutter(styles);
- let row_gutter = elem.row_gutter(styles);
- let fill = elem.fill(styles);
- let stroke = elem.stroke(styles);
-
- let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
- let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
- // Use trace to link back to the table when a specific cell errors
- let tracepoint = || Tracepoint::Call(Some(eco_format!("table")));
- let resolve_item = |item: &TableItem| table_item_to_resolvable(item, styles);
- let children = elem.children().iter().map(|child| match child {
- TableChild::Header(header) => ResolvableGridChild::Header {
- repeat: header.repeat(styles),
- span: header.span(),
- items: header.children().iter().map(resolve_item),
- },
- TableChild::Footer(footer) => ResolvableGridChild::Footer {
- repeat: footer.repeat(styles),
- span: footer.span(),
- items: footer.children().iter().map(resolve_item),
- },
- TableChild::Item(item) => {
- ResolvableGridChild::Item(table_item_to_resolvable(item, styles))
- }
- });
- let grid = CellGrid::resolve(
- tracks,
- gutter,
- locator,
- children,
- fill,
- align,
- &inset,
- &stroke,
- engine,
- styles,
- elem.span(),
- )
- .trace(engine.world, tracepoint, elem.span())?;
-
- let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
- layouter.layout(engine)
-}
-
-fn grid_item_to_resolvable(
- item: &GridItem,
- styles: StyleChain,
-) -> ResolvableGridItem> {
- match item {
- GridItem::HLine(hline) => ResolvableGridItem::HLine {
- y: hline.y(styles),
- start: hline.start(styles),
- end: hline.end(styles),
- stroke: hline.stroke(styles),
- span: hline.span(),
- position: match hline.position(styles) {
- OuterVAlignment::Top => LinePosition::Before,
- OuterVAlignment::Bottom => LinePosition::After,
- },
- },
- GridItem::VLine(vline) => ResolvableGridItem::VLine {
- x: vline.x(styles),
- start: vline.start(styles),
- end: vline.end(styles),
- stroke: vline.stroke(styles),
- span: vline.span(),
- position: match vline.position(styles) {
- OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => {
- LinePosition::After
- }
- OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => {
- LinePosition::Before
- }
- OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before,
- OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After,
- },
- },
- GridItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()),
- }
-}
-
-fn table_item_to_resolvable(
- item: &TableItem,
- styles: StyleChain,
-) -> ResolvableGridItem> {
- match item {
- TableItem::HLine(hline) => ResolvableGridItem::HLine {
- y: hline.y(styles),
- start: hline.start(styles),
- end: hline.end(styles),
- stroke: hline.stroke(styles),
- span: hline.span(),
- position: match hline.position(styles) {
- OuterVAlignment::Top => LinePosition::Before,
- OuterVAlignment::Bottom => LinePosition::After,
- },
- },
- TableItem::VLine(vline) => ResolvableGridItem::VLine {
- x: vline.x(styles),
- start: vline.start(styles),
- end: vline.end(styles),
- stroke: vline.stroke(styles),
- span: vline.span(),
- position: match vline.position(styles) {
- OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => {
- LinePosition::After
- }
- OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => {
- LinePosition::Before
- }
- OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before,
- OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After,
- },
- },
- TableItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()),
- }
-}
-
-impl ResolvableCell for Packed {
- fn resolve_cell<'a>(
- mut self,
- x: usize,
- y: usize,
- fill: &Option,
- align: Smart,
- inset: Sides