mirror of
https://github.com/typst/typst
synced 2025-08-10 13:17:55 +08:00
Compare commits
50 Commits
1cf412312c
...
574952f578
Author | SHA1 | Date | |
---|---|---|---|
|
574952f578 | ||
|
cd2a61c354 | ||
|
9362c0e63e | ||
|
3862102398 | ||
|
283fcbb71c | ||
|
477e1b22b3 | ||
|
9665eecdb6 | ||
|
85d1778974 | ||
|
176b070c77 | ||
|
26e65bfef5 | ||
|
467968af07 | ||
|
cd044825fc | ||
|
6fe1e20afb | ||
|
7838da02ec | ||
|
fecdc39846 | ||
|
c47b71b435 | ||
|
0b8b7d0f23 | ||
|
2d33393df9 | ||
|
b7546bace7 | ||
|
ce299d5832 | ||
|
58dbbd48fe | ||
|
f7bd03dd76 | ||
|
b3fb6c2326 | ||
|
e61cd6fb9e | ||
|
dda486a412 | ||
|
52ee33a275 | ||
|
1bd8ff0e0f | ||
|
6fcc432284 | ||
|
b45f574703 | ||
|
b90ad470d6 | ||
|
c22c47b9c9 | ||
|
63c4720ed2 | ||
|
a4ac4e6562 | ||
|
6b9b78596a | ||
|
9473aface1 | ||
|
be6629c7cb | ||
|
e2b37fef33 | ||
|
dacd6acd5e | ||
|
0a374d2380 | ||
|
265df6c29f | ||
|
e09b55f00f | ||
|
5c876535cc | ||
|
ce7f680fd5 | ||
|
cb8d862a55 | ||
|
e8bbf3794f | ||
|
ec1e8f9e8d | ||
|
3a1503154f | ||
|
36508c66db | ||
|
39eeb116a4 | ||
|
a2f685483a |
11
Cargo.lock
generated
11
Cargo.lock
generated
@ -3094,6 +3094,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3419,6 +3420,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)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -7,12 +7,11 @@ use typst_library::diag::{
|
||||
use typst_library::engine::{Engine, Sink, Traced};
|
||||
use typst_library::foundations::{
|
||||
Arg, Args, Bytes, Capturer, Closure, Content, Context, Func, IntoValue,
|
||||
NativeElement, Scope, Scopes, Value,
|
||||
NativeElement, Scope, Scopes, SymbolElem, Value,
|
||||
};
|
||||
use typst_library::introspection::Introspector;
|
||||
use typst_library::math::LrElem;
|
||||
use typst_library::routines::Routines;
|
||||
use typst_library::text::TextElem;
|
||||
use typst_library::World;
|
||||
use typst_syntax::ast::{self, AstNode, Ident};
|
||||
use typst_syntax::{Span, Spanned, SyntaxNode};
|
||||
@ -325,6 +324,13 @@ fn eval_field_call(
|
||||
} else if let Some(callee) = target.ty().scope().get(&field) {
|
||||
args.insert(0, target_expr.span(), target);
|
||||
Ok(FieldCall::Normal(callee.clone(), args))
|
||||
} else if let Value::Content(content) = &target {
|
||||
if let Some(callee) = content.elem().scope().get(&field) {
|
||||
args.insert(0, target_expr.span(), target);
|
||||
Ok(FieldCall::Normal(callee.clone(), args))
|
||||
} else {
|
||||
bail!(missing_field_call_error(target, field))
|
||||
}
|
||||
} else if matches!(
|
||||
target,
|
||||
Value::Symbol(_) | Value::Func(_) | Value::Type(_) | Value::Module(_)
|
||||
@ -341,8 +347,20 @@ fn eval_field_call(
|
||||
|
||||
/// Produce an error when we cannot call the field.
|
||||
fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic {
|
||||
let mut error =
|
||||
error!(field.span(), "type {} has no method `{}`", target.ty(), field.as_str());
|
||||
let mut error = match &target {
|
||||
Value::Content(content) => error!(
|
||||
field.span(),
|
||||
"element {} has no method `{}`",
|
||||
content.elem().name(),
|
||||
field.as_str(),
|
||||
),
|
||||
_ => error!(
|
||||
field.span(),
|
||||
"type {} has no method `{}`",
|
||||
target.ty(),
|
||||
field.as_str()
|
||||
),
|
||||
};
|
||||
|
||||
match target {
|
||||
Value::Dict(ref dict) if matches!(dict.get(&field), Ok(Value::Func(_))) => {
|
||||
@ -360,6 +378,7 @@ fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
error
|
||||
}
|
||||
|
||||
@ -382,16 +401,16 @@ fn wrap_args_in_math(
|
||||
let mut body = Content::empty();
|
||||
for (i, arg) in args.all::<Content>()?.into_iter().enumerate() {
|
||||
if i > 0 {
|
||||
body += TextElem::packed(',');
|
||||
body += SymbolElem::packed(',');
|
||||
}
|
||||
body += arg;
|
||||
}
|
||||
if trailing_comma {
|
||||
body += TextElem::packed(',');
|
||||
body += SymbolElem::packed(',');
|
||||
}
|
||||
Ok(Value::Content(
|
||||
callee.display().spanned(callee_span)
|
||||
+ LrElem::new(TextElem::packed('(') + body + TextElem::packed(')'))
|
||||
+ LrElem::new(SymbolElem::packed('(') + body + SymbolElem::packed(')'))
|
||||
.pack()
|
||||
.spanned(args.span),
|
||||
))
|
||||
@ -685,8 +704,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"]);
|
||||
|
@ -99,6 +99,7 @@ impl Eval for ast::Expr<'_> {
|
||||
Self::Term(v) => v.eval(vm).map(Value::Content),
|
||||
Self::Equation(v) => v.eval(vm).map(Value::Content),
|
||||
Self::Math(v) => v.eval(vm).map(Value::Content),
|
||||
Self::MathText(v) => v.eval(vm).map(Value::Content),
|
||||
Self::MathIdent(v) => v.eval(vm),
|
||||
Self::MathShorthand(v) => v.eval(vm),
|
||||
Self::MathAlignPoint(v) => v.eval(vm).map(Value::Content),
|
||||
|
@ -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)?;
|
||||
|
@ -1,11 +1,11 @@
|
||||
use ecow::eco_format;
|
||||
use typst_library::diag::{At, SourceResult};
|
||||
use typst_library::foundations::{Content, NativeElement, Symbol, Value};
|
||||
use typst_library::foundations::{Content, NativeElement, Symbol, SymbolElem, Value};
|
||||
use typst_library::math::{
|
||||
AlignPointElem, AttachElem, FracElem, LrElem, PrimesElem, RootElem,
|
||||
};
|
||||
use typst_library::text::TextElem;
|
||||
use typst_syntax::ast::{self, AstNode};
|
||||
use typst_syntax::ast::{self, AstNode, MathTextKind};
|
||||
|
||||
use crate::{Eval, Vm};
|
||||
|
||||
@ -20,6 +20,17 @@ impl Eval for ast::Math<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for ast::MathText<'_> {
|
||||
type Output = Content;
|
||||
|
||||
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
|
||||
match self.get() {
|
||||
MathTextKind::Character(c) => Ok(SymbolElem::packed(c)),
|
||||
MathTextKind::Number(text) => Ok(TextElem::packed(text.clone())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for ast::MathIdent<'_> {
|
||||
type Output = Value;
|
||||
|
||||
@ -102,6 +113,7 @@ impl Eval for ast::MathRoot<'_> {
|
||||
type Output = Content;
|
||||
|
||||
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
|
||||
// Use `TextElem` to match `MathTextKind::Number` above.
|
||||
let index = self.index().map(|i| TextElem::packed(eco_format!("{i}")));
|
||||
let radicand = self.radicand().eval_display(vm)?;
|
||||
Ok(RootElem::new(radicand).with_index(index).pack())
|
||||
|
@ -2,7 +2,7 @@ use std::fmt::Write;
|
||||
|
||||
use typst_library::diag::{bail, At, SourceResult, StrResult};
|
||||
use typst_library::foundations::Repr;
|
||||
use typst_library::html::{charsets, tag, HtmlDocument, HtmlElement, HtmlNode};
|
||||
use typst_library::html::{charsets, tag, HtmlDocument, HtmlElement, HtmlNode, HtmlTag};
|
||||
use typst_library::layout::Frame;
|
||||
use typst_syntax::Span;
|
||||
|
||||
@ -12,15 +12,19 @@ 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)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Writer {
|
||||
/// The output buffer.
|
||||
buf: String,
|
||||
/// current indentation level
|
||||
/// The current indentation level
|
||||
level: usize,
|
||||
/// pretty printing enabled?
|
||||
/// Whether pretty printing is enabled.
|
||||
pretty: bool,
|
||||
}
|
||||
|
||||
@ -85,26 +89,32 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
|
||||
|
||||
let pretty = w.pretty;
|
||||
if !element.children.is_empty() {
|
||||
w.pretty &= is_pretty(element);
|
||||
let pretty_inside = allows_pretty_inside(element.tag)
|
||||
&& element.children.iter().any(|node| match node {
|
||||
HtmlNode::Element(child) => wants_pretty_around(child.tag),
|
||||
_ => false,
|
||||
});
|
||||
|
||||
w.pretty &= pretty_inside;
|
||||
let mut indent = w.pretty;
|
||||
|
||||
w.level += 1;
|
||||
for c in &element.children {
|
||||
let pretty_child = match c {
|
||||
let pretty_around = match c {
|
||||
HtmlNode::Tag(_) => continue,
|
||||
HtmlNode::Element(element) => is_pretty(element),
|
||||
HtmlNode::Element(child) => w.pretty && wants_pretty_around(child.tag),
|
||||
HtmlNode::Text(..) | HtmlNode::Frame(_) => false,
|
||||
};
|
||||
|
||||
if core::mem::take(&mut indent) || pretty_child {
|
||||
if core::mem::take(&mut indent) || pretty_around {
|
||||
write_indent(w);
|
||||
}
|
||||
write_node(w, c)?;
|
||||
indent = pretty_child;
|
||||
indent = pretty_around;
|
||||
}
|
||||
w.level -= 1;
|
||||
|
||||
write_indent(w)
|
||||
write_indent(w);
|
||||
}
|
||||
w.pretty = pretty;
|
||||
|
||||
@ -115,9 +125,27 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether the element should be pretty-printed.
|
||||
fn is_pretty(element: &HtmlElement) -> bool {
|
||||
tag::is_block_by_default(element.tag) || matches!(element.tag, tag::meta)
|
||||
/// Whether we are allowed to add an extra newline at the start and end of the
|
||||
/// element's contents.
|
||||
///
|
||||
/// Technically, users can change CSS `display` properties such that the
|
||||
/// insertion of whitespace may actually impact the visual output. For example,
|
||||
/// <https://www.w3.org/TR/css-text-3/#example-af2745cd> shows how adding CSS
|
||||
/// rules to `<p>` can make it sensitive to whitespace. For this reason, we
|
||||
/// should also respect the `style` tag in the future.
|
||||
fn allows_pretty_inside(tag: HtmlTag) -> bool {
|
||||
(tag::is_block_by_default(tag) && tag != tag::pre)
|
||||
|| tag::is_tabular_by_default(tag)
|
||||
|| tag == tag::li
|
||||
}
|
||||
|
||||
/// Whether newlines should be added before and after the element if the parent
|
||||
/// allows it.
|
||||
///
|
||||
/// In contrast to `allows_pretty_inside`, which is purely spec-driven, this is
|
||||
/// more subjective and depends on preference.
|
||||
fn wants_pretty_around(tag: HtmlTag) -> bool {
|
||||
allows_pretty_inside(tag) || tag::is_metadata(tag) || tag == tag::pre
|
||||
}
|
||||
|
||||
/// Escape a character.
|
||||
|
@ -14,9 +14,9 @@ use typst_library::html::{
|
||||
use typst_library::introspection::{
|
||||
Introspector, Locator, LocatorLink, SplitLocator, TagElem,
|
||||
};
|
||||
use typst_library::layout::{Abs, Axes, BoxElem, Region, Size};
|
||||
use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Size};
|
||||
use typst_library::model::{DocumentInfo, ParElem};
|
||||
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
|
||||
use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines};
|
||||
use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
|
||||
use typst_library::World;
|
||||
use typst_syntax::Span;
|
||||
@ -139,7 +139,9 @@ fn html_fragment_impl(
|
||||
|
||||
let arenas = Arenas::default();
|
||||
let children = (engine.routines.realize)(
|
||||
RealizationKind::HtmlFragment,
|
||||
// No need to know about the `FragmentKind` because we handle both
|
||||
// uniformly.
|
||||
RealizationKind::HtmlFragment(&mut FragmentKind::Block),
|
||||
&mut engine,
|
||||
&mut locator,
|
||||
&arenas,
|
||||
@ -189,7 +191,8 @@ fn handle(
|
||||
};
|
||||
output.push(element.into());
|
||||
} else if let Some(elem) = child.to_packed::<ParElem>() {
|
||||
let children = handle_list(engine, locator, elem.children.iter(&styles))?;
|
||||
let children =
|
||||
html_fragment(engine, &elem.body, locator.next(&elem.span()), styles)?;
|
||||
output.push(
|
||||
HtmlElement::new(tag::p)
|
||||
.with_children(children)
|
||||
@ -197,13 +200,34 @@ fn handle(
|
||||
.into(),
|
||||
);
|
||||
} else if let Some(elem) = child.to_packed::<BoxElem>() {
|
||||
// FIXME: Very incomplete and hacky, but makes boxes kind fulfill their
|
||||
// purpose for now.
|
||||
// TODO: This is rather incomplete.
|
||||
if let Some(body) = elem.body(styles) {
|
||||
let children =
|
||||
html_fragment(engine, body, locator.next(&elem.span()), styles)?;
|
||||
output.extend(children);
|
||||
output.push(
|
||||
HtmlElement::new(tag::span)
|
||||
.with_attr(attr::style, "display: inline-block;")
|
||||
.with_children(children)
|
||||
.spanned(elem.span())
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
} else if let Some((elem, body)) =
|
||||
child
|
||||
.to_packed::<BlockElem>()
|
||||
.and_then(|elem| match elem.body(styles) {
|
||||
Some(BlockBody::Content(body)) => Some((elem, body)),
|
||||
_ => None,
|
||||
})
|
||||
{
|
||||
// TODO: This is rather incomplete.
|
||||
let children = html_fragment(engine, body, locator.next(&elem.span()), styles)?;
|
||||
output.push(
|
||||
HtmlElement::new(tag::div)
|
||||
.with_children(children)
|
||||
.spanned(elem.span())
|
||||
.into(),
|
||||
);
|
||||
} else if child.is::<SpaceElem>() {
|
||||
output.push(HtmlNode::text(' ', child.span()));
|
||||
} else if let Some(elem) = child.to_packed::<TextElem>() {
|
||||
|
@ -817,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.");
|
||||
@ -838,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>,
|
||||
|
@ -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))
|
||||
{
|
||||
@ -269,16 +275,18 @@ mod tests {
|
||||
use std::borrow::Borrow;
|
||||
|
||||
use ecow::EcoString;
|
||||
use typst::foundations::Value;
|
||||
use typst::syntax::{LinkedNode, Side};
|
||||
|
||||
use super::named_items;
|
||||
use crate::tests::{FilePos, WorldLike};
|
||||
use crate::tests::{FilePos, TestWorld, WorldLike};
|
||||
|
||||
type Response = Vec<EcoString>;
|
||||
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 {
|
||||
@ -286,7 +294,7 @@ mod tests {
|
||||
fn must_include<'a>(&self, includes: impl IntoIterator<Item = &'a str>) -> &Self {
|
||||
for item in includes {
|
||||
assert!(
|
||||
self.iter().any(|v| v == item),
|
||||
self.iter().any(|v| v.0 == item),
|
||||
"{item:?} was not contained in {self:?}",
|
||||
);
|
||||
}
|
||||
@ -297,12 +305,21 @@ mod tests {
|
||||
fn must_exclude<'a>(&self, excludes: impl IntoIterator<Item = &'a str>) -> &Self {
|
||||
for item in excludes {
|
||||
assert!(
|
||||
!self.iter().any(|v| v == item),
|
||||
!self.iter().any(|v| v.0 == item),
|
||||
"{item:?} was wrongly contained in {self:?}",
|
||||
);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn must_include_value(&self, name_value: (&str, Option<&Value>)) -> &Self {
|
||||
assert!(
|
||||
self.iter().any(|v| (v.0.as_str(), v.1.as_ref()) == name_value),
|
||||
"{name_value:?} was not contained in {self:?}",
|
||||
);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
@ -314,7 +331,7 @@ mod tests {
|
||||
let leaf = node.leaf_at(cursor, Side::After).unwrap();
|
||||
let mut items = vec![];
|
||||
named_items(world, leaf, |s| {
|
||||
items.push(s.name().clone());
|
||||
items.push((s.name().clone(), s.value().clone()));
|
||||
None::<()>
|
||||
});
|
||||
items
|
||||
@ -340,5 +357,10 @@ mod tests {
|
||||
#[test]
|
||||
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))));
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ impl TestWorld {
|
||||
pub fn with_asset_at(mut self, path: &str, filename: &str) -> Self {
|
||||
let id = FileId::new(None, VirtualPath::new(path));
|
||||
let data = typst_dev_assets::get_by_name(filename).unwrap();
|
||||
let bytes = Bytes::from_static(data);
|
||||
let bytes = Bytes::new(data);
|
||||
Arc::make_mut(&mut self.files).assets.insert(id, bytes);
|
||||
self
|
||||
}
|
||||
@ -152,7 +152,7 @@ impl Default for TestBase {
|
||||
fn default() -> Self {
|
||||
let fonts: Vec<_> = typst_assets::fonts()
|
||||
.chain(typst_dev_assets::fonts())
|
||||
.flat_map(|data| Font::iter(Bytes::from_static(data)))
|
||||
.flat_map(|data| Font::iter(Bytes::new(data)))
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
|
@ -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 {
|
||||
|
@ -20,12 +20,16 @@ use typst_library::model::ParElem;
|
||||
use typst_library::routines::{Pair, Routines};
|
||||
use typst_library::text::TextElem;
|
||||
use typst_library::World;
|
||||
use typst_utils::SliceExt;
|
||||
|
||||
use super::{layout_multi_block, layout_single_block};
|
||||
use super::{layout_multi_block, layout_single_block, FlowMode};
|
||||
use crate::inline::ParSituation;
|
||||
use crate::modifiers::layout_and_modify;
|
||||
|
||||
/// Collects all elements of the flow into prepared children. These are much
|
||||
/// simpler to handle than the raw elements.
|
||||
#[typst_macros::time]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn collect<'a>(
|
||||
engine: &mut Engine,
|
||||
bump: &'a Bump,
|
||||
@ -33,6 +37,7 @@ pub fn collect<'a>(
|
||||
locator: Locator<'a>,
|
||||
base: Size,
|
||||
expand: bool,
|
||||
mode: FlowMode,
|
||||
) -> SourceResult<Vec<Child<'a>>> {
|
||||
Collector {
|
||||
engine,
|
||||
@ -42,9 +47,9 @@ pub fn collect<'a>(
|
||||
base,
|
||||
expand,
|
||||
output: Vec::with_capacity(children.len()),
|
||||
last_was_par: false,
|
||||
par_situation: ParSituation::First,
|
||||
}
|
||||
.run()
|
||||
.run(mode)
|
||||
}
|
||||
|
||||
/// State for collection.
|
||||
@ -56,12 +61,20 @@ struct Collector<'a, 'x, 'y> {
|
||||
expand: bool,
|
||||
locator: SplitLocator<'a>,
|
||||
output: Vec<Child<'a>>,
|
||||
last_was_par: bool,
|
||||
par_situation: ParSituation,
|
||||
}
|
||||
|
||||
impl<'a> Collector<'a, '_, '_> {
|
||||
/// Perform the collection.
|
||||
fn run(mut self) -> SourceResult<Vec<Child<'a>>> {
|
||||
fn run(self, mode: FlowMode) -> SourceResult<Vec<Child<'a>>> {
|
||||
match mode {
|
||||
FlowMode::Root | FlowMode::Block => self.run_block(),
|
||||
FlowMode::Inline => self.run_inline(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform collection for block-level children.
|
||||
fn run_block(mut self) -> SourceResult<Vec<Child<'a>>> {
|
||||
for &(child, styles) in self.children {
|
||||
if let Some(elem) = child.to_packed::<TagElem>() {
|
||||
self.output.push(Child::Tag(&elem.tag));
|
||||
@ -94,6 +107,42 @@ impl<'a> Collector<'a, '_, '_> {
|
||||
Ok(self.output)
|
||||
}
|
||||
|
||||
/// Perform collection for inline-level children.
|
||||
fn run_inline(mut self) -> SourceResult<Vec<Child<'a>>> {
|
||||
// Extract leading and trailing tags.
|
||||
let (start, end) = self.children.split_prefix_suffix(|(c, _)| c.is::<TagElem>());
|
||||
let inner = &self.children[start..end];
|
||||
|
||||
// Compute the shared styles, ignoring tags.
|
||||
let styles = StyleChain::trunk(inner.iter().map(|&(_, s)| s)).unwrap_or_default();
|
||||
|
||||
// Layout the lines.
|
||||
let lines = crate::inline::layout_inline(
|
||||
self.engine,
|
||||
inner,
|
||||
&mut self.locator,
|
||||
styles,
|
||||
self.base,
|
||||
self.expand,
|
||||
None,
|
||||
)?
|
||||
.into_frames();
|
||||
|
||||
for (c, _) in &self.children[..start] {
|
||||
let elem = c.to_packed::<TagElem>().unwrap();
|
||||
self.output.push(Child::Tag(&elem.tag));
|
||||
}
|
||||
|
||||
self.lines(lines, styles);
|
||||
|
||||
for (c, _) in &self.children[end..] {
|
||||
let elem = c.to_packed::<TagElem>().unwrap();
|
||||
self.output.push(Child::Tag(&elem.tag));
|
||||
}
|
||||
|
||||
Ok(self.output)
|
||||
}
|
||||
|
||||
/// Collect vertical spacing into a relative or fractional child.
|
||||
fn v(&mut self, elem: &'a Packed<VElem>, styles: StyleChain<'a>) {
|
||||
self.output.push(match elem.amount {
|
||||
@ -109,24 +158,34 @@ impl<'a> Collector<'a, '_, '_> {
|
||||
elem: &'a Packed<ParElem>,
|
||||
styles: StyleChain<'a>,
|
||||
) -> SourceResult<()> {
|
||||
let align = AlignElem::alignment_in(styles).resolve(styles);
|
||||
let leading = ParElem::leading_in(styles);
|
||||
let spacing = ParElem::spacing_in(styles);
|
||||
let costs = TextElem::costs_in(styles);
|
||||
|
||||
let lines = crate::layout_inline(
|
||||
let lines = crate::inline::layout_par(
|
||||
elem,
|
||||
self.engine,
|
||||
&elem.children,
|
||||
self.locator.next(&elem.span()),
|
||||
styles,
|
||||
self.last_was_par,
|
||||
self.base,
|
||||
self.expand,
|
||||
self.par_situation,
|
||||
)?
|
||||
.into_frames();
|
||||
|
||||
let spacing = ParElem::spacing_in(styles);
|
||||
self.output.push(Child::Rel(spacing.into(), 4));
|
||||
|
||||
self.lines(lines, styles);
|
||||
|
||||
self.output.push(Child::Rel(spacing.into(), 4));
|
||||
self.par_situation = ParSituation::Consecutive;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Collect laid-out lines.
|
||||
fn lines(&mut self, lines: Vec<Frame>, styles: StyleChain<'a>) {
|
||||
let align = AlignElem::alignment_in(styles).resolve(styles);
|
||||
let leading = ParElem::leading_in(styles);
|
||||
let costs = TextElem::costs_in(styles);
|
||||
|
||||
// Determine whether to prevent widow and orphans.
|
||||
let len = lines.len();
|
||||
let prevent_orphans =
|
||||
@ -165,11 +224,6 @@ impl<'a> Collector<'a, '_, '_> {
|
||||
self.output
|
||||
.push(Child::Line(self.boxed(LineChild { frame, align, need })));
|
||||
}
|
||||
|
||||
self.output.push(Child::Rel(spacing.into(), 4));
|
||||
self.last_was_par = true;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Collect a block into a [`SingleChild`] or [`MultiChild`] depending on
|
||||
@ -218,7 +272,7 @@ impl<'a> Collector<'a, '_, '_> {
|
||||
};
|
||||
|
||||
self.output.push(spacing(elem.below(styles)));
|
||||
self.last_was_par = false;
|
||||
self.par_situation = ParSituation::Other;
|
||||
}
|
||||
|
||||
/// Collects a placed element into a [`PlacedChild`].
|
||||
@ -377,8 +431,9 @@ fn layout_single_impl(
|
||||
route: Route::extend(route),
|
||||
};
|
||||
|
||||
layout_single_block(elem, &mut engine, locator, styles, region)
|
||||
.map(|frame| frame.post_processed(styles))
|
||||
layout_and_modify(styles, |styles| {
|
||||
layout_single_block(elem, &mut engine, locator, styles, region)
|
||||
})
|
||||
}
|
||||
|
||||
/// A child that encapsulates a prepared breakable block.
|
||||
@ -473,11 +528,8 @@ fn layout_multi_impl(
|
||||
route: Route::extend(route),
|
||||
};
|
||||
|
||||
layout_multi_block(elem, &mut engine, locator, styles, regions).map(|mut fragment| {
|
||||
for frame in &mut fragment {
|
||||
frame.post_process(styles);
|
||||
}
|
||||
fragment
|
||||
layout_and_modify(styles, |styles| {
|
||||
layout_multi_block(elem, &mut engine, locator, styles, regions)
|
||||
})
|
||||
}
|
||||
|
||||
@ -579,20 +631,23 @@ impl PlacedChild<'_> {
|
||||
self.cell.get_or_init(base, |base| {
|
||||
let align = self.alignment.unwrap_or_else(|| Alignment::CENTER);
|
||||
let aligned = AlignElem::set_alignment(align).wrap();
|
||||
let styles = self.styles.chain(&aligned);
|
||||
|
||||
let mut frame = crate::layout_frame(
|
||||
engine,
|
||||
&self.elem.body,
|
||||
self.locator.relayout(),
|
||||
self.styles.chain(&aligned),
|
||||
Region::new(base, Axes::splat(false)),
|
||||
)?;
|
||||
let mut frame = layout_and_modify(styles, |styles| {
|
||||
crate::layout_frame(
|
||||
engine,
|
||||
&self.elem.body,
|
||||
self.locator.relayout(),
|
||||
styles,
|
||||
Region::new(base, Axes::splat(false)),
|
||||
)
|
||||
})?;
|
||||
|
||||
if self.float {
|
||||
frame.set_parent(self.elem.location().unwrap());
|
||||
}
|
||||
|
||||
Ok(frame.post_processed(self.styles))
|
||||
Ok(frame)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,9 @@ use typst_library::model::{
|
||||
use typst_syntax::Span;
|
||||
use typst_utils::{NonZeroExt, Numeric};
|
||||
|
||||
use super::{distribute, Config, FlowResult, LineNumberConfig, PlacedChild, Stop, Work};
|
||||
use super::{
|
||||
distribute, Config, FlowMode, FlowResult, LineNumberConfig, PlacedChild, Stop, Work,
|
||||
};
|
||||
|
||||
/// Composes the contents of a single page/region. A region can have multiple
|
||||
/// columns/subregions.
|
||||
@ -356,7 +358,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
|
||||
migratable: bool,
|
||||
) -> FlowResult<()> {
|
||||
// Footnotes are only supported at the root level.
|
||||
if !self.config.root {
|
||||
if self.config.mode != FlowMode::Root {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ use typst_library::layout::{
|
||||
Regions, Rel, Size,
|
||||
};
|
||||
use typst_library::model::{FootnoteElem, FootnoteEntry, LineNumberingScope, ParLine};
|
||||
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
|
||||
use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines};
|
||||
use typst_library::text::TextElem;
|
||||
use typst_library::World;
|
||||
use typst_utils::{NonZeroExt, Numeric};
|
||||
@ -140,9 +140,10 @@ fn layout_fragment_impl(
|
||||
|
||||
engine.route.check_layout_depth().at(content.span())?;
|
||||
|
||||
let mut kind = FragmentKind::Block;
|
||||
let arenas = Arenas::default();
|
||||
let children = (engine.routines.realize)(
|
||||
RealizationKind::LayoutFragment,
|
||||
RealizationKind::LayoutFragment(&mut kind),
|
||||
&mut engine,
|
||||
&mut locator,
|
||||
&arenas,
|
||||
@ -158,25 +159,46 @@ fn layout_fragment_impl(
|
||||
regions,
|
||||
columns,
|
||||
column_gutter,
|
||||
false,
|
||||
kind.into(),
|
||||
)
|
||||
}
|
||||
|
||||
/// The mode a flow can be laid out in.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub enum FlowMode {
|
||||
/// A root flow with block-level elements. Like `FlowMode::Block`, but can
|
||||
/// additionally host footnotes and line numbers.
|
||||
Root,
|
||||
/// A flow whose children are block-level elements.
|
||||
Block,
|
||||
/// A flow whose children are inline-level elements.
|
||||
Inline,
|
||||
}
|
||||
|
||||
impl From<FragmentKind> for FlowMode {
|
||||
fn from(value: FragmentKind) -> Self {
|
||||
match value {
|
||||
FragmentKind::Inline => Self::Inline,
|
||||
FragmentKind::Block => Self::Block,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Lays out realized content into regions, potentially with columns.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn layout_flow(
|
||||
pub fn layout_flow<'a>(
|
||||
engine: &mut Engine,
|
||||
children: &[Pair],
|
||||
locator: &mut SplitLocator,
|
||||
shared: StyleChain,
|
||||
children: &[Pair<'a>],
|
||||
locator: &mut SplitLocator<'a>,
|
||||
shared: StyleChain<'a>,
|
||||
mut regions: Regions,
|
||||
columns: NonZeroUsize,
|
||||
column_gutter: Rel<Abs>,
|
||||
root: bool,
|
||||
mode: FlowMode,
|
||||
) -> SourceResult<Fragment> {
|
||||
// Prepare configuration that is shared across the whole flow.
|
||||
let config = Config {
|
||||
root,
|
||||
mode,
|
||||
shared,
|
||||
columns: {
|
||||
let mut count = columns.get();
|
||||
@ -195,7 +217,7 @@ pub(crate) fn layout_flow(
|
||||
gap: FootnoteEntry::gap_in(shared),
|
||||
expand: regions.expand.x,
|
||||
},
|
||||
line_numbers: root.then(|| LineNumberConfig {
|
||||
line_numbers: (mode == FlowMode::Root).then(|| LineNumberConfig {
|
||||
scope: ParLine::numbering_scope_in(shared),
|
||||
default_clearance: {
|
||||
let width = if PageElem::flipped_in(shared) {
|
||||
@ -225,6 +247,7 @@ pub(crate) fn layout_flow(
|
||||
locator.next(&()),
|
||||
Size::new(config.columns.width, regions.full),
|
||||
regions.expand.x,
|
||||
mode,
|
||||
)?;
|
||||
|
||||
let mut work = Work::new(&children);
|
||||
@ -318,7 +341,7 @@ impl<'a, 'b> Work<'a, 'b> {
|
||||
struct Config<'x> {
|
||||
/// Whether this is the root flow, which can host footnotes and line
|
||||
/// numbers.
|
||||
root: bool,
|
||||
mode: FlowMode,
|
||||
/// The styles shared by the whole flow. This is used for footnotes and line
|
||||
/// numbers.
|
||||
shared: StyleChain<'x>,
|
||||
|
@ -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
|
||||
|
@ -2,16 +2,16 @@ use std::ffi::OsStr;
|
||||
|
||||
use typst_library::diag::{bail, warning, At, SourceResult, StrResult};
|
||||
use typst_library::engine::Engine;
|
||||
use typst_library::foundations::{Packed, Smart, StyleChain};
|
||||
use typst_library::foundations::{Bytes, Derived, Packed, Smart, StyleChain};
|
||||
use typst_library::introspection::Locator;
|
||||
use typst_library::layout::{
|
||||
Abs, Axes, FixedAlignment, Frame, FrameItem, Point, Region, Size,
|
||||
};
|
||||
use typst_library::loading::Readable;
|
||||
use typst_library::loading::DataSource;
|
||||
use typst_library::text::families;
|
||||
use typst_library::visualize::{
|
||||
Curve, Image, ImageElem, ImageFit, ImageFormat, ImageOptions, ImageSource,
|
||||
RasterFormat, VectorFormat,
|
||||
Curve, Image, ImageElem, ImageFit, ImageFormat, ImageKind, ImageSource, PixmapImage,
|
||||
RasterFormat, RasterImage, SvgImage, VectorFormat,
|
||||
};
|
||||
|
||||
/// Layout the image.
|
||||
@ -27,48 +27,52 @@ pub fn layout_image(
|
||||
|
||||
// Take the format that was explicitly defined, or parse the extension,
|
||||
// or try to detect the format.
|
||||
let source = elem.source();
|
||||
let format = match (elem.format(styles), source) {
|
||||
(Smart::Custom(v), _) => v,
|
||||
(Smart::Auto, ImageSource::Readable(data)) => {
|
||||
determine_format(elem.path().as_str(), data).at(span)?
|
||||
}
|
||||
(Smart::Auto, ImageSource::Pixmap(_)) => {
|
||||
bail!(span, "pixmaps require an explicit image format to be given");
|
||||
}
|
||||
let Derived { source, derived: data } = &elem.source;
|
||||
let format = match elem.format(styles) {
|
||||
Smart::Custom(v) => v,
|
||||
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 let ImageSource::Readable(data) = source {
|
||||
if format == ImageFormat::Vector(VectorFormat::Svg) {
|
||||
let has_foreign_object =
|
||||
data.as_str().is_some_and(|s| s.contains("<foreignObject"));
|
||||
if format == ImageFormat::Vector(VectorFormat::Svg) {
|
||||
let has_foreign_object =
|
||||
data.as_str().is_ok_and(|s| s.contains("<foreignObject"));
|
||||
|
||||
if has_foreign_object {
|
||||
engine.sink.warn(warning!(
|
||||
span,
|
||||
"image contains foreign object";
|
||||
hint: "SVG images with foreign objects might render incorrectly in typst";
|
||||
hint: "see https://github.com/typst/typst/issues/1421 for more information"
|
||||
));
|
||||
}
|
||||
if has_foreign_object {
|
||||
engine.sink.warn(warning!(
|
||||
span,
|
||||
"image contains foreign object";
|
||||
hint: "SVG images with foreign objects might render incorrectly in typst";
|
||||
hint: "see https://github.com/typst/typst/issues/1421 for more information"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Construct the image itself.
|
||||
let image = Image::new(
|
||||
source.clone(),
|
||||
format,
|
||||
&ImageOptions {
|
||||
alt: elem.alt(styles),
|
||||
world: Some(engine.world),
|
||||
families: &families(styles).map(|f| f.as_str()).collect::<Vec<_>>(),
|
||||
flatten_text: elem.flatten_text(styles),
|
||||
scaling: elem.scaling(styles),
|
||||
},
|
||||
)
|
||||
.at(span)?;
|
||||
let kind = match (format, source) {
|
||||
(ImageFormat::Pixmap(format), ImageSource::Pixmap(source)) => {
|
||||
ImageKind::Pixmap(PixmapImage::new(source.clone(), format).at(span)?)
|
||||
}
|
||||
(ImageFormat::Raster(format), ImageSource::Data(_)) => {
|
||||
ImageKind::Raster(RasterImage::new(data.clone(), format).at(span)?)
|
||||
}
|
||||
(ImageFormat::Vector(VectorFormat::Svg), ImageSource::Data(_)) => ImageKind::Svg(
|
||||
SvgImage::with_fonts(
|
||||
data.clone(),
|
||||
engine.world,
|
||||
elem.flatten_text(styles),
|
||||
&families(styles).map(|f| f.as_str()).collect::<Vec<_>>(),
|
||||
)
|
||||
.at(span)?,
|
||||
),
|
||||
(ImageFormat::Pixmap(_), _) => bail!(span, "source must be a pixmap"),
|
||||
(ImageFormat::Raster(_) | ImageFormat::Vector(_), _) => {
|
||||
bail!(span, "expected readable source for the given format (str or bytes)")
|
||||
}
|
||||
};
|
||||
|
||||
let image = Image::new(kind, elem.alt(styles), elem.scaling(styles));
|
||||
|
||||
// Determine the image's pixel aspect ratio.
|
||||
let pxw = image.width();
|
||||
@ -130,25 +134,29 @@ pub fn layout_image(
|
||||
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: &ImageSource, data: &Bytes) -> StrResult<ImageFormat> {
|
||||
match source {
|
||||
ImageSource::Data(DataSource::Path(path)) => {
|
||||
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)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
ImageSource::Data(DataSource::Bytes(_)) => {}
|
||||
ImageSource::Pixmap(_) => {
|
||||
bail!("pixmaps require an explicit image format to be given")
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ImageFormat::detect(data).ok_or("unknown image format")?)
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ use typst_utils::Numeric;
|
||||
use crate::flow::unbreakable_pod;
|
||||
use crate::shapes::{clip_rect, fill_and_stroke};
|
||||
|
||||
/// Lay out a box as part of a paragraph.
|
||||
/// Lay out a box as part of inline layout.
|
||||
#[typst_macros::time(name = "box", span = elem.span())]
|
||||
pub fn layout_box(
|
||||
elem: &Packed<BoxElem>,
|
||||
|
@ -1,10 +1,12 @@
|
||||
use typst_library::diag::bail;
|
||||
use typst_library::diag::warning;
|
||||
use typst_library::foundations::{Packed, Resolve};
|
||||
use typst_library::introspection::{SplitLocator, Tag, TagElem};
|
||||
use typst_library::layout::{
|
||||
Abs, AlignElem, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing,
|
||||
Spacing,
|
||||
};
|
||||
use typst_library::model::{EnumElem, ListElem, TermsElem};
|
||||
use typst_library::routines::Pair;
|
||||
use typst_library::text::{
|
||||
is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes,
|
||||
SpaceElem, TextElem,
|
||||
@ -13,9 +15,10 @@ use typst_syntax::Span;
|
||||
use typst_utils::Numeric;
|
||||
|
||||
use super::*;
|
||||
use crate::modifiers::{layout_and_modify, FrameModifiers, FrameModify};
|
||||
|
||||
// The characters by which spacing, inline content and pins are replaced in the
|
||||
// paragraph's full text.
|
||||
// full text.
|
||||
const SPACING_REPLACE: &str = " "; // Space
|
||||
const OBJ_REPLACE: &str = "\u{FFFC}"; // Object Replacement Character
|
||||
|
||||
@ -26,7 +29,7 @@ const POP_EMBEDDING: &str = "\u{202C}";
|
||||
const LTR_ISOLATE: &str = "\u{2066}";
|
||||
const POP_ISOLATE: &str = "\u{2069}";
|
||||
|
||||
/// A prepared item in a paragraph layout.
|
||||
/// A prepared item in a inline layout.
|
||||
#[derive(Debug)]
|
||||
pub enum Item<'a> {
|
||||
/// A shaped text run with consistent style and direction.
|
||||
@ -36,7 +39,7 @@ pub enum Item<'a> {
|
||||
/// Fractional spacing between other items.
|
||||
Fractional(Fr, Option<(&'a Packed<BoxElem>, Locator<'a>, StyleChain<'a>)>),
|
||||
/// Layouted inline-level content.
|
||||
Frame(Frame, StyleChain<'a>),
|
||||
Frame(Frame),
|
||||
/// A tag.
|
||||
Tag(&'a Tag),
|
||||
/// An item that is invisible and needs to be skipped, e.g. a Unicode
|
||||
@ -67,7 +70,7 @@ impl<'a> Item<'a> {
|
||||
match self {
|
||||
Self::Text(shaped) => shaped.text,
|
||||
Self::Absolute(_, _) | Self::Fractional(_, _) => SPACING_REPLACE,
|
||||
Self::Frame(_, _) => OBJ_REPLACE,
|
||||
Self::Frame(_) => OBJ_REPLACE,
|
||||
Self::Tag(_) => "",
|
||||
Self::Skip(s) => s,
|
||||
}
|
||||
@ -83,7 +86,7 @@ impl<'a> Item<'a> {
|
||||
match self {
|
||||
Self::Text(shaped) => shaped.width,
|
||||
Self::Absolute(v, _) => *v,
|
||||
Self::Frame(frame, _) => frame.width(),
|
||||
Self::Frame(frame) => frame.width(),
|
||||
Self::Fractional(_, _) | Self::Tag(_) => Abs::zero(),
|
||||
Self::Skip(_) => Abs::zero(),
|
||||
}
|
||||
@ -112,38 +115,51 @@ impl Segment<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Collects all text of the paragraph into one string and a collection of
|
||||
/// segments that correspond to pieces of that string. This also performs
|
||||
/// string-level preprocessing like case transformations.
|
||||
/// Collects all text into one string and a collection of segments that
|
||||
/// correspond to pieces of that string. This also performs string-level
|
||||
/// preprocessing like case transformations.
|
||||
#[typst_macros::time]
|
||||
pub fn collect<'a>(
|
||||
children: &'a StyleVec,
|
||||
children: &[Pair<'a>],
|
||||
engine: &mut Engine<'_>,
|
||||
locator: &mut SplitLocator<'a>,
|
||||
styles: &'a StyleChain<'a>,
|
||||
styles: StyleChain<'a>,
|
||||
region: Size,
|
||||
consecutive: bool,
|
||||
situation: Option<ParSituation>,
|
||||
) -> SourceResult<(String, Vec<Segment<'a>>, SpanMapper)> {
|
||||
let mut collector = Collector::new(2 + children.len());
|
||||
let mut quoter = SmartQuoter::new();
|
||||
|
||||
let outer_dir = TextElem::dir_in(*styles);
|
||||
let first_line_indent = ParElem::first_line_indent_in(*styles);
|
||||
if !first_line_indent.is_zero()
|
||||
&& consecutive
|
||||
&& AlignElem::alignment_in(*styles).resolve(*styles).x == outer_dir.start().into()
|
||||
{
|
||||
collector.push_item(Item::Absolute(first_line_indent.resolve(*styles), false));
|
||||
collector.spans.push(1, Span::detached());
|
||||
let outer_dir = TextElem::dir_in(styles);
|
||||
|
||||
if let Some(situation) = situation {
|
||||
let first_line_indent = ParElem::first_line_indent_in(styles);
|
||||
if !first_line_indent.amount.is_zero()
|
||||
&& match situation {
|
||||
// First-line indent for the first paragraph after a list bullet
|
||||
// just looks bad.
|
||||
ParSituation::First => first_line_indent.all && !in_list(styles),
|
||||
ParSituation::Consecutive => true,
|
||||
ParSituation::Other => first_line_indent.all,
|
||||
}
|
||||
&& AlignElem::alignment_in(styles).resolve(styles).x
|
||||
== outer_dir.start().into()
|
||||
{
|
||||
collector.push_item(Item::Absolute(
|
||||
first_line_indent.amount.resolve(styles),
|
||||
false,
|
||||
));
|
||||
collector.spans.push(1, Span::detached());
|
||||
}
|
||||
|
||||
let hang = ParElem::hanging_indent_in(styles);
|
||||
if !hang.is_zero() {
|
||||
collector.push_item(Item::Absolute(-hang, false));
|
||||
collector.spans.push(1, Span::detached());
|
||||
}
|
||||
}
|
||||
|
||||
let hang = ParElem::hanging_indent_in(*styles);
|
||||
if !hang.is_zero() {
|
||||
collector.push_item(Item::Absolute(-hang, false));
|
||||
collector.spans.push(1, Span::detached());
|
||||
}
|
||||
|
||||
for (child, styles) in children.iter(styles) {
|
||||
for &(child, styles) in children {
|
||||
let prev_len = collector.full.len();
|
||||
|
||||
if child.is::<SpaceElem>() {
|
||||
@ -161,9 +177,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 +188,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),
|
||||
@ -211,8 +226,10 @@ pub fn collect<'a>(
|
||||
InlineItem::Space(space, weak) => {
|
||||
collector.push_item(Item::Absolute(space, weak));
|
||||
}
|
||||
InlineItem::Frame(frame) => {
|
||||
collector.push_item(Item::Frame(frame, styles));
|
||||
InlineItem::Frame(mut frame) => {
|
||||
frame.modify(&FrameModifiers::get_in(styles));
|
||||
apply_baseline_shift(&mut frame, styles);
|
||||
collector.push_item(Item::Frame(frame));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -223,13 +240,22 @@ pub fn collect<'a>(
|
||||
if let Sizing::Fr(v) = elem.width(styles) {
|
||||
collector.push_item(Item::Fractional(v, Some((elem, loc, styles))));
|
||||
} else {
|
||||
let frame = layout_box(elem, engine, loc, styles, region)?;
|
||||
collector.push_item(Item::Frame(frame, styles));
|
||||
let mut frame = layout_and_modify(styles, |styles| {
|
||||
layout_box(elem, engine, loc, styles, region)
|
||||
})?;
|
||||
apply_baseline_shift(&mut frame, styles);
|
||||
collector.push_item(Item::Frame(frame));
|
||||
}
|
||||
} else if let Some(elem) = child.to_packed::<TagElem>() {
|
||||
collector.push_item(Item::Tag(&elem.tag));
|
||||
} else {
|
||||
bail!(child.span(), "unexpected paragraph child");
|
||||
// Non-paragraph inline layout should never trigger this since it
|
||||
// only won't be triggered if we see any non-inline content.
|
||||
engine.sink.warn(warning!(
|
||||
child.span(),
|
||||
"{} may not occur inside of a paragraph and was ignored",
|
||||
child.func().name()
|
||||
));
|
||||
};
|
||||
|
||||
let len = collector.full.len() - prev_len;
|
||||
@ -239,6 +265,16 @@ pub fn collect<'a>(
|
||||
Ok((collector.full, collector.segments, collector.spans))
|
||||
}
|
||||
|
||||
/// Whether we have a list ancestor.
|
||||
///
|
||||
/// When we support some kind of more general ancestry mechanism, this can
|
||||
/// become more elegant.
|
||||
fn in_list(styles: StyleChain) -> bool {
|
||||
ListElem::depth_in(styles).0 > 0
|
||||
|| !EnumElem::parents_in(styles).is_empty()
|
||||
|| TermsElem::within_in(styles)
|
||||
}
|
||||
|
||||
/// Collects segments.
|
||||
struct Collector<'a> {
|
||||
full: String,
|
||||
|
@ -14,7 +14,7 @@ pub fn finalize(
|
||||
expand: bool,
|
||||
locator: &mut SplitLocator<'_>,
|
||||
) -> SourceResult<Fragment> {
|
||||
// Determine the paragraph's width: Full width of the region if we should
|
||||
// Determine the resulting width: Full width of the region if we should
|
||||
// expand or there's fractional spacing, fit-to-width otherwise.
|
||||
let width = if !region.x.is_finite()
|
||||
|| (!expand && lines.iter().all(|line| line.fr().is_zero()))
|
||||
|
@ -10,6 +10,7 @@ use typst_library::text::{Lang, TextElem};
|
||||
use typst_utils::Numeric;
|
||||
|
||||
use super::*;
|
||||
use crate::modifiers::layout_and_modify;
|
||||
|
||||
const SHY: char = '\u{ad}';
|
||||
const HYPHEN: char = '-';
|
||||
@ -17,12 +18,12 @@ const EN_DASH: char = '–';
|
||||
const EM_DASH: char = '—';
|
||||
const LINE_SEPARATOR: char = '\u{2028}'; // We use LS to distinguish justified breaks.
|
||||
|
||||
/// A layouted line, consisting of a sequence of layouted paragraph items that
|
||||
/// are mostly borrowed from the preparation phase. This type enables you to
|
||||
/// measure the size of a line in a range before committing to building the
|
||||
/// line's frame.
|
||||
/// A layouted line, consisting of a sequence of layouted inline items that are
|
||||
/// mostly borrowed from the preparation phase. This type enables you to measure
|
||||
/// the size of a line in a range before committing to building the line's
|
||||
/// frame.
|
||||
///
|
||||
/// At most two paragraph items must be created individually for this line: The
|
||||
/// At most two inline items must be created individually for this line: The
|
||||
/// first and last one since they may be broken apart by the start or end of the
|
||||
/// line, respectively. But even those can partially reuse previous results when
|
||||
/// the break index is safe-to-break per rustybuzz.
|
||||
@ -93,7 +94,7 @@ impl Line<'_> {
|
||||
pub fn has_negative_width_items(&self) -> bool {
|
||||
self.items.iter().any(|item| match item {
|
||||
Item::Absolute(amount, _) => *amount < Abs::zero(),
|
||||
Item::Frame(frame, _) => frame.width() < Abs::zero(),
|
||||
Item::Frame(frame) => frame.width() < Abs::zero(),
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
@ -409,6 +410,11 @@ fn should_repeat_hyphen(pred_line: &Line, text: &str) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the current baseline shift to a frame.
|
||||
pub fn apply_baseline_shift(frame: &mut Frame, styles: StyleChain) {
|
||||
frame.translate(Point::with_y(TextElem::baseline_in(styles)));
|
||||
}
|
||||
|
||||
/// Commit to a line and build its frame.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn commit(
|
||||
@ -424,7 +430,7 @@ pub fn commit(
|
||||
let mut offset = Abs::zero();
|
||||
|
||||
// We always build the line from left to right. In an LTR paragraph, we must
|
||||
// thus add the hanging indent to the offset. When the paragraph is RTL, the
|
||||
// thus add the hanging indent to the offset. In an RTL paragraph, the
|
||||
// hanging indent arises naturally due to the line width.
|
||||
if p.dir == Dir::LTR {
|
||||
offset += p.hang;
|
||||
@ -509,10 +515,11 @@ pub fn commit(
|
||||
let amount = v.share(fr, remaining);
|
||||
if let Some((elem, loc, styles)) = elem {
|
||||
let region = Size::new(amount, full);
|
||||
let mut frame =
|
||||
layout_box(elem, engine, loc.relayout(), *styles, region)?;
|
||||
frame.translate(Point::with_y(TextElem::baseline_in(*styles)));
|
||||
push(&mut offset, frame.post_processed(*styles));
|
||||
let mut frame = layout_and_modify(*styles, |styles| {
|
||||
layout_box(elem, engine, loc.relayout(), styles, region)
|
||||
})?;
|
||||
apply_baseline_shift(&mut frame, *styles);
|
||||
push(&mut offset, frame);
|
||||
} else {
|
||||
offset += amount;
|
||||
}
|
||||
@ -524,12 +531,10 @@ pub fn commit(
|
||||
justification_ratio,
|
||||
extra_justification,
|
||||
);
|
||||
push(&mut offset, frame.post_processed(shaped.styles));
|
||||
push(&mut offset, frame);
|
||||
}
|
||||
Item::Frame(frame, styles) => {
|
||||
let mut frame = frame.clone();
|
||||
frame.translate(Point::with_y(TextElem::baseline_in(*styles)));
|
||||
push(&mut offset, frame.post_processed(*styles));
|
||||
Item::Frame(frame) => {
|
||||
push(&mut offset, frame.clone());
|
||||
}
|
||||
Item::Tag(tag) => {
|
||||
let mut frame = Frame::soft(Size::zero());
|
||||
@ -626,7 +631,7 @@ fn overhang(c: char) -> f64 {
|
||||
}
|
||||
}
|
||||
|
||||
/// A collection of owned or borrowed paragraph items.
|
||||
/// A collection of owned or borrowed inline items.
|
||||
pub struct Items<'a>(Vec<ItemEntry<'a>>);
|
||||
|
||||
impl<'a> Items<'a> {
|
||||
|
@ -17,7 +17,7 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// The cost of a line or paragraph layout.
|
||||
/// The cost of a line or inline layout.
|
||||
type Cost = f64;
|
||||
|
||||
// Cost parameters.
|
||||
@ -104,7 +104,7 @@ impl Breakpoint {
|
||||
}
|
||||
}
|
||||
|
||||
/// Breaks the paragraph into lines.
|
||||
/// Breaks the text into lines.
|
||||
pub fn linebreak<'a>(
|
||||
engine: &Engine,
|
||||
p: &'a Preparation<'a>,
|
||||
@ -181,13 +181,12 @@ fn linebreak_simple<'a>(
|
||||
/// lines with hyphens even more.
|
||||
///
|
||||
/// To find the layout with the minimal total cost the algorithm uses dynamic
|
||||
/// programming: For each possible breakpoint it determines the optimal
|
||||
/// paragraph layout _up to that point_. It walks over all possible start points
|
||||
/// for a line ending at that point and finds the one for which the cost of the
|
||||
/// line plus the cost of the optimal paragraph up to the start point (already
|
||||
/// computed and stored in dynamic programming table) is minimal. The final
|
||||
/// result is simply the layout determined for the last breakpoint at the end of
|
||||
/// text.
|
||||
/// programming: For each possible breakpoint, it determines the optimal layout
|
||||
/// _up to that point_. It walks over all possible start points for a line
|
||||
/// ending at that point and finds the one for which the cost of the line plus
|
||||
/// the cost of the optimal layout up to the start point (already computed and
|
||||
/// stored in dynamic programming table) is minimal. The final result is simply
|
||||
/// the layout determined for the last breakpoint at the end of text.
|
||||
#[typst_macros::time]
|
||||
fn linebreak_optimized<'a>(
|
||||
engine: &Engine,
|
||||
@ -215,7 +214,7 @@ fn linebreak_optimized_bounded<'a>(
|
||||
metrics: &CostMetrics,
|
||||
upper_bound: Cost,
|
||||
) -> Vec<Line<'a>> {
|
||||
/// An entry in the dynamic programming table for paragraph optimization.
|
||||
/// An entry in the dynamic programming table for inline layout optimization.
|
||||
struct Entry<'a> {
|
||||
pred: usize,
|
||||
total: Cost,
|
||||
@ -321,7 +320,7 @@ fn linebreak_optimized_bounded<'a>(
|
||||
// This should only happen if our bound was faulty. Which shouldn't happen!
|
||||
if table[idx].end != p.text.len() {
|
||||
#[cfg(debug_assertions)]
|
||||
panic!("bounded paragraph layout is incomplete");
|
||||
panic!("bounded inline layout is incomplete");
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
return linebreak_optimized_bounded(engine, p, width, metrics, Cost::INFINITY);
|
||||
@ -342,7 +341,7 @@ fn linebreak_optimized_bounded<'a>(
|
||||
/// (which is costly) to determine costs, it determines approximate costs using
|
||||
/// cumulative arrays.
|
||||
///
|
||||
/// This results in a likely good paragraph layouts, for which we then compute
|
||||
/// This results in a likely good inline layouts, for which we then compute
|
||||
/// the exact cost. This cost is an upper bound for proper optimized
|
||||
/// linebreaking. We can use it to heavily prune the search space.
|
||||
#[typst_macros::time]
|
||||
@ -355,7 +354,7 @@ fn linebreak_optimized_approximate(
|
||||
// Determine the cumulative estimation metrics.
|
||||
let estimates = Estimates::compute(p);
|
||||
|
||||
/// An entry in the dynamic programming table for paragraph optimization.
|
||||
/// An entry in the dynamic programming table for inline layout optimization.
|
||||
struct Entry {
|
||||
pred: usize,
|
||||
total: Cost,
|
||||
@ -862,7 +861,7 @@ struct CostMetrics {
|
||||
}
|
||||
|
||||
impl CostMetrics {
|
||||
/// Compute shared metrics for paragraph optimization.
|
||||
/// Compute shared metrics for inline layout optimization.
|
||||
fn compute(p: &Preparation) -> Self {
|
||||
Self {
|
||||
// When justifying, we may stretch spaces below their natural width.
|
||||
|
@ -13,17 +13,17 @@ pub use self::box_::layout_box;
|
||||
use comemo::{Track, Tracked, TrackedMut};
|
||||
use typst_library::diag::SourceResult;
|
||||
use typst_library::engine::{Engine, Route, Sink, Traced};
|
||||
use typst_library::foundations::{StyleChain, StyleVec};
|
||||
use typst_library::introspection::{Introspector, Locator, LocatorLink};
|
||||
use typst_library::foundations::{Packed, StyleChain};
|
||||
use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator};
|
||||
use typst_library::layout::{Fragment, Size};
|
||||
use typst_library::model::ParElem;
|
||||
use typst_library::routines::Routines;
|
||||
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
|
||||
use typst_library::World;
|
||||
|
||||
use self::collect::{collect, Item, Segment, SpanMapper};
|
||||
use self::deco::decorate;
|
||||
use self::finalize::finalize;
|
||||
use self::line::{commit, line, Line};
|
||||
use self::line::{apply_baseline_shift, commit, line, Line};
|
||||
use self::linebreak::{linebreak, Breakpoint};
|
||||
use self::prepare::{prepare, Preparation};
|
||||
use self::shaping::{
|
||||
@ -34,18 +34,18 @@ use self::shaping::{
|
||||
/// Range of a substring of text.
|
||||
type Range = std::ops::Range<usize>;
|
||||
|
||||
/// Layouts content inline.
|
||||
pub fn layout_inline(
|
||||
/// Layouts the paragraph.
|
||||
pub fn layout_par(
|
||||
elem: &Packed<ParElem>,
|
||||
engine: &mut Engine,
|
||||
children: &StyleVec,
|
||||
locator: Locator,
|
||||
styles: StyleChain,
|
||||
consecutive: bool,
|
||||
region: Size,
|
||||
expand: bool,
|
||||
situation: ParSituation,
|
||||
) -> SourceResult<Fragment> {
|
||||
layout_inline_impl(
|
||||
children,
|
||||
layout_par_impl(
|
||||
elem,
|
||||
engine.routines,
|
||||
engine.world,
|
||||
engine.introspector,
|
||||
@ -54,17 +54,17 @@ pub fn layout_inline(
|
||||
engine.route.track(),
|
||||
locator.track(),
|
||||
styles,
|
||||
consecutive,
|
||||
region,
|
||||
expand,
|
||||
situation,
|
||||
)
|
||||
}
|
||||
|
||||
/// The internal, memoized implementation of `layout_inline`.
|
||||
/// The internal, memoized implementation of `layout_par`.
|
||||
#[comemo::memoize]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_inline_impl(
|
||||
children: &StyleVec,
|
||||
fn layout_par_impl(
|
||||
elem: &Packed<ParElem>,
|
||||
routines: &Routines,
|
||||
world: Tracked<dyn World + '_>,
|
||||
introspector: Tracked<Introspector>,
|
||||
@ -73,12 +73,12 @@ fn layout_inline_impl(
|
||||
route: Tracked<Route>,
|
||||
locator: Tracked<Locator>,
|
||||
styles: StyleChain,
|
||||
consecutive: bool,
|
||||
region: Size,
|
||||
expand: bool,
|
||||
situation: ParSituation,
|
||||
) -> SourceResult<Fragment> {
|
||||
let link = LocatorLink::new(locator);
|
||||
let locator = Locator::link(&link);
|
||||
let mut locator = Locator::link(&link).split();
|
||||
let mut engine = Engine {
|
||||
routines,
|
||||
world,
|
||||
@ -88,18 +88,63 @@ fn layout_inline_impl(
|
||||
route: Route::extend(route),
|
||||
};
|
||||
|
||||
let mut locator = locator.split();
|
||||
let arenas = Arenas::default();
|
||||
let children = (engine.routines.realize)(
|
||||
RealizationKind::LayoutPar,
|
||||
&mut engine,
|
||||
&mut locator,
|
||||
&arenas,
|
||||
&elem.body,
|
||||
styles,
|
||||
)?;
|
||||
|
||||
layout_inline(
|
||||
&mut engine,
|
||||
&children,
|
||||
&mut locator,
|
||||
styles,
|
||||
region,
|
||||
expand,
|
||||
Some(situation),
|
||||
)
|
||||
}
|
||||
|
||||
/// Lays out realized content with inline layout.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn layout_inline<'a>(
|
||||
engine: &mut Engine,
|
||||
children: &[Pair<'a>],
|
||||
locator: &mut SplitLocator<'a>,
|
||||
styles: StyleChain<'a>,
|
||||
region: Size,
|
||||
expand: bool,
|
||||
par: Option<ParSituation>,
|
||||
) -> SourceResult<Fragment> {
|
||||
// Collect all text into one string for BiDi analysis.
|
||||
let (text, segments, spans) =
|
||||
collect(children, &mut engine, &mut locator, &styles, region, consecutive)?;
|
||||
collect(children, engine, locator, styles, region, par)?;
|
||||
|
||||
// Perform BiDi analysis and then prepares paragraph layout.
|
||||
let p = prepare(&mut engine, children, &text, segments, spans, styles)?;
|
||||
// Perform BiDi analysis and performs some preparation steps before we
|
||||
// proceed to line breaking.
|
||||
let p = prepare(engine, children, &text, segments, spans, styles, par)?;
|
||||
|
||||
// Break the paragraph into lines.
|
||||
let lines = linebreak(&engine, &p, region.x - p.hang);
|
||||
// Break the text into lines.
|
||||
let lines = linebreak(engine, &p, region.x - p.hang);
|
||||
|
||||
// Turn the selected lines into frames.
|
||||
finalize(&mut engine, &p, &lines, styles, region, expand, &mut locator)
|
||||
finalize(engine, &p, &lines, styles, region, expand, locator)
|
||||
}
|
||||
|
||||
/// Distinguishes between a few different kinds of paragraphs.
|
||||
///
|
||||
/// In the form `Option<ParSituation>`, `None` implies that we are creating an
|
||||
/// inline layout that isn't a semantic paragraph.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum ParSituation {
|
||||
/// The paragraph is the first thing in the flow.
|
||||
First,
|
||||
/// The paragraph follows another paragraph.
|
||||
Consecutive,
|
||||
/// Any other kind of paragraph.
|
||||
Other,
|
||||
}
|
||||
|
@ -1,23 +1,26 @@
|
||||
use typst_library::foundations::{Resolve, Smart};
|
||||
use typst_library::layout::{Abs, AlignElem, Dir, Em, FixedAlignment};
|
||||
use typst_library::model::Linebreaks;
|
||||
use typst_library::routines::Pair;
|
||||
use typst_library::text::{Costs, Lang, TextElem};
|
||||
use typst_utils::SliceExt;
|
||||
use unicode_bidi::{BidiInfo, Level as BidiLevel};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A paragraph representation in which children are already layouted and text
|
||||
/// is already preshaped.
|
||||
/// A representation in which children are already layouted and text is already
|
||||
/// preshaped.
|
||||
///
|
||||
/// In many cases, we can directly reuse these results when constructing a line.
|
||||
/// Only when a line break falls onto a text index that is not safe-to-break per
|
||||
/// rustybuzz, we have to reshape that portion.
|
||||
pub struct Preparation<'a> {
|
||||
/// The paragraph's full text.
|
||||
/// The full text.
|
||||
pub text: &'a str,
|
||||
/// Bidirectional text embedding levels for the paragraph.
|
||||
/// Bidirectional text embedding levels.
|
||||
///
|
||||
/// This is `None` if the paragraph is BiDi-uniform (all the base direction).
|
||||
/// This is `None` if all text directions are uniform (all the base
|
||||
/// direction).
|
||||
pub bidi: Option<BidiInfo<'a>>,
|
||||
/// Text runs, spacing and layouted elements.
|
||||
pub items: Vec<(Range, Item<'a>)>,
|
||||
@ -33,15 +36,15 @@ pub struct Preparation<'a> {
|
||||
pub dir: Dir,
|
||||
/// The text language if it's the same for all children.
|
||||
pub lang: Option<Lang>,
|
||||
/// The paragraph's resolved horizontal alignment.
|
||||
/// The resolved horizontal alignment.
|
||||
pub align: FixedAlignment,
|
||||
/// Whether to justify the paragraph.
|
||||
/// Whether to justify text.
|
||||
pub justify: bool,
|
||||
/// The paragraph's hanging indent.
|
||||
/// Hanging indent to apply.
|
||||
pub hang: Abs,
|
||||
/// Whether to add spacing between CJK and Latin characters.
|
||||
pub cjk_latin_spacing: bool,
|
||||
/// Whether font fallback is enabled for this paragraph.
|
||||
/// Whether font fallback is enabled.
|
||||
pub fallback: bool,
|
||||
/// How to determine line breaks.
|
||||
pub linebreaks: Smart<Linebreaks>,
|
||||
@ -71,17 +74,18 @@ impl<'a> Preparation<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs BiDi analysis and then prepares paragraph layout by building a
|
||||
/// Performs BiDi analysis and then prepares further layout by building a
|
||||
/// representation on which we can do line breaking without layouting each and
|
||||
/// every line from scratch.
|
||||
#[typst_macros::time]
|
||||
pub fn prepare<'a>(
|
||||
engine: &mut Engine,
|
||||
children: &'a StyleVec,
|
||||
children: &[Pair<'a>],
|
||||
text: &'a str,
|
||||
segments: Vec<Segment<'a>>,
|
||||
spans: SpanMapper,
|
||||
styles: StyleChain<'a>,
|
||||
situation: Option<ParSituation>,
|
||||
) -> SourceResult<Preparation<'a>> {
|
||||
let dir = TextElem::dir_in(styles);
|
||||
let default_level = match dir {
|
||||
@ -125,19 +129,26 @@ pub fn prepare<'a>(
|
||||
add_cjk_latin_spacing(&mut items);
|
||||
}
|
||||
|
||||
// Only apply hanging indent to real paragraphs.
|
||||
let hang = if situation.is_some() {
|
||||
ParElem::hanging_indent_in(styles)
|
||||
} else {
|
||||
Abs::zero()
|
||||
};
|
||||
|
||||
Ok(Preparation {
|
||||
text,
|
||||
bidi: is_bidi.then_some(bidi),
|
||||
items,
|
||||
indices,
|
||||
spans,
|
||||
hyphenate: children.shared_get(styles, TextElem::hyphenate_in),
|
||||
hyphenate: shared_get(children, styles, TextElem::hyphenate_in),
|
||||
costs: TextElem::costs_in(styles),
|
||||
dir,
|
||||
lang: children.shared_get(styles, TextElem::lang_in),
|
||||
lang: shared_get(children, styles, TextElem::lang_in),
|
||||
align: AlignElem::alignment_in(styles).resolve(styles).x,
|
||||
justify: ParElem::justify_in(styles),
|
||||
hang: ParElem::hanging_indent_in(styles),
|
||||
hang,
|
||||
cjk_latin_spacing,
|
||||
fallback: TextElem::fallback_in(styles),
|
||||
linebreaks: ParElem::linebreaks_in(styles),
|
||||
@ -145,6 +156,19 @@ pub fn prepare<'a>(
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a style property, but only if it is the same for all of the children.
|
||||
fn shared_get<T: PartialEq>(
|
||||
children: &[Pair],
|
||||
styles: StyleChain<'_>,
|
||||
getter: fn(StyleChain) -> T,
|
||||
) -> Option<T> {
|
||||
let value = getter(styles);
|
||||
children
|
||||
.group_by_key(|&(_, s)| s)
|
||||
.all(|(s, _)| getter(s) == value)
|
||||
.then_some(value)
|
||||
}
|
||||
|
||||
/// Add some spacing between Han characters and western characters. See
|
||||
/// Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition
|
||||
/// in Horizontal Written Mode
|
||||
|
@ -20,6 +20,7 @@ use unicode_bidi::{BidiInfo, Level as BidiLevel};
|
||||
use unicode_script::{Script, UnicodeScript};
|
||||
|
||||
use super::{decorate, Item, Range, SpanMapper};
|
||||
use crate::modifiers::{FrameModifiers, FrameModify};
|
||||
|
||||
/// The result of shaping text.
|
||||
///
|
||||
@ -28,7 +29,7 @@ use super::{decorate, Item, Range, SpanMapper};
|
||||
/// frame.
|
||||
#[derive(Clone)]
|
||||
pub struct ShapedText<'a> {
|
||||
/// The start of the text in the full paragraph.
|
||||
/// The start of the text in the full text.
|
||||
pub base: usize,
|
||||
/// The text that was shaped.
|
||||
pub text: &'a str,
|
||||
@ -65,9 +66,9 @@ pub struct ShapedGlyph {
|
||||
pub y_offset: Em,
|
||||
/// The adjustability of the glyph.
|
||||
pub adjustability: Adjustability,
|
||||
/// The byte range of this glyph's cluster in the full paragraph. A cluster
|
||||
/// is a sequence of one or multiple glyphs that cannot be separated and
|
||||
/// must always be treated as a union.
|
||||
/// The byte range of this glyph's cluster in the full inline layout. A
|
||||
/// cluster is a sequence of one or multiple glyphs that cannot be separated
|
||||
/// and must always be treated as a union.
|
||||
///
|
||||
/// The range values of the glyphs in a [`ShapedText`] should not overlap
|
||||
/// with each other, and they should be monotonically increasing (for
|
||||
@ -326,6 +327,7 @@ impl<'a> ShapedText<'a> {
|
||||
offset += width;
|
||||
}
|
||||
|
||||
frame.modify(&FrameModifiers::get_in(self.styles));
|
||||
frame
|
||||
}
|
||||
|
||||
@ -403,7 +405,7 @@ impl<'a> ShapedText<'a> {
|
||||
/// Reshape a range of the shaped text, reusing information from this
|
||||
/// shaping process if possible.
|
||||
///
|
||||
/// The text `range` is relative to the whole paragraph.
|
||||
/// The text `range` is relative to the whole inline layout.
|
||||
pub fn reshape(&'a self, engine: &Engine, text_range: Range) -> ShapedText<'a> {
|
||||
let text = &self.text[text_range.start - self.base..text_range.end - self.base];
|
||||
if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
|
||||
|
@ -6,6 +6,7 @@ mod image;
|
||||
mod inline;
|
||||
mod lists;
|
||||
mod math;
|
||||
mod modifiers;
|
||||
mod pad;
|
||||
mod pages;
|
||||
mod repeat;
|
||||
@ -16,7 +17,6 @@ mod transforms;
|
||||
pub use self::flow::{layout_columns, layout_fragment, layout_frame};
|
||||
pub use self::grid::{layout_grid, layout_table};
|
||||
pub use self::image::layout_image;
|
||||
pub use self::inline::{layout_box, layout_inline};
|
||||
pub use self::lists::{layout_enum, layout_list};
|
||||
pub use self::math::{layout_equation_block, layout_equation_inline};
|
||||
pub use self::pad::layout_pad;
|
||||
|
@ -4,11 +4,12 @@ use typst_library::diag::SourceResult;
|
||||
use typst_library::engine::Engine;
|
||||
use typst_library::foundations::{Content, Context, Depth, Packed, StyleChain};
|
||||
use typst_library::introspection::Locator;
|
||||
use typst_library::layout::grid::resolve::{Cell, CellGrid};
|
||||
use typst_library::layout::{Axes, Fragment, HAlignment, Regions, Sizing, VAlignment};
|
||||
use typst_library::model::{EnumElem, ListElem, Numbering, ParElem};
|
||||
use typst_library::model::{EnumElem, ListElem, Numbering, ParElem, ParbreakElem};
|
||||
use typst_library::text::TextElem;
|
||||
|
||||
use crate::grid::{Cell, CellGrid, GridLayouter};
|
||||
use crate::grid::GridLayouter;
|
||||
|
||||
/// Layout the list.
|
||||
#[typst_macros::time(span = elem.span())]
|
||||
@ -21,8 +22,9 @@ pub fn layout_list(
|
||||
) -> SourceResult<Fragment> {
|
||||
let indent = elem.indent(styles);
|
||||
let body_indent = elem.body_indent(styles);
|
||||
let tight = elem.tight(styles);
|
||||
let gutter = elem.spacing(styles).unwrap_or_else(|| {
|
||||
if elem.tight(styles) {
|
||||
if tight {
|
||||
ParElem::leading_in(styles).into()
|
||||
} else {
|
||||
ParElem::spacing_in(styles).into()
|
||||
@ -39,12 +41,18 @@ pub fn layout_list(
|
||||
let mut cells = vec![];
|
||||
let mut locator = locator.split();
|
||||
|
||||
for item in elem.children() {
|
||||
for item in &elem.children {
|
||||
// Text in wide lists shall always turn into paragraphs.
|
||||
let mut body = item.body.clone();
|
||||
if !tight {
|
||||
body += ParbreakElem::shared();
|
||||
}
|
||||
|
||||
cells.push(Cell::new(Content::empty(), locator.next(&())));
|
||||
cells.push(Cell::new(marker.clone(), locator.next(&marker.span())));
|
||||
cells.push(Cell::new(Content::empty(), locator.next(&())));
|
||||
cells.push(Cell::new(
|
||||
item.body.clone().styled(ListElem::set_depth(Depth(1))),
|
||||
body.styled(ListElem::set_depth(Depth(1))),
|
||||
locator.next(&item.body.span()),
|
||||
));
|
||||
}
|
||||
@ -77,8 +85,9 @@ pub fn layout_enum(
|
||||
let reversed = elem.reversed(styles);
|
||||
let indent = elem.indent(styles);
|
||||
let body_indent = elem.body_indent(styles);
|
||||
let tight = elem.tight(styles);
|
||||
let gutter = elem.spacing(styles).unwrap_or_else(|| {
|
||||
if elem.tight(styles) {
|
||||
if tight {
|
||||
ParElem::leading_in(styles).into()
|
||||
} else {
|
||||
ParElem::spacing_in(styles).into()
|
||||
@ -100,7 +109,7 @@ pub fn layout_enum(
|
||||
// relation to the item it refers to.
|
||||
let number_align = elem.number_align(styles);
|
||||
|
||||
for item in elem.children() {
|
||||
for item in &elem.children {
|
||||
number = item.number(styles).unwrap_or(number);
|
||||
|
||||
let context = Context::new(None, Some(styles));
|
||||
@ -123,11 +132,17 @@ pub fn layout_enum(
|
||||
let resolved =
|
||||
resolved.aligned(number_align).styled(TextElem::set_overhang(false));
|
||||
|
||||
// Text in wide enums shall always turn into paragraphs.
|
||||
let mut body = item.body.clone();
|
||||
if !tight {
|
||||
body += ParbreakElem::shared();
|
||||
}
|
||||
|
||||
cells.push(Cell::new(Content::empty(), locator.next(&())));
|
||||
cells.push(Cell::new(resolved, locator.next(&())));
|
||||
cells.push(Cell::new(Content::empty(), locator.next(&())));
|
||||
cells.push(Cell::new(
|
||||
item.body.clone().styled(EnumElem::set_parents(smallvec![number])),
|
||||
body.styled(EnumElem::set_parents(smallvec![number])),
|
||||
locator.next(&item.body.span()),
|
||||
));
|
||||
number =
|
||||
|
@ -16,7 +16,7 @@ pub fn layout_accent(
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let cramped = style_cramped();
|
||||
let mut base = ctx.layout_into_fragment(elem.base(), styles.chain(&cramped))?;
|
||||
let mut base = ctx.layout_into_fragment(&elem.base, styles.chain(&cramped))?;
|
||||
|
||||
// Try to replace a glyph with its dotless variant.
|
||||
if let MathFragment::Glyph(glyph) = &mut base {
|
||||
@ -29,8 +29,8 @@ pub fn layout_accent(
|
||||
|
||||
let width = elem.size(styles).relative_to(base.width());
|
||||
|
||||
let Accent(c) = elem.accent();
|
||||
let mut glyph = GlyphFragment::new(ctx, styles, *c, elem.span());
|
||||
let Accent(c) = elem.accent;
|
||||
let mut glyph = GlyphFragment::new(ctx, styles, c, elem.span());
|
||||
|
||||
// Try to replace accent glyph with flattened variant.
|
||||
let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height);
|
||||
|
@ -1,10 +1,9 @@
|
||||
use typst_library::diag::SourceResult;
|
||||
use typst_library::foundations::{Packed, StyleChain};
|
||||
use typst_library::foundations::{Packed, StyleChain, SymbolElem};
|
||||
use typst_library::layout::{Abs, Axis, Corner, Frame, Point, Rel, Size};
|
||||
use typst_library::math::{
|
||||
AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem,
|
||||
};
|
||||
use typst_library::text::TextElem;
|
||||
use typst_utils::OptionExt;
|
||||
|
||||
use super::{
|
||||
@ -29,7 +28,7 @@ pub fn layout_attach(
|
||||
let elem = merged.as_ref().unwrap_or(elem);
|
||||
let stretch = stretch_size(styles, elem);
|
||||
|
||||
let mut base = ctx.layout_into_fragment(elem.base(), styles)?;
|
||||
let mut base = ctx.layout_into_fragment(&elem.base, styles)?;
|
||||
let sup_style = style_for_superscript(styles);
|
||||
let sup_style_chain = styles.chain(&sup_style);
|
||||
let tl = elem.tl(sup_style_chain);
|
||||
@ -95,7 +94,7 @@ pub fn layout_primes(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
match *elem.count() {
|
||||
match elem.count {
|
||||
count @ 1..=4 => {
|
||||
let c = match count {
|
||||
1 => '′',
|
||||
@ -104,13 +103,14 @@ pub fn layout_primes(
|
||||
4 => '⁗',
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let f = ctx.layout_into_fragment(&TextElem::packed(c), styles)?;
|
||||
let f = ctx.layout_into_fragment(&SymbolElem::packed(c), styles)?;
|
||||
ctx.push(f);
|
||||
}
|
||||
count => {
|
||||
// Custom amount of primes
|
||||
let prime =
|
||||
ctx.layout_into_fragment(&TextElem::packed('′'), styles)?.into_frame();
|
||||
let prime = ctx
|
||||
.layout_into_fragment(&SymbolElem::packed('′'), styles)?
|
||||
.into_frame();
|
||||
let width = prime.width() * (count + 1) as f64 / 2.0;
|
||||
let mut frame = Frame::soft(Size::new(width, prime.height()));
|
||||
frame.set_baseline(prime.ascent());
|
||||
@ -134,7 +134,7 @@ pub fn layout_scripts(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?;
|
||||
let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
|
||||
fragment.set_limits(Limits::Never);
|
||||
ctx.push(fragment);
|
||||
Ok(())
|
||||
@ -148,7 +148,7 @@ pub fn layout_limits(
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let limits = if elem.inline(styles) { Limits::Always } else { Limits::Display };
|
||||
let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?;
|
||||
let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
|
||||
fragment.set_limits(limits);
|
||||
ctx.push(fragment);
|
||||
Ok(())
|
||||
@ -157,9 +157,9 @@ pub fn layout_limits(
|
||||
/// Get the size to stretch the base to.
|
||||
fn stretch_size(styles: StyleChain, elem: &Packed<AttachElem>) -> Option<Rel<Abs>> {
|
||||
// Extract from an EquationElem.
|
||||
let mut base = elem.base();
|
||||
let mut base = &elem.base;
|
||||
while let Some(equation) = base.to_packed::<EquationElem>() {
|
||||
base = equation.body();
|
||||
base = &equation.body;
|
||||
}
|
||||
|
||||
base.to_packed::<StretchElem>().map(|stretch| stretch.size(styles))
|
||||
|
@ -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();
|
||||
|
@ -1,5 +1,5 @@
|
||||
use typst_library::diag::SourceResult;
|
||||
use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
|
||||
use typst_library::foundations::{Content, Packed, Resolve, StyleChain, SymbolElem};
|
||||
use typst_library::layout::{Em, Frame, FrameItem, Point, Size};
|
||||
use typst_library::math::{BinomElem, FracElem};
|
||||
use typst_library::text::TextElem;
|
||||
@ -23,8 +23,8 @@ pub fn layout_frac(
|
||||
layout_frac_like(
|
||||
ctx,
|
||||
styles,
|
||||
elem.num(),
|
||||
std::slice::from_ref(elem.denom()),
|
||||
&elem.num,
|
||||
std::slice::from_ref(&elem.denom),
|
||||
false,
|
||||
elem.span(),
|
||||
)
|
||||
@ -37,7 +37,7 @@ pub fn layout_binom(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
layout_frac_like(ctx, styles, elem.upper(), elem.lower(), true, elem.span())
|
||||
layout_frac_like(ctx, styles, &elem.upper, &elem.lower, true, elem.span())
|
||||
}
|
||||
|
||||
/// Layout a fraction or binomial.
|
||||
@ -80,7 +80,10 @@ fn layout_frac_like(
|
||||
let denom = ctx.layout_into_frame(
|
||||
&Content::sequence(
|
||||
// Add a comma between each element.
|
||||
denom.iter().flat_map(|a| [TextElem::packed(','), a.clone()]).skip(1),
|
||||
denom
|
||||
.iter()
|
||||
.flat_map(|a| [SymbolElem::packed(','), a.clone()])
|
||||
.skip(1),
|
||||
),
|
||||
styles.chain(&denom_style),
|
||||
)?;
|
||||
|
@ -1,23 +1,22 @@
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
|
||||
use rustybuzz::Feature;
|
||||
use smallvec::SmallVec;
|
||||
use ttf_parser::gsub::{AlternateSubstitution, SingleSubstitution, SubstitutionSubtable};
|
||||
use ttf_parser::opentype_layout::LayoutTable;
|
||||
use ttf_parser::{GlyphId, Rect};
|
||||
use typst_library::foundations::StyleChain;
|
||||
use typst_library::introspection::Tag;
|
||||
use typst_library::layout::{
|
||||
Abs, Axis, Corner, Em, Frame, FrameItem, HideElem, Point, Size, VAlignment,
|
||||
Abs, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment,
|
||||
};
|
||||
use typst_library::math::{EquationElem, MathSize};
|
||||
use typst_library::model::{Destination, LinkElem};
|
||||
use typst_library::text::{Font, Glyph, Lang, Region, TextElem, TextItem};
|
||||
use typst_library::visualize::Paint;
|
||||
use typst_syntax::Span;
|
||||
use unicode_math_class::MathClass;
|
||||
|
||||
use super::{stretch_glyph, MathContext, Scaled};
|
||||
use crate::modifiers::{FrameModifiers, FrameModify};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MathFragment {
|
||||
@ -245,8 +244,7 @@ pub struct GlyphFragment {
|
||||
pub class: MathClass,
|
||||
pub math_size: MathSize,
|
||||
pub span: Span,
|
||||
pub dests: SmallVec<[Destination; 1]>,
|
||||
pub hidden: bool,
|
||||
pub modifiers: FrameModifiers,
|
||||
pub limits: Limits,
|
||||
pub extended_shape: bool,
|
||||
}
|
||||
@ -302,8 +300,7 @@ impl GlyphFragment {
|
||||
accent_attach: Abs::zero(),
|
||||
class,
|
||||
span,
|
||||
dests: LinkElem::dests_in(styles),
|
||||
hidden: HideElem::hidden_in(styles),
|
||||
modifiers: FrameModifiers::get_in(styles),
|
||||
extended_shape: false,
|
||||
};
|
||||
fragment.set_id(ctx, id);
|
||||
@ -390,7 +387,7 @@ impl GlyphFragment {
|
||||
let mut frame = Frame::soft(size);
|
||||
frame.set_baseline(self.ascent);
|
||||
frame.push(Point::with_y(self.ascent + self.shift), FrameItem::Text(item));
|
||||
frame.post_process_raw(self.dests, self.hidden);
|
||||
frame.modify(&self.modifiers);
|
||||
frame
|
||||
}
|
||||
|
||||
@ -516,7 +513,7 @@ impl FrameFragment {
|
||||
let base_ascent = frame.ascent();
|
||||
let accent_attach = frame.width() / 2.0;
|
||||
Self {
|
||||
frame: frame.post_processed(styles),
|
||||
frame: frame.modified(&FrameModifiers::get_in(styles)),
|
||||
font_size: TextElem::size_in(styles),
|
||||
class: EquationElem::class_in(styles).unwrap_or(MathClass::Normal),
|
||||
math_size: EquationElem::size_in(styles),
|
||||
|
@ -2,6 +2,7 @@ use typst_library::diag::SourceResult;
|
||||
use typst_library::foundations::{Packed, StyleChain};
|
||||
use typst_library::layout::{Abs, Axis, Rel};
|
||||
use typst_library::math::{EquationElem, LrElem, MidElem};
|
||||
use typst_utils::SliceExt;
|
||||
use unicode_math_class::MathClass;
|
||||
|
||||
use super::{stretch_fragment, MathContext, MathFragment, DELIM_SHORT_FALL};
|
||||
@ -13,32 +14,23 @@ pub fn layout_lr(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let mut body = elem.body();
|
||||
|
||||
// Extract from an EquationElem.
|
||||
let mut body = &elem.body;
|
||||
if let Some(equation) = body.to_packed::<EquationElem>() {
|
||||
body = equation.body();
|
||||
body = &equation.body;
|
||||
}
|
||||
|
||||
// Extract implicit LrElem.
|
||||
if let Some(lr) = body.to_packed::<LrElem>() {
|
||||
if lr.size(styles).is_one() {
|
||||
body = lr.body();
|
||||
body = &lr.body;
|
||||
}
|
||||
}
|
||||
|
||||
let mut fragments = ctx.layout_into_fragments(body, styles)?;
|
||||
|
||||
// Ignore leading and trailing ignorant fragments.
|
||||
let start_idx = fragments
|
||||
.iter()
|
||||
.position(|f| !f.is_ignorant())
|
||||
.unwrap_or(fragments.len());
|
||||
let end_idx = fragments
|
||||
.iter()
|
||||
.skip(start_idx)
|
||||
.rposition(|f| !f.is_ignorant())
|
||||
.map_or(start_idx, |i| start_idx + i + 1);
|
||||
let (start_idx, end_idx) = fragments.split_prefix_suffix(|f| f.is_ignorant());
|
||||
let inner_fragments = &mut fragments[start_idx..end_idx];
|
||||
|
||||
let axis = scaled!(ctx, styles, axis_height);
|
||||
@ -100,7 +92,7 @@ pub fn layout_mid(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let mut fragments = ctx.layout_into_fragments(elem.body(), styles)?;
|
||||
let mut fragments = ctx.layout_into_fragments(&elem.body, styles)?;
|
||||
|
||||
for fragment in &mut fragments {
|
||||
match fragment {
|
||||
|
@ -27,7 +27,7 @@ pub fn layout_vec(
|
||||
let frame = layout_vec_body(
|
||||
ctx,
|
||||
styles,
|
||||
elem.children(),
|
||||
&elem.children,
|
||||
elem.align(styles),
|
||||
elem.gap(styles),
|
||||
LeftRightAlternator::Right,
|
||||
@ -44,7 +44,7 @@ pub fn layout_mat(
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let augment = elem.augment(styles);
|
||||
let rows = elem.rows();
|
||||
let rows = &elem.rows;
|
||||
|
||||
if let Some(aug) = &augment {
|
||||
for &offset in &aug.hline.0 {
|
||||
@ -58,7 +58,7 @@ pub fn layout_mat(
|
||||
}
|
||||
}
|
||||
|
||||
let ncols = elem.rows().first().map_or(0, |row| row.len());
|
||||
let ncols = rows.first().map_or(0, |row| row.len());
|
||||
|
||||
for &offset in &aug.vline.0 {
|
||||
if offset == 0 || offset.unsigned_abs() >= ncols {
|
||||
@ -97,7 +97,7 @@ pub fn layout_cases(
|
||||
let frame = layout_vec_body(
|
||||
ctx,
|
||||
styles,
|
||||
elem.children(),
|
||||
&elem.children,
|
||||
FixedAlignment::Start,
|
||||
elem.gap(styles),
|
||||
LeftRightAlternator::None,
|
||||
|
@ -17,7 +17,9 @@ use rustybuzz::Feature;
|
||||
use ttf_parser::Tag;
|
||||
use typst_library::diag::{bail, SourceResult};
|
||||
use typst_library::engine::Engine;
|
||||
use typst_library::foundations::{Content, NativeElement, Packed, Resolve, StyleChain};
|
||||
use typst_library::foundations::{
|
||||
Content, NativeElement, Packed, Resolve, StyleChain, SymbolElem,
|
||||
};
|
||||
use typst_library::introspection::{Counter, Locator, SplitLocator, TagElem};
|
||||
use typst_library::layout::{
|
||||
Abs, AlignElem, Axes, BlockElem, BoxElem, Em, FixedAlignment, Fragment, Frame, HElem,
|
||||
@ -200,8 +202,7 @@ pub fn layout_equation_block(
|
||||
let counter = Counter::of(EquationElem::elem())
|
||||
.display_at_loc(engine, elem.location().unwrap(), styles, numbering)?
|
||||
.spanned(span);
|
||||
let number =
|
||||
(engine.routines.layout_frame)(engine, &counter, locator.next(&()), styles, pod)?;
|
||||
let number = crate::layout_frame(engine, &counter, locator.next(&()), styles, pod)?;
|
||||
|
||||
static NUMBER_GUTTER: Em = Em::new(0.5);
|
||||
let full_number_width = number.width() + NUMBER_GUTTER.resolve(styles);
|
||||
@ -535,6 +536,8 @@ fn layout_realized(
|
||||
layout_h(elem, ctx, styles)?;
|
||||
} else if let Some(elem) = elem.to_packed::<TextElem>() {
|
||||
self::text::layout_text(elem, ctx, styles)?;
|
||||
} else if let Some(elem) = elem.to_packed::<SymbolElem>() {
|
||||
self::text::layout_symbol(elem, ctx, styles)?;
|
||||
} else if let Some(elem) = elem.to_packed::<BoxElem>() {
|
||||
layout_box(elem, ctx, styles)?;
|
||||
} else if elem.is::<AlignPointElem>() {
|
||||
@ -615,7 +618,7 @@ fn layout_box(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let frame = (ctx.engine.routines.layout_box)(
|
||||
let frame = crate::inline::layout_box(
|
||||
elem,
|
||||
ctx.engine,
|
||||
ctx.locator.next(&elem.span()),
|
||||
@ -632,7 +635,7 @@ fn layout_h(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
if let Spacing::Rel(rel) = elem.amount() {
|
||||
if let Spacing::Rel(rel) = elem.amount {
|
||||
if rel.rel.is_zero() {
|
||||
ctx.push(MathFragment::Spacing(rel.abs.resolve(styles), elem.weak(styles)));
|
||||
}
|
||||
@ -647,11 +650,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(())
|
||||
}
|
||||
@ -663,7 +665,7 @@ fn layout_op(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let fragment = ctx.layout_into_fragment(elem.text(), styles)?;
|
||||
let fragment = ctx.layout_into_fragment(&elem.text, styles)?;
|
||||
let italics = fragment.italics_correction();
|
||||
let accent_attach = fragment.accent_attach();
|
||||
let text_like = fragment.is_text_like();
|
||||
@ -689,7 +691,7 @@ fn layout_external(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<Frame> {
|
||||
(ctx.engine.routines.layout_frame)(
|
||||
crate::layout_frame(
|
||||
ctx.engine,
|
||||
content,
|
||||
ctx.locator.next(&content.span()),
|
||||
|
@ -18,7 +18,6 @@ pub fn layout_root(
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let index = elem.index(styles);
|
||||
let radicand = elem.radicand();
|
||||
let span = elem.span();
|
||||
|
||||
let gap = scaled!(
|
||||
@ -36,7 +35,7 @@ pub fn layout_root(
|
||||
let radicand = {
|
||||
let cramped = style_cramped();
|
||||
let styles = styles.chain(&cramped);
|
||||
let run = ctx.layout_into_run(radicand, styles)?;
|
||||
let run = ctx.layout_into_run(&elem.radicand, styles)?;
|
||||
let multiline = run.is_multiline();
|
||||
let mut radicand = run.into_fragment(styles).into_frame();
|
||||
if multiline {
|
||||
|
@ -10,6 +10,7 @@ use super::{
|
||||
delimiter_alignment, GlyphFragment, MathContext, MathFragment, Scaled,
|
||||
VariantFragment,
|
||||
};
|
||||
use crate::modifiers::FrameModify;
|
||||
|
||||
/// Maximum number of times extenders can be repeated.
|
||||
const MAX_REPEATS: usize = 1024;
|
||||
@ -21,7 +22,7 @@ pub fn layout_stretch(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?;
|
||||
let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
|
||||
stretch_fragment(
|
||||
ctx,
|
||||
styles,
|
||||
@ -265,7 +266,7 @@ fn assemble(
|
||||
let mut frame = Frame::soft(size);
|
||||
let mut offset = Abs::zero();
|
||||
frame.set_baseline(baseline);
|
||||
frame.post_process_raw(base.dests, base.hidden);
|
||||
frame.modify(&base.modifiers);
|
||||
|
||||
for (fragment, advance) in selected {
|
||||
let pos = match axis {
|
||||
|
@ -1,8 +1,8 @@
|
||||
use std::f64::consts::SQRT_2;
|
||||
|
||||
use ecow::{eco_vec, EcoString};
|
||||
use ecow::EcoString;
|
||||
use typst_library::diag::SourceResult;
|
||||
use typst_library::foundations::{Packed, StyleChain, StyleVec};
|
||||
use typst_library::foundations::{Packed, StyleChain, SymbolElem};
|
||||
use typst_library::layout::{Abs, Size};
|
||||
use typst_library::math::{EquationElem, MathSize, MathVariant};
|
||||
use typst_library::text::{
|
||||
@ -20,56 +20,68 @@ pub fn layout_text(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let text = elem.text();
|
||||
let text = &elem.text;
|
||||
let span = elem.span();
|
||||
let mut chars = text.chars();
|
||||
let math_size = EquationElem::size_in(styles);
|
||||
let mut dtls = ctx.dtls_table.is_some();
|
||||
let fragment: MathFragment = if let Some(mut glyph) = chars
|
||||
.next()
|
||||
.filter(|_| chars.next().is_none())
|
||||
.map(|c| dtls_char(c, &mut dtls))
|
||||
.map(|c| styled_char(styles, c, true))
|
||||
.and_then(|c| GlyphFragment::try_new(ctx, styles, c, span))
|
||||
{
|
||||
// A single letter that is available in the math font.
|
||||
if dtls {
|
||||
glyph.make_dotless_form(ctx);
|
||||
}
|
||||
let fragment = if text.contains(is_newline) {
|
||||
layout_text_lines(text.split(is_newline), span, ctx, styles)?
|
||||
} else {
|
||||
layout_inline_text(text, span, ctx, styles)?
|
||||
};
|
||||
ctx.push(fragment);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
match math_size {
|
||||
MathSize::Script => {
|
||||
glyph.make_script_size(ctx);
|
||||
}
|
||||
MathSize::ScriptScript => {
|
||||
glyph.make_script_script_size(ctx);
|
||||
}
|
||||
_ => (),
|
||||
/// Layout multiple lines of text.
|
||||
fn layout_text_lines<'a>(
|
||||
lines: impl Iterator<Item = &'a str>,
|
||||
span: Span,
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<FrameFragment> {
|
||||
let mut fragments = vec![];
|
||||
for (i, line) in lines.enumerate() {
|
||||
if i != 0 {
|
||||
fragments.push(MathFragment::Linebreak);
|
||||
}
|
||||
if !line.is_empty() {
|
||||
fragments.push(layout_inline_text(line, span, ctx, styles)?.into());
|
||||
}
|
||||
}
|
||||
let mut frame = MathRun::new(fragments).into_frame(styles);
|
||||
let axis = scaled!(ctx, styles, axis_height);
|
||||
frame.set_baseline(frame.height() / 2.0 + axis);
|
||||
Ok(FrameFragment::new(styles, frame))
|
||||
}
|
||||
|
||||
if glyph.class == MathClass::Large {
|
||||
let mut variant = if math_size == MathSize::Display {
|
||||
let height = scaled!(ctx, styles, display_operator_min_height)
|
||||
.max(SQRT_2 * glyph.height());
|
||||
glyph.stretch_vertical(ctx, height, Abs::zero())
|
||||
} else {
|
||||
glyph.into_variant()
|
||||
};
|
||||
// TeXbook p 155. Large operators are always vertically centered on the axis.
|
||||
variant.center_on_axis(ctx);
|
||||
variant.into()
|
||||
} else {
|
||||
glyph.into()
|
||||
}
|
||||
} else if text.chars().all(|c| c.is_ascii_digit() || c == '.') {
|
||||
// Numbers aren't that difficult.
|
||||
/// Layout the given text string into a [`FrameFragment`] after styling all
|
||||
/// characters for the math font (without auto-italics).
|
||||
fn layout_inline_text(
|
||||
text: &str,
|
||||
span: Span,
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<FrameFragment> {
|
||||
if text.chars().all(|c| c.is_ascii_digit() || c == '.') {
|
||||
// Small optimization for numbers. Note that this lays out slightly
|
||||
// differently to normal text and is worth re-evaluating in the future.
|
||||
let mut fragments = vec![];
|
||||
for c in text.chars() {
|
||||
let c = styled_char(styles, c, false);
|
||||
fragments.push(GlyphFragment::new(ctx, styles, c, span).into());
|
||||
let is_single = text.chars().count() == 1;
|
||||
for unstyled_c in text.chars() {
|
||||
let c = styled_char(styles, unstyled_c, false);
|
||||
let mut glyph = GlyphFragment::new(ctx, styles, c, span);
|
||||
if is_single {
|
||||
// Duplicate what `layout_glyph` does exactly even if it's
|
||||
// probably incorrect here.
|
||||
match EquationElem::size_in(styles) {
|
||||
MathSize::Script => glyph.make_script_size(ctx),
|
||||
MathSize::ScriptScript => glyph.make_script_script_size(ctx),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
fragments.push(glyph.into());
|
||||
}
|
||||
let frame = MathRun::new(fragments).into_frame(styles);
|
||||
FrameFragment::new(styles, frame).with_text_like(true).into()
|
||||
Ok(FrameFragment::new(styles, frame).with_text_like(true))
|
||||
} else {
|
||||
let local = [
|
||||
TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds)),
|
||||
@ -77,64 +89,97 @@ pub fn layout_text(
|
||||
]
|
||||
.map(|p| p.wrap());
|
||||
|
||||
// Anything else is handled by Typst's standard text layout.
|
||||
let styles = styles.chain(&local);
|
||||
let text: EcoString =
|
||||
let styled_text: EcoString =
|
||||
text.chars().map(|c| styled_char(styles, c, false)).collect();
|
||||
if text.contains(is_newline) {
|
||||
let mut fragments = vec![];
|
||||
for (i, piece) in text.split(is_newline).enumerate() {
|
||||
if i != 0 {
|
||||
fragments.push(MathFragment::Linebreak);
|
||||
}
|
||||
if !piece.is_empty() {
|
||||
fragments.push(layout_complex_text(piece, ctx, span, styles)?.into());
|
||||
}
|
||||
}
|
||||
let mut frame = MathRun::new(fragments).into_frame(styles);
|
||||
let axis = scaled!(ctx, styles, axis_height);
|
||||
frame.set_baseline(frame.height() / 2.0 + axis);
|
||||
FrameFragment::new(styles, frame).into()
|
||||
} else {
|
||||
layout_complex_text(&text, ctx, span, styles)?.into()
|
||||
|
||||
let spaced = styled_text.graphemes(true).nth(1).is_some();
|
||||
let elem = TextElem::packed(styled_text).spanned(span);
|
||||
|
||||
// There isn't a natural width for a paragraph in a math environment;
|
||||
// because it will be placed somewhere probably not at the left margin
|
||||
// it will overflow. So emulate an `hbox` instead and allow the
|
||||
// paragraph to extend as far as needed.
|
||||
let frame = crate::inline::layout_inline(
|
||||
ctx.engine,
|
||||
&[(&elem, styles)],
|
||||
&mut ctx.locator.next(&span).split(),
|
||||
styles,
|
||||
Size::splat(Abs::inf()),
|
||||
false,
|
||||
None,
|
||||
)?
|
||||
.into_frame();
|
||||
|
||||
Ok(FrameFragment::new(styles, frame)
|
||||
.with_class(MathClass::Alphabetic)
|
||||
.with_text_like(true)
|
||||
.with_spaced(spaced))
|
||||
}
|
||||
}
|
||||
|
||||
/// Layout a single character in the math font with the correct styling applied
|
||||
/// (includes auto-italics).
|
||||
pub fn layout_symbol(
|
||||
elem: &Packed<SymbolElem>,
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
// Switch dotless char to normal when we have the dtls OpenType feature.
|
||||
// This should happen before the main styling pass.
|
||||
let (unstyled_c, dtls) = match try_dotless(elem.text) {
|
||||
Some(c) if ctx.dtls_table.is_some() => (c, true),
|
||||
_ => (elem.text, false),
|
||||
};
|
||||
let c = styled_char(styles, unstyled_c, true);
|
||||
let fragment = match GlyphFragment::try_new(ctx, styles, c, elem.span()) {
|
||||
Some(glyph) => layout_glyph(glyph, dtls, ctx, styles),
|
||||
None => {
|
||||
// Not in the math font, fallback to normal inline text layout.
|
||||
layout_inline_text(c.encode_utf8(&mut [0; 4]), elem.span(), ctx, styles)?
|
||||
.into()
|
||||
}
|
||||
};
|
||||
|
||||
ctx.push(fragment);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Layout the given text string into a [`FrameFragment`].
|
||||
fn layout_complex_text(
|
||||
text: &str,
|
||||
/// Layout a [`GlyphFragment`].
|
||||
fn layout_glyph(
|
||||
mut glyph: GlyphFragment,
|
||||
dtls: bool,
|
||||
ctx: &mut MathContext,
|
||||
span: Span,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<FrameFragment> {
|
||||
// There isn't a natural width for a paragraph in a math environment;
|
||||
// because it will be placed somewhere probably not at the left margin
|
||||
// it will overflow. So emulate an `hbox` instead and allow the paragraph
|
||||
// to extend as far as needed.
|
||||
let spaced = text.graphemes(true).nth(1).is_some();
|
||||
let elem = TextElem::packed(text).spanned(span);
|
||||
let frame = (ctx.engine.routines.layout_inline)(
|
||||
ctx.engine,
|
||||
&StyleVec::wrap(eco_vec![elem]),
|
||||
ctx.locator.next(&span),
|
||||
styles,
|
||||
false,
|
||||
Size::splat(Abs::inf()),
|
||||
false,
|
||||
)?
|
||||
.into_frame();
|
||||
) -> MathFragment {
|
||||
if dtls {
|
||||
glyph.make_dotless_form(ctx);
|
||||
}
|
||||
let math_size = EquationElem::size_in(styles);
|
||||
match math_size {
|
||||
MathSize::Script => glyph.make_script_size(ctx),
|
||||
MathSize::ScriptScript => glyph.make_script_script_size(ctx),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(FrameFragment::new(styles, frame)
|
||||
.with_class(MathClass::Alphabetic)
|
||||
.with_text_like(true)
|
||||
.with_spaced(spaced))
|
||||
if glyph.class == MathClass::Large {
|
||||
let mut variant = if math_size == MathSize::Display {
|
||||
let height = scaled!(ctx, styles, display_operator_min_height)
|
||||
.max(SQRT_2 * glyph.height());
|
||||
glyph.stretch_vertical(ctx, height, Abs::zero())
|
||||
} else {
|
||||
glyph.into_variant()
|
||||
};
|
||||
// TeXbook p 155. Large operators are always vertically centered on the
|
||||
// axis.
|
||||
variant.center_on_axis(ctx);
|
||||
variant.into()
|
||||
} else {
|
||||
glyph.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Select the correct styled math letter.
|
||||
/// Style the character by selecting the unicode codepoint for italic, bold,
|
||||
/// caligraphic, etc.
|
||||
///
|
||||
/// <https://www.w3.org/TR/mathml-core/#new-text-transform-mappings>
|
||||
/// <https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols>
|
||||
@ -353,15 +398,12 @@ fn greek_exception(
|
||||
})
|
||||
}
|
||||
|
||||
/// Switch dotless character to non dotless character for use of the dtls
|
||||
/// OpenType feature.
|
||||
pub fn dtls_char(c: char, dtls: &mut bool) -> char {
|
||||
match (c, *dtls) {
|
||||
('ı', true) => 'i',
|
||||
('ȷ', true) => 'j',
|
||||
_ => {
|
||||
*dtls = false;
|
||||
c
|
||||
}
|
||||
/// The non-dotless version of a dotless character that can be used with the
|
||||
/// `dtls` OpenType feature.
|
||||
pub fn try_dotless(c: char) -> Option<char> {
|
||||
match c {
|
||||
'ı' => Some('i'),
|
||||
'ȷ' => Some('j'),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
110
crates/typst-layout/src/modifiers.rs
Normal file
110
crates/typst-layout/src/modifiers.rs
Normal file
@ -0,0 +1,110 @@
|
||||
use typst_library::foundations::StyleChain;
|
||||
use typst_library::layout::{Fragment, Frame, FrameItem, HideElem, Point};
|
||||
use typst_library::model::{Destination, LinkElem};
|
||||
|
||||
/// Frame-level modifications resulting from styles that do not impose any
|
||||
/// layout structure.
|
||||
///
|
||||
/// These are always applied at the highest level of style uniformity.
|
||||
/// Consequently, they must be applied by all layouters that manually manage
|
||||
/// styles of their children (because they can produce children with varying
|
||||
/// styles). This currently includes flow, inline, and math layout.
|
||||
///
|
||||
/// Other layouters don't manually need to handle it because their parents that
|
||||
/// result from realization will take care of it and the styles can only apply
|
||||
/// to them as a whole, not part of it (since they don't manage styles).
|
||||
///
|
||||
/// Currently existing frame modifiers are:
|
||||
/// - `HideElem::hidden`
|
||||
/// - `LinkElem::dests`
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FrameModifiers {
|
||||
/// A destination to link to.
|
||||
dest: Option<Destination>,
|
||||
/// Whether the contents of the frame should be hidden.
|
||||
hidden: bool,
|
||||
}
|
||||
|
||||
impl FrameModifiers {
|
||||
/// Retrieve all modifications that should be applied per-frame.
|
||||
pub fn get_in(styles: StyleChain) -> Self {
|
||||
Self {
|
||||
dest: LinkElem::current_in(styles),
|
||||
hidden: HideElem::hidden_in(styles),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies [`FrameModifiers`].
|
||||
pub trait FrameModify {
|
||||
/// Apply the modifiers in-place.
|
||||
fn modify(&mut self, modifiers: &FrameModifiers);
|
||||
|
||||
/// Apply the modifiers, and return the modified result.
|
||||
fn modified(mut self, modifiers: &FrameModifiers) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.modify(modifiers);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl FrameModify for Frame {
|
||||
fn modify(&mut self, modifiers: &FrameModifiers) {
|
||||
if let Some(dest) = &modifiers.dest {
|
||||
let size = self.size();
|
||||
self.push(Point::zero(), FrameItem::Link(dest.clone(), size));
|
||||
}
|
||||
|
||||
if modifiers.hidden {
|
||||
self.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FrameModify for Fragment {
|
||||
fn modify(&mut self, modifiers: &FrameModifiers) {
|
||||
for frame in self.iter_mut() {
|
||||
frame.modify(modifiers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> FrameModify for Result<T, E>
|
||||
where
|
||||
T: FrameModify,
|
||||
{
|
||||
fn modify(&mut self, props: &FrameModifiers) {
|
||||
if let Ok(inner) = self {
|
||||
inner.modify(props);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs layout and modification in one step.
|
||||
///
|
||||
/// This just runs `layout(styles).modified(&FrameModifiers::get_in(styles))`,
|
||||
/// but with the additional step that redundant modifiers (which are already
|
||||
/// applied here) are removed from the `styles` passed to `layout`. This is used
|
||||
/// for the layout of containers like `block`.
|
||||
pub fn layout_and_modify<F, R>(styles: StyleChain, layout: F) -> R
|
||||
where
|
||||
F: FnOnce(StyleChain) -> R,
|
||||
R: FrameModify,
|
||||
{
|
||||
let modifiers = FrameModifiers::get_in(styles);
|
||||
|
||||
// Disable the current link internally since it's already applied at this
|
||||
// level of layout. This means we don't generate redundant nested links,
|
||||
// which may bloat the output considerably.
|
||||
let reset;
|
||||
let outer = styles;
|
||||
let mut styles = styles;
|
||||
if modifiers.dest.is_some() {
|
||||
reset = LinkElem::set_current(None).wrap();
|
||||
styles = outer.chain(&reset);
|
||||
}
|
||||
|
||||
layout(styles).modified(&modifiers)
|
||||
}
|
@ -23,7 +23,7 @@ pub enum Item<'a> {
|
||||
/// things like tags and weak pagebreaks.
|
||||
pub fn collect<'a>(
|
||||
mut children: &'a mut [Pair<'a>],
|
||||
mut locator: SplitLocator<'a>,
|
||||
locator: &mut SplitLocator<'a>,
|
||||
mut initial: StyleChain<'a>,
|
||||
) -> Vec<Item<'a>> {
|
||||
// The collected page-level items.
|
||||
|
@ -83,7 +83,7 @@ fn layout_document_impl(
|
||||
styles,
|
||||
)?;
|
||||
|
||||
let pages = layout_pages(&mut engine, &mut children, locator, styles)?;
|
||||
let pages = layout_pages(&mut engine, &mut children, &mut locator, styles)?;
|
||||
let introspector = Introspector::paged(&pages);
|
||||
|
||||
Ok(PagedDocument { pages, info, introspector })
|
||||
@ -93,7 +93,7 @@ fn layout_document_impl(
|
||||
fn layout_pages<'a>(
|
||||
engine: &mut Engine,
|
||||
children: &'a mut [Pair<'a>],
|
||||
locator: SplitLocator<'a>,
|
||||
locator: &mut SplitLocator<'a>,
|
||||
styles: StyleChain<'a>,
|
||||
) -> SourceResult<Vec<Page>> {
|
||||
// Slice up the children into logical parts.
|
||||
|
@ -19,7 +19,7 @@ use typst_library::visualize::Paint;
|
||||
use typst_library::World;
|
||||
use typst_utils::Numeric;
|
||||
|
||||
use crate::flow::layout_flow;
|
||||
use crate::flow::{layout_flow, FlowMode};
|
||||
|
||||
/// A mostly finished layout for one page. Needs only knowledge of its exact
|
||||
/// page number to be finalized into a `Page`. (Because the margins can depend
|
||||
@ -181,7 +181,7 @@ fn layout_page_run_impl(
|
||||
Regions::repeat(area, area.map(Abs::is_finite)),
|
||||
PageElem::columns_in(styles),
|
||||
ColumnsElem::gutter_in(styles),
|
||||
true,
|
||||
FlowMode::Root,
|
||||
)?;
|
||||
|
||||
// Layouts a single marginal.
|
||||
|
@ -62,7 +62,7 @@ pub fn layout_path(
|
||||
axes.resolve(styles).zip_map(region.size, Rel::relative_to).to_point()
|
||||
};
|
||||
|
||||
let vertices = elem.vertices();
|
||||
let vertices = &elem.vertices;
|
||||
let points: Vec<Point> = vertices.iter().map(|c| resolve(c.vertex())).collect();
|
||||
|
||||
let mut size = Size::zero();
|
||||
@ -150,7 +150,7 @@ pub fn layout_curve(
|
||||
) -> SourceResult<Frame> {
|
||||
let mut builder = CurveBuilder::new(region, styles);
|
||||
|
||||
for item in elem.components() {
|
||||
for item in &elem.components {
|
||||
match item {
|
||||
CurveComponent::Move(element) => {
|
||||
let relative = element.relative(styles);
|
||||
@ -399,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();
|
||||
|
@ -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,
|
||||
@ -994,7 +985,6 @@ pub fn div_euclid(
|
||||
/// ```
|
||||
#[func(title = "Euclidean Remainder", keywords = ["modulo", "modulus"])]
|
||||
pub fn rem_euclid(
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
/// The dividend of the remainder.
|
||||
dividend: DecNum,
|
||||
@ -1031,7 +1021,6 @@ pub fn rem_euclid(
|
||||
/// ```
|
||||
#[func(title = "Quotient")]
|
||||
pub fn quo(
|
||||
/// The span of the function call.
|
||||
span: Span,
|
||||
/// The dividend of the quotient.
|
||||
dividend: DecNum,
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ use std::sync::Arc;
|
||||
use comemo::Tracked;
|
||||
use ecow::{eco_format, EcoString};
|
||||
use serde::{Serialize, Serializer};
|
||||
use smallvec::smallvec;
|
||||
use typst_syntax::Span;
|
||||
use typst_utils::{fat, singleton, LazyHash, SmallBitSet};
|
||||
|
||||
@ -500,7 +499,7 @@ impl Content {
|
||||
|
||||
/// Link the content somewhere.
|
||||
pub fn linked(self, dest: Destination) -> Self {
|
||||
self.styled(LinkElem::set_dests(smallvec![dest]))
|
||||
self.styled(LinkElem::set_current(Some(dest)))
|
||||
}
|
||||
|
||||
/// Set alignments for this content.
|
||||
|
@ -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>,
|
||||
|
@ -6,7 +6,9 @@ use ecow::eco_format;
|
||||
use typst_utils::Numeric;
|
||||
|
||||
use crate::diag::{bail, HintedStrResult, StrResult};
|
||||
use crate::foundations::{format_str, Datetime, IntoValue, Regex, Repr, Value};
|
||||
use crate::foundations::{
|
||||
format_str, Datetime, IntoValue, Regex, Repr, SymbolElem, Value,
|
||||
};
|
||||
use crate::layout::{Alignment, Length, Rel};
|
||||
use crate::text::TextElem;
|
||||
use crate::visualize::Stroke;
|
||||
@ -30,12 +32,13 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> {
|
||||
(Symbol(a), Str(b)) => Str(format_str!("{a}{b}")),
|
||||
(Bytes(a), Bytes(b)) => Bytes(a + b),
|
||||
(Content(a), Content(b)) => Content(a + b),
|
||||
(Content(a), Symbol(b)) => Content(a + TextElem::packed(b.get())),
|
||||
(Content(a), Symbol(b)) => Content(a + SymbolElem::packed(b.get())),
|
||||
(Content(a), Str(b)) => Content(a + TextElem::packed(b)),
|
||||
(Str(a), Content(b)) => Content(TextElem::packed(a) + b),
|
||||
(Symbol(a), Content(b)) => Content(TextElem::packed(a.get()) + b),
|
||||
(Symbol(a), Content(b)) => Content(SymbolElem::packed(a.get()) + b),
|
||||
(Array(a), Array(b)) => Array(a + b),
|
||||
(Dict(a), Dict(b)) => Dict(a + b),
|
||||
(Args(a), Args(b)) => Args(a + b),
|
||||
(a, b) => mismatch!("cannot join {} with {}", a, b),
|
||||
})
|
||||
}
|
||||
@ -129,13 +132,14 @@ pub fn add(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
|
||||
(Symbol(a), Str(b)) => Str(format_str!("{a}{b}")),
|
||||
(Bytes(a), Bytes(b)) => Bytes(a + b),
|
||||
(Content(a), Content(b)) => Content(a + b),
|
||||
(Content(a), Symbol(b)) => Content(a + TextElem::packed(b.get())),
|
||||
(Content(a), Symbol(b)) => Content(a + SymbolElem::packed(b.get())),
|
||||
(Content(a), Str(b)) => Content(a + TextElem::packed(b)),
|
||||
(Str(a), Content(b)) => Content(TextElem::packed(a) + b),
|
||||
(Symbol(a), Content(b)) => Content(TextElem::packed(a.get()) + b),
|
||||
(Symbol(a), Content(b)) => Content(SymbolElem::packed(a.get()) + b),
|
||||
|
||||
(Array(a), Array(b)) => Array(a + b),
|
||||
(Dict(a), Dict(b)) => Dict(a + b),
|
||||
(Args(a), Args(b)) => Args(a + b),
|
||||
|
||||
(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,
|
||||
@ -577,9 +575,10 @@ impl Str {
|
||||
/// of the resulting parts.
|
||||
///
|
||||
/// When the empty string is used as a separator, it separates every
|
||||
/// character 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.
|
||||
/// 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,
|
||||
@ -783,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};
|
||||
|
||||
@ -775,107 +776,6 @@ impl<'a> Iterator for Links<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A sequence of elements with associated styles.
|
||||
#[derive(Clone, PartialEq, Hash)]
|
||||
pub struct StyleVec {
|
||||
/// The elements themselves.
|
||||
elements: EcoVec<Content>,
|
||||
/// A run-length encoded list of style lists.
|
||||
///
|
||||
/// Each element is a (styles, count) pair. Any elements whose
|
||||
/// style falls after the end of this list is considered to
|
||||
/// have an empty style list.
|
||||
styles: EcoVec<(Styles, usize)>,
|
||||
}
|
||||
|
||||
impl StyleVec {
|
||||
/// Create a style vector from an unstyled vector content.
|
||||
pub fn wrap(elements: EcoVec<Content>) -> Self {
|
||||
Self { elements, styles: EcoVec::new() }
|
||||
}
|
||||
|
||||
/// Create a `StyleVec` from a list of content with style chains.
|
||||
pub fn create<'a>(buf: &[(&'a Content, StyleChain<'a>)]) -> (Self, StyleChain<'a>) {
|
||||
let trunk = StyleChain::trunk(buf.iter().map(|&(_, s)| s)).unwrap_or_default();
|
||||
let depth = trunk.links().count();
|
||||
|
||||
let mut elements = EcoVec::with_capacity(buf.len());
|
||||
let mut styles = EcoVec::<(Styles, usize)>::new();
|
||||
let mut last: Option<(StyleChain<'a>, usize)> = None;
|
||||
|
||||
for &(element, chain) in buf {
|
||||
elements.push(element.clone());
|
||||
|
||||
if let Some((prev, run)) = &mut last {
|
||||
if chain == *prev {
|
||||
*run += 1;
|
||||
} else {
|
||||
styles.push((prev.suffix(depth), *run));
|
||||
last = Some((chain, 1));
|
||||
}
|
||||
} else {
|
||||
last = Some((chain, 1));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((last, run)) = last {
|
||||
let skippable = styles.is_empty() && last == trunk;
|
||||
if !skippable {
|
||||
styles.push((last.suffix(depth), run));
|
||||
}
|
||||
}
|
||||
|
||||
(StyleVec { elements, styles }, trunk)
|
||||
}
|
||||
|
||||
/// Whether there are no elements.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.elements.is_empty()
|
||||
}
|
||||
|
||||
/// The number of elements.
|
||||
pub fn len(&self) -> usize {
|
||||
self.elements.len()
|
||||
}
|
||||
|
||||
/// Iterate over the contained content and style chains.
|
||||
pub fn iter<'a>(
|
||||
&'a self,
|
||||
outer: &'a StyleChain<'_>,
|
||||
) -> impl Iterator<Item = (&'a Content, StyleChain<'a>)> {
|
||||
static EMPTY: Styles = Styles::new();
|
||||
self.elements
|
||||
.iter()
|
||||
.zip(
|
||||
self.styles
|
||||
.iter()
|
||||
.flat_map(|(local, count)| std::iter::repeat(local).take(*count))
|
||||
.chain(std::iter::repeat(&EMPTY)),
|
||||
)
|
||||
.map(|(element, local)| (element, outer.chain(local)))
|
||||
}
|
||||
|
||||
/// Get a style property, but only if it is the same for all children of the
|
||||
/// style vector.
|
||||
pub fn shared_get<T: PartialEq>(
|
||||
&self,
|
||||
styles: StyleChain<'_>,
|
||||
getter: fn(StyleChain) -> T,
|
||||
) -> Option<T> {
|
||||
let value = getter(styles);
|
||||
self.styles
|
||||
.iter()
|
||||
.all(|(local, _)| getter(styles.chain(local)) == value)
|
||||
.then_some(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for StyleVec {
|
||||
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
|
||||
f.debug_list().entries(&self.elements).finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A property that is resolved with other properties from the style chain.
|
||||
pub trait Resolve {
|
||||
/// The type of the resolved output.
|
||||
@ -939,6 +839,13 @@ impl<T, const N: usize> Fold for SmallVec<[T; N]> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Fold for OneOrMultiple<T> {
|
||||
fn fold(self, mut outer: Self) -> Self {
|
||||
outer.0.extend(self.0);
|
||||
outer
|
||||
}
|
||||
}
|
||||
|
||||
/// A variant of fold for foldable optional (`Option<T>`) values where an inner
|
||||
/// `None` value isn't respected (contrary to `Option`'s usual `Fold`
|
||||
/// implementation, with which folding with an inner `None` always returns
|
||||
|
@ -9,7 +9,10 @@ use typst_syntax::{is_ident, Span, Spanned};
|
||||
use typst_utils::hash128;
|
||||
|
||||
use crate::diag::{bail, SourceResult, StrResult};
|
||||
use crate::foundations::{cast, func, scope, ty, Array, Func, NativeFunc, Repr as _};
|
||||
use crate::foundations::{
|
||||
cast, elem, func, scope, ty, Array, Content, Func, NativeElement, NativeFunc, Packed,
|
||||
PlainText, Repr as _,
|
||||
};
|
||||
|
||||
/// A Unicode symbol.
|
||||
///
|
||||
@ -187,7 +190,6 @@ impl Symbol {
|
||||
/// ```
|
||||
#[func(constructor)]
|
||||
pub fn construct(
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
/// The variants of the symbol.
|
||||
///
|
||||
@ -426,3 +428,31 @@ fn parts(modifiers: &str) -> impl Iterator<Item = &str> {
|
||||
fn contained(modifiers: &str, m: &str) -> bool {
|
||||
parts(modifiers).any(|part| part == m)
|
||||
}
|
||||
|
||||
/// A single character.
|
||||
#[elem(Repr, PlainText)]
|
||||
pub struct SymbolElem {
|
||||
/// The symbol's character.
|
||||
#[required]
|
||||
pub text: char, // This is called `text` for consistency with `TextElem`.
|
||||
}
|
||||
|
||||
impl SymbolElem {
|
||||
/// Create a new packed symbol element.
|
||||
pub fn packed(text: impl Into<char>) -> Content {
|
||||
Self::new(text.into()).pack()
|
||||
}
|
||||
}
|
||||
|
||||
impl PlainText for Packed<SymbolElem> {
|
||||
fn plain_text(&self, text: &mut EcoString) {
|
||||
text.push(self.text);
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::foundations::Repr for SymbolElem {
|
||||
/// Use a custom repr that matches normal content.
|
||||
fn repr(&self) -> EcoString {
|
||||
eco_format!("[{}]", self.text)
|
||||
}
|
||||
}
|
||||
|
@ -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()?))
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ use crate::foundations::{
|
||||
fields, ops, repr, Args, Array, AutoValue, Bytes, CastInfo, Content, Datetime,
|
||||
Decimal, Dict, Duration, Fold, FromValue, Func, IntoValue, Label, Module,
|
||||
NativeElement, NativeType, NoneValue, Plugin, Reflect, Repr, Resolve, Scope, Str,
|
||||
Styles, Symbol, Type, Version,
|
||||
Styles, Symbol, SymbolElem, Type, Version,
|
||||
};
|
||||
use crate::layout::{Abs, Angle, Em, Fr, Length, Ratio, Rel};
|
||||
use crate::text::{RawContent, RawElem, TextElem};
|
||||
@ -209,7 +209,7 @@ impl Value {
|
||||
Self::Decimal(v) => TextElem::packed(eco_format!("{v}")),
|
||||
Self::Str(v) => TextElem::packed(v),
|
||||
Self::Version(v) => TextElem::packed(eco_format!("{v}")),
|
||||
Self::Symbol(v) => TextElem::packed(v.get()),
|
||||
Self::Symbol(v) => SymbolElem::packed(v.get()),
|
||||
Self::Content(v) => v,
|
||||
Self::Module(module) => module.content(),
|
||||
_ => RawElem::new(RawContent::Text(self.repr()))
|
||||
@ -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> {
|
||||
@ -656,7 +656,7 @@ primitive! { Duration: "duration", Duration }
|
||||
primitive! { Content: "content",
|
||||
Content,
|
||||
None => Content::empty(),
|
||||
Symbol(v) => TextElem::packed(v.get()),
|
||||
Symbol(v) => SymbolElem::packed(v.get()),
|
||||
Str(v) => TextElem::packed(v)
|
||||
}
|
||||
primitive! { Styles: "styles", Styles }
|
||||
|
@ -210,7 +210,10 @@ impl HtmlAttr {
|
||||
|
||||
/// Creates a compile-time constant `HtmlAttr`.
|
||||
///
|
||||
/// Should only be used in const contexts because it can panic.
|
||||
/// Must only be used in const contexts (in a constant definition or
|
||||
/// explicit `const { .. }` block) because otherwise a panic for a malformed
|
||||
/// attribute or not auto-internible constant will only be caught at
|
||||
/// runtime.
|
||||
#[track_caller]
|
||||
pub const fn constant(string: &'static str) -> Self {
|
||||
if string.is_empty() {
|
||||
@ -472,17 +475,55 @@ pub mod tag {
|
||||
wbr
|
||||
}
|
||||
|
||||
/// Whether this is a void tag whose associated element may not have a
|
||||
/// children.
|
||||
pub fn is_void(tag: HtmlTag) -> bool {
|
||||
matches!(
|
||||
tag,
|
||||
self::area
|
||||
| self::base
|
||||
| self::br
|
||||
| self::col
|
||||
| self::embed
|
||||
| self::hr
|
||||
| self::img
|
||||
| self::input
|
||||
| self::link
|
||||
| self::meta
|
||||
| self::param
|
||||
| self::source
|
||||
| self::track
|
||||
| self::wbr
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether this is a tag containing raw text.
|
||||
pub fn is_raw(tag: HtmlTag) -> bool {
|
||||
matches!(tag, self::script | self::style)
|
||||
}
|
||||
|
||||
/// Whether this is a tag containing escapable raw text.
|
||||
pub fn is_escapable_raw(tag: HtmlTag) -> bool {
|
||||
matches!(tag, self::textarea | self::title)
|
||||
}
|
||||
|
||||
/// Whether an element is considered metadata.
|
||||
pub fn is_metadata(tag: HtmlTag) -> bool {
|
||||
matches!(
|
||||
tag,
|
||||
self::base
|
||||
| self::link
|
||||
| self::meta
|
||||
| self::noscript
|
||||
| self::script
|
||||
| self::style
|
||||
| self::template
|
||||
| self::title
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether nodes with the tag have the CSS property `display: block` by
|
||||
/// default.
|
||||
///
|
||||
/// If this is true, then pretty-printing can insert spaces around such
|
||||
/// nodes and around the contents of such nodes.
|
||||
///
|
||||
/// However, when users change the properties of such tags via CSS, the
|
||||
/// insertion of whitespace may actually impact the visual output; for
|
||||
/// example, <https://www.w3.org/TR/css-text-3/#example-af2745cd> shows how
|
||||
/// adding CSS rules to `<p>` can make it sensitive to whitespace. In such
|
||||
/// cases, users should disable pretty-printing.
|
||||
pub fn is_block_by_default(tag: HtmlTag) -> bool {
|
||||
matches!(
|
||||
tag,
|
||||
@ -569,42 +610,29 @@ pub mod tag {
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether this is a void tag whose associated element may not have a
|
||||
/// children.
|
||||
pub fn is_void(tag: HtmlTag) -> bool {
|
||||
/// Whether nodes with the tag have the CSS property `display: table(-.*)?`
|
||||
/// by default.
|
||||
pub fn is_tabular_by_default(tag: HtmlTag) -> bool {
|
||||
matches!(
|
||||
tag,
|
||||
self::area
|
||||
| self::base
|
||||
| self::br
|
||||
self::table
|
||||
| self::thead
|
||||
| self::tbody
|
||||
| self::tfoot
|
||||
| self::tr
|
||||
| self::th
|
||||
| self::td
|
||||
| self::caption
|
||||
| self::col
|
||||
| self::embed
|
||||
| self::hr
|
||||
| self::img
|
||||
| self::input
|
||||
| self::link
|
||||
| self::meta
|
||||
| self::param
|
||||
| self::source
|
||||
| self::track
|
||||
| self::wbr
|
||||
| self::colgroup
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether this is a tag containing raw text.
|
||||
pub fn is_raw(tag: HtmlTag) -> bool {
|
||||
matches!(tag, self::script | self::style)
|
||||
}
|
||||
|
||||
/// Whether this is a tag containing escapable raw text.
|
||||
pub fn is_escapable_raw(tag: HtmlTag) -> bool {
|
||||
matches!(tag, self::textarea | self::title)
|
||||
}
|
||||
}
|
||||
|
||||
/// Predefined constants for HTML attributes.
|
||||
///
|
||||
/// Note: These are very incomplete.
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub mod attr {
|
||||
use super::HtmlAttr;
|
||||
|
||||
@ -619,13 +647,18 @@ pub mod attr {
|
||||
|
||||
attrs! {
|
||||
charset
|
||||
cite
|
||||
colspan
|
||||
content
|
||||
href
|
||||
name
|
||||
value
|
||||
reversed
|
||||
role
|
||||
rowspan
|
||||
start
|
||||
style
|
||||
value
|
||||
}
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub const aria_level: HtmlAttr = HtmlAttr::constant("aria-level");
|
||||
}
|
||||
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,9 +14,9 @@ use crate::visualize::{Paint, Stroke};
|
||||
/// An inline-level container that sizes content.
|
||||
///
|
||||
/// All elements except inline math, text, and boxes are block-level and cannot
|
||||
/// occur inside of a paragraph. The box function can be used to integrate such
|
||||
/// elements into a paragraph. Boxes take the size of their contents by default
|
||||
/// but can also be sized explicitly.
|
||||
/// occur inside of a [paragraph]($par). The box function can be used to
|
||||
/// integrate such elements into a paragraph. Boxes take the size of their
|
||||
/// contents by default but can also be sized explicitly.
|
||||
///
|
||||
/// # Example
|
||||
/// ```example
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -184,6 +184,10 @@ pub enum InlineItem {
|
||||
/// Such a container can be used to separate content, size it, and give it a
|
||||
/// background or border.
|
||||
///
|
||||
/// Blocks are also the primary way to control whether text becomes part of a
|
||||
/// paragraph or not. See [the paragraph documentation]($par/#what-becomes-a-paragraph)
|
||||
/// for more details.
|
||||
///
|
||||
/// # Examples
|
||||
/// With a block, you can give a background to content while still allowing it
|
||||
/// to break across multiple pages.
|
||||
|
@ -4,16 +4,13 @@ use std::fmt::{self, Debug, Formatter};
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::Arc;
|
||||
|
||||
use smallvec::SmallVec;
|
||||
use typst_syntax::Span;
|
||||
use typst_utils::{LazyHash, Numeric};
|
||||
|
||||
use crate::foundations::{cast, dict, Dict, Label, StyleChain, Value};
|
||||
use crate::foundations::{cast, dict, Dict, Label, Value};
|
||||
use crate::introspection::{Location, Tag};
|
||||
use crate::layout::{
|
||||
Abs, Axes, FixedAlignment, HideElem, Length, Point, Size, Transform,
|
||||
};
|
||||
use crate::model::{Destination, LinkElem};
|
||||
use crate::layout::{Abs, Axes, FixedAlignment, Length, Point, Size, Transform};
|
||||
use crate::model::Destination;
|
||||
use crate::text::TextItem;
|
||||
use crate::visualize::{Color, Curve, FixedStroke, Geometry, Image, Paint, Shape};
|
||||
|
||||
@ -304,49 +301,6 @@ impl Frame {
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply late-stage properties from the style chain to this frame. This
|
||||
/// includes:
|
||||
/// - `HideElem::hidden`
|
||||
/// - `LinkElem::dests`
|
||||
///
|
||||
/// This must be called on all frames produced by elements
|
||||
/// that manually handle styles (because their children can have varying
|
||||
/// styles). This currently includes flow, par, and equation.
|
||||
///
|
||||
/// Other elements don't manually need to handle it because their parents
|
||||
/// that result from realization will take care of it and the styles can
|
||||
/// only apply to them as a whole, not part of it (because they don't manage
|
||||
/// styles).
|
||||
pub fn post_processed(mut self, styles: StyleChain) -> Self {
|
||||
self.post_process(styles);
|
||||
self
|
||||
}
|
||||
|
||||
/// Post process in place.
|
||||
pub fn post_process(&mut self, styles: StyleChain) {
|
||||
if !self.is_empty() {
|
||||
self.post_process_raw(
|
||||
LinkElem::dests_in(styles),
|
||||
HideElem::hidden_in(styles),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply raw late-stage properties from the raw data.
|
||||
pub fn post_process_raw(&mut self, dests: SmallVec<[Destination; 1]>, hide: bool) {
|
||||
if !self.is_empty() {
|
||||
let size = self.size;
|
||||
self.push_multiple(
|
||||
dests
|
||||
.into_iter()
|
||||
.map(|dest| (Point::zero(), FrameItem::Link(dest, size))),
|
||||
);
|
||||
if hide {
|
||||
self.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Hide all content in the frame, but keep metadata.
|
||||
pub fn hide(&mut self) {
|
||||
Arc::make_mut(&mut self.items).retain_mut(|(_, item)| match item {
|
||||
|
@ -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
|
||||
@ -178,7 +602,7 @@ pub enum Entry<'a> {
|
||||
|
||||
impl<'a> Entry<'a> {
|
||||
/// Obtains the cell inside this entry, if this is not a merged cell.
|
||||
fn as_cell(&self) -> Option<&Cell<'a>> {
|
||||
pub fn as_cell(&self) -> Option<&Cell<'a>> {
|
||||
match self {
|
||||
Self::Cell(cell) => Some(cell),
|
||||
Self::Merged { .. } => None,
|
@ -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_;
|
||||
|
@ -10,7 +10,7 @@ use crate::layout::{BlockElem, Length};
|
||||
/// Space may be inserted between the instances of the body parameter, so be
|
||||
/// sure to adjust the [`justify`]($repeat.justify) parameter accordingly.
|
||||
///
|
||||
/// Errors if there no bounds on the available space, as it would create
|
||||
/// Errors if there are no bounds on the available space, as it would create
|
||||
/// infinite content.
|
||||
///
|
||||
/// # Example
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,18 +136,10 @@ impl Default for Delimiter {
|
||||
cast! {
|
||||
Delimiter,
|
||||
self => self.0.into_value(),
|
||||
v: EcoString => {
|
||||
let mut chars = v.chars();
|
||||
let first = chars.next().ok_or("delimiter must not be empty")?;
|
||||
if chars.next().is_some() {
|
||||
bail!("delimiter must be a single character");
|
||||
}
|
||||
|
||||
if !first.is_ascii() {
|
||||
bail!("delimiter must be an ASCII character");
|
||||
}
|
||||
|
||||
Self(first)
|
||||
c: char => if c.is_ascii() {
|
||||
Self(c)
|
||||
} else {
|
||||
bail!("delimiter must be an ASCII character")
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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,8 +1,7 @@
|
||||
use crate::diag::bail;
|
||||
use crate::foundations::{cast, elem, func, Content, NativeElement, Value};
|
||||
use crate::foundations::{cast, elem, func, Content, NativeElement, SymbolElem};
|
||||
use crate::layout::{Length, Rel};
|
||||
use crate::math::Mathy;
|
||||
use crate::text::TextElem;
|
||||
|
||||
/// Attaches an accent to a base.
|
||||
///
|
||||
@ -142,8 +141,8 @@ cast! {
|
||||
Accent,
|
||||
self => self.0.into_value(),
|
||||
v: char => Self::new(v),
|
||||
v: Content => match v.to_packed::<TextElem>() {
|
||||
Some(elem) => Value::Str(elem.text().clone().into()).cast()?,
|
||||
None => bail!("expected text"),
|
||||
v: Content => match v.to_packed::<SymbolElem>() {
|
||||
Some(elem) => Self::new(elem.text),
|
||||
None => bail!("expected a symbol"),
|
||||
},
|
||||
}
|
||||
|
@ -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();
|
||||
let mut base = &self.base;
|
||||
while let Some(equation) = base.to_packed::<EquationElem>() {
|
||||
base = equation.body();
|
||||
base = &equation.body;
|
||||
}
|
||||
|
||||
// Move attachments from elem into base where possible.
|
||||
|
@ -20,7 +20,9 @@ use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem};
|
||||
|
||||
/// A mathematical equation.
|
||||
///
|
||||
/// Can be displayed inline with text or as a separate block.
|
||||
/// Can be displayed inline with text or as a separate block. An equation
|
||||
/// becomes block-level through the presence of at least one space after the
|
||||
/// opening dollar sign and one space before the closing dollar sign.
|
||||
///
|
||||
/// # Example
|
||||
/// ```example
|
||||
@ -229,35 +231,20 @@ impl Refable for Packed<EquationElem> {
|
||||
}
|
||||
|
||||
impl Outlinable for Packed<EquationElem> {
|
||||
fn outline(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<Option<Content>> {
|
||||
if !self.block(StyleChain::default()) {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(numbering) = self.numbering() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// After synthesis, this should always be custom content.
|
||||
let mut supplement = match (**self).supplement(StyleChain::default()) {
|
||||
Smart::Custom(Some(Supplement::Content(content))) => content,
|
||||
_ => Content::empty(),
|
||||
};
|
||||
fn outlined(&self) -> bool {
|
||||
self.block(StyleChain::default()) && self.numbering().is_some()
|
||||
}
|
||||
|
||||
fn prefix(&self, numbers: Content) -> Content {
|
||||
let supplement = self.supplement();
|
||||
if !supplement.is_empty() {
|
||||
supplement += TextElem::packed("\u{a0}");
|
||||
supplement + TextElem::packed('\u{a0}') + numbers
|
||||
} else {
|
||||
numbers
|
||||
}
|
||||
}
|
||||
|
||||
let numbers = self.counter().display_at_loc(
|
||||
engine,
|
||||
self.location().unwrap(),
|
||||
styles,
|
||||
numbering,
|
||||
)?;
|
||||
|
||||
Ok(Some(supplement + numbers))
|
||||
fn body(&self) -> Content {
|
||||
Content::empty()
|
||||
}
|
||||
}
|
||||
|
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