mirror of
https://github.com/typst/typst
synced 2025-08-07 11:47:53 +08:00
Merge remote-tracking branch 'origin/main' into issue5719
This commit is contained in:
commit
aa8f1c8f83
@ -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
|
||||
|
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -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"
|
||||
|
@ -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"
|
||||
|
12
README.md
12
README.md
@ -5,19 +5,19 @@
|
||||
<p align="center">
|
||||
<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"
|
||||
/></a>
|
||||
></a>
|
||||
<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"
|
||||
/></a>
|
||||
></a>
|
||||
<a href="https://discord.gg/2uDybryKPe">
|
||||
<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">
|
||||
<img alt="Apache-2 License" src="https://img.shields.io/badge/license-Apache%202-brightgreen"
|
||||
/></a>
|
||||
></a>
|
||||
<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"
|
||||
/></a>
|
||||
></a>
|
||||
</p>
|
||||
|
||||
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:
|
||||
<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>
|
||||
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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::<Vec<_>>();
|
||||
PdfStandards::new(&list)?
|
||||
|
@ -305,7 +305,7 @@ impl FileSlot {
|
||||
) -> FileResult<Bytes> {
|
||||
self.file.get_or_init(
|
||||
|| read(self.id, project_root, package_storage),
|
||||
|data, _| Ok(data.into()),
|
||||
|data, _| Ok(Bytes::new(data)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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"]);
|
||||
|
@ -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)?;
|
||||
|
@ -12,6 +12,9 @@ pub fn html(document: &HtmlDocument) -> SourceResult<String> {
|
||||
w.buf.push_str("<!DOCTYPE html>");
|
||||
write_indent(&mut w);
|
||||
write_element(&mut w, &document.root)?;
|
||||
if w.pretty {
|
||||
w.buf.push('\n');
|
||||
}
|
||||
Ok(w.buf)
|
||||
}
|
||||
|
||||
|
@ -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::<ast::Expr>());
|
||||
if let Some(source) = great.children().find(|child| child.is::<ast::Expr>());
|
||||
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"]);
|
||||
}
|
||||
}
|
||||
|
@ -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<Definition>);
|
||||
|
||||
@ -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[] <hi> See @hi", 21, Side::After).must_be_at("main.typ", 1..9);
|
||||
test("#figure[] <hi> See @hi", -2, Side::After).must_be_at("main.typ", 1..9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -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<Jump>) {
|
||||
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<Jump>) {
|
||||
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<Position>) {
|
||||
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<Position>) {
|
||||
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);
|
||||
|
@ -89,15 +89,21 @@ pub fn named_items<T>(
|
||||
// ```
|
||||
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<Value>)>;
|
||||
|
||||
trait ResponseExt {
|
||||
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_include_value(&self, name_value: (&str, Option<&Value>)) -> &Self;
|
||||
}
|
||||
|
||||
impl ResponseExt for Response {
|
||||
#[track_caller]
|
||||
fn must_include<'a>(&self, includes: impl IntoIterator<Item = &'a str>) -> &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<Item = &'a str>) -> &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))));
|
||||
}
|
||||
}
|
||||
|
@ -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<FileId, Bytes>,
|
||||
sources: HashMap<FileId, Source>,
|
||||
files: Arc<TestFiles>,
|
||||
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<Source> {
|
||||
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<Bytes> {
|
||||
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<FileId> {
|
||||
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<FileId, Bytes>,
|
||||
sources: HashMap<FileId, Source>,
|
||||
}
|
||||
|
||||
/// 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<TestWorld>;
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -274,10 +274,12 @@ fn font_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option<Tooltip> {
|
||||
|
||||
#[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<Tooltip>;
|
||||
|
||||
@ -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`");
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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(());
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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<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,
|
||||
}
|
||||
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> {
|
||||
|
@ -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<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.
|
||||
#[typst_macros::time(span = elem.span())]
|
||||
pub fn layout_grid(
|
||||
@ -44,54 +48,8 @@ pub fn layout_grid(
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
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<Fragment> {
|
||||
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<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)
|
||||
}
|
||||
let grid = table_to_cellgrid(elem, engine, locator, styles)?;
|
||||
GridLayouter::new(&grid, regions, styles, elem.span()).layout(engine)
|
||||
}
|
||||
|
@ -1,50 +1,11 @@
|
||||
use typst_library::diag::SourceResult;
|
||||
use typst_library::engine::Engine;
|
||||
use typst_library::layout::grid::resolve::{Footer, Header, Repeatable};
|
||||
use typst_library::layout::{Abs, Axes, Frame, Regions};
|
||||
|
||||
use super::layouter::GridLayouter;
|
||||
use super::rowspans::UnbreakableRowGroup;
|
||||
|
||||
/// A repeatable grid header. Starts at the first row.
|
||||
pub struct Header {
|
||||
/// The index after the last row included in this header.
|
||||
pub end: usize,
|
||||
}
|
||||
|
||||
/// A repeatable grid footer. Stops at the last row.
|
||||
pub struct Footer {
|
||||
/// The first row included in this footer.
|
||||
pub start: usize,
|
||||
}
|
||||
|
||||
/// A possibly repeatable grid object.
|
||||
/// It still exists even when not repeatable, but must not have additional
|
||||
/// considerations by grid layout, other than for consistency (such as making
|
||||
/// a certain group of rows unbreakable).
|
||||
pub enum Repeatable<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<'_> {
|
||||
/// Layouts the header's rows.
|
||||
/// Skips regions as necessary.
|
||||
|
@ -1,12 +1,12 @@
|
||||
use typst_library::diag::SourceResult;
|
||||
use typst_library::engine::Engine;
|
||||
use typst_library::foundations::Resolve;
|
||||
use typst_library::layout::grid::resolve::Repeatable;
|
||||
use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing};
|
||||
use typst_utils::MaybeReverseIter;
|
||||
|
||||
use super::layouter::{in_last_with_offset, points, Row, RowPiece};
|
||||
use super::repeated::Repeatable;
|
||||
use super::{Cell, GridLayouter};
|
||||
use super::{layout_cell, Cell, GridLayouter};
|
||||
|
||||
/// All information needed to layout a single rowspan.
|
||||
pub struct Rowspan {
|
||||
@ -141,7 +141,7 @@ impl GridLayouter<'_> {
|
||||
}
|
||||
|
||||
// Push the layouted frames directly into the finished frames.
|
||||
let fragment = cell.layout(engine, disambiguator, self.styles, pod)?;
|
||||
let fragment = layout_cell(cell, engine, disambiguator, self.styles, pod)?;
|
||||
let (current_region, current_rrows) = current_region_data.unzip();
|
||||
for ((i, finished), frame) in self
|
||||
.finished
|
||||
|
@ -1,16 +1,16 @@
|
||||
use std::ffi::OsStr;
|
||||
|
||||
use typst_library::diag::{bail, warning, At, SourceResult, StrResult};
|
||||
use typst_library::diag::{warning, At, SourceResult, StrResult};
|
||||
use typst_library::engine::Engine;
|
||||
use typst_library::foundations::{Packed, Smart, StyleChain};
|
||||
use typst_library::foundations::{Bytes, Derived, Packed, Smart, StyleChain};
|
||||
use typst_library::introspection::Locator;
|
||||
use typst_library::layout::{
|
||||
Abs, Axes, FixedAlignment, Frame, FrameItem, Point, Region, Size,
|
||||
};
|
||||
use typst_library::loading::Readable;
|
||||
use typst_library::loading::DataSource;
|
||||
use typst_library::text::families;
|
||||
use typst_library::visualize::{
|
||||
Image, ImageElem, ImageFit, ImageFormat, Path, RasterFormat, VectorFormat,
|
||||
Curve, Image, ImageElem, ImageFit, ImageFormat, RasterFormat, VectorFormat,
|
||||
};
|
||||
|
||||
/// Layout the image.
|
||||
@ -26,17 +26,17 @@ pub fn layout_image(
|
||||
|
||||
// Take the format that was explicitly defined, or parse the extension,
|
||||
// or try to detect the format.
|
||||
let data = elem.data();
|
||||
let Derived { source, derived: data } = &elem.source;
|
||||
let format = match elem.format(styles) {
|
||||
Smart::Custom(v) => v,
|
||||
Smart::Auto => determine_format(elem.path().as_str(), data).at(span)?,
|
||||
Smart::Auto => determine_format(source, data).at(span)?,
|
||||
};
|
||||
|
||||
// Warn the user if the image contains a foreign object. Not perfect
|
||||
// because the svg could also be encoded, but that's an edge case.
|
||||
if format == ImageFormat::Vector(VectorFormat::Svg) {
|
||||
let has_foreign_object =
|
||||
data.as_str().is_some_and(|s| s.contains("<foreignObject"));
|
||||
data.as_str().is_ok_and(|s| s.contains("<foreignObject"));
|
||||
|
||||
if has_foreign_object {
|
||||
engine.sink.warn(warning!(
|
||||
@ -50,7 +50,7 @@ pub fn layout_image(
|
||||
|
||||
// Construct the image itself.
|
||||
let image = Image::with_fonts(
|
||||
data.clone().into(),
|
||||
data.clone(),
|
||||
format,
|
||||
elem.alt(styles),
|
||||
engine.world,
|
||||
@ -113,31 +113,29 @@ pub fn layout_image(
|
||||
|
||||
// Create a clipping group if only part of the image should be visible.
|
||||
if fit == ImageFit::Cover && !target.fits(fitted) {
|
||||
frame.clip(Path::rect(frame.size()));
|
||||
frame.clip(Curve::rect(frame.size()));
|
||||
}
|
||||
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
/// Determine the image format based on path and data.
|
||||
fn determine_format(path: &str, data: &Readable) -> StrResult<ImageFormat> {
|
||||
let ext = std::path::Path::new(path)
|
||||
.extension()
|
||||
.and_then(OsStr::to_str)
|
||||
.unwrap_or_default()
|
||||
.to_lowercase();
|
||||
/// Try to determine the image format based on the data.
|
||||
fn determine_format(source: &DataSource, data: &Bytes) -> StrResult<ImageFormat> {
|
||||
if let DataSource::Path(path) = source {
|
||||
let ext = std::path::Path::new(path.as_str())
|
||||
.extension()
|
||||
.and_then(OsStr::to_str)
|
||||
.unwrap_or_default()
|
||||
.to_lowercase();
|
||||
|
||||
Ok(match ext.as_str() {
|
||||
"png" => ImageFormat::Raster(RasterFormat::Png),
|
||||
"jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg),
|
||||
"gif" => ImageFormat::Raster(RasterFormat::Gif),
|
||||
"svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg),
|
||||
_ => match &data {
|
||||
Readable::Str(_) => ImageFormat::Vector(VectorFormat::Svg),
|
||||
Readable::Bytes(bytes) => match RasterFormat::detect(bytes) {
|
||||
Some(f) => ImageFormat::Raster(f),
|
||||
None => bail!("unknown image format"),
|
||||
},
|
||||
},
|
||||
})
|
||||
match ext.as_str() {
|
||||
"png" => return Ok(ImageFormat::Raster(RasterFormat::Png)),
|
||||
"jpg" | "jpeg" => return Ok(ImageFormat::Raster(RasterFormat::Jpg)),
|
||||
"gif" => return Ok(ImageFormat::Raster(RasterFormat::Gif)),
|
||||
"svg" | "svgz" => return Ok(ImageFormat::Vector(VectorFormat::Svg)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ImageFormat::detect(data).ok_or("unknown image format")?)
|
||||
}
|
||||
|
@ -161,9 +161,9 @@ pub fn collect<'a>(
|
||||
}
|
||||
|
||||
if let Some(case) = TextElem::case_in(styles) {
|
||||
full.push_str(&case.apply(elem.text()));
|
||||
full.push_str(&case.apply(&elem.text));
|
||||
} else {
|
||||
full.push_str(elem.text());
|
||||
full.push_str(&elem.text);
|
||||
}
|
||||
|
||||
if dir != outer_dir {
|
||||
@ -172,13 +172,12 @@ pub fn collect<'a>(
|
||||
}
|
||||
});
|
||||
} else if let Some(elem) = child.to_packed::<HElem>() {
|
||||
let amount = elem.amount();
|
||||
if amount.is_zero() {
|
||||
if elem.amount.is_zero() {
|
||||
continue;
|
||||
}
|
||||
|
||||
collector.push_item(match amount {
|
||||
Spacing::Fr(fr) => Item::Fractional(*fr, None),
|
||||
collector.push_item(match elem.amount {
|
||||
Spacing::Fr(fr) => Item::Fractional(fr, None),
|
||||
Spacing::Rel(rel) => Item::Absolute(
|
||||
rel.resolve(styles).relative_to(region.x),
|
||||
elem.weak(styles),
|
||||
|
@ -23,8 +23,8 @@ pub use self::pad::layout_pad;
|
||||
pub use self::pages::layout_document;
|
||||
pub use self::repeat::layout_repeat;
|
||||
pub use self::shapes::{
|
||||
layout_circle, layout_ellipse, layout_line, layout_path, layout_polygon, layout_rect,
|
||||
layout_square,
|
||||
layout_circle, layout_curve, layout_ellipse, layout_line, layout_path,
|
||||
layout_polygon, layout_rect, layout_square,
|
||||
};
|
||||
pub use self::stack::layout_stack;
|
||||
pub use self::transforms::{layout_move, layout_rotate, layout_scale, layout_skew};
|
||||
|
@ -4,11 +4,12 @@ use typst_library::diag::SourceResult;
|
||||
use typst_library::engine::Engine;
|
||||
use typst_library::foundations::{Content, Context, Depth, Packed, StyleChain};
|
||||
use typst_library::introspection::Locator;
|
||||
use typst_library::layout::grid::resolve::{Cell, CellGrid};
|
||||
use typst_library::layout::{Axes, Fragment, HAlignment, Regions, Sizing, VAlignment};
|
||||
use typst_library::model::{EnumElem, ListElem, Numbering, ParElem};
|
||||
use typst_library::text::TextElem;
|
||||
|
||||
use crate::grid::{Cell, CellGrid, GridLayouter};
|
||||
use crate::grid::GridLayouter;
|
||||
|
||||
/// Layout the list.
|
||||
#[typst_macros::time(span = elem.span())]
|
||||
@ -39,7 +40,7 @@ pub fn layout_list(
|
||||
let mut cells = vec![];
|
||||
let mut locator = locator.split();
|
||||
|
||||
for item in elem.children() {
|
||||
for item in &elem.children {
|
||||
cells.push(Cell::new(Content::empty(), locator.next(&())));
|
||||
cells.push(Cell::new(marker.clone(), locator.next(&marker.span())));
|
||||
cells.push(Cell::new(Content::empty(), locator.next(&())));
|
||||
@ -100,7 +101,7 @@ pub fn layout_enum(
|
||||
// relation to the item it refers to.
|
||||
let number_align = elem.number_align(styles);
|
||||
|
||||
for item in elem.children() {
|
||||
for item in &elem.children {
|
||||
number = item.number(styles).unwrap_or(number);
|
||||
|
||||
let context = Context::new(None, Some(styles));
|
||||
|
@ -1,12 +1,9 @@
|
||||
use typst_library::diag::SourceResult;
|
||||
use typst_library::foundations::{Packed, StyleChain};
|
||||
use typst_library::layout::{Em, Frame, Point, Rel, Size};
|
||||
use typst_library::layout::{Em, Frame, Point, Size};
|
||||
use typst_library::math::{Accent, AccentElem};
|
||||
|
||||
use super::{
|
||||
scaled_font_size, style_cramped, FrameFragment, GlyphFragment, MathContext,
|
||||
MathFragment,
|
||||
};
|
||||
use super::{style_cramped, FrameFragment, GlyphFragment, MathContext, MathFragment};
|
||||
|
||||
/// How much the accent can be shorter than the base.
|
||||
const ACCENT_SHORT_FALL: Em = Em::new(0.5);
|
||||
@ -19,7 +16,7 @@ pub fn layout_accent(
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let cramped = style_cramped();
|
||||
let mut base = ctx.layout_into_fragment(elem.base(), styles.chain(&cramped))?;
|
||||
let mut base = ctx.layout_into_fragment(&elem.base, styles.chain(&cramped))?;
|
||||
|
||||
// Try to replace a glyph with its dotless variant.
|
||||
if let MathFragment::Glyph(glyph) = &mut base {
|
||||
@ -30,14 +27,10 @@ pub fn layout_accent(
|
||||
let base_class = base.class();
|
||||
let base_attach = base.accent_attach();
|
||||
|
||||
let width = elem
|
||||
.size(styles)
|
||||
.unwrap_or(Rel::one())
|
||||
.at(scaled_font_size(ctx, styles))
|
||||
.relative_to(base.width());
|
||||
let width = elem.size(styles).relative_to(base.width());
|
||||
|
||||
let Accent(c) = elem.accent();
|
||||
let mut glyph = GlyphFragment::new(ctx, styles, *c, elem.span());
|
||||
let Accent(c) = elem.accent;
|
||||
let mut glyph = GlyphFragment::new(ctx, styles, c, elem.span());
|
||||
|
||||
// Try to replace accent glyph with flattened variant.
|
||||
let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height);
|
||||
@ -75,7 +68,7 @@ pub fn layout_accent(
|
||||
frame.push_frame(accent_pos, accent);
|
||||
frame.push_frame(base_pos, base.into_frame());
|
||||
ctx.push(
|
||||
FrameFragment::new(ctx, styles, frame)
|
||||
FrameFragment::new(styles, frame)
|
||||
.with_class(base_class)
|
||||
.with_base_ascent(base_ascent)
|
||||
.with_italics_correction(base_italics_correction)
|
||||
|
@ -1,6 +1,6 @@
|
||||
use typst_library::diag::SourceResult;
|
||||
use typst_library::foundations::{Packed, Smart, StyleChain};
|
||||
use typst_library::layout::{Abs, Axis, Corner, Frame, Length, Point, Rel, Size};
|
||||
use typst_library::foundations::{Packed, StyleChain};
|
||||
use typst_library::layout::{Abs, Axis, Corner, Frame, Point, Rel, Size};
|
||||
use typst_library::math::{
|
||||
AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem,
|
||||
};
|
||||
@ -29,7 +29,7 @@ pub fn layout_attach(
|
||||
let elem = merged.as_ref().unwrap_or(elem);
|
||||
let stretch = stretch_size(styles, elem);
|
||||
|
||||
let mut base = ctx.layout_into_fragment(elem.base(), styles)?;
|
||||
let mut base = ctx.layout_into_fragment(&elem.base, styles)?;
|
||||
let sup_style = style_for_superscript(styles);
|
||||
let sup_style_chain = styles.chain(&sup_style);
|
||||
let tl = elem.tl(sup_style_chain);
|
||||
@ -95,7 +95,7 @@ pub fn layout_primes(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
match *elem.count() {
|
||||
match elem.count {
|
||||
count @ 1..=4 => {
|
||||
let c = match count {
|
||||
1 => '′',
|
||||
@ -121,7 +121,7 @@ pub fn layout_primes(
|
||||
prime.clone(),
|
||||
)
|
||||
}
|
||||
ctx.push(FrameFragment::new(ctx, styles, frame).with_text_like(true));
|
||||
ctx.push(FrameFragment::new(styles, frame).with_text_like(true));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@ -134,7 +134,7 @@ pub fn layout_scripts(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?;
|
||||
let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
|
||||
fragment.set_limits(Limits::Never);
|
||||
ctx.push(fragment);
|
||||
Ok(())
|
||||
@ -148,21 +148,18 @@ pub fn layout_limits(
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let limits = if elem.inline(styles) { Limits::Always } else { Limits::Display };
|
||||
let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?;
|
||||
let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
|
||||
fragment.set_limits(limits);
|
||||
ctx.push(fragment);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the size to stretch the base to, if the attach argument is true.
|
||||
fn stretch_size(
|
||||
styles: StyleChain,
|
||||
elem: &Packed<AttachElem>,
|
||||
) -> Option<Smart<Rel<Length>>> {
|
||||
/// Get the size to stretch the base to.
|
||||
fn stretch_size(styles: StyleChain, elem: &Packed<AttachElem>) -> Option<Rel<Abs>> {
|
||||
// Extract from an EquationElem.
|
||||
let mut base = elem.base();
|
||||
if let Some(equation) = base.to_packed::<EquationElem>() {
|
||||
base = equation.body();
|
||||
let mut base = &elem.base;
|
||||
while let Some(equation) = base.to_packed::<EquationElem>() {
|
||||
base = &equation.body;
|
||||
}
|
||||
|
||||
base.to_packed::<StretchElem>().map(|stretch| stretch.size(styles))
|
||||
@ -277,7 +274,7 @@ fn layout_attachments(
|
||||
layout!(b, b_x, b_y); // lower-limit
|
||||
|
||||
// Done! Note that we retain the class of the base.
|
||||
ctx.push(FrameFragment::new(ctx, styles, frame).with_class(base_class));
|
||||
ctx.push(FrameFragment::new(styles, frame).with_class(base_class));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ use typst_library::text::TextElem;
|
||||
use typst_library::visualize::{FixedStroke, Geometry};
|
||||
use typst_syntax::Span;
|
||||
|
||||
use super::{scaled_font_size, FrameFragment, MathContext};
|
||||
use super::{FrameFragment, MathContext};
|
||||
|
||||
/// Lays out a [`CancelElem`].
|
||||
#[typst_macros::time(name = "math.cancel", span = elem.span())]
|
||||
@ -16,7 +16,7 @@ pub fn layout_cancel(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let body = ctx.layout_into_fragment(elem.body(), styles)?;
|
||||
let body = ctx.layout_into_fragment(&elem.body, styles)?;
|
||||
|
||||
// Preserve properties of body.
|
||||
let body_class = body.class();
|
||||
@ -27,7 +27,7 @@ pub fn layout_cancel(
|
||||
let mut body = body.into_frame();
|
||||
let body_size = body.size();
|
||||
let span = elem.span();
|
||||
let length = elem.length(styles).at(scaled_font_size(ctx, styles));
|
||||
let length = elem.length(styles);
|
||||
|
||||
let stroke = elem.stroke(styles).unwrap_or(FixedStroke {
|
||||
paint: TextElem::fill_in(styles).as_decoration(),
|
||||
@ -63,7 +63,7 @@ pub fn layout_cancel(
|
||||
}
|
||||
|
||||
ctx.push(
|
||||
FrameFragment::new(ctx, styles, body)
|
||||
FrameFragment::new(styles, body)
|
||||
.with_class(body_class)
|
||||
.with_italics_correction(body_italics)
|
||||
.with_accent_attach(body_attach)
|
||||
|
@ -1,5 +1,5 @@
|
||||
use typst_library::diag::SourceResult;
|
||||
use typst_library::foundations::{Content, Packed, StyleChain};
|
||||
use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
|
||||
use typst_library::layout::{Em, Frame, FrameItem, Point, Size};
|
||||
use typst_library::math::{BinomElem, FracElem};
|
||||
use typst_library::text::TextElem;
|
||||
@ -7,8 +7,8 @@ use typst_library::visualize::{FixedStroke, Geometry};
|
||||
use typst_syntax::Span;
|
||||
|
||||
use super::{
|
||||
scaled_font_size, style_for_denominator, style_for_numerator, FrameFragment,
|
||||
GlyphFragment, MathContext, DELIM_SHORT_FALL,
|
||||
style_for_denominator, style_for_numerator, FrameFragment, GlyphFragment,
|
||||
MathContext, DELIM_SHORT_FALL,
|
||||
};
|
||||
|
||||
const FRAC_AROUND: Em = Em::new(0.1);
|
||||
@ -23,8 +23,8 @@ pub fn layout_frac(
|
||||
layout_frac_like(
|
||||
ctx,
|
||||
styles,
|
||||
elem.num(),
|
||||
std::slice::from_ref(elem.denom()),
|
||||
&elem.num,
|
||||
std::slice::from_ref(&elem.denom),
|
||||
false,
|
||||
elem.span(),
|
||||
)
|
||||
@ -37,7 +37,7 @@ pub fn layout_binom(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
layout_frac_like(ctx, styles, elem.upper(), elem.lower(), true, elem.span())
|
||||
layout_frac_like(ctx, styles, &elem.upper, &elem.lower, true, elem.span())
|
||||
}
|
||||
|
||||
/// Layout a fraction or binomial.
|
||||
@ -49,8 +49,7 @@ fn layout_frac_like(
|
||||
binom: bool,
|
||||
span: Span,
|
||||
) -> SourceResult<()> {
|
||||
let font_size = scaled_font_size(ctx, styles);
|
||||
let short_fall = DELIM_SHORT_FALL.at(font_size);
|
||||
let short_fall = DELIM_SHORT_FALL.resolve(styles);
|
||||
let axis = scaled!(ctx, styles, axis_height);
|
||||
let thickness = scaled!(ctx, styles, fraction_rule_thickness);
|
||||
let shift_up = scaled!(
|
||||
@ -86,7 +85,7 @@ fn layout_frac_like(
|
||||
styles.chain(&denom_style),
|
||||
)?;
|
||||
|
||||
let around = FRAC_AROUND.at(font_size);
|
||||
let around = FRAC_AROUND.resolve(styles);
|
||||
let num_gap = (shift_up - (axis + thickness / 2.0) - num.descent()).max(num_min);
|
||||
let denom_gap =
|
||||
(shift_down + (axis - thickness / 2.0) - denom.ascent()).max(denom_min);
|
||||
@ -111,7 +110,7 @@ fn layout_frac_like(
|
||||
.stretch_vertical(ctx, height, short_fall);
|
||||
left.center_on_axis(ctx);
|
||||
ctx.push(left);
|
||||
ctx.push(FrameFragment::new(ctx, styles, frame));
|
||||
ctx.push(FrameFragment::new(styles, frame));
|
||||
let mut right = GlyphFragment::new(ctx, styles, ')', span)
|
||||
.stretch_vertical(ctx, height, short_fall);
|
||||
right.center_on_axis(ctx);
|
||||
@ -129,7 +128,7 @@ fn layout_frac_like(
|
||||
span,
|
||||
),
|
||||
);
|
||||
ctx.push(FrameFragment::new(ctx, styles, frame));
|
||||
ctx.push(FrameFragment::new(styles, frame));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -17,7 +17,7 @@ use typst_library::visualize::Paint;
|
||||
use typst_syntax::Span;
|
||||
use unicode_math_class::MathClass;
|
||||
|
||||
use super::{scaled_font_size, stretch_glyph, MathContext, Scaled};
|
||||
use super::{stretch_glyph, MathContext, Scaled};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MathFragment {
|
||||
@ -292,7 +292,7 @@ impl GlyphFragment {
|
||||
region: TextElem::region_in(styles),
|
||||
fill: TextElem::fill_in(styles).as_decoration(),
|
||||
shift: TextElem::baseline_in(styles),
|
||||
font_size: scaled_font_size(ctx, styles),
|
||||
font_size: TextElem::size_in(styles),
|
||||
math_size: EquationElem::size_in(styles),
|
||||
width: Abs::zero(),
|
||||
ascent: Abs::zero(),
|
||||
@ -512,12 +512,12 @@ pub struct FrameFragment {
|
||||
}
|
||||
|
||||
impl FrameFragment {
|
||||
pub fn new(ctx: &MathContext, styles: StyleChain, frame: Frame) -> Self {
|
||||
pub fn new(styles: StyleChain, frame: Frame) -> Self {
|
||||
let base_ascent = frame.ascent();
|
||||
let accent_attach = frame.width() / 2.0;
|
||||
Self {
|
||||
frame: frame.post_processed(styles),
|
||||
font_size: scaled_font_size(ctx, styles),
|
||||
font_size: TextElem::size_in(styles),
|
||||
class: EquationElem::class_in(styles).unwrap_or(MathClass::Normal),
|
||||
math_size: EquationElem::size_in(styles),
|
||||
limits: Limits::Never,
|
||||
|
@ -1,6 +1,6 @@
|
||||
use typst_library::diag::SourceResult;
|
||||
use typst_library::foundations::{Packed, Smart, StyleChain};
|
||||
use typst_library::layout::{Abs, Axis, Length, Rel};
|
||||
use typst_library::foundations::{Packed, StyleChain};
|
||||
use typst_library::layout::{Abs, Axis, Rel};
|
||||
use typst_library::math::{EquationElem, LrElem, MidElem};
|
||||
use unicode_math_class::MathClass;
|
||||
|
||||
@ -13,17 +13,16 @@ pub fn layout_lr(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let mut body = elem.body();
|
||||
|
||||
// Extract from an EquationElem.
|
||||
let mut body = &elem.body;
|
||||
if let Some(equation) = body.to_packed::<EquationElem>() {
|
||||
body = equation.body();
|
||||
body = &equation.body;
|
||||
}
|
||||
|
||||
// Extract implicit LrElem.
|
||||
if let Some(lr) = body.to_packed::<LrElem>() {
|
||||
if lr.size(styles).is_auto() {
|
||||
body = lr.body();
|
||||
if lr.size(styles).is_one() {
|
||||
body = &lr.body;
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,7 +99,7 @@ pub fn layout_mid(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let mut fragments = ctx.layout_into_fragments(elem.body(), styles)?;
|
||||
let mut fragments = ctx.layout_into_fragments(&elem.body, styles)?;
|
||||
|
||||
for fragment in &mut fragments {
|
||||
match fragment {
|
||||
@ -128,7 +127,7 @@ fn scale(
|
||||
styles: StyleChain,
|
||||
fragment: &mut MathFragment,
|
||||
relative_to: Abs,
|
||||
height: Smart<Rel<Length>>,
|
||||
height: Rel<Abs>,
|
||||
apply: Option<MathClass>,
|
||||
) {
|
||||
if matches!(
|
||||
|
@ -1,5 +1,5 @@
|
||||
use typst_library::diag::{bail, SourceResult};
|
||||
use typst_library::foundations::{Content, Packed, StyleChain};
|
||||
use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
|
||||
use typst_library::layout::{
|
||||
Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, Ratio, Rel, Size,
|
||||
};
|
||||
@ -9,9 +9,8 @@ use typst_library::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape};
|
||||
use typst_syntax::Span;
|
||||
|
||||
use super::{
|
||||
alignments, delimiter_alignment, scaled_font_size, stack, style_for_denominator,
|
||||
AlignmentResult, FrameFragment, GlyphFragment, LeftRightAlternator, MathContext,
|
||||
Scaled, DELIM_SHORT_FALL,
|
||||
alignments, delimiter_alignment, stack, style_for_denominator, AlignmentResult,
|
||||
FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, DELIM_SHORT_FALL,
|
||||
};
|
||||
|
||||
const VERTICAL_PADDING: Ratio = Ratio::new(0.1);
|
||||
@ -28,9 +27,9 @@ pub fn layout_vec(
|
||||
let frame = layout_vec_body(
|
||||
ctx,
|
||||
styles,
|
||||
elem.children(),
|
||||
&elem.children,
|
||||
elem.align(styles),
|
||||
elem.gap(styles).at(scaled_font_size(ctx, styles)),
|
||||
elem.gap(styles),
|
||||
LeftRightAlternator::Right,
|
||||
)?;
|
||||
|
||||
@ -45,7 +44,7 @@ pub fn layout_mat(
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let augment = elem.augment(styles);
|
||||
let rows = elem.rows();
|
||||
let rows = &elem.rows;
|
||||
|
||||
if let Some(aug) = &augment {
|
||||
for &offset in &aug.hline.0 {
|
||||
@ -59,7 +58,7 @@ pub fn layout_mat(
|
||||
}
|
||||
}
|
||||
|
||||
let ncols = elem.rows().first().map_or(0, |row| row.len());
|
||||
let ncols = rows.first().map_or(0, |row| row.len());
|
||||
|
||||
for &offset in &aug.vline.0 {
|
||||
if offset == 0 || offset.unsigned_abs() >= ncols {
|
||||
@ -73,9 +72,6 @@ pub fn layout_mat(
|
||||
}
|
||||
}
|
||||
|
||||
let font_size = scaled_font_size(ctx, styles);
|
||||
let column_gap = elem.column_gap(styles).at(font_size);
|
||||
let row_gap = elem.row_gap(styles).at(font_size);
|
||||
let delim = elem.delim(styles);
|
||||
let frame = layout_mat_body(
|
||||
ctx,
|
||||
@ -83,7 +79,7 @@ pub fn layout_mat(
|
||||
rows,
|
||||
elem.align(styles),
|
||||
augment,
|
||||
Axes::new(column_gap, row_gap),
|
||||
Axes::new(elem.column_gap(styles), elem.row_gap(styles)),
|
||||
elem.span(),
|
||||
)?;
|
||||
|
||||
@ -101,9 +97,9 @@ pub fn layout_cases(
|
||||
let frame = layout_vec_body(
|
||||
ctx,
|
||||
styles,
|
||||
elem.children(),
|
||||
&elem.children,
|
||||
FixedAlignment::Start,
|
||||
elem.gap(styles).at(scaled_font_size(ctx, styles)),
|
||||
elem.gap(styles),
|
||||
LeftRightAlternator::None,
|
||||
)?;
|
||||
|
||||
@ -162,8 +158,7 @@ fn layout_mat_body(
|
||||
// with font size to ensure that augmentation lines
|
||||
// look correct by default at all matrix sizes.
|
||||
// The line cap is also set to square because it looks more "correct".
|
||||
let font_size = scaled_font_size(ctx, styles);
|
||||
let default_stroke_thickness = DEFAULT_STROKE_THICKNESS.at(font_size);
|
||||
let default_stroke_thickness = DEFAULT_STROKE_THICKNESS.resolve(styles);
|
||||
let default_stroke = FixedStroke {
|
||||
thickness: default_stroke_thickness,
|
||||
paint: TextElem::fill_in(styles).as_decoration(),
|
||||
@ -308,9 +303,8 @@ fn layout_delimiters(
|
||||
right: Option<char>,
|
||||
span: Span,
|
||||
) -> SourceResult<()> {
|
||||
let font_size = scaled_font_size(ctx, styles);
|
||||
let short_fall = DELIM_SHORT_FALL.at(font_size);
|
||||
let axis = ctx.constants.axis_height().scaled(ctx, font_size);
|
||||
let short_fall = DELIM_SHORT_FALL.resolve(styles);
|
||||
let axis = scaled!(ctx, styles, axis_height);
|
||||
let height = frame.height();
|
||||
let target = height + VERTICAL_PADDING.of(height);
|
||||
frame.set_baseline(height / 2.0 + axis);
|
||||
@ -322,7 +316,7 @@ fn layout_delimiters(
|
||||
ctx.push(left);
|
||||
}
|
||||
|
||||
ctx.push(FrameFragment::new(ctx, styles, frame));
|
||||
ctx.push(FrameFragment::new(styles, frame));
|
||||
|
||||
if let Some(right) = right {
|
||||
let mut right = GlyphFragment::new(ctx, styles, right, span)
|
||||
|
@ -28,8 +28,7 @@ use typst_library::math::*;
|
||||
use typst_library::model::ParElem;
|
||||
use typst_library::routines::{Arenas, RealizationKind};
|
||||
use typst_library::text::{
|
||||
families, features, variant, Font, LinebreakElem, SpaceElem, TextEdgeBounds,
|
||||
TextElem, TextSize,
|
||||
families, features, variant, Font, LinebreakElem, SpaceElem, TextEdgeBounds, TextElem,
|
||||
};
|
||||
use typst_library::World;
|
||||
use typst_syntax::Span;
|
||||
@ -58,12 +57,16 @@ pub fn layout_equation_inline(
|
||||
|
||||
let mut locator = locator.split();
|
||||
let mut ctx = MathContext::new(engine, &mut locator, styles, region, &font);
|
||||
|
||||
let scale_style = style_for_script_scale(&ctx);
|
||||
let styles = styles.chain(&scale_style);
|
||||
|
||||
let run = ctx.layout_into_run(&elem.body, styles)?;
|
||||
|
||||
let mut items = if run.row_count() == 1 {
|
||||
run.into_par_items()
|
||||
} else {
|
||||
vec![InlineItem::Frame(run.into_fragment(&ctx, styles).into_frame())]
|
||||
vec![InlineItem::Frame(run.into_fragment(styles).into_frame())]
|
||||
};
|
||||
|
||||
// An empty equation should have a height, so we still create a frame
|
||||
@ -75,13 +78,12 @@ pub fn layout_equation_inline(
|
||||
for item in &mut items {
|
||||
let InlineItem::Frame(frame) = item else { continue };
|
||||
|
||||
let font_size = scaled_font_size(&ctx, styles);
|
||||
let slack = ParElem::leading_in(styles) * 0.7;
|
||||
|
||||
let (t, b) = font.edges(
|
||||
TextElem::top_edge_in(styles),
|
||||
TextElem::bottom_edge_in(styles),
|
||||
font_size,
|
||||
TextElem::size_in(styles),
|
||||
TextEdgeBounds::Frame(frame),
|
||||
);
|
||||
|
||||
@ -110,9 +112,13 @@ pub fn layout_equation_block(
|
||||
|
||||
let mut locator = locator.split();
|
||||
let mut ctx = MathContext::new(engine, &mut locator, styles, regions.base(), &font);
|
||||
|
||||
let scale_style = style_for_script_scale(&ctx);
|
||||
let styles = styles.chain(&scale_style);
|
||||
|
||||
let full_equation_builder = ctx
|
||||
.layout_into_run(&elem.body, styles)?
|
||||
.multiline_frame_builder(&ctx, styles);
|
||||
.multiline_frame_builder(styles);
|
||||
let width = full_equation_builder.size.x;
|
||||
|
||||
let equation_builders = if BlockElem::breakable_in(styles) {
|
||||
@ -469,7 +475,7 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
|
||||
elem: &Content,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<MathFragment> {
|
||||
Ok(self.layout_into_run(elem, styles)?.into_fragment(self, styles))
|
||||
Ok(self.layout_into_run(elem, styles)?.into_fragment(styles))
|
||||
}
|
||||
|
||||
/// Layout the given element and return the result as a [`Frame`].
|
||||
@ -502,7 +508,7 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
|
||||
// Hack because the font is fixed in math.
|
||||
if styles != outer && TextElem::font_in(styles) != TextElem::font_in(outer) {
|
||||
let frame = layout_external(elem, self, styles)?;
|
||||
self.push(FrameFragment::new(self, styles, frame).with_spaced(true));
|
||||
self.push(FrameFragment::new(styles, frame).with_spaced(true));
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -522,8 +528,7 @@ fn layout_realized(
|
||||
if let Some(elem) = elem.to_packed::<TagElem>() {
|
||||
ctx.push(MathFragment::Tag(elem.tag.clone()));
|
||||
} else if elem.is::<SpaceElem>() {
|
||||
let font_size = scaled_font_size(ctx, styles);
|
||||
ctx.push(MathFragment::Space(ctx.space_width.at(font_size)));
|
||||
ctx.push(MathFragment::Space(ctx.space_width.resolve(styles)));
|
||||
} else if elem.is::<LinebreakElem>() {
|
||||
ctx.push(MathFragment::Linebreak);
|
||||
} else if let Some(elem) = elem.to_packed::<HElem>() {
|
||||
@ -595,7 +600,7 @@ fn layout_realized(
|
||||
frame.set_baseline(frame.height() / 2.0 + axis);
|
||||
}
|
||||
ctx.push(
|
||||
FrameFragment::new(ctx, styles, frame)
|
||||
FrameFragment::new(styles, frame)
|
||||
.with_spaced(true)
|
||||
.with_ignorant(elem.is::<PlaceElem>()),
|
||||
);
|
||||
@ -610,15 +615,14 @@ fn layout_box(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let local = TextElem::set_size(TextSize(scaled_font_size(ctx, styles).into())).wrap();
|
||||
let frame = (ctx.engine.routines.layout_box)(
|
||||
elem,
|
||||
ctx.engine,
|
||||
ctx.locator.next(&elem.span()),
|
||||
styles.chain(&local),
|
||||
styles,
|
||||
ctx.region.size,
|
||||
)?;
|
||||
ctx.push(FrameFragment::new(ctx, styles, frame).with_spaced(true));
|
||||
ctx.push(FrameFragment::new(styles, frame).with_spaced(true));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -628,12 +632,9 @@ fn layout_h(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
if let Spacing::Rel(rel) = elem.amount() {
|
||||
if let Spacing::Rel(rel) = elem.amount {
|
||||
if rel.rel.is_zero() {
|
||||
ctx.push(MathFragment::Spacing(
|
||||
rel.abs.at(scaled_font_size(ctx, styles)),
|
||||
elem.weak(styles),
|
||||
));
|
||||
ctx.push(MathFragment::Spacing(rel.abs.resolve(styles), elem.weak(styles)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@ -646,11 +647,10 @@ fn layout_class(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let class = *elem.class();
|
||||
let style = EquationElem::set_class(Some(class)).wrap();
|
||||
let mut fragment = ctx.layout_into_fragment(elem.body(), styles.chain(&style))?;
|
||||
fragment.set_class(class);
|
||||
fragment.set_limits(Limits::for_class(class));
|
||||
let style = EquationElem::set_class(Some(elem.class)).wrap();
|
||||
let mut fragment = ctx.layout_into_fragment(&elem.body, styles.chain(&style))?;
|
||||
fragment.set_class(elem.class);
|
||||
fragment.set_limits(Limits::for_class(elem.class));
|
||||
ctx.push(fragment);
|
||||
Ok(())
|
||||
}
|
||||
@ -662,13 +662,13 @@ fn layout_op(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let fragment = ctx.layout_into_fragment(elem.text(), styles)?;
|
||||
let fragment = ctx.layout_into_fragment(&elem.text, styles)?;
|
||||
let italics = fragment.italics_correction();
|
||||
let accent_attach = fragment.accent_attach();
|
||||
let text_like = fragment.is_text_like();
|
||||
|
||||
ctx.push(
|
||||
FrameFragment::new(ctx, styles, fragment.into_frame())
|
||||
FrameFragment::new(styles, fragment.into_frame())
|
||||
.with_class(MathClass::Large)
|
||||
.with_italics_correction(italics)
|
||||
.with_accent_attach(accent_attach)
|
||||
@ -688,12 +688,11 @@ fn layout_external(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<Frame> {
|
||||
let local = TextElem::set_size(TextSize(scaled_font_size(ctx, styles).into())).wrap();
|
||||
(ctx.engine.routines.layout_frame)(
|
||||
ctx.engine,
|
||||
content,
|
||||
ctx.locator.next(&content.span()),
|
||||
styles.chain(&local),
|
||||
styles,
|
||||
ctx.region,
|
||||
)
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ pub fn layout_root(
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let index = elem.index(styles);
|
||||
let radicand = elem.radicand();
|
||||
let span = elem.span();
|
||||
|
||||
let gap = scaled!(
|
||||
@ -36,9 +35,9 @@ pub fn layout_root(
|
||||
let radicand = {
|
||||
let cramped = style_cramped();
|
||||
let styles = styles.chain(&cramped);
|
||||
let run = ctx.layout_into_run(radicand, styles)?;
|
||||
let run = ctx.layout_into_run(&elem.radicand, styles)?;
|
||||
let multiline = run.is_multiline();
|
||||
let mut radicand = run.into_fragment(ctx, styles).into_frame();
|
||||
let mut radicand = run.into_fragment(styles).into_frame();
|
||||
if multiline {
|
||||
// Align the frame center line with the math axis.
|
||||
radicand.set_baseline(
|
||||
@ -120,7 +119,7 @@ pub fn layout_root(
|
||||
);
|
||||
|
||||
frame.push_frame(radicand_pos, radicand);
|
||||
ctx.push(FrameFragment::new(ctx, styles, frame));
|
||||
ctx.push(FrameFragment::new(styles, frame));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ use typst_library::math::{EquationElem, MathSize, MEDIUM, THICK, THIN};
|
||||
use typst_library::model::ParElem;
|
||||
use unicode_math_class::MathClass;
|
||||
|
||||
use super::{alignments, scaled_font_size, FrameFragment, MathContext, MathFragment};
|
||||
use super::{alignments, FrameFragment, MathFragment};
|
||||
|
||||
const TIGHT_LEADING: Em = Em::new(0.25);
|
||||
|
||||
@ -161,15 +161,15 @@ impl MathRun {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_frame(self, ctx: &MathContext, styles: StyleChain) -> Frame {
|
||||
pub fn into_frame(self, styles: StyleChain) -> Frame {
|
||||
if !self.is_multiline() {
|
||||
self.into_line_frame(&[], LeftRightAlternator::Right)
|
||||
} else {
|
||||
self.multiline_frame_builder(ctx, styles).build()
|
||||
self.multiline_frame_builder(styles).build()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_fragment(self, ctx: &MathContext, styles: StyleChain) -> MathFragment {
|
||||
pub fn into_fragment(self, styles: StyleChain) -> MathFragment {
|
||||
if self.0.len() == 1 {
|
||||
return self.0.into_iter().next().unwrap();
|
||||
}
|
||||
@ -181,7 +181,7 @@ impl MathRun {
|
||||
.filter(|e| e.math_size().is_some())
|
||||
.all(|e| e.is_text_like());
|
||||
|
||||
FrameFragment::new(ctx, styles, self.into_frame(ctx, styles))
|
||||
FrameFragment::new(styles, self.into_frame(styles))
|
||||
.with_text_like(text_like)
|
||||
.into()
|
||||
}
|
||||
@ -189,11 +189,7 @@ impl MathRun {
|
||||
/// Returns a builder that lays out the [`MathFragment`]s into a possibly
|
||||
/// multi-row [`Frame`]. The rows are aligned using the same set of alignment
|
||||
/// points computed from them as a whole.
|
||||
pub fn multiline_frame_builder(
|
||||
self,
|
||||
ctx: &MathContext,
|
||||
styles: StyleChain,
|
||||
) -> MathRunFrameBuilder {
|
||||
pub fn multiline_frame_builder(self, styles: StyleChain) -> MathRunFrameBuilder {
|
||||
let rows: Vec<_> = self.rows();
|
||||
let row_count = rows.len();
|
||||
let alignments = alignments(&rows);
|
||||
@ -201,8 +197,7 @@ impl MathRun {
|
||||
let leading = if EquationElem::size_in(styles) >= MathSize::Text {
|
||||
ParElem::leading_in(styles)
|
||||
} else {
|
||||
let font_size = scaled_font_size(ctx, styles);
|
||||
TIGHT_LEADING.at(font_size)
|
||||
TIGHT_LEADING.resolve(styles)
|
||||
};
|
||||
|
||||
let align = AlignElem::alignment_in(styles).resolve(styles).x;
|
||||
|
@ -2,7 +2,6 @@ use ttf_parser::math::MathValue;
|
||||
use typst_library::foundations::{Style, StyleChain};
|
||||
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size, VAlignment};
|
||||
use typst_library::math::{EquationElem, MathSize};
|
||||
use typst_library::text::TextElem;
|
||||
use typst_utils::LazyHash;
|
||||
|
||||
use super::{LeftRightAlternator, MathContext, MathFragment, MathRun};
|
||||
@ -18,7 +17,7 @@ macro_rules! scaled {
|
||||
$crate::math::Scaled::scaled(
|
||||
$ctx.constants.$name(),
|
||||
$ctx,
|
||||
$crate::math::scaled_font_size($ctx, $styles),
|
||||
typst_library::text::TextElem::size_in($styles),
|
||||
)
|
||||
};
|
||||
}
|
||||
@ -55,16 +54,6 @@ impl Scaled for MathValue<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the font size scaled with the `MathSize`.
|
||||
pub fn scaled_font_size(ctx: &MathContext, styles: StyleChain) -> Abs {
|
||||
let factor = match EquationElem::size_in(styles) {
|
||||
MathSize::Display | MathSize::Text => 1.0,
|
||||
MathSize::Script => percent!(ctx, script_percent_scale_down),
|
||||
MathSize::ScriptScript => percent!(ctx, script_script_percent_scale_down),
|
||||
};
|
||||
factor * TextElem::size_in(styles)
|
||||
}
|
||||
|
||||
/// Styles something as cramped.
|
||||
pub fn style_cramped() -> LazyHash<Style> {
|
||||
EquationElem::set_cramped(true).wrap()
|
||||
@ -99,6 +88,15 @@ pub fn style_for_denominator(styles: StyleChain) -> [LazyHash<Style>; 2] {
|
||||
[style_for_numerator(styles), EquationElem::set_cramped(true).wrap()]
|
||||
}
|
||||
|
||||
/// Styles to add font constants to the style chain.
|
||||
pub fn style_for_script_scale(ctx: &MathContext) -> LazyHash<Style> {
|
||||
EquationElem::set_script_scale((
|
||||
ctx.constants.script_percent_scale_down(),
|
||||
ctx.constants.script_script_percent_scale_down(),
|
||||
))
|
||||
.wrap()
|
||||
}
|
||||
|
||||
/// How a delimieter should be aligned when scaling.
|
||||
pub fn delimiter_alignment(delimiter: char) -> VAlignment {
|
||||
match delimiter {
|
||||
|
@ -1,14 +1,14 @@
|
||||
use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
|
||||
use ttf_parser::LazyArray16;
|
||||
use typst_library::diag::{warning, SourceResult};
|
||||
use typst_library::foundations::{Packed, Smart, StyleChain};
|
||||
use typst_library::layout::{Abs, Axis, Frame, Length, Point, Rel, Size};
|
||||
use typst_library::foundations::{Packed, StyleChain};
|
||||
use typst_library::layout::{Abs, Axis, Frame, Point, Rel, Size};
|
||||
use typst_library::math::StretchElem;
|
||||
use typst_utils::Get;
|
||||
|
||||
use super::{
|
||||
delimiter_alignment, scaled_font_size, GlyphFragment, MathContext, MathFragment,
|
||||
Scaled, VariantFragment,
|
||||
delimiter_alignment, GlyphFragment, MathContext, MathFragment, Scaled,
|
||||
VariantFragment,
|
||||
};
|
||||
|
||||
/// Maximum number of times extenders can be repeated.
|
||||
@ -21,7 +21,7 @@ pub fn layout_stretch(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?;
|
||||
let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
|
||||
stretch_fragment(
|
||||
ctx,
|
||||
styles,
|
||||
@ -42,7 +42,7 @@ pub fn stretch_fragment(
|
||||
fragment: &mut MathFragment,
|
||||
axis: Option<Axis>,
|
||||
relative_to: Option<Abs>,
|
||||
stretch: Smart<Rel<Length>>,
|
||||
stretch: Rel<Abs>,
|
||||
short_fall: Abs,
|
||||
) {
|
||||
let glyph = match fragment {
|
||||
@ -66,10 +66,7 @@ pub fn stretch_fragment(
|
||||
let mut variant = stretch_glyph(
|
||||
ctx,
|
||||
glyph,
|
||||
stretch
|
||||
.unwrap_or(Rel::one())
|
||||
.at(scaled_font_size(ctx, styles))
|
||||
.relative_to(relative_to_size),
|
||||
stretch.relative_to(relative_to_size),
|
||||
short_fall,
|
||||
axis,
|
||||
);
|
||||
|
@ -6,15 +6,13 @@ use typst_library::foundations::{Packed, StyleChain, StyleVec};
|
||||
use typst_library::layout::{Abs, Size};
|
||||
use typst_library::math::{EquationElem, MathSize, MathVariant};
|
||||
use typst_library::text::{
|
||||
BottomEdge, BottomEdgeMetric, TextElem, TextSize, TopEdge, TopEdgeMetric,
|
||||
BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric,
|
||||
};
|
||||
use typst_syntax::{is_newline, Span};
|
||||
use unicode_math_class::MathClass;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use super::{
|
||||
scaled_font_size, FrameFragment, GlyphFragment, MathContext, MathFragment, MathRun,
|
||||
};
|
||||
use super::{FrameFragment, GlyphFragment, MathContext, MathFragment, MathRun};
|
||||
|
||||
/// Lays out a [`TextElem`].
|
||||
pub fn layout_text(
|
||||
@ -22,7 +20,7 @@ pub fn layout_text(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let text = elem.text();
|
||||
let text = &elem.text;
|
||||
let span = elem.span();
|
||||
let mut chars = text.chars();
|
||||
let math_size = EquationElem::size_in(styles);
|
||||
@ -70,13 +68,12 @@ pub fn layout_text(
|
||||
let c = styled_char(styles, c, false);
|
||||
fragments.push(GlyphFragment::new(ctx, styles, c, span).into());
|
||||
}
|
||||
let frame = MathRun::new(fragments).into_frame(ctx, styles);
|
||||
FrameFragment::new(ctx, styles, frame).with_text_like(true).into()
|
||||
let frame = MathRun::new(fragments).into_frame(styles);
|
||||
FrameFragment::new(styles, frame).with_text_like(true).into()
|
||||
} else {
|
||||
let local = [
|
||||
TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds)),
|
||||
TextElem::set_bottom_edge(BottomEdge::Metric(BottomEdgeMetric::Bounds)),
|
||||
TextElem::set_size(TextSize(scaled_font_size(ctx, styles).into())),
|
||||
]
|
||||
.map(|p| p.wrap());
|
||||
|
||||
@ -94,10 +91,10 @@ pub fn layout_text(
|
||||
fragments.push(layout_complex_text(piece, ctx, span, styles)?.into());
|
||||
}
|
||||
}
|
||||
let mut frame = MathRun::new(fragments).into_frame(ctx, styles);
|
||||
let mut frame = MathRun::new(fragments).into_frame(styles);
|
||||
let axis = scaled!(ctx, styles, axis_height);
|
||||
frame.set_baseline(frame.height() / 2.0 + axis);
|
||||
FrameFragment::new(ctx, styles, frame).into()
|
||||
FrameFragment::new(styles, frame).into()
|
||||
} else {
|
||||
layout_complex_text(&text, ctx, span, styles)?.into()
|
||||
}
|
||||
@ -131,7 +128,7 @@ fn layout_complex_text(
|
||||
)?
|
||||
.into_frame();
|
||||
|
||||
Ok(FrameFragment::new(ctx, styles, frame)
|
||||
Ok(FrameFragment::new(styles, frame)
|
||||
.with_class(MathClass::Alphabetic)
|
||||
.with_text_like(true)
|
||||
.with_spaced(spaced))
|
||||
|
@ -1,5 +1,5 @@
|
||||
use typst_library::diag::SourceResult;
|
||||
use typst_library::foundations::{Content, Packed, StyleChain};
|
||||
use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
|
||||
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, FrameItem, Point, Size};
|
||||
use typst_library::math::{
|
||||
OverbraceElem, OverbracketElem, OverlineElem, OverparenElem, OvershellElem,
|
||||
@ -10,8 +10,8 @@ use typst_library::visualize::{FixedStroke, Geometry};
|
||||
use typst_syntax::Span;
|
||||
|
||||
use super::{
|
||||
scaled_font_size, stack, style_cramped, style_for_subscript, style_for_superscript,
|
||||
FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, MathRun,
|
||||
stack, style_cramped, style_for_subscript, style_for_superscript, FrameFragment,
|
||||
GlyphFragment, LeftRightAlternator, MathContext, MathRun,
|
||||
};
|
||||
|
||||
const BRACE_GAP: Em = Em::new(0.25);
|
||||
@ -32,7 +32,7 @@ pub fn layout_underline(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
layout_underoverline(ctx, styles, elem.body(), elem.span(), Position::Under)
|
||||
layout_underoverline(ctx, styles, &elem.body, elem.span(), Position::Under)
|
||||
}
|
||||
|
||||
/// Lays out an [`OverlineElem`].
|
||||
@ -42,7 +42,7 @@ pub fn layout_overline(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
layout_underoverline(ctx, styles, elem.body(), elem.span(), Position::Over)
|
||||
layout_underoverline(ctx, styles, &elem.body, elem.span(), Position::Over)
|
||||
}
|
||||
|
||||
/// Lays out an [`UnderbraceElem`].
|
||||
@ -55,7 +55,7 @@ pub fn layout_underbrace(
|
||||
layout_underoverspreader(
|
||||
ctx,
|
||||
styles,
|
||||
elem.body(),
|
||||
&elem.body,
|
||||
&elem.annotation(styles),
|
||||
'⏟',
|
||||
BRACE_GAP,
|
||||
@ -74,7 +74,7 @@ pub fn layout_overbrace(
|
||||
layout_underoverspreader(
|
||||
ctx,
|
||||
styles,
|
||||
elem.body(),
|
||||
&elem.body,
|
||||
&elem.annotation(styles),
|
||||
'⏞',
|
||||
BRACE_GAP,
|
||||
@ -93,7 +93,7 @@ pub fn layout_underbracket(
|
||||
layout_underoverspreader(
|
||||
ctx,
|
||||
styles,
|
||||
elem.body(),
|
||||
&elem.body,
|
||||
&elem.annotation(styles),
|
||||
'⎵',
|
||||
BRACKET_GAP,
|
||||
@ -112,7 +112,7 @@ pub fn layout_overbracket(
|
||||
layout_underoverspreader(
|
||||
ctx,
|
||||
styles,
|
||||
elem.body(),
|
||||
&elem.body,
|
||||
&elem.annotation(styles),
|
||||
'⎴',
|
||||
BRACKET_GAP,
|
||||
@ -131,7 +131,7 @@ pub fn layout_underparen(
|
||||
layout_underoverspreader(
|
||||
ctx,
|
||||
styles,
|
||||
elem.body(),
|
||||
&elem.body,
|
||||
&elem.annotation(styles),
|
||||
'⏝',
|
||||
PAREN_GAP,
|
||||
@ -150,7 +150,7 @@ pub fn layout_overparen(
|
||||
layout_underoverspreader(
|
||||
ctx,
|
||||
styles,
|
||||
elem.body(),
|
||||
&elem.body,
|
||||
&elem.annotation(styles),
|
||||
'⏜',
|
||||
PAREN_GAP,
|
||||
@ -169,7 +169,7 @@ pub fn layout_undershell(
|
||||
layout_underoverspreader(
|
||||
ctx,
|
||||
styles,
|
||||
elem.body(),
|
||||
&elem.body,
|
||||
&elem.annotation(styles),
|
||||
'⏡',
|
||||
SHELL_GAP,
|
||||
@ -188,7 +188,7 @@ pub fn layout_overshell(
|
||||
layout_underoverspreader(
|
||||
ctx,
|
||||
styles,
|
||||
elem.body(),
|
||||
&elem.body,
|
||||
&elem.annotation(styles),
|
||||
'⏠',
|
||||
SHELL_GAP,
|
||||
@ -260,7 +260,7 @@ fn layout_underoverline(
|
||||
);
|
||||
|
||||
ctx.push(
|
||||
FrameFragment::new(ctx, styles, frame)
|
||||
FrameFragment::new(styles, frame)
|
||||
.with_class(content_class)
|
||||
.with_text_like(content_is_text_like)
|
||||
.with_italics_correction(content_italics_correction),
|
||||
@ -281,11 +281,10 @@ fn layout_underoverspreader(
|
||||
position: Position,
|
||||
span: Span,
|
||||
) -> SourceResult<()> {
|
||||
let font_size = scaled_font_size(ctx, styles);
|
||||
let gap = gap.at(font_size);
|
||||
let gap = gap.resolve(styles);
|
||||
let body = ctx.layout_into_run(body, styles)?;
|
||||
let body_class = body.class();
|
||||
let body = body.into_fragment(ctx, styles);
|
||||
let body = body.into_fragment(styles);
|
||||
let glyph = GlyphFragment::new(ctx, styles, c, span);
|
||||
let stretched = glyph.stretch_horizontal(ctx, body.width(), Abs::zero());
|
||||
|
||||
@ -321,7 +320,7 @@ fn layout_underoverspreader(
|
||||
LeftRightAlternator::Right,
|
||||
None,
|
||||
);
|
||||
ctx.push(FrameFragment::new(ctx, styles, frame).with_class(body_class));
|
||||
ctx.push(FrameFragment::new(styles, frame).with_class(body_class));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::f64::consts::SQRT_2;
|
||||
|
||||
use kurbo::ParamCurveExtrema;
|
||||
use kurbo::{CubicBez, ParamCurveExtrema};
|
||||
use typst_library::diag::{bail, SourceResult};
|
||||
use typst_library::engine::Engine;
|
||||
use typst_library::foundations::{Content, Packed, Resolve, Smart, StyleChain};
|
||||
@ -10,8 +10,9 @@ use typst_library::layout::{
|
||||
Sides, Size,
|
||||
};
|
||||
use typst_library::visualize::{
|
||||
CircleElem, EllipseElem, FillRule, FixedStroke, Geometry, LineElem, Paint, Path,
|
||||
PathElem, PathVertex, PolygonElem, RectElem, Shape, SquareElem, Stroke,
|
||||
CircleElem, CloseMode, Curve, CurveComponent, CurveElem, EllipseElem, FillRule,
|
||||
FixedStroke, Geometry, LineElem, Paint, PathElem, PathVertex, PolygonElem, RectElem,
|
||||
Shape, SquareElem, Stroke,
|
||||
};
|
||||
use typst_syntax::Span;
|
||||
use typst_utils::{Get, Numeric};
|
||||
@ -61,7 +62,7 @@ pub fn layout_path(
|
||||
axes.resolve(styles).zip_map(region.size, Rel::relative_to).to_point()
|
||||
};
|
||||
|
||||
let vertices = elem.vertices();
|
||||
let vertices = &elem.vertices;
|
||||
let points: Vec<Point> = vertices.iter().map(|c| resolve(c.vertex())).collect();
|
||||
|
||||
let mut size = Size::zero();
|
||||
@ -71,8 +72,8 @@ pub fn layout_path(
|
||||
|
||||
// Only create a path if there are more than zero points.
|
||||
// Construct a closed path given all points.
|
||||
let mut path = Path::new();
|
||||
path.move_to(points[0]);
|
||||
let mut curve = Curve::new();
|
||||
curve.move_(points[0]);
|
||||
|
||||
let mut add_cubic = |from_point: Point,
|
||||
to_point: Point,
|
||||
@ -80,7 +81,7 @@ pub fn layout_path(
|
||||
to: PathVertex| {
|
||||
let from_control_point = resolve(from.control_point_from()) + from_point;
|
||||
let to_control_point = resolve(to.control_point_to()) + to_point;
|
||||
path.cubic_to(from_control_point, to_control_point, to_point);
|
||||
curve.cubic(from_control_point, to_control_point, to_point);
|
||||
|
||||
let p0 = kurbo::Point::new(from_point.x.to_raw(), from_point.y.to_raw());
|
||||
let p1 = kurbo::Point::new(
|
||||
@ -111,7 +112,7 @@ pub fn layout_path(
|
||||
let to_point = points[0];
|
||||
|
||||
add_cubic(from_point, to_point, from, to);
|
||||
path.close_path();
|
||||
curve.close();
|
||||
}
|
||||
|
||||
if !size.is_finite() {
|
||||
@ -129,7 +130,7 @@ pub fn layout_path(
|
||||
|
||||
let mut frame = Frame::soft(size);
|
||||
let shape = Shape {
|
||||
geometry: Geometry::Path(path),
|
||||
geometry: Geometry::Curve(curve),
|
||||
stroke,
|
||||
fill,
|
||||
fill_rule,
|
||||
@ -138,6 +139,256 @@ pub fn layout_path(
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
/// Layout the curve.
|
||||
#[typst_macros::time(span = elem.span())]
|
||||
pub fn layout_curve(
|
||||
elem: &Packed<CurveElem>,
|
||||
_: &mut Engine,
|
||||
_: Locator,
|
||||
styles: StyleChain,
|
||||
region: Region,
|
||||
) -> SourceResult<Frame> {
|
||||
let mut builder = CurveBuilder::new(region, styles);
|
||||
|
||||
for item in &elem.components {
|
||||
match item {
|
||||
CurveComponent::Move(element) => {
|
||||
let relative = element.relative(styles);
|
||||
let point = builder.resolve_point(element.start, relative);
|
||||
builder.move_(point);
|
||||
}
|
||||
|
||||
CurveComponent::Line(element) => {
|
||||
let relative = element.relative(styles);
|
||||
let point = builder.resolve_point(element.end, relative);
|
||||
builder.line(point);
|
||||
}
|
||||
|
||||
CurveComponent::Quad(element) => {
|
||||
let relative = element.relative(styles);
|
||||
let end = builder.resolve_point(element.end, relative);
|
||||
let control = match element.control {
|
||||
Smart::Auto => {
|
||||
control_c2q(builder.last_point, builder.last_control_from)
|
||||
}
|
||||
Smart::Custom(Some(p)) => builder.resolve_point(p, relative),
|
||||
Smart::Custom(None) => end,
|
||||
};
|
||||
builder.quad(control, end);
|
||||
}
|
||||
|
||||
CurveComponent::Cubic(element) => {
|
||||
let relative = element.relative(styles);
|
||||
let end = builder.resolve_point(element.end, relative);
|
||||
let c1 = match element.control_start {
|
||||
Some(Smart::Custom(p)) => builder.resolve_point(p, relative),
|
||||
Some(Smart::Auto) => builder.last_control_from,
|
||||
None => builder.last_point,
|
||||
};
|
||||
let c2 = match element.control_end {
|
||||
Some(p) => builder.resolve_point(p, relative),
|
||||
None => end,
|
||||
};
|
||||
builder.cubic(c1, c2, end);
|
||||
}
|
||||
|
||||
CurveComponent::Close(element) => {
|
||||
builder.close(element.mode(styles));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (curve, size) = builder.finish();
|
||||
if curve.is_empty() {
|
||||
return Ok(Frame::soft(size));
|
||||
}
|
||||
|
||||
if !size.is_finite() {
|
||||
bail!(elem.span(), "cannot create curve with infinite size");
|
||||
}
|
||||
|
||||
// Prepare fill and stroke.
|
||||
let fill = elem.fill(styles);
|
||||
let fill_rule = elem.fill_rule(styles);
|
||||
let stroke = match elem.stroke(styles) {
|
||||
Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
|
||||
Smart::Auto => None,
|
||||
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
|
||||
};
|
||||
|
||||
let mut frame = Frame::soft(size);
|
||||
let shape = Shape {
|
||||
geometry: Geometry::Curve(curve),
|
||||
stroke,
|
||||
fill,
|
||||
fill_rule,
|
||||
};
|
||||
frame.push(Point::zero(), FrameItem::Shape(shape, elem.span()));
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
/// Builds a `Curve` from a [`CurveElem`]'s parts.
|
||||
struct CurveBuilder<'a> {
|
||||
/// The output curve.
|
||||
curve: Curve,
|
||||
/// The curve's bounds.
|
||||
size: Size,
|
||||
/// The region relative to which points are resolved.
|
||||
region: Region,
|
||||
/// The styles for the curve.
|
||||
styles: StyleChain<'a>,
|
||||
/// The next start point.
|
||||
start_point: Point,
|
||||
/// Mirror of the first cubic start control point (for closing).
|
||||
start_control_into: Point,
|
||||
/// The point we previously ended on.
|
||||
last_point: Point,
|
||||
/// Mirror of the last cubic control point (for auto control points).
|
||||
last_control_from: Point,
|
||||
/// Whether a component has been start. This does not mean that something
|
||||
/// has been added to `self.curve` yet.
|
||||
is_started: bool,
|
||||
/// Whether anything was added to `self.curve` for the current component.
|
||||
is_empty: bool,
|
||||
}
|
||||
|
||||
impl<'a> CurveBuilder<'a> {
|
||||
/// Create a new curve builder.
|
||||
fn new(region: Region, styles: StyleChain<'a>) -> Self {
|
||||
Self {
|
||||
curve: Curve::new(),
|
||||
size: Size::zero(),
|
||||
region,
|
||||
styles,
|
||||
start_point: Point::zero(),
|
||||
start_control_into: Point::zero(),
|
||||
last_point: Point::zero(),
|
||||
last_control_from: Point::zero(),
|
||||
is_started: false,
|
||||
is_empty: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Finish building, returning the curve and its bounding size.
|
||||
fn finish(self) -> (Curve, Size) {
|
||||
(self.curve, self.size)
|
||||
}
|
||||
|
||||
/// Move to a point, starting a new segment.
|
||||
fn move_(&mut self, point: Point) {
|
||||
// Delay calling `curve.move` in case there is another move element
|
||||
// before any actual drawing.
|
||||
self.expand_bounds(point);
|
||||
self.start_point = point;
|
||||
self.start_control_into = point;
|
||||
self.last_point = point;
|
||||
self.last_control_from = point;
|
||||
self.is_started = true;
|
||||
}
|
||||
|
||||
/// Add a line segment.
|
||||
fn line(&mut self, point: Point) {
|
||||
if self.is_empty {
|
||||
self.start_component();
|
||||
self.start_control_into = self.start_point;
|
||||
}
|
||||
self.curve.line(point);
|
||||
self.expand_bounds(point);
|
||||
self.last_point = point;
|
||||
self.last_control_from = point;
|
||||
}
|
||||
|
||||
/// Add a quadratic curve segment.
|
||||
fn quad(&mut self, control: Point, end: Point) {
|
||||
let c1 = control_q2c(self.last_point, control);
|
||||
let c2 = control_q2c(end, control);
|
||||
self.cubic(c1, c2, end);
|
||||
}
|
||||
|
||||
/// Add a cubic curve segment.
|
||||
fn cubic(&mut self, c1: Point, c2: Point, end: Point) {
|
||||
if self.is_empty {
|
||||
self.start_component();
|
||||
self.start_control_into = mirror_c(self.start_point, c1);
|
||||
}
|
||||
self.curve.cubic(c1, c2, end);
|
||||
|
||||
let p0 = point_to_kurbo(self.last_point);
|
||||
let p1 = point_to_kurbo(c1);
|
||||
let p2 = point_to_kurbo(c2);
|
||||
let p3 = point_to_kurbo(end);
|
||||
let extrema = CubicBez::new(p0, p1, p2, p3).bounding_box();
|
||||
self.size.x.set_max(Abs::raw(extrema.x1));
|
||||
self.size.y.set_max(Abs::raw(extrema.y1));
|
||||
|
||||
self.last_point = end;
|
||||
self.last_control_from = mirror_c(end, c2);
|
||||
}
|
||||
|
||||
/// Close the curve if it was opened.
|
||||
fn close(&mut self, mode: CloseMode) {
|
||||
if self.is_started && !self.is_empty {
|
||||
if mode == CloseMode::Smooth {
|
||||
self.cubic(
|
||||
self.last_control_from,
|
||||
self.start_control_into,
|
||||
self.start_point,
|
||||
);
|
||||
}
|
||||
self.curve.close();
|
||||
self.last_point = self.start_point;
|
||||
self.last_control_from = self.start_point;
|
||||
}
|
||||
self.is_started = false;
|
||||
self.is_empty = true;
|
||||
}
|
||||
|
||||
/// Push the initial move component.
|
||||
fn start_component(&mut self) {
|
||||
self.curve.move_(self.start_point);
|
||||
self.is_empty = false;
|
||||
self.is_started = true;
|
||||
}
|
||||
|
||||
/// Expand the curve's bounding box.
|
||||
fn expand_bounds(&mut self, point: Point) {
|
||||
self.size.x.set_max(point.x);
|
||||
self.size.y.set_max(point.y);
|
||||
}
|
||||
|
||||
/// Resolve the point relative to the region.
|
||||
fn resolve_point(&self, point: Axes<Rel>, relative: bool) -> Point {
|
||||
let mut p = point
|
||||
.resolve(self.styles)
|
||||
.zip_map(self.region.size, Rel::relative_to)
|
||||
.to_point();
|
||||
if relative {
|
||||
p += self.last_point;
|
||||
}
|
||||
p
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a cubic control point into a quadratic one.
|
||||
fn control_c2q(p: Point, c: Point) -> Point {
|
||||
1.5 * c - 0.5 * p
|
||||
}
|
||||
|
||||
/// Convert a quadratic control point into a cubic one.
|
||||
fn control_q2c(p: Point, c: Point) -> Point {
|
||||
(p + 2.0 * c) / 3.0
|
||||
}
|
||||
|
||||
/// Mirror a control point.
|
||||
fn mirror_c(p: Point, c: Point) -> Point {
|
||||
2.0 * p - c
|
||||
}
|
||||
|
||||
/// Convert a point to a `kurbo::Point`.
|
||||
fn point_to_kurbo(point: Point) -> kurbo::Point {
|
||||
kurbo::Point::new(point.x.to_raw(), point.y.to_raw())
|
||||
}
|
||||
|
||||
/// Layout the polygon.
|
||||
#[typst_macros::time(span = elem.span())]
|
||||
pub fn layout_polygon(
|
||||
@ -148,7 +399,7 @@ pub fn layout_polygon(
|
||||
region: Region,
|
||||
) -> SourceResult<Frame> {
|
||||
let points: Vec<Point> = elem
|
||||
.vertices()
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|c| c.resolve(styles).zip_map(region.size, Rel::relative_to).to_point())
|
||||
.collect();
|
||||
@ -160,7 +411,7 @@ pub fn layout_polygon(
|
||||
|
||||
let mut frame = Frame::hard(size);
|
||||
|
||||
// Only create a path if there are more than zero points.
|
||||
// Only create a curve if there are more than zero points.
|
||||
if points.is_empty() {
|
||||
return Ok(frame);
|
||||
}
|
||||
@ -174,16 +425,16 @@ pub fn layout_polygon(
|
||||
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
|
||||
};
|
||||
|
||||
// Construct a closed path given all points.
|
||||
let mut path = Path::new();
|
||||
path.move_to(points[0]);
|
||||
// Construct a closed curve given all points.
|
||||
let mut curve = Curve::new();
|
||||
curve.move_(points[0]);
|
||||
for &point in &points[1..] {
|
||||
path.line_to(point);
|
||||
curve.line(point);
|
||||
}
|
||||
path.close_path();
|
||||
curve.close();
|
||||
|
||||
let shape = Shape {
|
||||
geometry: Geometry::Path(path),
|
||||
geometry: Geometry::Curve(curve),
|
||||
stroke,
|
||||
fill,
|
||||
fill_rule,
|
||||
@ -409,7 +660,7 @@ fn layout_shape(
|
||||
let size = frame.size() + outset.sum_by_axis();
|
||||
let pos = Point::new(-outset.left, -outset.top);
|
||||
let shape = Shape {
|
||||
geometry: Geometry::Path(Path::ellipse(size)),
|
||||
geometry: Geometry::Curve(Curve::ellipse(size)),
|
||||
fill,
|
||||
stroke: stroke.left,
|
||||
fill_rule: FillRule::default(),
|
||||
@ -448,13 +699,13 @@ fn quadratic_size(region: Region) -> Option<Abs> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new rectangle as a path.
|
||||
/// Creates a new rectangle as a curve.
|
||||
pub fn clip_rect(
|
||||
size: Size,
|
||||
radius: &Corners<Rel<Abs>>,
|
||||
stroke: &Sides<Option<FixedStroke>>,
|
||||
outset: &Sides<Rel<Abs>>,
|
||||
) -> Path {
|
||||
) -> Curve {
|
||||
let outset = outset.relative_to(size);
|
||||
let size = size + outset.sum_by_axis();
|
||||
|
||||
@ -468,26 +719,30 @@ pub fn clip_rect(
|
||||
let radius = radius.map(|side| side.relative_to(max_radius * 2.0).min(max_radius));
|
||||
let corners = corners_control_points(size, &radius, stroke, &stroke_widths);
|
||||
|
||||
let mut path = Path::new();
|
||||
let mut curve = Curve::new();
|
||||
if corners.top_left.arc_inner() {
|
||||
path.arc_move(
|
||||
curve.arc_move(
|
||||
corners.top_left.start_inner(),
|
||||
corners.top_left.center_inner(),
|
||||
corners.top_left.end_inner(),
|
||||
);
|
||||
} else {
|
||||
path.move_to(corners.top_left.center_inner());
|
||||
curve.move_(corners.top_left.center_inner());
|
||||
}
|
||||
for corner in [&corners.top_right, &corners.bottom_right, &corners.bottom_left] {
|
||||
if corner.arc_inner() {
|
||||
path.arc_line(corner.start_inner(), corner.center_inner(), corner.end_inner())
|
||||
curve.arc_line(
|
||||
corner.start_inner(),
|
||||
corner.center_inner(),
|
||||
corner.end_inner(),
|
||||
)
|
||||
} else {
|
||||
path.line_to(corner.center_inner());
|
||||
curve.line(corner.center_inner());
|
||||
}
|
||||
}
|
||||
path.close_path();
|
||||
path.translate(Point::new(-outset.left, -outset.top));
|
||||
path
|
||||
curve.close();
|
||||
curve.translate(Point::new(-outset.left, -outset.top));
|
||||
curve
|
||||
}
|
||||
|
||||
/// Add a fill and stroke with optional radius and outset to the frame.
|
||||
@ -592,25 +847,25 @@ fn segmented_rect(
|
||||
|
||||
// fill shape with inner curve
|
||||
if let Some(fill) = fill {
|
||||
let mut path = Path::new();
|
||||
let mut curve = Curve::new();
|
||||
let c = corners.get_ref(Corner::TopLeft);
|
||||
if c.arc() {
|
||||
path.arc_move(c.start(), c.center(), c.end());
|
||||
curve.arc_move(c.start(), c.center(), c.end());
|
||||
} else {
|
||||
path.move_to(c.center());
|
||||
curve.move_(c.center());
|
||||
};
|
||||
|
||||
for corner in [Corner::TopRight, Corner::BottomRight, Corner::BottomLeft] {
|
||||
let c = corners.get_ref(corner);
|
||||
if c.arc() {
|
||||
path.arc_line(c.start(), c.center(), c.end());
|
||||
curve.arc_line(c.start(), c.center(), c.end());
|
||||
} else {
|
||||
path.line_to(c.center());
|
||||
curve.line(c.center());
|
||||
}
|
||||
}
|
||||
path.close_path();
|
||||
curve.close();
|
||||
res.push(Shape {
|
||||
geometry: Geometry::Path(path),
|
||||
geometry: Geometry::Curve(curve),
|
||||
fill: Some(fill),
|
||||
fill_rule: FillRule::default(),
|
||||
stroke: None,
|
||||
@ -649,18 +904,18 @@ fn segmented_rect(
|
||||
res
|
||||
}
|
||||
|
||||
fn path_segment(
|
||||
fn curve_segment(
|
||||
start: Corner,
|
||||
end: Corner,
|
||||
corners: &Corners<ControlPoints>,
|
||||
path: &mut Path,
|
||||
curve: &mut Curve,
|
||||
) {
|
||||
// create start corner
|
||||
let c = corners.get_ref(start);
|
||||
if start == end || !c.arc() {
|
||||
path.move_to(c.end());
|
||||
curve.move_(c.end());
|
||||
} else {
|
||||
path.arc_move(c.mid(), c.center(), c.end());
|
||||
curve.arc_move(c.mid(), c.center(), c.end());
|
||||
}
|
||||
|
||||
// create corners between start and end
|
||||
@ -668,9 +923,9 @@ fn path_segment(
|
||||
while current != end {
|
||||
let c = corners.get_ref(current);
|
||||
if c.arc() {
|
||||
path.arc_line(c.start(), c.center(), c.end());
|
||||
curve.arc_line(c.start(), c.center(), c.end());
|
||||
} else {
|
||||
path.line_to(c.end());
|
||||
curve.line(c.end());
|
||||
}
|
||||
current = current.next_cw();
|
||||
}
|
||||
@ -678,11 +933,11 @@ fn path_segment(
|
||||
// create end corner
|
||||
let c = corners.get_ref(end);
|
||||
if !c.arc() {
|
||||
path.line_to(c.start());
|
||||
curve.line(c.start());
|
||||
} else if start == end {
|
||||
path.arc_line(c.start(), c.center(), c.end());
|
||||
curve.arc_line(c.start(), c.center(), c.end());
|
||||
} else {
|
||||
path.arc_line(c.start(), c.center(), c.mid());
|
||||
curve.arc_line(c.start(), c.center(), c.mid());
|
||||
}
|
||||
}
|
||||
|
||||
@ -739,11 +994,11 @@ fn stroke_segment(
|
||||
stroke: FixedStroke,
|
||||
) -> Shape {
|
||||
// Create start corner.
|
||||
let mut path = Path::new();
|
||||
path_segment(start, end, corners, &mut path);
|
||||
let mut curve = Curve::new();
|
||||
curve_segment(start, end, corners, &mut curve);
|
||||
|
||||
Shape {
|
||||
geometry: Geometry::Path(path),
|
||||
geometry: Geometry::Curve(curve),
|
||||
stroke: Some(stroke),
|
||||
fill: None,
|
||||
fill_rule: FillRule::default(),
|
||||
@ -757,7 +1012,7 @@ fn fill_segment(
|
||||
corners: &Corners<ControlPoints>,
|
||||
stroke: &FixedStroke,
|
||||
) -> Shape {
|
||||
let mut path = Path::new();
|
||||
let mut curve = Curve::new();
|
||||
|
||||
// create the start corner
|
||||
// begin on the inside and finish on the outside
|
||||
@ -765,33 +1020,33 @@ fn fill_segment(
|
||||
// half corner if different
|
||||
if start == end {
|
||||
let c = corners.get_ref(start);
|
||||
path.move_to(c.end_inner());
|
||||
path.line_to(c.end_outer());
|
||||
curve.move_(c.end_inner());
|
||||
curve.line(c.end_outer());
|
||||
} else {
|
||||
let c = corners.get_ref(start);
|
||||
|
||||
if c.arc_inner() {
|
||||
path.arc_move(c.end_inner(), c.center_inner(), c.mid_inner());
|
||||
curve.arc_move(c.end_inner(), c.center_inner(), c.mid_inner());
|
||||
} else {
|
||||
path.move_to(c.end_inner());
|
||||
curve.move_(c.end_inner());
|
||||
}
|
||||
|
||||
if c.arc_outer() {
|
||||
path.arc_line(c.mid_outer(), c.center_outer(), c.end_outer());
|
||||
curve.arc_line(c.mid_outer(), c.center_outer(), c.end_outer());
|
||||
} else {
|
||||
path.line_to(c.outer());
|
||||
path.line_to(c.end_outer());
|
||||
curve.line(c.outer());
|
||||
curve.line(c.end_outer());
|
||||
}
|
||||
}
|
||||
|
||||
// create the clockwise outside path for the corners between start and end
|
||||
// create the clockwise outside curve for the corners between start and end
|
||||
let mut current = start.next_cw();
|
||||
while current != end {
|
||||
let c = corners.get_ref(current);
|
||||
if c.arc_outer() {
|
||||
path.arc_line(c.start_outer(), c.center_outer(), c.end_outer());
|
||||
curve.arc_line(c.start_outer(), c.center_outer(), c.end_outer());
|
||||
} else {
|
||||
path.line_to(c.outer());
|
||||
curve.line(c.outer());
|
||||
}
|
||||
current = current.next_cw();
|
||||
}
|
||||
@ -803,46 +1058,46 @@ fn fill_segment(
|
||||
if start == end {
|
||||
let c = corners.get_ref(end);
|
||||
if c.arc_outer() {
|
||||
path.arc_line(c.start_outer(), c.center_outer(), c.end_outer());
|
||||
curve.arc_line(c.start_outer(), c.center_outer(), c.end_outer());
|
||||
} else {
|
||||
path.line_to(c.outer());
|
||||
path.line_to(c.end_outer());
|
||||
curve.line(c.outer());
|
||||
curve.line(c.end_outer());
|
||||
}
|
||||
if c.arc_inner() {
|
||||
path.arc_line(c.end_inner(), c.center_inner(), c.start_inner());
|
||||
curve.arc_line(c.end_inner(), c.center_inner(), c.start_inner());
|
||||
} else {
|
||||
path.line_to(c.center_inner());
|
||||
curve.line(c.center_inner());
|
||||
}
|
||||
} else {
|
||||
let c = corners.get_ref(end);
|
||||
if c.arc_outer() {
|
||||
path.arc_line(c.start_outer(), c.center_outer(), c.mid_outer());
|
||||
curve.arc_line(c.start_outer(), c.center_outer(), c.mid_outer());
|
||||
} else {
|
||||
path.line_to(c.outer());
|
||||
curve.line(c.outer());
|
||||
}
|
||||
if c.arc_inner() {
|
||||
path.arc_line(c.mid_inner(), c.center_inner(), c.start_inner());
|
||||
curve.arc_line(c.mid_inner(), c.center_inner(), c.start_inner());
|
||||
} else {
|
||||
path.line_to(c.center_inner());
|
||||
curve.line(c.center_inner());
|
||||
}
|
||||
}
|
||||
|
||||
// create the counterclockwise inside path for the corners between start and end
|
||||
// create the counterclockwise inside curve for the corners between start and end
|
||||
let mut current = end.next_ccw();
|
||||
while current != start {
|
||||
let c = corners.get_ref(current);
|
||||
if c.arc_inner() {
|
||||
path.arc_line(c.end_inner(), c.center_inner(), c.start_inner());
|
||||
curve.arc_line(c.end_inner(), c.center_inner(), c.start_inner());
|
||||
} else {
|
||||
path.line_to(c.center_inner());
|
||||
curve.line(c.center_inner());
|
||||
}
|
||||
current = current.next_ccw();
|
||||
}
|
||||
|
||||
path.close_path();
|
||||
curve.close();
|
||||
|
||||
Shape {
|
||||
geometry: Geometry::Path(path),
|
||||
geometry: Geometry::Curve(curve),
|
||||
stroke: None,
|
||||
fill: Some(stroke.paint.clone()),
|
||||
fill_rule: FillRule::default(),
|
||||
@ -1027,25 +1282,25 @@ impl ControlPoints {
|
||||
}
|
||||
|
||||
/// Helper to draw arcs with bezier curves.
|
||||
trait PathExt {
|
||||
trait CurveExt {
|
||||
fn arc(&mut self, start: Point, center: Point, end: Point);
|
||||
fn arc_move(&mut self, start: Point, center: Point, end: Point);
|
||||
fn arc_line(&mut self, start: Point, center: Point, end: Point);
|
||||
}
|
||||
|
||||
impl PathExt for Path {
|
||||
impl CurveExt for Curve {
|
||||
fn arc(&mut self, start: Point, center: Point, end: Point) {
|
||||
let arc = bezier_arc_control(start, center, end);
|
||||
self.cubic_to(arc[0], arc[1], end);
|
||||
self.cubic(arc[0], arc[1], end);
|
||||
}
|
||||
|
||||
fn arc_move(&mut self, start: Point, center: Point, end: Point) {
|
||||
self.move_to(start);
|
||||
self.move_(start);
|
||||
self.arc(start, center, end);
|
||||
}
|
||||
|
||||
fn arc_line(&mut self, start: Point, center: Point, end: Point) {
|
||||
self.line_to(start);
|
||||
self.line(start);
|
||||
self.arc(start, center, end);
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ pub fn layout_stack(
|
||||
let spacing = elem.spacing(styles);
|
||||
let mut deferred = None;
|
||||
|
||||
for child in elem.children() {
|
||||
for child in &elem.children {
|
||||
match child {
|
||||
StackChild::Spacing(kind) => {
|
||||
layouter.layout_spacing(*kind);
|
||||
@ -36,14 +36,14 @@ pub fn layout_stack(
|
||||
StackChild::Block(block) => {
|
||||
// Transparently handle `h`.
|
||||
if let (Axis::X, Some(h)) = (axis, block.to_packed::<HElem>()) {
|
||||
layouter.layout_spacing(*h.amount());
|
||||
layouter.layout_spacing(h.amount);
|
||||
deferred = None;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Transparently handle `v`.
|
||||
if let (Axis::Y, Some(v)) = (axis, block.to_packed::<VElem>()) {
|
||||
layouter.layout_spacing(*v.amount());
|
||||
layouter.layout_spacing(v.amount);
|
||||
deferred = None;
|
||||
continue;
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ pub fn layout_rotate(
|
||||
region,
|
||||
size,
|
||||
styles,
|
||||
elem.body(),
|
||||
&elem.body,
|
||||
Transform::rotate(angle),
|
||||
align,
|
||||
elem.reflow(styles),
|
||||
@ -81,7 +81,7 @@ pub fn layout_scale(
|
||||
region,
|
||||
size,
|
||||
styles,
|
||||
elem.body(),
|
||||
&elem.body,
|
||||
Transform::scale(scale.x, scale.y),
|
||||
elem.origin(styles).resolve(styles),
|
||||
elem.reflow(styles),
|
||||
@ -169,7 +169,7 @@ pub fn layout_skew(
|
||||
region,
|
||||
size,
|
||||
styles,
|
||||
elem.body(),
|
||||
&elem.body,
|
||||
Transform::skew(ax, ay),
|
||||
align,
|
||||
elem.reflow(styles),
|
||||
|
@ -1,4 +1,5 @@
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::ops::Add;
|
||||
|
||||
use ecow::{eco_format, eco_vec, EcoString, EcoVec};
|
||||
use typst_syntax::{Span, Spanned};
|
||||
@ -304,8 +305,6 @@ impl Args {
|
||||
/// ```
|
||||
#[func(constructor)]
|
||||
pub fn construct(
|
||||
/// The real arguments (the other argument is just for the docs).
|
||||
/// The docs argument cannot be called `args`.
|
||||
args: &mut Args,
|
||||
/// The arguments to construct.
|
||||
#[external]
|
||||
@ -366,7 +365,7 @@ impl Debug for Args {
|
||||
impl Repr for Args {
|
||||
fn repr(&self) -> EcoString {
|
||||
let pieces = self.items.iter().map(Arg::repr).collect::<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`.
|
||||
#[derive(Clone, Hash)]
|
||||
#[allow(clippy::derived_hash_with_manual_eq)]
|
||||
|
@ -301,9 +301,7 @@ impl Array {
|
||||
#[func]
|
||||
pub fn find(
|
||||
&self,
|
||||
/// The engine.
|
||||
engine: &mut Engine,
|
||||
/// The callsite context.
|
||||
context: Tracked<Context>,
|
||||
/// The function to apply to each item. Must return a boolean.
|
||||
searcher: Func,
|
||||
@ -325,9 +323,7 @@ impl Array {
|
||||
#[func]
|
||||
pub fn position(
|
||||
&self,
|
||||
/// The engine.
|
||||
engine: &mut Engine,
|
||||
/// The callsite context.
|
||||
context: Tracked<Context>,
|
||||
/// The function to apply to each item. Must return a boolean.
|
||||
searcher: Func,
|
||||
@ -363,8 +359,6 @@ impl Array {
|
||||
/// ```
|
||||
#[func]
|
||||
pub fn range(
|
||||
/// The real arguments (the other arguments are just for the docs, this
|
||||
/// function is a bit involved, so we parse the arguments manually).
|
||||
args: &mut Args,
|
||||
/// The start of the range (inclusive).
|
||||
#[external]
|
||||
@ -402,9 +396,7 @@ impl Array {
|
||||
#[func]
|
||||
pub fn filter(
|
||||
&self,
|
||||
/// The engine.
|
||||
engine: &mut Engine,
|
||||
/// The callsite context.
|
||||
context: Tracked<Context>,
|
||||
/// The function to apply to each item. Must return a boolean.
|
||||
test: Func,
|
||||
@ -427,9 +419,7 @@ impl Array {
|
||||
#[func]
|
||||
pub fn map(
|
||||
self,
|
||||
/// The engine.
|
||||
engine: &mut Engine,
|
||||
/// The callsite context.
|
||||
context: Tracked<Context>,
|
||||
/// The function to apply to each item.
|
||||
mapper: Func,
|
||||
@ -481,8 +471,6 @@ impl Array {
|
||||
#[func]
|
||||
pub fn zip(
|
||||
self,
|
||||
/// The real arguments (the `others` arguments are just for the docs, this
|
||||
/// function is a bit involved, so we parse the positional arguments manually).
|
||||
args: &mut Args,
|
||||
/// Whether all arrays have to have the same length.
|
||||
/// For example, `{(1, 2).zip((1, 2, 3), exact: true)}` produces an
|
||||
@ -569,9 +557,7 @@ impl Array {
|
||||
#[func]
|
||||
pub fn fold(
|
||||
self,
|
||||
/// The engine.
|
||||
engine: &mut Engine,
|
||||
/// The callsite context.
|
||||
context: Tracked<Context>,
|
||||
/// The initial value to start with.
|
||||
init: Value,
|
||||
@ -631,9 +617,7 @@ impl Array {
|
||||
#[func]
|
||||
pub fn any(
|
||||
self,
|
||||
/// The engine.
|
||||
engine: &mut Engine,
|
||||
/// The callsite context.
|
||||
context: Tracked<Context>,
|
||||
/// The function to apply to each item. Must return a boolean.
|
||||
test: Func,
|
||||
@ -651,9 +635,7 @@ impl Array {
|
||||
#[func]
|
||||
pub fn all(
|
||||
self,
|
||||
/// The engine.
|
||||
engine: &mut Engine,
|
||||
/// The callsite context.
|
||||
context: Tracked<Context>,
|
||||
/// The function to apply to each item. Must return a boolean.
|
||||
test: Func,
|
||||
@ -831,11 +813,8 @@ impl Array {
|
||||
#[func]
|
||||
pub fn sorted(
|
||||
self,
|
||||
/// The engine.
|
||||
engine: &mut Engine,
|
||||
/// The callsite context.
|
||||
context: Tracked<Context>,
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
/// If given, applies this function to the elements in the array to
|
||||
/// determine the keys to sort by.
|
||||
@ -881,9 +860,7 @@ impl Array {
|
||||
#[func(title = "Deduplicate")]
|
||||
pub fn dedup(
|
||||
self,
|
||||
/// The engine.
|
||||
engine: &mut Engine,
|
||||
/// The callsite context.
|
||||
context: Tracked<Context>,
|
||||
/// If given, applies this function to the elements in the array to
|
||||
/// determine the keys to deduplicate by.
|
||||
@ -967,9 +944,7 @@ impl Array {
|
||||
#[func]
|
||||
pub fn reduce(
|
||||
self,
|
||||
/// The engine.
|
||||
engine: &mut Engine,
|
||||
/// The callsite context.
|
||||
context: Tracked<Context>,
|
||||
/// The reducing function. Must have two parameters: One for the
|
||||
/// accumulated value and one for an item.
|
||||
@ -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.
|
||||
#[cold]
|
||||
fn array_is_empty() -> EcoString {
|
||||
|
@ -1,6 +1,8 @@
|
||||
use std::borrow::Cow;
|
||||
use std::any::Any;
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::ops::{Add, AddAssign, Deref};
|
||||
use std::str::Utf8Error;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ecow::{eco_format, EcoString};
|
||||
@ -39,28 +41,75 @@ use crate::foundations::{cast, func, scope, ty, Array, Reflect, Repr, Str, Value
|
||||
/// #str(data.slice(1, 4))
|
||||
/// ```
|
||||
#[ty(scope, cast)]
|
||||
#[derive(Clone, Hash, Eq, PartialEq)]
|
||||
pub struct Bytes(Arc<LazyHash<Cow<'static, [u8]>>>);
|
||||
#[derive(Clone, Hash)]
|
||||
#[allow(clippy::derived_hash_with_manual_eq)]
|
||||
pub struct Bytes(Arc<LazyHash<dyn Bytelike>>);
|
||||
|
||||
impl Bytes {
|
||||
/// Create a buffer from a static byte slice.
|
||||
pub fn from_static(slice: &'static [u8]) -> Self {
|
||||
Self(Arc::new(LazyHash::new(Cow::Borrowed(slice))))
|
||||
/// Create `Bytes` from anything byte-like.
|
||||
///
|
||||
/// The `data` type will directly back this bytes object. This means you can
|
||||
/// e.g. pass `&'static [u8]` or `[u8; 8]` and no extra vector will be
|
||||
/// allocated.
|
||||
///
|
||||
/// If the type is `Vec<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.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
self.as_slice().is_empty()
|
||||
}
|
||||
|
||||
/// Return a view into the buffer.
|
||||
/// Return a view into the bytes.
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
self
|
||||
}
|
||||
|
||||
/// Return a copy of the buffer as a vector.
|
||||
/// Try to view the bytes as an UTF-8 string.
|
||||
///
|
||||
/// If these bytes were created via `Bytes::from_string`, UTF-8 validation
|
||||
/// is skipped.
|
||||
pub fn as_str(&self) -> Result<&str, Utf8Error> {
|
||||
self.inner().as_str()
|
||||
}
|
||||
|
||||
/// Return a copy of the bytes as a vector.
|
||||
pub fn to_vec(&self) -> Vec<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.
|
||||
@ -72,12 +121,15 @@ impl Bytes {
|
||||
///
|
||||
/// `index == len` is considered in bounds.
|
||||
fn locate_opt(&self, index: i64) -> Option<usize> {
|
||||
let len = self.as_slice().len();
|
||||
let wrapped =
|
||||
if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) };
|
||||
if index >= 0 { Some(index) } else { (len as i64).checked_add(index) };
|
||||
wrapped.and_then(|v| usize::try_from(v).ok()).filter(|&v| v <= len)
|
||||
}
|
||||
|
||||
wrapped
|
||||
.and_then(|v| usize::try_from(v).ok())
|
||||
.filter(|&v| v <= self.0.len())
|
||||
/// Access the inner `dyn Bytelike`.
|
||||
fn inner(&self) -> &dyn Bytelike {
|
||||
&**self.0
|
||||
}
|
||||
}
|
||||
|
||||
@ -106,7 +158,7 @@ impl Bytes {
|
||||
/// The length in bytes.
|
||||
#[func(title = "Length")]
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
self.as_slice().len()
|
||||
}
|
||||
|
||||
/// Returns the byte at the specified index. Returns the default value if
|
||||
@ -122,13 +174,13 @@ impl Bytes {
|
||||
default: Option<Value>,
|
||||
) -> StrResult<Value> {
|
||||
self.locate_opt(index)
|
||||
.and_then(|i| self.0.get(i).map(|&b| Value::Int(b.into())))
|
||||
.and_then(|i| self.as_slice().get(i).map(|&b| Value::Int(b.into())))
|
||||
.or(default)
|
||||
.ok_or_else(|| out_of_bounds_no_default(index, self.len()))
|
||||
}
|
||||
|
||||
/// Extracts a subslice of the bytes. Fails with an error if the start or end
|
||||
/// index is out of bounds.
|
||||
/// Extracts a subslice of the bytes. Fails with an error if the start or
|
||||
/// end index is out of bounds.
|
||||
#[func]
|
||||
pub fn slice(
|
||||
&self,
|
||||
@ -148,9 +200,17 @@ impl Bytes {
|
||||
if end.is_none() {
|
||||
end = count.map(|c: i64| start + c);
|
||||
}
|
||||
|
||||
let start = self.locate(start)?;
|
||||
let end = self.locate(end.unwrap_or(self.len() as i64))?.max(start);
|
||||
Ok(self.0[start..end].into())
|
||||
let slice = &self.as_slice()[start..end];
|
||||
|
||||
// We could hold a view into the original bytes here instead of
|
||||
// making a copy, but it's unclear when that's worth it. Java
|
||||
// originally did that for strings, but went back on it because a
|
||||
// very small view into a very large buffer would be a sort of
|
||||
// memory leak.
|
||||
Ok(Bytes::new(slice.to_vec()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -170,7 +230,15 @@ impl Deref for Bytes {
|
||||
type Target = [u8];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
self.inner().as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Bytes {}
|
||||
|
||||
impl PartialEq for Bytes {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.eq(&other.0)
|
||||
}
|
||||
}
|
||||
|
||||
@ -180,18 +248,6 @@ impl AsRef<[u8]> for Bytes {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[u8]> for Bytes {
|
||||
fn from(slice: &[u8]) -> Self {
|
||||
Self(Arc::new(LazyHash::new(slice.to_vec().into())))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for Bytes {
|
||||
fn from(vec: Vec<u8>) -> Self {
|
||||
Self(Arc::new(LazyHash::new(vec.into())))
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for Bytes {
|
||||
type Output = Self;
|
||||
|
||||
@ -207,10 +263,12 @@ impl AddAssign for Bytes {
|
||||
// Nothing to do
|
||||
} else if self.is_empty() {
|
||||
*self = rhs;
|
||||
} else if Arc::strong_count(&self.0) == 1 && matches!(**self.0, Cow::Owned(_)) {
|
||||
Arc::make_mut(&mut self.0).to_mut().extend_from_slice(&rhs);
|
||||
} else if let Some(vec) = Arc::get_mut(&mut self.0)
|
||||
.and_then(|unique| unique.as_any_mut().downcast_mut::<Vec<u8>>())
|
||||
{
|
||||
vec.extend_from_slice(&rhs);
|
||||
} else {
|
||||
*self = Self::from([self.as_slice(), rhs.as_slice()].concat());
|
||||
*self = Self::new([self.as_slice(), rhs.as_slice()].concat());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -228,20 +286,79 @@ impl Serialize for Bytes {
|
||||
}
|
||||
}
|
||||
|
||||
/// Any type that can back a byte buffer.
|
||||
trait Bytelike: Send + Sync {
|
||||
fn as_bytes(&self) -> &[u8];
|
||||
fn as_str(&self) -> Result<&str, Utf8Error>;
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
}
|
||||
|
||||
impl<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.
|
||||
pub struct ToBytes(Bytes);
|
||||
|
||||
cast! {
|
||||
ToBytes,
|
||||
v: Str => Self(v.as_bytes().into()),
|
||||
v: Str => Self(Bytes::from_string(v)),
|
||||
v: Array => Self(v.iter()
|
||||
.map(|item| match item {
|
||||
Value::Int(byte @ 0..=255) => Ok(*byte as u8),
|
||||
Value::Int(_) => bail!("number must be between 0 and 255"),
|
||||
value => Err(<u8 as Reflect>::error(value)),
|
||||
})
|
||||
.collect::<Result<Vec<u8>, _>>()?
|
||||
.into()
|
||||
.collect::<Result<Vec<u8>, _>>()
|
||||
.map(Bytes::new)?
|
||||
),
|
||||
v: Bytes => Self(v),
|
||||
}
|
||||
|
@ -97,7 +97,6 @@ cast! {
|
||||
/// ```
|
||||
#[func(title = "Power")]
|
||||
pub fn pow(
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
/// The base of the power.
|
||||
///
|
||||
@ -159,7 +158,6 @@ pub fn pow(
|
||||
/// ```
|
||||
#[func(title = "Exponential")]
|
||||
pub fn exp(
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
/// The exponent of the power.
|
||||
exponent: Spanned<Num>,
|
||||
@ -412,7 +410,6 @@ pub fn tanh(
|
||||
/// ```
|
||||
#[func(title = "Logarithm")]
|
||||
pub fn log(
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
/// The number whose logarithm to calculate. Must be strictly positive.
|
||||
value: Spanned<Num>,
|
||||
@ -454,7 +451,6 @@ pub fn log(
|
||||
/// ```
|
||||
#[func(title = "Natural Logarithm")]
|
||||
pub fn ln(
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
/// The number whose logarithm to calculate. Must be strictly positive.
|
||||
value: Spanned<Num>,
|
||||
@ -782,7 +778,6 @@ pub fn round(
|
||||
/// ```
|
||||
#[func]
|
||||
pub fn clamp(
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
/// The number to clamp.
|
||||
value: DecNum,
|
||||
@ -815,7 +810,6 @@ pub fn clamp(
|
||||
/// ```
|
||||
#[func(title = "Minimum")]
|
||||
pub fn min(
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
/// The sequence of values from which to extract the minimum.
|
||||
/// Must not be empty.
|
||||
@ -833,7 +827,6 @@ pub fn min(
|
||||
/// ```
|
||||
#[func(title = "Maximum")]
|
||||
pub fn max(
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
/// The sequence of values from which to extract the maximum.
|
||||
/// Must not be empty.
|
||||
@ -911,7 +904,6 @@ pub fn odd(
|
||||
/// ```
|
||||
#[func(title = "Remainder")]
|
||||
pub fn rem(
|
||||
/// The span of the function call.
|
||||
span: Span,
|
||||
/// The dividend of the remainder.
|
||||
dividend: DecNum,
|
||||
@ -950,7 +942,6 @@ pub fn rem(
|
||||
/// ```
|
||||
#[func(title = "Euclidean Division")]
|
||||
pub fn div_euclid(
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
/// The dividend of the division.
|
||||
dividend: DecNum,
|
||||
@ -992,9 +983,8 @@ pub fn div_euclid(
|
||||
/// #calc.rem-euclid(1.75, 0.5) \
|
||||
/// #calc.rem-euclid(decimal("1.75"), decimal("0.5"))
|
||||
/// ```
|
||||
#[func(title = "Euclidean Remainder")]
|
||||
#[func(title = "Euclidean Remainder", keywords = ["modulo", "modulus"])]
|
||||
pub fn rem_euclid(
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
/// The dividend of the remainder.
|
||||
dividend: DecNum,
|
||||
@ -1031,7 +1021,6 @@ pub fn rem_euclid(
|
||||
/// ```
|
||||
#[func(title = "Quotient")]
|
||||
pub fn quo(
|
||||
/// The span of the function call.
|
||||
span: Span,
|
||||
/// The dividend of the quotient.
|
||||
dividend: DecNum,
|
||||
|
@ -13,7 +13,9 @@ use typst_syntax::{Span, Spanned};
|
||||
use unicode_math_class::MathClass;
|
||||
|
||||
use crate::diag::{At, HintedStrResult, HintedString, SourceResult, StrResult};
|
||||
use crate::foundations::{array, repr, NativeElement, Packed, Repr, Str, Type, Value};
|
||||
use crate::foundations::{
|
||||
array, repr, Fold, NativeElement, Packed, Repr, Str, Type, Value,
|
||||
};
|
||||
|
||||
/// Determine details of a type.
|
||||
///
|
||||
@ -497,3 +499,58 @@ cast! {
|
||||
/// An operator that can be both unary or binary like `+`.
|
||||
"vary" => MathClass::Vary,
|
||||
}
|
||||
|
||||
/// A type that contains a user-visible source portion and something that is
|
||||
/// derived from it, but not user-visible.
|
||||
///
|
||||
/// An example usage would be `source` being a `DataSource` and `derived` a
|
||||
/// TextMate theme parsed from it. With `Derived`, we can store both parts in
|
||||
/// the `RawElem::theme` field and get automatic nice `Reflect` and `IntoValue`
|
||||
/// impls.
|
||||
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Derived<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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -211,9 +211,10 @@ impl Content {
|
||||
/// instead.
|
||||
pub fn get_by_name(&self, name: &str) -> Result<Value, FieldAccessError> {
|
||||
if name == "label" {
|
||||
if let Some(label) = self.label() {
|
||||
return Ok(label.into_value());
|
||||
}
|
||||
return self
|
||||
.label()
|
||||
.map(|label| label.into_value())
|
||||
.ok_or(FieldAccessError::Unknown);
|
||||
}
|
||||
let id = self.elem().field_id(name).ok_or(FieldAccessError::Unknown)?;
|
||||
self.get(id, None)
|
||||
|
@ -318,7 +318,6 @@ impl Datetime {
|
||||
/// ```
|
||||
#[func]
|
||||
pub fn today(
|
||||
/// The engine.
|
||||
engine: &mut Engine,
|
||||
/// An offset to apply to the current UTC date. If set to `{auto}`, the
|
||||
/// offset will be the local offset.
|
||||
|
@ -163,18 +163,14 @@ impl f64 {
|
||||
size: u32,
|
||||
) -> StrResult<Bytes> {
|
||||
Ok(match size {
|
||||
8 => match endian {
|
||||
8 => Bytes::new(match endian {
|
||||
Endianness::Little => self.to_le_bytes(),
|
||||
Endianness::Big => self.to_be_bytes(),
|
||||
}
|
||||
.as_slice()
|
||||
.into(),
|
||||
4 => match endian {
|
||||
}),
|
||||
4 => Bytes::new(match endian {
|
||||
Endianness::Little => (self as f32).to_le_bytes(),
|
||||
Endianness::Big => (self as f32).to_be_bytes(),
|
||||
}
|
||||
.as_slice()
|
||||
.into(),
|
||||
}),
|
||||
_ => bail!("size must be either 4 or 8"),
|
||||
})
|
||||
}
|
||||
|
@ -334,8 +334,6 @@ impl Func {
|
||||
#[func]
|
||||
pub fn with(
|
||||
self,
|
||||
/// The real arguments (the other argument is just for the docs).
|
||||
/// The docs argument cannot be called `args`.
|
||||
args: &mut Args,
|
||||
/// The arguments to apply to the function.
|
||||
#[external]
|
||||
@ -361,8 +359,6 @@ impl Func {
|
||||
#[func]
|
||||
pub fn where_(
|
||||
self,
|
||||
/// The real arguments (the other argument is just for the docs).
|
||||
/// The docs argument cannot be called `args`.
|
||||
args: &mut Args,
|
||||
/// The fields to filter for.
|
||||
#[variadic]
|
||||
|
@ -1,6 +1,7 @@
|
||||
use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize, ParseIntError};
|
||||
|
||||
use ecow::{eco_format, EcoString};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::diag::{bail, StrResult};
|
||||
use crate::foundations::{
|
||||
@ -322,7 +323,7 @@ impl i64 {
|
||||
Endianness::Little => self.to_le_bytes(),
|
||||
};
|
||||
|
||||
let mut buf = vec![0u8; size];
|
||||
let mut buf = SmallVec::<[u8; 8]>::from_elem(0, size);
|
||||
match endian {
|
||||
Endianness::Big => {
|
||||
// Copy the bytes from the array to the buffer, starting from
|
||||
@ -339,7 +340,7 @@ impl i64 {
|
||||
}
|
||||
}
|
||||
|
||||
Bytes::from(buf)
|
||||
Bytes::new(buf)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -266,7 +266,6 @@ impl assert {
|
||||
/// ```
|
||||
#[func(title = "Evaluate")]
|
||||
pub fn eval(
|
||||
/// The engine.
|
||||
engine: &mut Engine,
|
||||
/// A string of Typst code to evaluate.
|
||||
source: Spanned<String>,
|
||||
|
@ -36,6 +36,7 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> {
|
||||
(Symbol(a), Content(b)) => Content(TextElem::packed(a.get()) + b),
|
||||
(Array(a), Array(b)) => Array(a + b),
|
||||
(Dict(a), Dict(b)) => Dict(a + b),
|
||||
(Args(a), Args(b)) => Args(a + b),
|
||||
(a, b) => mismatch!("cannot join {} with {}", a, b),
|
||||
})
|
||||
}
|
||||
@ -136,6 +137,7 @@ pub fn add(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
|
||||
|
||||
(Array(a), Array(b)) => Array(a + b),
|
||||
(Dict(a), Dict(b)) => Dict(a + b),
|
||||
(Args(a), Args(b)) => Args(a + b),
|
||||
|
||||
(Color(color), Length(thickness)) | (Length(thickness), Color(color)) => {
|
||||
Stroke::from_pair(color, thickness).into_value()
|
||||
|
@ -9,7 +9,7 @@ use wasmi::{AsContext, AsContextMut};
|
||||
use crate::diag::{bail, At, SourceResult, StrResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{func, repr, scope, ty, Bytes};
|
||||
use crate::World;
|
||||
use crate::loading::{DataSource, Load};
|
||||
|
||||
/// A WebAssembly plugin.
|
||||
///
|
||||
@ -152,17 +152,14 @@ impl Plugin {
|
||||
/// Creates a new plugin from a WebAssembly file.
|
||||
#[func(constructor)]
|
||||
pub fn construct(
|
||||
/// The 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).
|
||||
path: Spanned<EcoString>,
|
||||
/// For more details about paths, see the [Paths section]($syntax/#paths).
|
||||
source: Spanned<DataSource>,
|
||||
) -> SourceResult<Plugin> {
|
||||
let Spanned { v: path, span } = path;
|
||||
let id = span.resolve_path(&path).at(span)?;
|
||||
let data = engine.world.file(id).at(span)?;
|
||||
Plugin::new(data).at(span)
|
||||
let data = source.load(engine.world)?;
|
||||
Plugin::new(data).at(source.span)
|
||||
}
|
||||
}
|
||||
|
||||
@ -293,7 +290,7 @@ impl Plugin {
|
||||
_ => 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.
|
||||
|
@ -425,9 +425,7 @@ impl Str {
|
||||
#[func]
|
||||
pub fn replace(
|
||||
&self,
|
||||
/// The engine.
|
||||
engine: &mut Engine,
|
||||
/// The callsite context.
|
||||
context: Tracked<Context>,
|
||||
/// The pattern to search for.
|
||||
pattern: StrPattern,
|
||||
@ -575,6 +573,12 @@ impl Str {
|
||||
|
||||
/// Splits a string at matches of a specified pattern and returns an array
|
||||
/// of the resulting parts.
|
||||
///
|
||||
/// When the empty string is used as a separator, it separates every
|
||||
/// character (i.e., Unicode code point) in the string, along with the
|
||||
/// beginning and end of the string. In practice, this means that the
|
||||
/// resulting list of parts will contain the empty string at the start
|
||||
/// and end of the list.
|
||||
#[func]
|
||||
pub fn split(
|
||||
&self,
|
||||
@ -778,11 +782,7 @@ cast! {
|
||||
v: f64 => Self::Str(repr::display_float(v).into()),
|
||||
v: Decimal => Self::Str(format_str!("{}", v)),
|
||||
v: Version => Self::Str(format_str!("{}", v)),
|
||||
v: Bytes => Self::Str(
|
||||
std::str::from_utf8(&v)
|
||||
.map_err(|_| "bytes are not valid utf-8")?
|
||||
.into()
|
||||
),
|
||||
v: Bytes => Self::Str(v.to_str().map_err(|_| "bytes are not valid utf-8")?),
|
||||
v: Label => Self::Str(v.resolve().as_str().into()),
|
||||
v: Type => Self::Str(v.long_name().into()),
|
||||
v: Str => Self::Str(v),
|
||||
|
@ -12,7 +12,8 @@ use typst_utils::LazyHash;
|
||||
use crate::diag::{SourceResult, Trace, Tracepoint};
|
||||
use crate::engine::Engine;
|
||||
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};
|
||||
|
||||
@ -939,6 +940,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
|
||||
/// `None` value isn't respected (contrary to `Option`'s usual `Fold`
|
||||
/// implementation, with which folding with an inner `None` always returns
|
||||
|
@ -1,11 +1,12 @@
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::fmt::{self, Debug, Display, Formatter, Write};
|
||||
use std::sync::Arc;
|
||||
|
||||
use ecow::{eco_format, EcoString};
|
||||
use serde::{Serialize, Serializer};
|
||||
use typst_syntax::{is_ident, Span, Spanned};
|
||||
use typst_utils::hash128;
|
||||
|
||||
use crate::diag::{bail, SourceResult, StrResult};
|
||||
use crate::foundations::{cast, func, scope, ty, Array, Func, NativeFunc, Repr as _};
|
||||
@ -186,7 +187,6 @@ impl Symbol {
|
||||
/// ```
|
||||
#[func(constructor)]
|
||||
pub fn construct(
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
/// The variants of the symbol.
|
||||
///
|
||||
@ -198,24 +198,62 @@ impl Symbol {
|
||||
#[variadic]
|
||||
variants: Vec<Spanned<SymbolVariant>>,
|
||||
) -> SourceResult<Symbol> {
|
||||
let mut list = Vec::new();
|
||||
if variants.is_empty() {
|
||||
bail!(span, "expected at least one variant");
|
||||
}
|
||||
for Spanned { v, span } in variants {
|
||||
if list.iter().any(|(prev, _)| &v.0 == prev) {
|
||||
bail!(span, "duplicate variant");
|
||||
}
|
||||
|
||||
// Maps from canonicalized 128-bit hashes to indices of variants we've
|
||||
// seen before.
|
||||
let mut seen = HashMap::<u128, usize>::new();
|
||||
|
||||
// A list of modifiers, cleared & reused in each iteration.
|
||||
let mut modifiers = Vec::new();
|
||||
|
||||
// Validate the variants.
|
||||
for (i, &Spanned { ref v, span }) in variants.iter().enumerate() {
|
||||
modifiers.clear();
|
||||
|
||||
if !v.0.is_empty() {
|
||||
// Collect all modifiers.
|
||||
for modifier in v.0.split('.') {
|
||||
if !is_ident(modifier) {
|
||||
bail!(span, "invalid symbol modifier: {}", modifier.repr());
|
||||
}
|
||||
modifiers.push(modifier);
|
||||
}
|
||||
}
|
||||
list.push((v.0, v.1));
|
||||
|
||||
// Canonicalize the modifier order.
|
||||
modifiers.sort();
|
||||
|
||||
// Ensure that there are no duplicate modifiers.
|
||||
if let Some(ms) = modifiers.windows(2).find(|ms| ms[0] == ms[1]) {
|
||||
bail!(
|
||||
span, "duplicate modifier within variant: {}", ms[0].repr();
|
||||
hint: "modifiers are not ordered, so each one may appear only once"
|
||||
)
|
||||
}
|
||||
|
||||
// Check whether we had this set of modifiers before.
|
||||
let hash = hash128(&modifiers);
|
||||
if let Some(&i) = seen.get(&hash) {
|
||||
if v.0.is_empty() {
|
||||
bail!(span, "duplicate default variant");
|
||||
} else if v.0 == variants[i].v.0 {
|
||||
bail!(span, "duplicate variant: {}", v.0.repr());
|
||||
} else {
|
||||
bail!(
|
||||
span, "duplicate variant: {}", v.0.repr();
|
||||
hint: "variants with the same modifiers are identical, regardless of their order"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
seen.insert(hash, i);
|
||||
}
|
||||
Ok(Symbol::runtime(list.into_boxed_slice()))
|
||||
|
||||
let list = variants.into_iter().map(|s| (s.v.0, s.v.1)).collect();
|
||||
Ok(Symbol::runtime(list))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,9 +30,6 @@ pub struct TargetElem {
|
||||
|
||||
/// Returns the current compilation target.
|
||||
#[func(contextual)]
|
||||
pub fn target(
|
||||
/// The callsite context.
|
||||
context: Tracked<Context>,
|
||||
) -> HintedStrResult<Target> {
|
||||
pub fn target(context: Tracked<Context>) -> HintedStrResult<Target> {
|
||||
Ok(TargetElem::target_in(context.styles()?))
|
||||
}
|
||||
|
@ -459,15 +459,15 @@ impl<'de> Visitor<'de> for ValueVisitor {
|
||||
}
|
||||
|
||||
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> {
|
||||
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> {
|
||||
Ok(Bytes::from(v).into_value())
|
||||
Ok(Bytes::new(v).into_value())
|
||||
}
|
||||
|
||||
fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
|
||||
|
@ -428,11 +428,8 @@ impl Counter {
|
||||
#[func(contextual)]
|
||||
pub fn get(
|
||||
&self,
|
||||
/// The engine.
|
||||
engine: &mut Engine,
|
||||
/// The callsite context.
|
||||
context: Tracked<Context>,
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
) -> SourceResult<CounterState> {
|
||||
let loc = context.location().at(span)?;
|
||||
@ -444,11 +441,8 @@ impl Counter {
|
||||
#[func(contextual)]
|
||||
pub fn display(
|
||||
self,
|
||||
/// The engine.
|
||||
engine: &mut Engine,
|
||||
/// The callsite context.
|
||||
context: Tracked<Context>,
|
||||
/// The call span of the display.
|
||||
span: Span,
|
||||
/// A [numbering pattern or a function]($numbering), which specifies how
|
||||
/// to display the counter. If given a function, that function receives
|
||||
@ -482,11 +476,8 @@ impl Counter {
|
||||
#[func(contextual)]
|
||||
pub fn at(
|
||||
&self,
|
||||
/// The engine.
|
||||
engine: &mut Engine,
|
||||
/// The callsite context.
|
||||
context: Tracked<Context>,
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
/// The place at which the counter's value should be retrieved.
|
||||
selector: LocatableSelector,
|
||||
@ -500,11 +491,8 @@ impl Counter {
|
||||
#[func(contextual)]
|
||||
pub fn final_(
|
||||
&self,
|
||||
/// The engine.
|
||||
engine: &mut Engine,
|
||||
/// The callsite context.
|
||||
context: Tracked<Context>,
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
) -> SourceResult<CounterState> {
|
||||
context.introspect().at(span)?;
|
||||
@ -528,7 +516,6 @@ impl Counter {
|
||||
#[func]
|
||||
pub fn step(
|
||||
self,
|
||||
/// The call span of the update.
|
||||
span: Span,
|
||||
/// The depth at which to step the counter. Defaults to `{1}`.
|
||||
#[named]
|
||||
@ -545,7 +532,6 @@ impl Counter {
|
||||
#[func]
|
||||
pub fn update(
|
||||
self,
|
||||
/// The call span of the update.
|
||||
span: Span,
|
||||
/// If given an integer or array of integers, sets the counter to that
|
||||
/// value. If given a function, that function receives the previous
|
||||
@ -800,7 +786,7 @@ impl ManualPageCounter {
|
||||
let Some(elem) = elem.to_packed::<CounterUpdateElem>() else {
|
||||
continue;
|
||||
};
|
||||
if *elem.key() == CounterKey::Page {
|
||||
if elem.key == CounterKey::Page {
|
||||
let mut state = CounterState(smallvec![self.logical]);
|
||||
state.update(engine, elem.update.clone())?;
|
||||
self.logical = state.first();
|
||||
|
@ -44,9 +44,6 @@ use crate::introspection::Location;
|
||||
/// ```
|
||||
/// Refer to the [`selector`] type for more details on before/after selectors.
|
||||
#[func(contextual)]
|
||||
pub fn here(
|
||||
/// The callsite context.
|
||||
context: Tracked<Context>,
|
||||
) -> HintedStrResult<Location> {
|
||||
pub fn here(context: Tracked<Context>) -> HintedStrResult<Location> {
|
||||
context.location()
|
||||
}
|
||||
|
@ -24,9 +24,7 @@ use crate::introspection::Location;
|
||||
/// ```
|
||||
#[func(contextual)]
|
||||
pub fn locate(
|
||||
/// The engine.
|
||||
engine: &mut Engine,
|
||||
/// The callsite context.
|
||||
context: Tracked<Context>,
|
||||
/// A selector that should match exactly one element. This element will be
|
||||
/// located.
|
||||
|
@ -136,9 +136,7 @@ use crate::foundations::{func, Array, Context, LocatableSelector, Value};
|
||||
/// ```
|
||||
#[func(contextual)]
|
||||
pub fn query(
|
||||
/// The engine.
|
||||
engine: &mut Engine,
|
||||
/// The callsite context.
|
||||
context: Tracked<Context>,
|
||||
/// Can be
|
||||
/// - an element function like a `heading` or `figure`,
|
||||
|
@ -245,7 +245,7 @@ impl State {
|
||||
|
||||
for elem in introspector.query(&self.selector()) {
|
||||
let elem = elem.to_packed::<StateUpdateElem>().unwrap();
|
||||
match elem.update() {
|
||||
match &elem.update {
|
||||
StateUpdate::Set(value) => state = value.clone(),
|
||||
StateUpdate::Func(func) => {
|
||||
state = func.call(&mut engine, Context::none().track(), [state])?
|
||||
@ -289,11 +289,8 @@ impl State {
|
||||
#[func(contextual)]
|
||||
pub fn get(
|
||||
&self,
|
||||
/// The engine.
|
||||
engine: &mut Engine,
|
||||
/// The callsite context.
|
||||
context: Tracked<Context>,
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
) -> SourceResult<Value> {
|
||||
let loc = context.location().at(span)?;
|
||||
@ -309,11 +306,8 @@ impl State {
|
||||
#[func(contextual)]
|
||||
pub fn at(
|
||||
&self,
|
||||
/// The engine.
|
||||
engine: &mut Engine,
|
||||
/// The callsite context.
|
||||
context: Tracked<Context>,
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
/// The place at which the state's value should be retrieved.
|
||||
selector: LocatableSelector,
|
||||
@ -326,11 +320,8 @@ impl State {
|
||||
#[func(contextual)]
|
||||
pub fn final_(
|
||||
&self,
|
||||
/// The engine.
|
||||
engine: &mut Engine,
|
||||
/// The callsite context.
|
||||
context: Tracked<Context>,
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
) -> SourceResult<Value> {
|
||||
context.introspect().at(span)?;
|
||||
@ -349,7 +340,6 @@ impl State {
|
||||
#[func]
|
||||
pub fn update(
|
||||
self,
|
||||
/// The span of the `update` call.
|
||||
span: Span,
|
||||
/// If given a non function-value, sets the state to that value. If
|
||||
/// given a function, that function receives the previous state and has
|
||||
|
@ -100,7 +100,7 @@ pub struct AlignElem {
|
||||
impl Show for Packed<AlignElem> {
|
||||
#[typst_macros::time(name = "align", span = self.span())]
|
||||
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)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -166,7 +166,7 @@ impl Packed<InlineElem> {
|
||||
styles: StyleChain,
|
||||
region: Size,
|
||||
) -> SourceResult<Vec<InlineItem>> {
|
||||
self.body().call(engine, locator, styles, region)
|
||||
self.body.call(engine, locator, styles, region)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@ use crate::layout::{
|
||||
};
|
||||
use crate::model::{Destination, LinkElem};
|
||||
use crate::text::TextItem;
|
||||
use crate::visualize::{Color, FixedStroke, Geometry, Image, Paint, Path, Shape};
|
||||
use crate::visualize::{Color, Curve, FixedStroke, Geometry, Image, Paint, Shape};
|
||||
|
||||
/// A finished layout with items at fixed positions.
|
||||
#[derive(Default, Clone, Hash)]
|
||||
@ -374,14 +374,14 @@ impl Frame {
|
||||
}
|
||||
}
|
||||
|
||||
/// Clip the contents of a frame to a clip path.
|
||||
/// Clip the contents of a frame to a clip curve.
|
||||
///
|
||||
/// The clip path can be the size of the frame in the case of a
|
||||
/// rectangular frame. In the case of a frame with rounded corner,
|
||||
/// this should be a path that matches the frame's outline.
|
||||
pub fn clip(&mut self, clip_path: Path) {
|
||||
/// The clip curve can be the size of the frame in the case of a rectangular
|
||||
/// frame. In the case of a frame with rounded corner, this should be a
|
||||
/// curve that matches the frame's outline.
|
||||
pub fn clip(&mut self, clip_curve: Curve) {
|
||||
if !self.is_empty() {
|
||||
self.group(|g| g.clip_path = Some(clip_path));
|
||||
self.group(|g| g.clip = Some(clip_curve));
|
||||
}
|
||||
}
|
||||
|
||||
@ -447,7 +447,7 @@ impl Frame {
|
||||
self.push(
|
||||
pos - Point::splat(radius),
|
||||
FrameItem::Shape(
|
||||
Geometry::Path(Path::ellipse(Size::splat(2.0 * radius)))
|
||||
Geometry::Curve(Curve::ellipse(Size::splat(2.0 * radius)))
|
||||
.filled(Color::GREEN),
|
||||
Span::detached(),
|
||||
),
|
||||
@ -544,8 +544,8 @@ pub struct GroupItem {
|
||||
pub frame: Frame,
|
||||
/// A transformation to apply to the group.
|
||||
pub transform: Transform,
|
||||
/// Whether the frame should be a clipping boundary.
|
||||
pub clip_path: Option<Path>,
|
||||
/// A curve which should be used to clip the group.
|
||||
pub clip: Option<Curve>,
|
||||
/// The group's label.
|
||||
pub label: Option<Label>,
|
||||
/// The group's logical parent. All elements in this group are logically
|
||||
@ -559,7 +559,7 @@ impl GroupItem {
|
||||
Self {
|
||||
frame,
|
||||
transform: Transform::identity(),
|
||||
clip_path: None,
|
||||
clip: None,
|
||||
label: None,
|
||||
parent: None,
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
pub mod resolve;
|
||||
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::Arc;
|
||||
|
||||
@ -747,7 +749,7 @@ cast! {
|
||||
|
||||
impl Show for Packed<GridCell> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
@ -2,19 +2,463 @@ use std::num::NonZeroUsize;
|
||||
use std::sync::Arc;
|
||||
|
||||
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::foundations::{Content, Smart, StyleChain};
|
||||
use typst_library::foundations::{Content, Fold, Packed, Smart, StyleChain};
|
||||
use typst_library::introspection::Locator;
|
||||
use typst_library::layout::{
|
||||
Abs, Alignment, Axes, Celled, Fragment, Length, Regions, Rel, ResolvedCelled, Sides,
|
||||
Sizing,
|
||||
Abs, Alignment, Axes, Celled, GridCell, GridChild, GridElem, GridItem, Length,
|
||||
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::Dir;
|
||||
|
||||
use typst_syntax::Span;
|
||||
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
|
||||
/// the table, and may have property overrides.
|
||||
@ -131,26 +575,6 @@ impl<'a> Cell<'a> {
|
||||
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
|
@ -29,6 +29,6 @@ pub struct HideElem {
|
||||
impl Show for Packed<HideElem> {
|
||||
#[typst_macros::time(name = "hide", span = self.span())]
|
||||
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)))
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,6 @@ use crate::layout::{BlockElem, Size};
|
||||
/// corresponding page dimension is set to `{auto}`.
|
||||
#[func]
|
||||
pub fn layout(
|
||||
/// The call span of this function.
|
||||
span: Span,
|
||||
/// A function to call with the outer container's size. Its return value is
|
||||
/// displayed in the document.
|
||||
@ -89,7 +88,7 @@ impl Show for Packed<LayoutElem> {
|
||||
let loc = elem.location().unwrap();
|
||||
let context = Context::new(Some(loc), Some(styles));
|
||||
let result = elem
|
||||
.func()
|
||||
.func
|
||||
.call(
|
||||
engine,
|
||||
context.track(),
|
||||
|
@ -43,11 +43,8 @@ use crate::layout::{Abs, Axes, Length, Region, Size};
|
||||
/// `height`, both of type [`length`].
|
||||
#[func(contextual)]
|
||||
pub fn measure(
|
||||
/// The engine.
|
||||
engine: &mut Engine,
|
||||
/// The callsite context.
|
||||
context: Tracked<Context>,
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
/// The width available to layout the content.
|
||||
///
|
||||
|
@ -12,7 +12,7 @@ mod em;
|
||||
mod fr;
|
||||
mod fragment;
|
||||
mod frame;
|
||||
mod grid;
|
||||
pub mod grid;
|
||||
mod hide;
|
||||
#[path = "layout.rs"]
|
||||
mod layout_;
|
||||
|
@ -86,12 +86,6 @@ impl Rel<Length> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert to a relative length with the absolute part resolved at the
|
||||
/// given font size.
|
||||
pub fn at(self, font_size: Abs) -> Rel<Abs> {
|
||||
self.map(|abs| abs.at(font_size))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Numeric + Debug> Debug for Rel<T> {
|
||||
|
@ -21,6 +21,7 @@ pub mod layout;
|
||||
pub mod loading;
|
||||
pub mod math;
|
||||
pub mod model;
|
||||
pub mod pdf;
|
||||
pub mod routines;
|
||||
pub mod symbols;
|
||||
pub mod text;
|
||||
@ -249,6 +250,7 @@ fn global(math: Module, inputs: Dict, features: &Features) -> Module {
|
||||
self::introspection::define(&mut global);
|
||||
self::loading::define(&mut global);
|
||||
self::symbols::define(&mut global);
|
||||
self::pdf::define(&mut global);
|
||||
global.reset_category();
|
||||
if features.is_enabled(Feature::Html) {
|
||||
global.define_module(self::html::module());
|
||||
|
@ -1,10 +1,10 @@
|
||||
use ecow::{eco_format, EcoString};
|
||||
use ecow::eco_format;
|
||||
use typst_syntax::Spanned;
|
||||
|
||||
use crate::diag::{At, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{func, scope, Bytes, Value};
|
||||
use crate::World;
|
||||
use crate::loading::{DataSource, Load};
|
||||
|
||||
/// Reads structured data from a CBOR file.
|
||||
///
|
||||
@ -19,31 +19,31 @@ use crate::World;
|
||||
/// floating point numbers, which may result in an approximative value.
|
||||
#[func(scope, title = "CBOR")]
|
||||
pub fn cbor(
|
||||
/// The 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).
|
||||
path: Spanned<EcoString>,
|
||||
/// For more details about paths, see the [Paths section]($syntax/#paths).
|
||||
source: Spanned<DataSource>,
|
||||
) -> SourceResult<Value> {
|
||||
let Spanned { v: path, span } = path;
|
||||
let id = span.resolve_path(&path).at(span)?;
|
||||
let data = engine.world.file(id).at(span)?;
|
||||
cbor::decode(Spanned::new(data, span))
|
||||
let data = source.load(engine.world)?;
|
||||
ciborium::from_reader(data.as_slice())
|
||||
.map_err(|err| eco_format!("failed to parse CBOR ({err})"))
|
||||
.at(source.span)
|
||||
}
|
||||
|
||||
#[scope]
|
||||
impl cbor {
|
||||
/// Reads structured data from CBOR bytes.
|
||||
///
|
||||
/// This function is deprecated. The [`cbor`] function now accepts bytes
|
||||
/// directly.
|
||||
#[func(title = "Decode CBOR")]
|
||||
pub fn decode(
|
||||
/// cbor data.
|
||||
engine: &mut Engine,
|
||||
/// CBOR data.
|
||||
data: Spanned<Bytes>,
|
||||
) -> SourceResult<Value> {
|
||||
let Spanned { v: data, span } = data;
|
||||
ciborium::from_reader(data.as_slice())
|
||||
.map_err(|err| eco_format!("failed to parse CBOR ({err})"))
|
||||
.at(span)
|
||||
cbor(engine, data.map(DataSource::Bytes))
|
||||
}
|
||||
|
||||
/// Encode structured data into CBOR bytes.
|
||||
@ -55,7 +55,7 @@ impl cbor {
|
||||
let Spanned { v: value, span } = value;
|
||||
let mut res = Vec::new();
|
||||
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})"))
|
||||
.at(span)
|
||||
}
|
||||
|
@ -4,8 +4,7 @@ use typst_syntax::Spanned;
|
||||
use crate::diag::{bail, At, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{cast, func, scope, Array, Dict, IntoValue, Type, Value};
|
||||
use crate::loading::Readable;
|
||||
use crate::World;
|
||||
use crate::loading::{DataSource, Load, Readable};
|
||||
|
||||
/// Reads structured data from a CSV file.
|
||||
///
|
||||
@ -26,12 +25,11 @@ use crate::World;
|
||||
/// ```
|
||||
#[func(scope, title = "CSV")]
|
||||
pub fn csv(
|
||||
/// The 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).
|
||||
path: Spanned<EcoString>,
|
||||
/// For more details about paths, see the [Paths section]($syntax/#paths).
|
||||
source: Spanned<DataSource>,
|
||||
/// The delimiter that separates columns in the CSV file.
|
||||
/// Must be a single ASCII character.
|
||||
#[named]
|
||||
@ -48,17 +46,62 @@ pub fn csv(
|
||||
#[default(RowType::Array)]
|
||||
row_type: RowType,
|
||||
) -> SourceResult<Array> {
|
||||
let Spanned { v: path, span } = path;
|
||||
let id = span.resolve_path(&path).at(span)?;
|
||||
let data = engine.world.file(id).at(span)?;
|
||||
self::csv::decode(Spanned::new(Readable::Bytes(data), span), delimiter, row_type)
|
||||
let data = source.load(engine.world)?;
|
||||
|
||||
let mut builder = ::csv::ReaderBuilder::new();
|
||||
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]
|
||||
impl csv {
|
||||
/// Reads structured data from a CSV string/bytes.
|
||||
///
|
||||
/// This function is deprecated. The [`csv`] function now accepts bytes
|
||||
/// directly.
|
||||
#[func(title = "Decode CSV")]
|
||||
pub fn decode(
|
||||
engine: &mut Engine,
|
||||
/// CSV data.
|
||||
data: Spanned<Readable>,
|
||||
/// The delimiter that separates columns in the CSV file.
|
||||
@ -77,51 +120,7 @@ impl csv {
|
||||
#[default(RowType::Array)]
|
||||
row_type: RowType,
|
||||
) -> SourceResult<Array> {
|
||||
let Spanned { v: data, span } = data;
|
||||
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)
|
||||
csv(engine, data.map(Readable::into_source), delimiter, row_type)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,10 @@
|
||||
use ecow::{eco_format, EcoString};
|
||||
use ecow::eco_format;
|
||||
use typst_syntax::Spanned;
|
||||
|
||||
use crate::diag::{At, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{func, scope, Str, Value};
|
||||
use crate::loading::Readable;
|
||||
use crate::World;
|
||||
use crate::loading::{DataSource, Load, Readable};
|
||||
|
||||
/// Reads structured data from a JSON file.
|
||||
///
|
||||
@ -51,31 +50,31 @@ use crate::World;
|
||||
/// ```
|
||||
#[func(scope, title = "JSON")]
|
||||
pub fn json(
|
||||
/// The 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).
|
||||
path: Spanned<EcoString>,
|
||||
/// For more details about paths, see the [Paths section]($syntax/#paths).
|
||||
source: Spanned<DataSource>,
|
||||
) -> SourceResult<Value> {
|
||||
let Spanned { v: path, span } = path;
|
||||
let id = span.resolve_path(&path).at(span)?;
|
||||
let data = engine.world.file(id).at(span)?;
|
||||
json::decode(Spanned::new(Readable::Bytes(data), span))
|
||||
let data = source.load(engine.world)?;
|
||||
serde_json::from_slice(data.as_slice())
|
||||
.map_err(|err| eco_format!("failed to parse JSON ({err})"))
|
||||
.at(source.span)
|
||||
}
|
||||
|
||||
#[scope]
|
||||
impl json {
|
||||
/// Reads structured data from a JSON string/bytes.
|
||||
///
|
||||
/// This function is deprecated. The [`json`] function now accepts bytes
|
||||
/// directly.
|
||||
#[func(title = "Decode JSON")]
|
||||
pub fn decode(
|
||||
engine: &mut Engine,
|
||||
/// JSON data.
|
||||
data: Spanned<Readable>,
|
||||
) -> SourceResult<Value> {
|
||||
let Spanned { v: data, span } = data;
|
||||
serde_json::from_slice(data.as_slice())
|
||||
.map_err(|err| eco_format!("failed to parse JSON ({err})"))
|
||||
.at(span)
|
||||
json(engine, data.map(Readable::into_source))
|
||||
}
|
||||
|
||||
/// Encodes structured data into a JSON string.
|
||||
|
@ -15,6 +15,10 @@ mod xml_;
|
||||
#[path = "yaml.rs"]
|
||||
mod yaml_;
|
||||
|
||||
use comemo::Tracked;
|
||||
use ecow::EcoString;
|
||||
use typst_syntax::Spanned;
|
||||
|
||||
pub use self::cbor_::*;
|
||||
pub use self::csv_::*;
|
||||
pub use self::json_::*;
|
||||
@ -23,7 +27,10 @@ pub use self::toml_::*;
|
||||
pub use self::xml_::*;
|
||||
pub use self::yaml_::*;
|
||||
|
||||
use crate::diag::{At, SourceResult};
|
||||
use crate::foundations::OneOrMultiple;
|
||||
use crate::foundations::{cast, category, Bytes, Category, Scope, Str};
|
||||
use crate::World;
|
||||
|
||||
/// Data loading from external files.
|
||||
///
|
||||
@ -44,6 +51,76 @@ pub(super) fn define(global: &mut Scope) {
|
||||
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.
|
||||
#[derive(Debug, Clone, PartialEq, Hash)]
|
||||
pub enum Readable {
|
||||
@ -54,18 +131,15 @@ pub enum Readable {
|
||||
}
|
||||
|
||||
impl Readable {
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
pub fn into_bytes(self) -> Bytes {
|
||||
match self {
|
||||
Readable::Bytes(v) => v,
|
||||
Readable::Str(v) => v.as_bytes(),
|
||||
Self::Bytes(v) => v,
|
||||
Self::Str(v) => Bytes::from_string(v),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> Option<&str> {
|
||||
match self {
|
||||
Readable::Str(v) => Some(v.as_str()),
|
||||
Readable::Bytes(v) => std::str::from_utf8(v).ok(),
|
||||
}
|
||||
pub fn into_source(self) -> DataSource {
|
||||
DataSource::Bytes(self.into_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,12 +152,3 @@ cast! {
|
||||
v: Str => Self::Str(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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use ecow::EcoString;
|
||||
use typst_syntax::Spanned;
|
||||
|
||||
use crate::diag::{At, SourceResult};
|
||||
use crate::diag::{At, FileError, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{func, Cast};
|
||||
use crate::loading::Readable;
|
||||
@ -24,7 +24,6 @@ use crate::World;
|
||||
/// ```
|
||||
#[func]
|
||||
pub fn read(
|
||||
/// The engine.
|
||||
engine: &mut Engine,
|
||||
/// Path to a file.
|
||||
///
|
||||
@ -42,12 +41,9 @@ pub fn read(
|
||||
let data = engine.world.file(id).at(span)?;
|
||||
Ok(match encoding {
|
||||
None => Readable::Bytes(data),
|
||||
Some(Encoding::Utf8) => Readable::Str(
|
||||
std::str::from_utf8(&data)
|
||||
.map_err(|_| "file is not valid utf-8")
|
||||
.at(span)?
|
||||
.into(),
|
||||
),
|
||||
Some(Encoding::Utf8) => {
|
||||
Readable::Str(data.to_str().map_err(FileError::from).at(span)?)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,10 @@
|
||||
use ecow::{eco_format, EcoString};
|
||||
use typst_syntax::{is_newline, Spanned};
|
||||
|
||||
use crate::diag::{At, SourceResult};
|
||||
use crate::diag::{At, FileError, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{func, scope, Str, Value};
|
||||
use crate::loading::Readable;
|
||||
use crate::World;
|
||||
use crate::loading::{DataSource, Load, Readable};
|
||||
|
||||
/// Reads structured data from a TOML file.
|
||||
///
|
||||
@ -29,34 +28,32 @@ use crate::World;
|
||||
/// ```
|
||||
#[func(scope, title = "TOML")]
|
||||
pub fn toml(
|
||||
/// The 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).
|
||||
path: Spanned<EcoString>,
|
||||
/// For more details about paths, see the [Paths section]($syntax/#paths).
|
||||
source: Spanned<DataSource>,
|
||||
) -> SourceResult<Value> {
|
||||
let Spanned { v: path, span } = path;
|
||||
let id = span.resolve_path(&path).at(span)?;
|
||||
let data = engine.world.file(id).at(span)?;
|
||||
toml::decode(Spanned::new(Readable::Bytes(data), span))
|
||||
let data = source.load(engine.world)?;
|
||||
let raw = data.as_str().map_err(FileError::from).at(source.span)?;
|
||||
::toml::from_str(raw)
|
||||
.map_err(|err| format_toml_error(err, raw))
|
||||
.at(source.span)
|
||||
}
|
||||
|
||||
#[scope]
|
||||
impl toml {
|
||||
/// Reads structured data from a TOML string/bytes.
|
||||
///
|
||||
/// This function is deprecated. The [`toml`] function now accepts bytes
|
||||
/// directly.
|
||||
#[func(title = "Decode TOML")]
|
||||
pub fn decode(
|
||||
engine: &mut Engine,
|
||||
/// TOML data.
|
||||
data: Spanned<Readable>,
|
||||
) -> SourceResult<Value> {
|
||||
let Spanned { v: data, span } = data;
|
||||
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)
|
||||
toml(engine, data.map(Readable::into_source))
|
||||
}
|
||||
|
||||
/// Encodes structured data into a TOML string.
|
||||
|
@ -5,8 +5,7 @@ use typst_syntax::Spanned;
|
||||
use crate::diag::{format_xml_like_error, At, FileError, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{dict, func, scope, Array, Dict, IntoValue, Str, Value};
|
||||
use crate::loading::Readable;
|
||||
use crate::World;
|
||||
use crate::loading::{DataSource, Load, Readable};
|
||||
|
||||
/// Reads structured data from an XML file.
|
||||
///
|
||||
@ -58,38 +57,36 @@ use crate::World;
|
||||
/// ```
|
||||
#[func(scope, title = "XML")]
|
||||
pub fn xml(
|
||||
/// The 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).
|
||||
path: Spanned<EcoString>,
|
||||
/// For more details about paths, see the [Paths section]($syntax/#paths).
|
||||
source: Spanned<DataSource>,
|
||||
) -> SourceResult<Value> {
|
||||
let Spanned { v: path, span } = path;
|
||||
let id = span.resolve_path(&path).at(span)?;
|
||||
let data = engine.world.file(id).at(span)?;
|
||||
xml::decode(Spanned::new(Readable::Bytes(data), span))
|
||||
let data = source.load(engine.world)?;
|
||||
let text = data.as_str().map_err(FileError::from).at(source.span)?;
|
||||
let document = roxmltree::Document::parse_with_options(
|
||||
text,
|
||||
ParsingOptions { allow_dtd: true, ..Default::default() },
|
||||
)
|
||||
.map_err(format_xml_error)
|
||||
.at(source.span)?;
|
||||
Ok(convert_xml(document.root()))
|
||||
}
|
||||
|
||||
#[scope]
|
||||
impl xml {
|
||||
/// Reads structured data from an XML string/bytes.
|
||||
///
|
||||
/// This function is deprecated. The [`xml`] function now accepts bytes
|
||||
/// directly.
|
||||
#[func(title = "Decode XML")]
|
||||
pub fn decode(
|
||||
engine: &mut Engine,
|
||||
/// XML data.
|
||||
data: Spanned<Readable>,
|
||||
) -> SourceResult<Value> {
|
||||
let Spanned { v: data, span } = data;
|
||||
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()))
|
||||
xml(engine, data.map(Readable::into_source))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,10 @@
|
||||
use ecow::{eco_format, EcoString};
|
||||
use ecow::eco_format;
|
||||
use typst_syntax::Spanned;
|
||||
|
||||
use crate::diag::{At, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{func, scope, Str, Value};
|
||||
use crate::loading::Readable;
|
||||
use crate::World;
|
||||
use crate::loading::{DataSource, Load, Readable};
|
||||
|
||||
/// Reads structured data from a YAML file.
|
||||
///
|
||||
@ -41,31 +40,31 @@ use crate::World;
|
||||
/// ```
|
||||
#[func(scope, title = "YAML")]
|
||||
pub fn yaml(
|
||||
/// The 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).
|
||||
path: Spanned<EcoString>,
|
||||
/// For more details about paths, see the [Paths section]($syntax/#paths).
|
||||
source: Spanned<DataSource>,
|
||||
) -> SourceResult<Value> {
|
||||
let Spanned { v: path, span } = path;
|
||||
let id = span.resolve_path(&path).at(span)?;
|
||||
let data = engine.world.file(id).at(span)?;
|
||||
yaml::decode(Spanned::new(Readable::Bytes(data), span))
|
||||
let data = source.load(engine.world)?;
|
||||
serde_yaml::from_slice(data.as_slice())
|
||||
.map_err(|err| eco_format!("failed to parse YAML ({err})"))
|
||||
.at(source.span)
|
||||
}
|
||||
|
||||
#[scope]
|
||||
impl yaml {
|
||||
/// Reads structured data from a YAML string/bytes.
|
||||
///
|
||||
/// This function is deprecated. The [`yaml`] function now accepts bytes
|
||||
/// directly.
|
||||
#[func(title = "Decode YAML")]
|
||||
pub fn decode(
|
||||
engine: &mut Engine,
|
||||
/// YAML data.
|
||||
data: Spanned<Readable>,
|
||||
) -> SourceResult<Value> {
|
||||
let Spanned { v: data, span } = data;
|
||||
serde_yaml::from_slice(data.as_slice())
|
||||
.map_err(|err| eco_format!("failed to parse YAML ({err})"))
|
||||
.at(span)
|
||||
yaml(engine, data.map(Readable::into_source))
|
||||
}
|
||||
|
||||
/// Encode structured data into a YAML string.
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::diag::bail;
|
||||
use crate::foundations::{cast, elem, func, Content, NativeElement, Smart, Value};
|
||||
use crate::foundations::{cast, elem, func, Content, NativeElement, Value};
|
||||
use crate::layout::{Length, Rel};
|
||||
use crate::math::Mathy;
|
||||
use crate::text::TextElem;
|
||||
@ -52,7 +52,9 @@ pub struct AccentElem {
|
||||
pub accent: Accent,
|
||||
|
||||
/// The size of the accent, relative to the width of the base.
|
||||
pub size: Smart<Rel<Length>>,
|
||||
#[resolve]
|
||||
#[default(Rel::one())]
|
||||
pub size: Rel<Length>,
|
||||
}
|
||||
|
||||
/// An accent character.
|
||||
@ -101,7 +103,7 @@ macro_rules! accents {
|
||||
base: Content,
|
||||
/// The size of the accent, relative to the width of the base.
|
||||
#[named]
|
||||
size: Option<Smart<Rel<Length>>>,
|
||||
size: Option<Rel<Length>>,
|
||||
) -> Content {
|
||||
let mut accent = AccentElem::new(base, Accent::new($primary));
|
||||
if let Some(size) = size {
|
||||
@ -141,7 +143,7 @@ cast! {
|
||||
self => self.0.into_value(),
|
||||
v: char => Self::new(v),
|
||||
v: Content => match v.to_packed::<TextElem>() {
|
||||
Some(elem) => Value::Str(elem.text().clone().into()).cast()?,
|
||||
Some(elem) => Value::Str(elem.text.clone().into()).cast()?,
|
||||
None => bail!("expected text"),
|
||||
},
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::foundations::{elem, Content, Packed, Smart};
|
||||
use crate::foundations::{elem, Content, Packed};
|
||||
use crate::layout::{Length, Rel};
|
||||
use crate::math::{EquationElem, Mathy};
|
||||
|
||||
@ -47,9 +47,9 @@ impl Packed<AttachElem> {
|
||||
/// base AttachElem where possible.
|
||||
pub fn merge_base(&self) -> Option<Self> {
|
||||
// Extract from an EquationElem.
|
||||
let mut base = self.base();
|
||||
if let Some(equation) = base.to_packed::<EquationElem>() {
|
||||
base = equation.body();
|
||||
let mut base = &self.base;
|
||||
while let Some(equation) = base.to_packed::<EquationElem>() {
|
||||
base = &equation.body;
|
||||
}
|
||||
|
||||
// Move attachments from elem into base where possible.
|
||||
@ -152,5 +152,7 @@ pub struct StretchElem {
|
||||
|
||||
/// The size to stretch to, relative to the maximum size of the glyph and
|
||||
/// its attachments.
|
||||
pub size: Smart<Rel<Length>>,
|
||||
#[resolve]
|
||||
#[default(Rel::one())]
|
||||
pub size: Rel<Length>,
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ pub struct CancelElem {
|
||||
/// $ a + cancel(x, length: #200%)
|
||||
/// - cancel(x, length: #200%) $
|
||||
/// ```
|
||||
#[resolve]
|
||||
#[default(Rel::new(Ratio::one(), Abs::pt(3.0).into()))]
|
||||
pub length: Rel<Length>,
|
||||
|
||||
|
@ -135,6 +135,13 @@ pub struct EquationElem {
|
||||
#[internal]
|
||||
#[ghost]
|
||||
pub class: Option<MathClass>,
|
||||
|
||||
/// Values of `scriptPercentScaleDown` and `scriptScriptPercentScaleDown`
|
||||
/// respectively in the current font's MathConstants table.
|
||||
#[internal]
|
||||
#[default((70, 50))]
|
||||
#[ghost]
|
||||
pub script_scale: (i16, i16),
|
||||
}
|
||||
|
||||
impl Synthesize for Packed<EquationElem> {
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::foundations::{elem, func, Content, NativeElement, Smart};
|
||||
use crate::foundations::{elem, func, Content, NativeElement};
|
||||
use crate::layout::{Length, Rel};
|
||||
use crate::math::Mathy;
|
||||
use crate::text::TextElem;
|
||||
@ -10,7 +10,9 @@ use crate::text::TextElem;
|
||||
#[elem(title = "Left/Right", Mathy)]
|
||||
pub struct LrElem {
|
||||
/// The size of the brackets, relative to the height of the wrapped content.
|
||||
pub size: Smart<Rel<Length>>,
|
||||
#[resolve]
|
||||
#[default(Rel::one())]
|
||||
pub size: Rel<Length>,
|
||||
|
||||
/// The delimited content, including the delimiters.
|
||||
#[required]
|
||||
@ -44,7 +46,7 @@ pub struct MidElem {
|
||||
pub fn floor(
|
||||
/// The size of the brackets, relative to the height of the wrapped content.
|
||||
#[named]
|
||||
size: Option<Smart<Rel<Length>>>,
|
||||
size: Option<Rel<Length>>,
|
||||
/// The expression to floor.
|
||||
body: Content,
|
||||
) -> Content {
|
||||
@ -60,7 +62,7 @@ pub fn floor(
|
||||
pub fn ceil(
|
||||
/// The size of the brackets, relative to the height of the wrapped content.
|
||||
#[named]
|
||||
size: Option<Smart<Rel<Length>>>,
|
||||
size: Option<Rel<Length>>,
|
||||
/// The expression to ceil.
|
||||
body: Content,
|
||||
) -> Content {
|
||||
@ -76,7 +78,7 @@ pub fn ceil(
|
||||
pub fn round(
|
||||
/// The size of the brackets, relative to the height of the wrapped content.
|
||||
#[named]
|
||||
size: Option<Smart<Rel<Length>>>,
|
||||
size: Option<Rel<Length>>,
|
||||
/// The expression to round.
|
||||
body: Content,
|
||||
) -> Content {
|
||||
@ -92,7 +94,7 @@ pub fn round(
|
||||
pub fn abs(
|
||||
/// The size of the brackets, relative to the height of the wrapped content.
|
||||
#[named]
|
||||
size: Option<Smart<Rel<Length>>>,
|
||||
size: Option<Rel<Length>>,
|
||||
/// The expression to take the absolute value of.
|
||||
body: Content,
|
||||
) -> Content {
|
||||
@ -108,7 +110,7 @@ pub fn abs(
|
||||
pub fn norm(
|
||||
/// The size of the brackets, relative to the height of the wrapped content.
|
||||
#[named]
|
||||
size: Option<Smart<Rel<Length>>>,
|
||||
size: Option<Rel<Length>>,
|
||||
/// The expression to take the norm of.
|
||||
body: Content,
|
||||
) -> Content {
|
||||
@ -119,7 +121,7 @@ fn delimited(
|
||||
body: Content,
|
||||
left: char,
|
||||
right: char,
|
||||
size: Option<Smart<Rel<Length>>>,
|
||||
size: Option<Rel<Length>>,
|
||||
) -> Content {
|
||||
let span = body.span();
|
||||
let mut elem = LrElem::new(Content::sequence([
|
||||
|
@ -56,6 +56,7 @@ pub struct VecElem {
|
||||
/// #set math.vec(gap: 1em)
|
||||
/// $ vec(1, 2) $
|
||||
/// ```
|
||||
#[resolve]
|
||||
#[default(DEFAULT_ROW_GAP.into())]
|
||||
pub gap: Rel<Length>,
|
||||
|
||||
@ -161,6 +162,7 @@ pub struct MatElem {
|
||||
/// #set math.mat(row-gap: 1em)
|
||||
/// $ mat(1, 2; 3, 4) $
|
||||
/// ```
|
||||
#[resolve]
|
||||
#[parse(
|
||||
let gap = args.named("gap")?;
|
||||
args.named("row-gap")?.or(gap)
|
||||
@ -174,6 +176,7 @@ pub struct MatElem {
|
||||
/// #set math.mat(column-gap: 1em)
|
||||
/// $ mat(1, 2; 3, 4) $
|
||||
/// ```
|
||||
#[resolve]
|
||||
#[parse(args.named("column-gap")?.or(gap))]
|
||||
#[default(DEFAULT_COL_GAP.into())]
|
||||
pub column_gap: Rel<Length>,
|
||||
@ -256,6 +259,7 @@ pub struct CasesElem {
|
||||
/// #set math.cases(gap: 1em)
|
||||
/// $ x = cases(1, 2) $
|
||||
/// ```
|
||||
#[resolve]
|
||||
#[default(DEFAULT_ROW_GAP.into())]
|
||||
pub gap: Rel<Length>,
|
||||
|
||||
|
@ -82,8 +82,9 @@ use crate::text::TextElem;
|
||||
/// - Within them, Typst is still in "math mode". Thus, you can write math
|
||||
/// directly into them, but need to use hash syntax to pass code expressions
|
||||
/// (except for strings, which are available in the math syntax).
|
||||
/// - They support positional and named arguments, but don't support trailing
|
||||
/// content blocks and argument spreading.
|
||||
/// - They support positional and named arguments, as well as argument
|
||||
/// spreading.
|
||||
/// - They don't support trailing content blocks.
|
||||
/// - They provide additional syntax for 2-dimensional argument lists. The
|
||||
/// semicolon (`;`) merges preceding arguments separated by commas into an
|
||||
/// array argument.
|
||||
@ -92,6 +93,7 @@ use crate::text::TextElem;
|
||||
/// $ frac(a^2, 2) $
|
||||
/// $ vec(1, 2, delim: "[") $
|
||||
/// $ mat(1, 2; 3, 4) $
|
||||
/// $ mat(..#range(1, 5).chunks(2)) $
|
||||
/// $ lim_x =
|
||||
/// op("lim", limits: #true)_x $
|
||||
/// ```
|
||||
|
@ -10,7 +10,6 @@ use crate::math::Mathy;
|
||||
/// ```
|
||||
#[func(title = "Square Root")]
|
||||
pub fn sqrt(
|
||||
/// The call span of this function.
|
||||
span: Span,
|
||||
/// The expression to take the square root of.
|
||||
radicand: Content,
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::any::TypeId;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::num::NonZeroUsize;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
@ -12,26 +12,26 @@ use hayagriva::archive::ArchivedStyle;
|
||||
use hayagriva::io::BibLaTeXError;
|
||||
use hayagriva::{
|
||||
citationberg, BibliographyDriver, BibliographyRequest, CitationItem, CitationRequest,
|
||||
SpecificLocator,
|
||||
Library, SpecificLocator,
|
||||
};
|
||||
use indexmap::IndexMap;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use typed_arena::Arena;
|
||||
use typst_syntax::{Span, Spanned};
|
||||
use typst_utils::{LazyHash, NonZeroExt, PicoStr};
|
||||
use typst_utils::{ManuallyHash, NonZeroExt, PicoStr};
|
||||
|
||||
use crate::diag::{bail, error, At, FileError, HintedStrResult, SourceResult, StrResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
cast, elem, ty, Args, Array, Bytes, CastInfo, Content, FromValue, IntoValue, Label,
|
||||
NativeElement, Packed, Reflect, Repr, Scope, Show, ShowSet, Smart, Str, StyleChain,
|
||||
Styles, Synthesize, Type, Value,
|
||||
elem, Bytes, CastInfo, Content, Derived, FromValue, IntoValue, Label, NativeElement,
|
||||
OneOrMultiple, Packed, Reflect, Scope, Show, ShowSet, Smart, StyleChain, Styles,
|
||||
Synthesize, Value,
|
||||
};
|
||||
use crate::introspection::{Introspector, Locatable, Location};
|
||||
use crate::layout::{
|
||||
BlockBody, BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem,
|
||||
Sizing, TrackSizings, VElem,
|
||||
};
|
||||
use crate::loading::{DataSource, Load};
|
||||
use crate::model::{
|
||||
CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem,
|
||||
Url,
|
||||
@ -86,13 +86,20 @@ use crate::World;
|
||||
/// ```
|
||||
#[elem(Locatable, Synthesize, Show, ShowSet, LocalName)]
|
||||
pub struct BibliographyElem {
|
||||
/// Path(s) to Hayagriva `.yml` and/or BibLaTeX `.bib` files.
|
||||
/// One or multiple paths to or raw bytes for Hayagriva `.yml` and/or
|
||||
/// BibLaTeX `.bib` files.
|
||||
///
|
||||
/// This can be a:
|
||||
/// - A path string to load a bibliography file from the given path. For
|
||||
/// more details about paths, see the [Paths section]($syntax/#paths).
|
||||
/// - Raw bytes from which the bibliography should be decoded.
|
||||
/// - An array where each item is one the above.
|
||||
#[required]
|
||||
#[parse(
|
||||
let (paths, bibliography) = Bibliography::parse(engine, args)?;
|
||||
paths
|
||||
let sources = args.expect("sources")?;
|
||||
Bibliography::load(engine.world, sources)?
|
||||
)]
|
||||
pub path: BibliographyPaths,
|
||||
pub sources: Derived<OneOrMultiple<DataSource>, Bibliography>,
|
||||
|
||||
/// The title of the bibliography.
|
||||
///
|
||||
@ -116,19 +123,22 @@ pub struct BibliographyElem {
|
||||
|
||||
/// The bibliography style.
|
||||
///
|
||||
/// Should be either one of the built-in styles (see below) or a path to
|
||||
/// a [CSL file](https://citationstyles.org/). Some of the styles listed
|
||||
/// below appear twice, once with their full name and once with a short
|
||||
/// alias.
|
||||
#[parse(CslStyle::parse(engine, args)?)]
|
||||
#[default(CslStyle::from_name("ieee").unwrap())]
|
||||
pub style: CslStyle,
|
||||
|
||||
/// The loaded bibliography.
|
||||
#[internal]
|
||||
#[required]
|
||||
#[parse(bibliography)]
|
||||
pub bibliography: Bibliography,
|
||||
/// This can be:
|
||||
/// - A string with the name of one of the built-in styles (see below). Some
|
||||
/// of the styles listed below appear twice, once with their full name and
|
||||
/// once with a short alias.
|
||||
/// - A path string to a [CSL file](https://citationstyles.org/). For more
|
||||
/// details about paths, see the [Paths section]($syntax/#paths).
|
||||
/// - Raw bytes from which a CSL style should be decoded.
|
||||
#[parse(match args.named::<Spanned<CslSource>>("style")? {
|
||||
Some(source) => Some(CslStyle::load(engine.world, source)?),
|
||||
None => None,
|
||||
})]
|
||||
#[default({
|
||||
let default = ArchivedStyle::InstituteOfElectricalAndElectronicsEngineers;
|
||||
Derived::new(CslSource::Named(default), CslStyle::from_archived(default))
|
||||
})]
|
||||
pub style: Derived<CslSource, CslStyle>,
|
||||
|
||||
/// The language setting where the bibliography is.
|
||||
#[internal]
|
||||
@ -141,17 +151,6 @@ pub struct BibliographyElem {
|
||||
pub region: Option<Region>,
|
||||
}
|
||||
|
||||
/// A list of bibliography file paths.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct BibliographyPaths(Vec<EcoString>);
|
||||
|
||||
cast! {
|
||||
BibliographyPaths,
|
||||
self => self.0.into_value(),
|
||||
v: EcoString => Self(vec![v]),
|
||||
v: Array => Self(v.into_iter().map(Value::cast).collect::<HintedStrResult<_>>()?),
|
||||
}
|
||||
|
||||
impl BibliographyElem {
|
||||
/// Find the document's bibliography.
|
||||
pub fn find(introspector: Tracked<Introspector>) -> StrResult<Packed<Self>> {
|
||||
@ -169,13 +168,12 @@ impl BibliographyElem {
|
||||
}
|
||||
|
||||
/// Whether the bibliography contains the given key.
|
||||
pub fn has(engine: &Engine, key: impl Into<PicoStr>) -> bool {
|
||||
let key = key.into();
|
||||
pub fn has(engine: &Engine, key: Label) -> bool {
|
||||
engine
|
||||
.introspector
|
||||
.query(&Self::elem().select())
|
||||
.iter()
|
||||
.any(|elem| elem.to_packed::<Self>().unwrap().bibliography().has(key))
|
||||
.any(|elem| elem.to_packed::<Self>().unwrap().sources.derived.has(key))
|
||||
}
|
||||
|
||||
/// Find all bibliography keys.
|
||||
@ -183,9 +181,9 @@ impl BibliographyElem {
|
||||
let mut vec = vec![];
|
||||
for elem in introspector.query(&Self::elem().select()).iter() {
|
||||
let this = elem.to_packed::<Self>().unwrap();
|
||||
for (key, entry) in this.bibliography().iter() {
|
||||
for (key, entry) in this.sources.derived.iter() {
|
||||
let detail = entry.title().map(|title| title.value.to_str().into());
|
||||
vec.push((Label::new(key), detail))
|
||||
vec.push((key, detail))
|
||||
}
|
||||
}
|
||||
vec
|
||||
@ -282,63 +280,35 @@ impl LocalName for Packed<BibliographyElem> {
|
||||
}
|
||||
|
||||
/// A loaded bibliography.
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct Bibliography {
|
||||
map: Arc<IndexMap<PicoStr, hayagriva::Entry>>,
|
||||
hash: u128,
|
||||
}
|
||||
#[derive(Clone, PartialEq, Hash)]
|
||||
pub struct Bibliography(Arc<ManuallyHash<IndexMap<Label, hayagriva::Entry>>>);
|
||||
|
||||
impl Bibliography {
|
||||
/// Parse the bibliography argument.
|
||||
fn parse(
|
||||
engine: &mut Engine,
|
||||
args: &mut Args,
|
||||
) -> SourceResult<(BibliographyPaths, Bibliography)> {
|
||||
let Spanned { v: paths, span } =
|
||||
args.expect::<Spanned<BibliographyPaths>>("path to bibliography file")?;
|
||||
|
||||
// Load bibliography files.
|
||||
let data = paths
|
||||
.0
|
||||
.iter()
|
||||
.map(|path| {
|
||||
let id = span.resolve_path(path).at(span)?;
|
||||
engine.world.file(id).at(span)
|
||||
})
|
||||
.collect::<SourceResult<Vec<Bytes>>>()?;
|
||||
|
||||
// Parse.
|
||||
let bibliography = Self::load(&paths, &data).at(span)?;
|
||||
|
||||
Ok((paths, bibliography))
|
||||
/// Load a bibliography from data sources.
|
||||
fn load(
|
||||
world: Tracked<dyn World + '_>,
|
||||
sources: Spanned<OneOrMultiple<DataSource>>,
|
||||
) -> SourceResult<Derived<OneOrMultiple<DataSource>, Self>> {
|
||||
let data = sources.load(world)?;
|
||||
let bibliography = Self::decode(&sources.v, &data).at(sources.span)?;
|
||||
Ok(Derived::new(sources.v, bibliography))
|
||||
}
|
||||
|
||||
/// Load bibliography entries from paths.
|
||||
/// Decode a bibliography from loaded data sources.
|
||||
#[comemo::memoize]
|
||||
#[typst_macros::time(name = "load bibliography")]
|
||||
fn load(paths: &BibliographyPaths, data: &[Bytes]) -> StrResult<Bibliography> {
|
||||
fn decode(
|
||||
sources: &OneOrMultiple<DataSource>,
|
||||
data: &[Bytes],
|
||||
) -> StrResult<Bibliography> {
|
||||
let mut map = IndexMap::new();
|
||||
let mut duplicates = Vec::<EcoString>::new();
|
||||
|
||||
// We might have multiple bib/yaml files
|
||||
for (path, bytes) in paths.0.iter().zip(data) {
|
||||
let src = std::str::from_utf8(bytes).map_err(FileError::from)?;
|
||||
|
||||
let ext = Path::new(path.as_str())
|
||||
.extension()
|
||||
.and_then(OsStr::to_str)
|
||||
.unwrap_or_default();
|
||||
|
||||
let library = match ext.to_lowercase().as_str() {
|
||||
"yml" | "yaml" => hayagriva::io::from_yaml_str(src)
|
||||
.map_err(|err| eco_format!("failed to parse YAML ({err})"))?,
|
||||
"bib" => hayagriva::io::from_biblatex_str(src)
|
||||
.map_err(|errors| format_biblatex_error(path, src, errors))?,
|
||||
_ => bail!("unknown bibliography format (must be .yml/.yaml or .bib)"),
|
||||
};
|
||||
|
||||
for (source, data) in sources.0.iter().zip(data) {
|
||||
let library = decode_library(source, data)?;
|
||||
for entry in library {
|
||||
match map.entry(PicoStr::intern(entry.key())) {
|
||||
match map.entry(Label::new(PicoStr::intern(entry.key()))) {
|
||||
indexmap::map::Entry::Vacant(vacant) => {
|
||||
vacant.insert(entry);
|
||||
}
|
||||
@ -353,182 +323,210 @@ impl Bibliography {
|
||||
bail!("duplicate bibliography keys: {}", duplicates.join(", "));
|
||||
}
|
||||
|
||||
Ok(Bibliography {
|
||||
map: Arc::new(map),
|
||||
hash: typst_utils::hash128(data),
|
||||
})
|
||||
Ok(Bibliography(Arc::new(ManuallyHash::new(map, typst_utils::hash128(data)))))
|
||||
}
|
||||
|
||||
fn has(&self, key: impl Into<PicoStr>) -> bool {
|
||||
self.map.contains_key(&key.into())
|
||||
fn has(&self, key: Label) -> bool {
|
||||
self.0.contains_key(&key)
|
||||
}
|
||||
|
||||
fn iter(&self) -> impl Iterator<Item = (PicoStr, &hayagriva::Entry)> {
|
||||
self.map.iter().map(|(&k, v)| (k, v))
|
||||
fn get(&self, key: Label) -> Option<&hayagriva::Entry> {
|
||||
self.0.get(&key)
|
||||
}
|
||||
|
||||
fn iter(&self) -> impl Iterator<Item = (Label, &hayagriva::Entry)> {
|
||||
self.0.iter().map(|(&k, v)| (k, v))
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Bibliography {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.debug_set().entries(self.map.keys()).finish()
|
||||
f.debug_set().entries(self.0.keys()).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Bibliography {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.hash.hash(state);
|
||||
/// Decode on library from one data source.
|
||||
fn decode_library(source: &DataSource, data: &Bytes) -> StrResult<Library> {
|
||||
let src = data.as_str().map_err(FileError::from)?;
|
||||
|
||||
if let DataSource::Path(path) = source {
|
||||
// If we got a path, use the extension to determine whether it is
|
||||
// YAML or BibLaTeX.
|
||||
let ext = Path::new(path.as_str())
|
||||
.extension()
|
||||
.and_then(OsStr::to_str)
|
||||
.unwrap_or_default();
|
||||
|
||||
match ext.to_lowercase().as_str() {
|
||||
"yml" | "yaml" => hayagriva::io::from_yaml_str(src)
|
||||
.map_err(|err| eco_format!("failed to parse YAML ({err})")),
|
||||
"bib" => hayagriva::io::from_biblatex_str(src)
|
||||
.map_err(|errors| format_biblatex_error(src, Some(path), errors)),
|
||||
_ => bail!("unknown bibliography format (must be .yml/.yaml or .bib)"),
|
||||
}
|
||||
} else {
|
||||
// If we just got bytes, we need to guess. If it can be decoded as
|
||||
// hayagriva YAML, we'll use that.
|
||||
let haya_err = match hayagriva::io::from_yaml_str(src) {
|
||||
Ok(library) => return Ok(library),
|
||||
Err(err) => err,
|
||||
};
|
||||
|
||||
// If it can be decoded as BibLaTeX, we use that isntead.
|
||||
let bib_errs = match hayagriva::io::from_biblatex_str(src) {
|
||||
Ok(library) => return Ok(library),
|
||||
Err(err) => err,
|
||||
};
|
||||
|
||||
// If neither decoded correctly, check whether `:` or `{` appears
|
||||
// more often to guess whether it's more likely to be YAML or BibLaTeX
|
||||
// and emit the more appropriate error.
|
||||
let mut yaml = 0;
|
||||
let mut biblatex = 0;
|
||||
for c in src.chars() {
|
||||
match c {
|
||||
':' => yaml += 1,
|
||||
'{' => biblatex += 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if yaml > biblatex {
|
||||
bail!("failed to parse YAML ({haya_err})")
|
||||
} else {
|
||||
Err(format_biblatex_error(src, None, bib_errs))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Format a BibLaTeX loading error.
|
||||
fn format_biblatex_error(path: &str, src: &str, errors: Vec<BibLaTeXError>) -> EcoString {
|
||||
fn format_biblatex_error(
|
||||
src: &str,
|
||||
path: Option<&str>,
|
||||
errors: Vec<BibLaTeXError>,
|
||||
) -> EcoString {
|
||||
let Some(error) = errors.first() else {
|
||||
return eco_format!("failed to parse BibLaTeX file ({path})");
|
||||
return match path {
|
||||
Some(path) => eco_format!("failed to parse BibLaTeX file ({path})"),
|
||||
None => eco_format!("failed to parse BibLaTeX"),
|
||||
};
|
||||
};
|
||||
|
||||
let (span, msg) = match error {
|
||||
BibLaTeXError::Parse(error) => (&error.span, error.kind.to_string()),
|
||||
BibLaTeXError::Type(error) => (&error.span, error.kind.to_string()),
|
||||
};
|
||||
|
||||
let line = src.get(..span.start).unwrap_or_default().lines().count();
|
||||
eco_format!("failed to parse BibLaTeX file ({path}:{line}: {msg})")
|
||||
match path {
|
||||
Some(path) => eco_format!("failed to parse BibLaTeX file ({path}:{line}: {msg})"),
|
||||
None => eco_format!("failed to parse BibLaTeX ({line}: {msg})"),
|
||||
}
|
||||
}
|
||||
|
||||
/// A loaded CSL style.
|
||||
#[ty(cast)]
|
||||
#[derive(Debug, Clone, PartialEq, Hash)]
|
||||
pub struct CslStyle {
|
||||
name: Option<EcoString>,
|
||||
style: Arc<LazyHash<citationberg::IndependentStyle>>,
|
||||
}
|
||||
pub struct CslStyle(Arc<ManuallyHash<citationberg::IndependentStyle>>);
|
||||
|
||||
impl CslStyle {
|
||||
/// Parse the style argument.
|
||||
pub fn parse(engine: &mut Engine, args: &mut Args) -> SourceResult<Option<CslStyle>> {
|
||||
let Some(Spanned { v: string, span }) =
|
||||
args.named::<Spanned<EcoString>>("style")?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
Ok(Some(Self::parse_impl(engine, &string, span).at(span)?))
|
||||
}
|
||||
|
||||
/// Parse the style argument with `Smart`.
|
||||
pub fn parse_smart(
|
||||
engine: &mut Engine,
|
||||
args: &mut Args,
|
||||
) -> SourceResult<Option<Smart<CslStyle>>> {
|
||||
let Some(Spanned { v: smart, span }) =
|
||||
args.named::<Spanned<Smart<EcoString>>>("style")?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
Ok(Some(match smart {
|
||||
Smart::Auto => Smart::Auto,
|
||||
Smart::Custom(string) => {
|
||||
Smart::Custom(Self::parse_impl(engine, &string, span).at(span)?)
|
||||
/// Load a CSL style from a data source.
|
||||
pub fn load(
|
||||
world: Tracked<dyn World + '_>,
|
||||
Spanned { v: source, span }: Spanned<CslSource>,
|
||||
) -> SourceResult<Derived<CslSource, Self>> {
|
||||
let style = match &source {
|
||||
CslSource::Named(style) => Self::from_archived(*style),
|
||||
CslSource::Normal(source) => {
|
||||
let data = Spanned::new(source, span).load(world)?;
|
||||
Self::from_data(data).at(span)?
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/// Parse internally.
|
||||
fn parse_impl(engine: &mut Engine, string: &str, span: Span) -> StrResult<CslStyle> {
|
||||
let ext = Path::new(string)
|
||||
.extension()
|
||||
.and_then(OsStr::to_str)
|
||||
.unwrap_or_default()
|
||||
.to_lowercase();
|
||||
|
||||
if ext == "csl" {
|
||||
let id = span.resolve_path(string)?;
|
||||
let data = engine.world.file(id)?;
|
||||
CslStyle::from_data(&data)
|
||||
} else {
|
||||
CslStyle::from_name(string)
|
||||
}
|
||||
};
|
||||
Ok(Derived::new(source, style))
|
||||
}
|
||||
|
||||
/// Load a built-in CSL style.
|
||||
#[comemo::memoize]
|
||||
pub fn from_name(name: &str) -> StrResult<CslStyle> {
|
||||
match hayagriva::archive::ArchivedStyle::by_name(name).map(ArchivedStyle::get) {
|
||||
Some(citationberg::Style::Independent(style)) => Ok(Self {
|
||||
name: Some(name.into()),
|
||||
style: Arc::new(LazyHash::new(style)),
|
||||
}),
|
||||
_ => bail!("unknown style: `{name}`"),
|
||||
pub fn from_archived(archived: ArchivedStyle) -> CslStyle {
|
||||
match archived.get() {
|
||||
citationberg::Style::Independent(style) => Self(Arc::new(ManuallyHash::new(
|
||||
style,
|
||||
typst_utils::hash128(&(TypeId::of::<ArchivedStyle>(), archived)),
|
||||
))),
|
||||
// Ensured by `test_bibliography_load_builtin_styles`.
|
||||
_ => unreachable!("archive should not contain dependant styles"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a CSL style from file contents.
|
||||
#[comemo::memoize]
|
||||
pub fn from_data(data: &Bytes) -> StrResult<CslStyle> {
|
||||
let text = std::str::from_utf8(data.as_slice()).map_err(FileError::from)?;
|
||||
pub fn from_data(data: Bytes) -> StrResult<CslStyle> {
|
||||
let text = data.as_str().map_err(FileError::from)?;
|
||||
citationberg::IndependentStyle::from_xml(text)
|
||||
.map(|style| Self { name: None, style: Arc::new(LazyHash::new(style)) })
|
||||
.map(|style| {
|
||||
Self(Arc::new(ManuallyHash::new(
|
||||
style,
|
||||
typst_utils::hash128(&(TypeId::of::<Bytes>(), data)),
|
||||
)))
|
||||
})
|
||||
.map_err(|err| eco_format!("failed to load CSL style ({err})"))
|
||||
}
|
||||
|
||||
/// Get the underlying independent style.
|
||||
pub fn get(&self) -> &citationberg::IndependentStyle {
|
||||
self.style.as_ref()
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
// This Reflect impl is technically a bit wrong because it doesn't say what
|
||||
// FromValue and IntoValue really do. Instead, it says what the `style` argument
|
||||
// on `bibliography` and `cite` expect (through manual parsing).
|
||||
impl Reflect for CslStyle {
|
||||
/// Source for a CSL style.
|
||||
#[derive(Debug, Clone, PartialEq, Hash)]
|
||||
pub enum CslSource {
|
||||
/// A predefined named style.
|
||||
Named(ArchivedStyle),
|
||||
/// A normal data source.
|
||||
Normal(DataSource),
|
||||
}
|
||||
|
||||
impl Reflect for CslSource {
|
||||
#[comemo::memoize]
|
||||
fn input() -> CastInfo {
|
||||
let ty = std::iter::once(CastInfo::Type(Type::of::<Str>()));
|
||||
let options = hayagriva::archive::ArchivedStyle::all().iter().map(|name| {
|
||||
let source = std::iter::once(DataSource::input());
|
||||
let names = ArchivedStyle::all().iter().map(|name| {
|
||||
CastInfo::Value(name.names()[0].into_value(), name.display_name())
|
||||
});
|
||||
CastInfo::Union(ty.chain(options).collect())
|
||||
CastInfo::Union(source.into_iter().chain(names).collect())
|
||||
}
|
||||
|
||||
fn output() -> CastInfo {
|
||||
EcoString::output()
|
||||
DataSource::output()
|
||||
}
|
||||
|
||||
fn castable(value: &Value) -> bool {
|
||||
if let Value::Dyn(dynamic) = &value {
|
||||
if dynamic.is::<Self>() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
DataSource::castable(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for CslStyle {
|
||||
impl FromValue for CslSource {
|
||||
fn from_value(value: Value) -> HintedStrResult<Self> {
|
||||
if let Value::Dyn(dynamic) = &value {
|
||||
if let Some(concrete) = dynamic.downcast::<Self>() {
|
||||
return Ok(concrete.clone());
|
||||
if EcoString::castable(&value) {
|
||||
let string = EcoString::from_value(value.clone())?;
|
||||
if Path::new(string.as_str()).extension().is_none() {
|
||||
let style = ArchivedStyle::by_name(&string)
|
||||
.ok_or_else(|| eco_format!("unknown style: {}", string))?;
|
||||
return Ok(CslSource::Named(style));
|
||||
}
|
||||
}
|
||||
|
||||
Err(<Self as Reflect>::error(&value))
|
||||
DataSource::from_value(value).map(CslSource::Normal)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue for CslStyle {
|
||||
impl IntoValue for CslSource {
|
||||
fn into_value(self) -> Value {
|
||||
Value::dynamic(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Repr for CslStyle {
|
||||
fn repr(&self) -> EcoString {
|
||||
self.name
|
||||
.as_ref()
|
||||
.map(|name| name.repr())
|
||||
.unwrap_or_else(|| "..".into())
|
||||
match self {
|
||||
// We prefer the shorter names which are at the back of the array.
|
||||
Self::Named(v) => v.names().last().unwrap().into_value(),
|
||||
Self::Normal(v) => v.into_value(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -632,16 +630,15 @@ impl<'a> Generator<'a> {
|
||||
static LOCALES: LazyLock<Vec<citationberg::Locale>> =
|
||||
LazyLock::new(hayagriva::archive::locales);
|
||||
|
||||
let database = self.bibliography.bibliography();
|
||||
let bibliography_style = self.bibliography.style(StyleChain::default());
|
||||
let styles = Arena::new();
|
||||
let database = &self.bibliography.sources.derived;
|
||||
let bibliography_style = &self.bibliography.style(StyleChain::default()).derived;
|
||||
|
||||
// Process all citation groups.
|
||||
let mut driver = BibliographyDriver::new();
|
||||
for elem in &self.groups {
|
||||
let group = elem.to_packed::<CiteGroup>().unwrap();
|
||||
let location = elem.location().unwrap();
|
||||
let children = group.children();
|
||||
let children = &group.children;
|
||||
|
||||
// Groups should never be empty.
|
||||
let Some(first) = children.first() else { continue };
|
||||
@ -653,12 +650,11 @@ impl<'a> Generator<'a> {
|
||||
|
||||
// Create infos and items for each child in the group.
|
||||
for child in children {
|
||||
let key = *child.key();
|
||||
let Some(entry) = database.map.get(&key.into_inner()) else {
|
||||
let Some(entry) = database.get(child.key) else {
|
||||
errors.push(error!(
|
||||
child.span(),
|
||||
"key `{}` does not exist in the bibliography",
|
||||
key.resolve()
|
||||
child.key.resolve()
|
||||
));
|
||||
continue;
|
||||
};
|
||||
@ -685,7 +681,7 @@ impl<'a> Generator<'a> {
|
||||
};
|
||||
|
||||
normal &= special_form.is_none();
|
||||
subinfos.push(CiteInfo { key, supplement, hidden });
|
||||
subinfos.push(CiteInfo { key: child.key, supplement, hidden });
|
||||
items.push(CitationItem::new(entry, locator, None, hidden, special_form));
|
||||
}
|
||||
|
||||
@ -695,8 +691,8 @@ impl<'a> Generator<'a> {
|
||||
}
|
||||
|
||||
let style = match first.style(StyleChain::default()) {
|
||||
Smart::Auto => &bibliography_style.style,
|
||||
Smart::Custom(style) => styles.alloc(style.style),
|
||||
Smart::Auto => bibliography_style.get(),
|
||||
Smart::Custom(style) => style.derived.get(),
|
||||
};
|
||||
|
||||
self.infos.push(GroupInfo {
|
||||
@ -727,7 +723,7 @@ impl<'a> Generator<'a> {
|
||||
// Add hidden items for everything if we should print the whole
|
||||
// bibliography.
|
||||
if self.bibliography.full(StyleChain::default()) {
|
||||
for entry in database.map.values() {
|
||||
for (_, entry) in database.iter() {
|
||||
driver.citation(CitationRequest::new(
|
||||
vec![CitationItem::new(entry, None, None, true, None)],
|
||||
bibliography_style.get(),
|
||||
@ -1097,3 +1093,15 @@ fn locale(lang: Lang, region: Option<Region>) -> citationberg::LocaleCode {
|
||||
}
|
||||
citationberg::LocaleCode(value)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_bibliography_load_builtin_styles() {
|
||||
for &archived in ArchivedStyle::all() {
|
||||
let _ = CslStyle::from_archived(archived);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,14 @@
|
||||
use typst_syntax::Spanned;
|
||||
|
||||
use crate::diag::{error, At, HintedString, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
cast, elem, Cast, Content, Label, Packed, Show, Smart, StyleChain, Synthesize,
|
||||
cast, elem, Cast, Content, Derived, Label, Packed, Show, Smart, StyleChain,
|
||||
Synthesize,
|
||||
};
|
||||
use crate::introspection::Locatable;
|
||||
use crate::model::bibliography::Works;
|
||||
use crate::model::CslStyle;
|
||||
use crate::model::{CslSource, CslStyle};
|
||||
use crate::text::{Lang, Region, TextElem};
|
||||
|
||||
/// Cite a work from the bibliography.
|
||||
@ -87,15 +90,24 @@ pub struct CiteElem {
|
||||
|
||||
/// The citation style.
|
||||
///
|
||||
/// Should be either `{auto}`, one of the built-in styles (see below) or a
|
||||
/// path to a [CSL file](https://citationstyles.org/). Some of the styles
|
||||
/// listed below appear twice, once with their full name and once with a
|
||||
/// short alias.
|
||||
///
|
||||
/// When set to `{auto}`, automatically use the
|
||||
/// [bibliography's style]($bibliography.style) for the citations.
|
||||
#[parse(CslStyle::parse_smart(engine, args)?)]
|
||||
pub style: Smart<CslStyle>,
|
||||
/// This can be:
|
||||
/// - `{auto}` to automatically use the
|
||||
/// [bibliography's style]($bibliography.style) for citations.
|
||||
/// - A string with the name of one of the built-in styles (see below). Some
|
||||
/// of the styles listed below appear twice, once with their full name and
|
||||
/// once with a short alias.
|
||||
/// - A path string to a [CSL file](https://citationstyles.org/). For more
|
||||
/// details about paths, see the [Paths section]($syntax/#paths).
|
||||
/// - Raw bytes from which a CSL style should be decoded.
|
||||
#[parse(match args.named::<Spanned<Smart<CslSource>>>("style")? {
|
||||
Some(Spanned { v: Smart::Custom(source), span }) => Some(Smart::Custom(
|
||||
CslStyle::load(engine.world, Spanned::new(source, span))?
|
||||
)),
|
||||
Some(Spanned { v: Smart::Auto, .. }) => Some(Smart::Auto),
|
||||
None => None,
|
||||
})]
|
||||
#[borrowed]
|
||||
pub style: Smart<Derived<CslSource, CslStyle>>,
|
||||
|
||||
/// The text language setting where the citation is.
|
||||
#[internal]
|
||||
|
@ -3,8 +3,8 @@ use ecow::EcoString;
|
||||
use crate::diag::{bail, HintedStrResult, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
cast, elem, Args, Array, Construct, Content, Datetime, Fields, Smart, StyleChain,
|
||||
Styles, Value,
|
||||
cast, elem, Args, Array, Construct, Content, Datetime, Fields, OneOrMultiple, Smart,
|
||||
StyleChain, Styles, Value,
|
||||
};
|
||||
|
||||
/// The root element of a document and its metadata.
|
||||
@ -35,7 +35,7 @@ pub struct DocumentElem {
|
||||
|
||||
/// The document's authors.
|
||||
#[ghost]
|
||||
pub author: Author,
|
||||
pub author: OneOrMultiple<EcoString>,
|
||||
|
||||
/// The document's description.
|
||||
#[ghost]
|
||||
@ -43,7 +43,7 @@ pub struct DocumentElem {
|
||||
|
||||
/// The document's keywords.
|
||||
#[ghost]
|
||||
pub keywords: Keywords,
|
||||
pub keywords: OneOrMultiple<EcoString>,
|
||||
|
||||
/// The document's creation date.
|
||||
///
|
||||
@ -93,7 +93,7 @@ cast! {
|
||||
pub struct DocumentInfo {
|
||||
/// The document's title.
|
||||
pub title: Option<EcoString>,
|
||||
/// The document's author.
|
||||
/// The document's author(s).
|
||||
pub author: Vec<EcoString>,
|
||||
/// The document's description.
|
||||
pub description: Option<EcoString>,
|
||||
|
@ -257,7 +257,7 @@ impl Synthesize for Packed<FigureElem> {
|
||||
|
||||
// Determine the figure's kind.
|
||||
let kind = elem.kind(styles).unwrap_or_else(|| {
|
||||
elem.body()
|
||||
elem.body
|
||||
.query_first(&Selector::can::<dyn Figurable>())
|
||||
.map(|elem| FigureKind::Elem(elem.func()))
|
||||
.unwrap_or_else(|| FigureKind::Elem(ImageElem::elem()))
|
||||
@ -288,14 +288,13 @@ impl Synthesize for Packed<FigureElem> {
|
||||
// Resolve the supplement with the first descendant of the kind or
|
||||
// just the body, if none was found.
|
||||
let descendant = match kind {
|
||||
FigureKind::Elem(func) => elem
|
||||
.body()
|
||||
.query_first(&Selector::Elem(func, None))
|
||||
.map(Cow::Owned),
|
||||
FigureKind::Elem(func) => {
|
||||
elem.body.query_first(&Selector::Elem(func, None)).map(Cow::Owned)
|
||||
}
|
||||
FigureKind::Name(_) => None,
|
||||
};
|
||||
|
||||
let target = descendant.unwrap_or_else(|| Cow::Borrowed(elem.body()));
|
||||
let target = descendant.unwrap_or_else(|| Cow::Borrowed(&elem.body));
|
||||
Some(supplement.resolve(engine, styles, [target])?)
|
||||
}
|
||||
};
|
||||
@ -437,7 +436,7 @@ impl Outlinable for Packed<FigureElem> {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut realized = caption.body().clone();
|
||||
let mut realized = caption.body.clone();
|
||||
if let (
|
||||
Smart::Custom(Some(Supplement::Content(mut supplement))),
|
||||
Some(Some(counter)),
|
||||
@ -460,7 +459,7 @@ impl Outlinable for Packed<FigureElem> {
|
||||
|
||||
let separator = caption.get_separator(StyleChain::default());
|
||||
|
||||
realized = supplement + numbers + separator + caption.body();
|
||||
realized = supplement + numbers + separator + caption.body.clone();
|
||||
}
|
||||
|
||||
Ok(Some(realized))
|
||||
@ -604,7 +603,7 @@ impl Synthesize for Packed<FigureCaption> {
|
||||
impl Show for Packed<FigureCaption> {
|
||||
#[typst_macros::time(name = "figure.caption", span = self.span())]
|
||||
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||
let mut realized = self.body().clone();
|
||||
let mut realized = self.body.clone();
|
||||
|
||||
if let (
|
||||
Some(Some(mut supplement)),
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user