Merge branch 'main' into opentype-text-scripts-t3

# Conflicts:
#	tests/ref/issue-4454-footnote-ref-numbering.png
This commit is contained in:
Malo 2025-06-07 22:32:54 +01:00
commit 6b8c5263d6
211 changed files with 1069 additions and 616 deletions

View File

@ -40,7 +40,7 @@ jobs:
sudo dpkg --add-architecture i386 sudo dpkg --add-architecture i386
sudo apt update sudo apt update
sudo apt install -y gcc-multilib libssl-dev:i386 pkg-config:i386 sudo apt install -y gcc-multilib libssl-dev:i386 pkg-config:i386
- uses: dtolnay/rust-toolchain@1.85.0 - uses: dtolnay/rust-toolchain@1.87.0
with: with:
targets: ${{ matrix.bits == 32 && 'i686-unknown-linux-gnu' || '' }} targets: ${{ matrix.bits == 32 && 'i686-unknown-linux-gnu' || '' }}
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
@ -73,7 +73,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.85.0 - uses: dtolnay/rust-toolchain@1.87.0
with: with:
components: clippy, rustfmt components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2

View File

@ -44,7 +44,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.85.0 - uses: dtolnay/rust-toolchain@1.87.0
with: with:
target: ${{ matrix.target }} target: ${{ matrix.target }}

14
Cargo.lock generated
View File

@ -508,9 +508,9 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-channel" name = "crossbeam-channel"
version = "0.5.14" version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
@ -1215,6 +1215,7 @@ dependencies = [
"byteorder-lite", "byteorder-lite",
"color_quant", "color_quant",
"gif", "gif",
"image-webp",
"num-traits", "num-traits",
"png", "png",
"zune-core", "zune-core",
@ -1259,6 +1260,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "infer"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7"
[[package]] [[package]]
name = "inotify" name = "inotify"
version = "0.11.0" version = "0.11.0"
@ -2857,7 +2864,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-assets" name = "typst-assets"
version = "0.13.1" version = "0.13.1"
source = "git+https://github.com/typst/typst-assets?rev=ab1295f#ab1295ff896444e51902e03c2669955e1d73604a" source = "git+https://github.com/typst/typst-assets?rev=c74e539#c74e539b090070a0c66fd007c550f5b6d3b724bd"
[[package]] [[package]]
name = "typst-cli" name = "typst-cli"
@ -3127,6 +3134,7 @@ dependencies = [
"comemo", "comemo",
"ecow", "ecow",
"image", "image",
"infer",
"krilla", "krilla",
"krilla-svg", "krilla-svg",
"serde", "serde",

View File

@ -32,7 +32,7 @@ typst-svg = { path = "crates/typst-svg", version = "0.13.1" }
typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" } typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" }
typst-timing = { path = "crates/typst-timing", version = "0.13.1" } typst-timing = { path = "crates/typst-timing", version = "0.13.1" }
typst-utils = { path = "crates/typst-utils", version = "0.13.1" } typst-utils = { path = "crates/typst-utils", version = "0.13.1" }
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "ab1295f" } typst-assets = { git = "https://github.com/typst/typst-assets", rev = "c74e539" }
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "fddbf8b" } typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "fddbf8b" }
arrayvec = "0.7.4" arrayvec = "0.7.4"
az = "1.2" az = "1.2"
@ -69,8 +69,9 @@ icu_provider_adapters = "1.4"
icu_provider_blob = "1.4" icu_provider_blob = "1.4"
icu_segmenter = { version = "1.4", features = ["serde"] } icu_segmenter = { version = "1.4", features = ["serde"] }
if_chain = "1" if_chain = "1"
image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif"] } image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif", "webp"] }
indexmap = { version = "2", features = ["serde"] } indexmap = { version = "2", features = ["serde"] }
infer = { version = "0.19.0", default-features = false }
kamadak-exif = "0.6" kamadak-exif = "0.6"
krilla = { version = "0.4.0", default-features = false, features = ["raster-images", "comemo", "rayon"] } krilla = { version = "0.4.0", default-features = false, features = ["raster-images", "comemo", "rayon"] }
krilla-svg = "0.1.0" krilla-svg = "0.1.0"

View File

@ -25,19 +25,22 @@ impl Eval for ast::FuncCall<'_> {
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let span = self.span(); let span = self.span();
let callee = self.callee(); let callee = self.callee();
let in_math = in_math(callee);
let callee_span = callee.span(); let callee_span = callee.span();
let args = self.args(); let args = self.args();
let trailing_comma = args.trailing_comma();
vm.engine.route.check_call_depth().at(span)?; vm.engine.route.check_call_depth().at(span)?;
// Try to evaluate as a call to an associated function or field. // Try to evaluate as a call to an associated function or field.
let (callee, args) = if let ast::Expr::FieldAccess(access) = callee { let (callee_value, args_value) = if let ast::Expr::FieldAccess(access) = callee {
let target = access.target(); let target = access.target();
let field = access.field(); let field = access.field();
match eval_field_call(target, field, args, span, vm)? { match eval_field_call(target, field, args, span, vm)? {
FieldCall::Normal(callee, args) => (callee, args), FieldCall::Normal(callee, args) => {
if vm.inspected == Some(callee_span) {
vm.trace(callee.clone());
}
(callee, args)
}
FieldCall::Resolved(value) => return Ok(value), FieldCall::Resolved(value) => return Ok(value),
} }
} else { } else {
@ -45,9 +48,15 @@ impl Eval for ast::FuncCall<'_> {
(callee.eval(vm)?, args.eval(vm)?.spanned(span)) (callee.eval(vm)?, args.eval(vm)?.spanned(span))
}; };
let func_result = callee.clone().cast::<Func>(); let func_result = callee_value.clone().cast::<Func>();
if in_math && func_result.is_err() {
return wrap_args_in_math(callee, callee_span, args, trailing_comma); if func_result.is_err() && in_math(callee) {
return wrap_args_in_math(
callee_value,
callee_span,
args_value,
args.trailing_comma(),
);
} }
let func = func_result let func = func_result
@ -56,8 +65,11 @@ impl Eval for ast::FuncCall<'_> {
let point = || Tracepoint::Call(func.name().map(Into::into)); let point = || Tracepoint::Call(func.name().map(Into::into));
let f = || { let f = || {
func.call(&mut vm.engine, vm.context, args) func.call(&mut vm.engine, vm.context, args_value).trace(
.trace(vm.world(), point, span) vm.world(),
point,
span,
)
}; };
// Stacker is broken on WASM. // Stacker is broken on WASM.
@ -404,12 +416,14 @@ fn wrap_args_in_math(
if trailing_comma { if trailing_comma {
body += SymbolElem::packed(','); body += SymbolElem::packed(',');
} }
Ok(Value::Content(
callee.display().spanned(callee_span) let formatted = callee.display().spanned(callee_span)
+ LrElem::new(SymbolElem::packed('(') + body + SymbolElem::packed(')')) + LrElem::new(SymbolElem::packed('(') + body + SymbolElem::packed(')'))
.pack() .pack()
.spanned(args.span), .spanned(args.span);
))
args.finish()?;
Ok(Value::Content(formatted))
} }
/// Provide a hint if the callee is a shadowed standard library function. /// Provide a hint if the callee is a shadowed standard library function.

View File

@ -15,7 +15,7 @@ use typst::syntax::{
ast, is_id_continue, is_id_start, is_ident, FileId, LinkedNode, Side, Source, ast, is_id_continue, is_id_start, is_ident, FileId, LinkedNode, Side, Source,
SyntaxKind, SyntaxKind,
}; };
use typst::text::RawElem; use typst::text::{FontFlags, RawElem};
use typst::visualize::Color; use typst::visualize::Color;
use unscanny::Scanner; use unscanny::Scanner;
@ -841,7 +841,9 @@ fn param_value_completions<'a>(
/// Returns which file extensions to complete for the given parameter if any. /// Returns which file extensions to complete for the given parameter if any.
fn path_completion(func: &Func, param: &ParamInfo) -> Option<&'static [&'static str]> { fn path_completion(func: &Func, param: &ParamInfo) -> Option<&'static [&'static str]> {
Some(match (func.name(), param.name) { Some(match (func.name(), param.name) {
(Some("image"), "source") => &["png", "jpg", "jpeg", "gif", "svg", "svgz"], (Some("image"), "source") => {
&["png", "jpg", "jpeg", "gif", "svg", "svgz", "webp"]
}
(Some("csv"), "source") => &["csv"], (Some("csv"), "source") => &["csv"],
(Some("plugin"), "source") => &["wasm"], (Some("plugin"), "source") => &["wasm"],
(Some("cbor"), "source") => &["cbor"], (Some("cbor"), "source") => &["cbor"],
@ -1081,6 +1083,24 @@ fn code_completions(ctx: &mut CompletionContext, hash: bool) {
} }
} }
/// See if the AST node is somewhere within a show rule applying to equations
fn is_in_equation_show_rule(leaf: &LinkedNode<'_>) -> bool {
let mut node = leaf;
while let Some(parent) = node.parent() {
if_chain! {
if let Some(expr) = parent.get().cast::<ast::Expr>();
if let ast::Expr::ShowRule(show) = expr;
if let Some(ast::Expr::FieldAccess(field)) = show.selector();
if field.field().as_str() == "equation";
then {
return true;
}
}
node = parent;
}
false
}
/// Context for autocompletion. /// Context for autocompletion.
struct CompletionContext<'a> { struct CompletionContext<'a> {
world: &'a (dyn IdeWorld + 'a), world: &'a (dyn IdeWorld + 'a),
@ -1152,10 +1172,12 @@ impl<'a> CompletionContext<'a> {
/// Add completions for all font families. /// Add completions for all font families.
fn font_completions(&mut self) { fn font_completions(&mut self) {
let equation = self.before_window(25).contains("equation"); let equation = is_in_equation_show_rule(self.leaf);
for (family, iter) in self.world.book().families() { for (family, iter) in self.world.book().families() {
let detail = summarize_font_family(iter); let variants: Vec<_> = iter.collect();
if !equation || family.contains("Math") { let is_math = variants.iter().any(|f| f.flags.contains(FontFlags::MATH));
let detail = summarize_font_family(variants);
if !equation || is_math {
self.str_completion( self.str_completion(
family, family,
Some(CompletionKind::Font), Some(CompletionKind::Font),
@ -1790,4 +1812,21 @@ mod tests {
.must_include(["r", "dashed"]) .must_include(["r", "dashed"])
.must_exclude(["cases"]); .must_exclude(["cases"]);
} }
#[test]
fn test_autocomplete_fonts() {
test("#text(font:)", -1)
.must_include(["\"Libertinus Serif\"", "\"New Computer Modern Math\""]);
test("#show link: set text(font: )", -1)
.must_include(["\"Libertinus Serif\"", "\"New Computer Modern Math\""]);
test("#show math.equation: set text(font: )", -1)
.must_include(["\"New Computer Modern Math\""])
.must_exclude(["\"Libertinus Serif\""]);
test("#show math.equation: it => { set text(font: )\nit }", -6)
.must_include(["\"New Computer Modern Math\""])
.must_exclude(["\"Libertinus Serif\""]);
}
} }

View File

@ -86,7 +86,7 @@ fn expr_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option<Tooltip> {
*count += 1; *count += 1;
continue; continue;
} else if *count > 1 { } else if *count > 1 {
write!(pieces.last_mut().unwrap(), " (x{count})").unwrap(); write!(pieces.last_mut().unwrap(), " (×{count})").unwrap();
} }
} }
pieces.push(value.repr()); pieces.push(value.repr());
@ -95,7 +95,7 @@ fn expr_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option<Tooltip> {
if let Some((_, count)) = last { if let Some((_, count)) = last {
if count > 1 { if count > 1 {
write!(pieces.last_mut().unwrap(), " (x{count})").unwrap(); write!(pieces.last_mut().unwrap(), " (×{count})").unwrap();
} }
} }
@ -269,7 +269,7 @@ fn font_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option<Tooltip> {
.find(|&(family, _)| family.to_lowercase().as_str() == lower.as_str()); .find(|&(family, _)| family.to_lowercase().as_str() == lower.as_str());
then { then {
let detail = summarize_font_family(iter); let detail = summarize_font_family(iter.collect());
return Some(Tooltip::Text(detail)); return Some(Tooltip::Text(detail));
} }
}; };
@ -371,4 +371,11 @@ mod tests {
test(&world, -2, Side::Before).must_be_none(); test(&world, -2, Side::Before).must_be_none();
test(&world, -2, Side::After).must_be_text("This star imports `a`, `b`, and `c`"); test(&world, -2, Side::After).must_be_text("This star imports `a`, `b`, and `c`");
} }
#[test]
fn test_tooltip_field_call() {
let world = TestWorld::new("#import \"other.typ\"\n#other.f()")
.with_source("other.typ", "#let f = (x) => 1");
test(&world, -4, Side::After).must_be_code("(..) => ..");
}
} }

View File

@ -77,23 +77,20 @@ pub fn plain_docs_sentence(docs: &str) -> EcoString {
} }
/// Create a short description of a font family. /// Create a short description of a font family.
pub fn summarize_font_family<'a>( pub fn summarize_font_family(mut variants: Vec<&FontInfo>) -> EcoString {
variants: impl Iterator<Item = &'a FontInfo>, variants.sort_by_key(|info| info.variant);
) -> EcoString {
let mut infos: Vec<_> = variants.collect();
infos.sort_by_key(|info| info.variant);
let mut has_italic = false; let mut has_italic = false;
let mut min_weight = u16::MAX; let mut min_weight = u16::MAX;
let mut max_weight = 0; let mut max_weight = 0;
for info in &infos { for info in &variants {
let weight = info.variant.weight.to_number(); let weight = info.variant.weight.to_number();
has_italic |= info.variant.style == FontStyle::Italic; has_italic |= info.variant.style == FontStyle::Italic;
min_weight = min_weight.min(weight); min_weight = min_weight.min(weight);
max_weight = min_weight.max(weight); max_weight = min_weight.max(weight);
} }
let count = infos.len(); let count = variants.len();
let mut detail = eco_format!("{count} variant{}.", if count == 1 { "" } else { "s" }); let mut detail = eco_format!("{count} variant{}.", if count == 1 { "" } else { "s" });
if min_weight == max_weight { if min_weight == max_weight {

View File

@ -128,8 +128,7 @@ impl Downloader {
} }
// Configure native TLS. // Configure native TLS.
let connector = let connector = tls.build().map_err(io::Error::other)?;
tls.build().map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
builder = builder.tls_connector(Arc::new(connector)); builder = builder.tls_connector(Arc::new(connector));
builder.build().get(url).call() builder.build().get(url).call()

View File

@ -11,7 +11,7 @@ use typst_library::layout::{
use typst_library::text::TextElem; use typst_library::text::TextElem;
use typst_library::visualize::Geometry; use typst_library::visualize::Geometry;
use typst_syntax::Span; use typst_syntax::Span;
use typst_utils::{MaybeReverseIter, Numeric}; use typst_utils::Numeric;
use super::{ use super::{
generate_line_segments, hline_stroke_at_column, layout_cell, vline_stroke_at_row, generate_line_segments, hline_stroke_at_column, layout_cell, vline_stroke_at_row,
@ -574,7 +574,7 @@ impl<'a> GridLayouter<'a> {
// Reverse with RTL so that later columns start first. // Reverse with RTL so that later columns start first.
let mut dx = Abs::zero(); let mut dx = Abs::zero();
for (x, &col) in self.rcols.iter().enumerate().rev_if(self.is_rtl) { for (x, &col) in self.rcols.iter().enumerate() {
let mut dy = Abs::zero(); let mut dy = Abs::zero();
for row in rows { for row in rows {
// We want to only draw the fill starting at the parent // We want to only draw the fill starting at the parent
@ -643,18 +643,13 @@ impl<'a> GridLayouter<'a> {
.sum() .sum()
}; };
let width = self.cell_spanned_width(cell, x); let width = self.cell_spanned_width(cell, x);
// In the grid, cell colspans expand to the right, let mut pos = Point::new(dx, dy);
// so we're at the leftmost (lowest 'x') column if self.is_rtl {
// spanned by the cell. However, in RTL, cells // In RTL cells expand to the left, thus the
// expand to the left. Therefore, without the // position must additionally be offset by the
// offset below, cell fills would start at the // cell's width.
// rightmost visual position of a cell and extend pos.x = self.width - (dx + width);
// over to unrelated columns to the right in RTL. }
// We avoid this by ensuring the fill starts at the
// very left of the cell, even with colspan > 1.
let offset =
if self.is_rtl { -width + col } else { Abs::zero() };
let pos = Point::new(dx + offset, dy);
let size = Size::new(width, height); let size = Size::new(width, height);
let rect = Geometry::Rect(size).filled(fill); let rect = Geometry::Rect(size).filled(fill);
fills.push((pos, FrameItem::Shape(rect, self.span))); fills.push((pos, FrameItem::Shape(rect, self.span)));
@ -1236,10 +1231,9 @@ impl<'a> GridLayouter<'a> {
} }
let mut output = Frame::soft(Size::new(self.width, height)); let mut output = Frame::soft(Size::new(self.width, height));
let mut pos = Point::zero(); let mut offset = Point::zero();
// Reverse the column order when using RTL. for (x, &rcol) in self.rcols.iter().enumerate() {
for (x, &rcol) in self.rcols.iter().enumerate().rev_if(self.is_rtl) {
if let Some(cell) = self.grid.cell(x, y) { if let Some(cell) = self.grid.cell(x, y) {
// Rowspans have a separate layout step // Rowspans have a separate layout step
if cell.rowspan.get() == 1 { if cell.rowspan.get() == 1 {
@ -1257,25 +1251,17 @@ impl<'a> GridLayouter<'a> {
let frame = let frame =
layout_cell(cell, engine, disambiguator, self.styles, pod)? layout_cell(cell, engine, disambiguator, self.styles, pod)?
.into_frame(); .into_frame();
let mut pos = pos; let mut pos = offset;
if self.is_rtl { if self.is_rtl {
// In the grid, cell colspans expand to the right, // In RTL cells expand to the left, thus the position
// so we're at the leftmost (lowest 'x') column // must additionally be offset by the cell's width.
// spanned by the cell. However, in RTL, cells pos.x = self.width - (pos.x + width);
// expand to the left. Therefore, without the
// offset below, the cell's contents would be laid out
// starting at its rightmost visual position and extend
// over to unrelated cells to its right in RTL.
// We avoid this by ensuring the rendered cell starts at
// the very left of the cell, even with colspan > 1.
let offset = -width + rcol;
pos.x += offset;
} }
output.push_frame(pos, frame); output.push_frame(pos, frame);
} }
} }
pos.x += rcol; offset.x += rcol;
} }
Ok(output) Ok(output)
@ -1302,8 +1288,8 @@ impl<'a> GridLayouter<'a> {
pod.backlog = &heights[1..]; pod.backlog = &heights[1..];
// Layout the row. // Layout the row.
let mut pos = Point::zero(); let mut offset = Point::zero();
for (x, &rcol) in self.rcols.iter().enumerate().rev_if(self.is_rtl) { for (x, &rcol) in self.rcols.iter().enumerate() {
if let Some(cell) = self.grid.cell(x, y) { if let Some(cell) = self.grid.cell(x, y) {
// Rowspans have a separate layout step // Rowspans have a separate layout step
if cell.rowspan.get() == 1 { if cell.rowspan.get() == 1 {
@ -1314,17 +1300,19 @@ impl<'a> GridLayouter<'a> {
let fragment = let fragment =
layout_cell(cell, engine, disambiguator, self.styles, pod)?; layout_cell(cell, engine, disambiguator, self.styles, pod)?;
for (output, frame) in outputs.iter_mut().zip(fragment) { for (output, frame) in outputs.iter_mut().zip(fragment) {
let mut pos = pos; let mut pos = offset;
if self.is_rtl { if self.is_rtl {
let offset = -width + rcol; // In RTL cells expand to the left, thus the
pos.x += offset; // position must additionally be offset by the
// cell's width.
pos.x = self.width - (offset.x + width);
} }
output.push_frame(pos, frame); output.push_frame(pos, frame);
} }
} }
} }
pos.x += rcol; offset.x += rcol;
} }
Ok(Fragment::frames(outputs)) Ok(Fragment::frames(outputs))

View File

@ -3,7 +3,6 @@ use typst_library::engine::Engine;
use typst_library::foundations::Resolve; use typst_library::foundations::Resolve;
use typst_library::layout::grid::resolve::Repeatable; use typst_library::layout::grid::resolve::Repeatable;
use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing}; use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing};
use typst_utils::MaybeReverseIter;
use super::layouter::{in_last_with_offset, points, Row, RowPiece}; use super::layouter::{in_last_with_offset, points, Row, RowPiece};
use super::{layout_cell, Cell, GridLayouter}; use super::{layout_cell, Cell, GridLayouter};
@ -23,6 +22,10 @@ pub struct Rowspan {
/// specified for the parent cell's `breakable` field. /// specified for the parent cell's `breakable` field.
pub is_effectively_unbreakable: bool, pub is_effectively_unbreakable: bool,
/// The horizontal offset of this rowspan in all regions. /// The horizontal offset of this rowspan in all regions.
///
/// This is the offset from the text direction start, meaning that, on RTL
/// grids, this is the offset from the right of the grid, whereas, on LTR
/// grids, it is the offset from the left.
pub dx: Abs, pub dx: Abs,
/// The vertical offset of this rowspan in the first region. /// The vertical offset of this rowspan in the first region.
pub dy: Abs, pub dy: Abs,
@ -118,10 +121,11 @@ impl GridLayouter<'_> {
// Nothing to layout. // Nothing to layout.
return Ok(()); return Ok(());
}; };
let first_column = self.rcols[x];
let cell = self.grid.cell(x, y).unwrap(); let cell = self.grid.cell(x, y).unwrap();
let width = self.cell_spanned_width(cell, x); let width = self.cell_spanned_width(cell, x);
let dx = if self.is_rtl { dx - width + first_column } else { dx }; // In RTL cells expand to the left, thus the position
// must additionally be offset by the cell's width.
let dx = if self.is_rtl { self.width - (dx + width) } else { dx };
// Prepare regions. // Prepare regions.
let size = Size::new(width, *first_height); let size = Size::new(width, *first_height);
@ -185,10 +189,8 @@ impl GridLayouter<'_> {
/// Checks if a row contains the beginning of one or more rowspan cells. /// Checks if a row contains the beginning of one or more rowspan cells.
/// If so, adds them to the rowspans vector. /// If so, adds them to the rowspans vector.
pub fn check_for_rowspans(&mut self, disambiguator: usize, y: usize) { pub fn check_for_rowspans(&mut self, disambiguator: usize, y: usize) {
// We will compute the horizontal offset of each rowspan in advance. let offsets = points(self.rcols.iter().copied());
// For that reason, we must reverse the column order when using RTL. for (x, dx) in (0..self.rcols.len()).zip(offsets) {
let offsets = points(self.rcols.iter().copied().rev_if(self.is_rtl));
for (x, dx) in (0..self.rcols.len()).rev_if(self.is_rtl).zip(offsets) {
let Some(cell) = self.grid.cell(x, y) else { let Some(cell) = self.grid.cell(x, y) else {
continue; continue;
}; };

View File

@ -147,6 +147,7 @@ fn determine_format(source: &DataSource, data: &Bytes) -> StrResult<ImageFormat>
"jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()), "jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()),
"gif" => return Ok(ExchangeFormat::Gif.into()), "gif" => return Ok(ExchangeFormat::Gif.into()),
"svg" | "svgz" => return Ok(VectorFormat::Svg.into()), "svg" | "svgz" => return Ok(VectorFormat::Svg.into()),
"webp" => return Ok(ExchangeFormat::Webp.into()),
_ => {} _ => {}
} }
} }

View File

@ -690,13 +690,34 @@ fn breakpoints(p: &Preparation, mut f: impl FnMut(usize, Breakpoint)) {
let breakpoint = if point == text.len() { let breakpoint = if point == text.len() {
Breakpoint::Mandatory Breakpoint::Mandatory
} else { } else {
const OBJ_REPLACE: char = '\u{FFFC}';
match lb.get(c) { match lb.get(c) {
// Fix for: https://github.com/unicode-org/icu4x/issues/4146
LineBreak::Glue | LineBreak::WordJoiner | LineBreak::ZWJ => continue,
LineBreak::MandatoryBreak LineBreak::MandatoryBreak
| LineBreak::CarriageReturn | LineBreak::CarriageReturn
| LineBreak::LineFeed | LineBreak::LineFeed
| LineBreak::NextLine => Breakpoint::Mandatory, | LineBreak::NextLine => Breakpoint::Mandatory,
// https://github.com/typst/typst/issues/5489
//
// OBJECT-REPLACEMENT-CHARACTERs provide Contingent Break
// opportunities before and after by default. This behaviour
// is however tailorable, see:
// https://www.unicode.org/reports/tr14/#CB
// https://www.unicode.org/reports/tr14/#TailorableBreakingRules
// https://www.unicode.org/reports/tr14/#LB20
//
// Don't provide a line breaking opportunity between a LTR-
// ISOLATE (or any other Combining Mark) and an OBJECT-
// REPLACEMENT-CHARACTER representing an inline item, if the
// LTR-ISOLATE could end up as the only character on the
// previous line.
LineBreak::CombiningMark
if text[point..].starts_with(OBJ_REPLACE)
&& last + c.len_utf8() == point =>
{
continue;
}
_ => Breakpoint::Normal, _ => Breakpoint::Normal,
} }
}; };

View File

@ -21,7 +21,7 @@ use unicode_bidi::{BidiInfo, Level as BidiLevel};
use unicode_script::{Script, UnicodeScript}; use unicode_script::{Script, UnicodeScript};
use super::{decorate, Item, Range, SpanMapper}; use super::{decorate, Item, Range, SpanMapper};
use crate::modifiers::{FrameModifiers, FrameModify}; use crate::modifiers::FrameModifyText;
/// The result of shaping text. /// The result of shaping text.
/// ///
@ -332,7 +332,7 @@ impl<'a> ShapedText<'a> {
offset += width; offset += width;
} }
frame.modify(&FrameModifiers::get_in(self.styles)); frame.modify_text(self.styles);
frame frame
} }

View File

@ -1,7 +1,7 @@
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain}; use typst_library::foundations::{Packed, StyleChain};
use typst_library::layout::{Em, Frame, Point, Size}; use typst_library::layout::{Em, Frame, Point, Size};
use typst_library::math::{Accent, AccentElem}; use typst_library::math::AccentElem;
use super::{style_cramped, FrameFragment, GlyphFragment, MathContext, MathFragment}; use super::{style_cramped, FrameFragment, GlyphFragment, MathContext, MathFragment};
@ -18,8 +18,11 @@ pub fn layout_accent(
let cramped = style_cramped(); let cramped = style_cramped();
let mut base = ctx.layout_into_fragment(&elem.base, styles.chain(&cramped))?; let mut base = ctx.layout_into_fragment(&elem.base, styles.chain(&cramped))?;
// Try to replace a glyph with its dotless variant. let accent = elem.accent;
if elem.dotless(styles) { let top_accent = !accent.is_bottom();
// Try to replace base glyph with its dotless variant.
if top_accent && elem.dotless(styles) {
if let MathFragment::Glyph(glyph) = &mut base { if let MathFragment::Glyph(glyph) = &mut base {
glyph.make_dotless_form(ctx); glyph.make_dotless_form(ctx);
} }
@ -29,41 +32,54 @@ pub fn layout_accent(
let base_class = base.class(); let base_class = base.class();
let base_attach = base.accent_attach(); let base_attach = base.accent_attach();
let width = elem.size(styles).relative_to(base.width()); let mut glyph = GlyphFragment::new(ctx, styles, accent.0, elem.span());
let Accent(c) = elem.accent; // Try to replace accent glyph with its flattened variant.
let mut glyph = GlyphFragment::new(ctx, styles, c, elem.span()); if top_accent {
let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height);
// Try to replace accent glyph with flattened variant. if base.ascent() > flattened_base_height {
let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height); glyph.make_flattened_accent_form(ctx);
if base.ascent() > flattened_base_height { }
glyph.make_flattened_accent_form(ctx);
} }
// Forcing the accent to be at least as large as the base makes it too // Forcing the accent to be at least as large as the base makes it too
// wide in many case. // wide in many case.
let width = elem.size(styles).relative_to(base.width());
let short_fall = ACCENT_SHORT_FALL.at(glyph.font_size); let short_fall = ACCENT_SHORT_FALL.at(glyph.font_size);
let variant = glyph.stretch_horizontal(ctx, width, short_fall); let variant = glyph.stretch_horizontal(ctx, width - short_fall);
let accent = variant.frame; let accent = variant.frame;
let accent_attach = variant.accent_attach; let accent_attach = variant.accent_attach.0;
let (gap, accent_pos, base_pos) = if top_accent {
// Descent is negative because the accent's ink bottom is above the
// baseline. Therefore, the default gap is the accent's negated descent
// minus the accent base height. Only if the base is very small, we
// need a larger gap so that the accent doesn't move too low.
let accent_base_height = scaled!(ctx, styles, accent_base_height);
let gap = -accent.descent() - base.ascent().min(accent_base_height);
let accent_pos = Point::with_x(base_attach.0 - accent_attach);
let base_pos = Point::with_y(accent.height() + gap);
(gap, accent_pos, base_pos)
} else {
let gap = -accent.ascent();
let accent_pos = Point::new(base_attach.1 - accent_attach, base.height() + gap);
let base_pos = Point::zero();
(gap, accent_pos, base_pos)
};
// Descent is negative because the accent's ink bottom is above the
// baseline. Therefore, the default gap is the accent's negated descent
// minus the accent base height. Only if the base is very small, we need
// a larger gap so that the accent doesn't move too low.
let accent_base_height = scaled!(ctx, styles, accent_base_height);
let gap = -accent.descent() - base.ascent().min(accent_base_height);
let size = Size::new(base.width(), accent.height() + gap + base.height()); let size = Size::new(base.width(), accent.height() + gap + base.height());
let accent_pos = Point::with_x(base_attach - accent_attach);
let base_pos = Point::with_y(accent.height() + gap);
let baseline = base_pos.y + base.ascent(); let baseline = base_pos.y + base.ascent();
let base_italics_correction = base.italics_correction(); let base_italics_correction = base.italics_correction();
let base_text_like = base.is_text_like(); let base_text_like = base.is_text_like();
let base_ascent = match &base { let base_ascent = match &base {
MathFragment::Frame(frame) => frame.base_ascent, MathFragment::Frame(frame) => frame.base_ascent,
_ => base.ascent(), _ => base.ascent(),
}; };
let base_descent = match &base {
MathFragment::Frame(frame) => frame.base_descent,
_ => base.descent(),
};
let mut frame = Frame::soft(size); let mut frame = Frame::soft(size);
frame.set_baseline(baseline); frame.set_baseline(baseline);
@ -73,6 +89,7 @@ pub fn layout_accent(
FrameFragment::new(styles, frame) FrameFragment::new(styles, frame)
.with_class(base_class) .with_class(base_class)
.with_base_ascent(base_ascent) .with_base_ascent(base_ascent)
.with_base_descent(base_descent)
.with_italics_correction(base_italics_correction) .with_italics_correction(base_italics_correction)
.with_accent_attach(base_attach) .with_accent_attach(base_attach)
.with_text_like(base_text_like), .with_text_like(base_text_like),

View File

@ -434,9 +434,13 @@ fn compute_script_shifts(
} }
if bl.is_some() || br.is_some() { if bl.is_some() || br.is_some() {
let descent = match &base {
MathFragment::Frame(frame) => frame.base_descent,
_ => base.descent(),
};
shift_down = shift_down shift_down = shift_down
.max(sub_shift_down) .max(sub_shift_down)
.max(if is_text_like { Abs::zero() } else { base.descent() + sub_drop_min }) .max(if is_text_like { Abs::zero() } else { descent + sub_drop_min })
.max(measure!(bl, ascent) - sub_top_max) .max(measure!(bl, ascent) - sub_top_max)
.max(measure!(br, ascent) - sub_top_max); .max(measure!(br, ascent) - sub_top_max);
} }

View File

@ -110,12 +110,12 @@ fn layout_frac_like(
if binom { if binom {
let mut left = GlyphFragment::new(ctx, styles, '(', span) let mut left = GlyphFragment::new(ctx, styles, '(', span)
.stretch_vertical(ctx, height, short_fall); .stretch_vertical(ctx, height - short_fall);
left.center_on_axis(ctx); left.center_on_axis(ctx);
ctx.push(left); ctx.push(left);
ctx.push(FrameFragment::new(styles, frame)); ctx.push(FrameFragment::new(styles, frame));
let mut right = GlyphFragment::new(ctx, styles, ')', span) let mut right = GlyphFragment::new(ctx, styles, ')', span)
.stretch_vertical(ctx, height, short_fall); .stretch_vertical(ctx, height - short_fall);
right.center_on_axis(ctx); right.center_on_axis(ctx);
ctx.push(right); ctx.push(right);
} else { } else {

View File

@ -11,7 +11,7 @@ use typst_library::layout::{
}; };
use typst_library::math::{EquationElem, MathSize}; use typst_library::math::{EquationElem, MathSize};
use typst_library::text::{Font, Glyph, Lang, Region, TextElem, TextItem}; use typst_library::text::{Font, Glyph, Lang, Region, TextElem, TextItem};
use typst_library::visualize::Paint; use typst_library::visualize::{FixedStroke, Paint};
use typst_syntax::Span; use typst_syntax::Span;
use typst_utils::default_math_class; use typst_utils::default_math_class;
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
@ -164,12 +164,12 @@ impl MathFragment {
} }
} }
pub fn accent_attach(&self) -> Abs { pub fn accent_attach(&self) -> (Abs, Abs) {
match self { match self {
Self::Glyph(glyph) => glyph.accent_attach, Self::Glyph(glyph) => glyph.accent_attach,
Self::Variant(variant) => variant.accent_attach, Self::Variant(variant) => variant.accent_attach,
Self::Frame(fragment) => fragment.accent_attach, Self::Frame(fragment) => fragment.accent_attach,
_ => self.width() / 2.0, _ => (self.width() / 2.0, self.width() / 2.0),
} }
} }
@ -235,12 +235,13 @@ pub struct GlyphFragment {
pub lang: Lang, pub lang: Lang,
pub region: Option<Region>, pub region: Option<Region>,
pub fill: Paint, pub fill: Paint,
pub stroke: Option<FixedStroke>,
pub shift: Abs, pub shift: Abs,
pub width: Abs, pub width: Abs,
pub ascent: Abs, pub ascent: Abs,
pub descent: Abs, pub descent: Abs,
pub italics_correction: Abs, pub italics_correction: Abs,
pub accent_attach: Abs, pub accent_attach: (Abs, Abs),
pub font_size: Abs, pub font_size: Abs,
pub class: MathClass, pub class: MathClass,
pub math_size: MathSize, pub math_size: MathSize,
@ -286,6 +287,7 @@ impl GlyphFragment {
lang: TextElem::lang_in(styles), lang: TextElem::lang_in(styles),
region: TextElem::region_in(styles), region: TextElem::region_in(styles),
fill: TextElem::fill_in(styles).as_decoration(), fill: TextElem::fill_in(styles).as_decoration(),
stroke: TextElem::stroke_in(styles).map(|s| s.unwrap_or_default()),
shift: TextElem::baseline_in(styles), shift: TextElem::baseline_in(styles),
font_size: TextElem::size_in(styles), font_size: TextElem::size_in(styles),
math_size: EquationElem::size_in(styles), math_size: EquationElem::size_in(styles),
@ -294,7 +296,7 @@ impl GlyphFragment {
descent: Abs::zero(), descent: Abs::zero(),
limits: Limits::for_char(c), limits: Limits::for_char(c),
italics_correction: Abs::zero(), italics_correction: Abs::zero(),
accent_attach: Abs::zero(), accent_attach: (Abs::zero(), Abs::zero()),
class, class,
span, span,
modifiers: FrameModifiers::get_in(styles), modifiers: FrameModifiers::get_in(styles),
@ -326,8 +328,14 @@ impl GlyphFragment {
}); });
let mut width = advance.scaled(ctx, self.font_size); let mut width = advance.scaled(ctx, self.font_size);
let accent_attach =
// The fallback for accents is half the width plus or minus the italics
// correction. This is similar to how top and bottom attachments are
// shifted. For bottom accents we do not use the accent attach of the
// base as it is meant for top acccents.
let top_accent_attach =
accent_attach(ctx, id, self.font_size).unwrap_or((width + italics) / 2.0); accent_attach(ctx, id, self.font_size).unwrap_or((width + italics) / 2.0);
let bottom_accent_attach = (width - italics) / 2.0;
let extended_shape = is_extended_shape(ctx, id); let extended_shape = is_extended_shape(ctx, id);
if !extended_shape { if !extended_shape {
@ -339,7 +347,7 @@ impl GlyphFragment {
self.ascent = bbox.y_max.scaled(ctx, self.font_size); self.ascent = bbox.y_max.scaled(ctx, self.font_size);
self.descent = -bbox.y_min.scaled(ctx, self.font_size); self.descent = -bbox.y_min.scaled(ctx, self.font_size);
self.italics_correction = italics; self.italics_correction = italics;
self.accent_attach = accent_attach; self.accent_attach = (top_accent_attach, bottom_accent_attach);
self.extended_shape = extended_shape; self.extended_shape = extended_shape;
} }
@ -368,10 +376,10 @@ impl GlyphFragment {
font: self.font.clone(), font: self.font.clone(),
size: self.font_size, size: self.font_size,
fill: self.fill, fill: self.fill,
stroke: self.stroke,
lang: self.lang, lang: self.lang,
region: self.region, region: self.region,
text: self.c.into(), text: self.c.into(),
stroke: None,
glyphs: vec![Glyph { glyphs: vec![Glyph {
id: self.id.0, id: self.id.0,
x_advance: Em::from_length(self.width, self.font_size), x_advance: Em::from_length(self.width, self.font_size),
@ -427,13 +435,8 @@ impl GlyphFragment {
} }
/// Try to stretch a glyph to a desired height. /// Try to stretch a glyph to a desired height.
pub fn stretch_vertical( pub fn stretch_vertical(self, ctx: &mut MathContext, height: Abs) -> VariantFragment {
self, stretch_glyph(ctx, self, height, Axis::Y)
ctx: &mut MathContext,
height: Abs,
short_fall: Abs,
) -> VariantFragment {
stretch_glyph(ctx, self, height, short_fall, Axis::Y)
} }
/// Try to stretch a glyph to a desired width. /// Try to stretch a glyph to a desired width.
@ -441,9 +444,8 @@ impl GlyphFragment {
self, self,
ctx: &mut MathContext, ctx: &mut MathContext,
width: Abs, width: Abs,
short_fall: Abs,
) -> VariantFragment { ) -> VariantFragment {
stretch_glyph(ctx, self, width, short_fall, Axis::X) stretch_glyph(ctx, self, width, Axis::X)
} }
} }
@ -457,7 +459,7 @@ impl Debug for GlyphFragment {
pub struct VariantFragment { pub struct VariantFragment {
pub c: char, pub c: char,
pub italics_correction: Abs, pub italics_correction: Abs,
pub accent_attach: Abs, pub accent_attach: (Abs, Abs),
pub frame: Frame, pub frame: Frame,
pub font_size: Abs, pub font_size: Abs,
pub class: MathClass, pub class: MathClass,
@ -499,8 +501,9 @@ pub struct FrameFragment {
pub limits: Limits, pub limits: Limits,
pub spaced: bool, pub spaced: bool,
pub base_ascent: Abs, pub base_ascent: Abs,
pub base_descent: Abs,
pub italics_correction: Abs, pub italics_correction: Abs,
pub accent_attach: Abs, pub accent_attach: (Abs, Abs),
pub text_like: bool, pub text_like: bool,
pub ignorant: bool, pub ignorant: bool,
} }
@ -508,6 +511,7 @@ pub struct FrameFragment {
impl FrameFragment { impl FrameFragment {
pub fn new(styles: StyleChain, frame: Frame) -> Self { pub fn new(styles: StyleChain, frame: Frame) -> Self {
let base_ascent = frame.ascent(); let base_ascent = frame.ascent();
let base_descent = frame.descent();
let accent_attach = frame.width() / 2.0; let accent_attach = frame.width() / 2.0;
Self { Self {
frame: frame.modified(&FrameModifiers::get_in(styles)), frame: frame.modified(&FrameModifiers::get_in(styles)),
@ -517,8 +521,9 @@ impl FrameFragment {
limits: Limits::Never, limits: Limits::Never,
spaced: false, spaced: false,
base_ascent, base_ascent,
base_descent,
italics_correction: Abs::zero(), italics_correction: Abs::zero(),
accent_attach, accent_attach: (accent_attach, accent_attach),
text_like: false, text_like: false,
ignorant: false, ignorant: false,
} }
@ -540,11 +545,15 @@ impl FrameFragment {
Self { base_ascent, ..self } Self { base_ascent, ..self }
} }
pub fn with_base_descent(self, base_descent: Abs) -> Self {
Self { base_descent, ..self }
}
pub fn with_italics_correction(self, italics_correction: Abs) -> Self { pub fn with_italics_correction(self, italics_correction: Abs) -> Self {
Self { italics_correction, ..self } Self { italics_correction, ..self }
} }
pub fn with_accent_attach(self, accent_attach: Abs) -> Self { pub fn with_accent_attach(self, accent_attach: (Abs, Abs)) -> Self {
Self { accent_attach, ..self } Self { accent_attach, ..self }
} }

View File

@ -314,7 +314,7 @@ fn layout_delimiters(
if let Some(left) = left { if let Some(left) = left {
let mut left = GlyphFragment::new(ctx, styles, left, span) let mut left = GlyphFragment::new(ctx, styles, left, span)
.stretch_vertical(ctx, target, short_fall); .stretch_vertical(ctx, target - short_fall);
left.align_on_axis(ctx, delimiter_alignment(left.c)); left.align_on_axis(ctx, delimiter_alignment(left.c));
ctx.push(left); ctx.push(left);
} }
@ -323,7 +323,7 @@ fn layout_delimiters(
if let Some(right) = right { if let Some(right) = right {
let mut right = GlyphFragment::new(ctx, styles, right, span) let mut right = GlyphFragment::new(ctx, styles, right, span)
.stretch_vertical(ctx, target, short_fall); .stretch_vertical(ctx, target - short_fall);
right.align_on_axis(ctx, delimiter_alignment(right.c)); right.align_on_axis(ctx, delimiter_alignment(right.c));
ctx.push(right); ctx.push(right);
} }

View File

@ -50,7 +50,7 @@ pub fn layout_root(
// Layout root symbol. // Layout root symbol.
let target = radicand.height() + thickness + gap; let target = radicand.height() + thickness + gap;
let sqrt = GlyphFragment::new(ctx, styles, '√', span) let sqrt = GlyphFragment::new(ctx, styles, '√', span)
.stretch_vertical(ctx, target, Abs::zero()) .stretch_vertical(ctx, target)
.frame; .frame;
// Layout the index. // Layout the index.

View File

@ -278,6 +278,9 @@ impl MathRun {
frame frame
} }
/// Convert this run of math fragments into a vector of inline items for
/// paragraph layout. Creates multiple fragments when relation or binary
/// operators are present to allow for line-breaking opportunities later.
pub fn into_par_items(self) -> Vec<InlineItem> { pub fn into_par_items(self) -> Vec<InlineItem> {
let mut items = vec![]; let mut items = vec![];
@ -295,21 +298,24 @@ impl MathRun {
let mut space_is_visible = false; let mut space_is_visible = false;
let is_relation = |f: &MathFragment| matches!(f.class(), MathClass::Relation);
let is_space = |f: &MathFragment| { let is_space = |f: &MathFragment| {
matches!(f, MathFragment::Space(_) | MathFragment::Spacing(_, _)) matches!(f, MathFragment::Space(_) | MathFragment::Spacing(_, _))
}; };
let is_line_break_opportunity = |class, next_fragment| match class {
// Don't split when two relations are in a row or when preceding a
// closing parenthesis.
MathClass::Binary => next_fragment != Some(MathClass::Closing),
MathClass::Relation => {
!matches!(next_fragment, Some(MathClass::Relation | MathClass::Closing))
}
_ => false,
};
let mut iter = self.0.into_iter().peekable(); let mut iter = self.0.into_iter().peekable();
while let Some(fragment) = iter.next() { while let Some(fragment) = iter.next() {
if space_is_visible { if space_is_visible && is_space(&fragment) {
match fragment { items.push(InlineItem::Space(fragment.width(), true));
MathFragment::Space(width) | MathFragment::Spacing(width, _) => { continue;
items.push(InlineItem::Space(width, true));
continue;
}
_ => {}
}
} }
let class = fragment.class(); let class = fragment.class();
@ -323,10 +329,9 @@ impl MathRun {
frame.push_frame(pos, fragment.into_frame()); frame.push_frame(pos, fragment.into_frame());
empty = false; empty = false;
if class == MathClass::Binary // Split our current frame when we encounter a binary operator or
|| (class == MathClass::Relation // relation so that there is a line-breaking opportunity.
&& !iter.peek().map(is_relation).unwrap_or_default()) if is_line_break_opportunity(class, iter.peek().map(|f| f.class())) {
{
let mut frame_prev = let mut frame_prev =
std::mem::replace(&mut frame, Frame::soft(Size::zero())); std::mem::replace(&mut frame, Frame::soft(Size::zero()));

View File

@ -67,8 +67,7 @@ pub fn stretch_fragment(
let mut variant = stretch_glyph( let mut variant = stretch_glyph(
ctx, ctx,
glyph, glyph,
stretch.relative_to(relative_to_size), stretch.relative_to(relative_to_size) - short_fall,
short_fall,
axis, axis,
); );
@ -120,7 +119,6 @@ pub fn stretch_glyph(
ctx: &mut MathContext, ctx: &mut MathContext,
mut base: GlyphFragment, mut base: GlyphFragment,
target: Abs, target: Abs,
short_fall: Abs,
axis: Axis, axis: Axis,
) -> VariantFragment { ) -> VariantFragment {
// If the base glyph is good enough, use it. // If the base glyph is good enough, use it.
@ -128,8 +126,7 @@ pub fn stretch_glyph(
Axis::X => base.width, Axis::X => base.width,
Axis::Y => base.height(), Axis::Y => base.height(),
}; };
let short_target = target - short_fall; if target <= advance {
if short_target <= advance {
return base.into_variant(); return base.into_variant();
} }
@ -153,13 +150,13 @@ pub fn stretch_glyph(
for variant in construction.variants { for variant in construction.variants {
best_id = variant.variant_glyph; best_id = variant.variant_glyph;
best_advance = base.font.to_em(variant.advance_measurement).at(base.font_size); best_advance = base.font.to_em(variant.advance_measurement).at(base.font_size);
if short_target <= best_advance { if target <= best_advance {
break; break;
} }
} }
// This is either good or the best we've got. // This is either good or the best we've got.
if short_target <= best_advance || construction.assembly.is_none() { if target <= best_advance || construction.assembly.is_none() {
base.set_id(ctx, best_id); base.set_id(ctx, best_id);
return base.into_variant(); return base.into_variant();
} }
@ -278,7 +275,7 @@ fn assemble(
} }
let accent_attach = match axis { let accent_attach = match axis {
Axis::X => frame.width() / 2.0, Axis::X => (frame.width() / 2.0, frame.width() / 2.0),
Axis::Y => base.accent_attach, Axis::Y => base.accent_attach,
}; };

View File

@ -65,18 +65,13 @@ fn layout_inline_text(
// Small optimization for numbers. Note that this lays out slightly // Small optimization for numbers. Note that this lays out slightly
// differently to normal text and is worth re-evaluating in the future. // differently to normal text and is worth re-evaluating in the future.
let mut fragments = vec![]; let mut fragments = vec![];
let is_single = text.chars().count() == 1;
for unstyled_c in text.chars() { for unstyled_c in text.chars() {
let c = styled_char(styles, unstyled_c, false); let c = styled_char(styles, unstyled_c, false);
let mut glyph = GlyphFragment::new(ctx, styles, c, span); let mut glyph = GlyphFragment::new(ctx, styles, c, span);
if is_single { match EquationElem::size_in(styles) {
// Duplicate what `layout_glyph` does exactly even if it's MathSize::Script => glyph.make_script_size(ctx),
// probably incorrect here. MathSize::ScriptScript => glyph.make_script_script_size(ctx),
match EquationElem::size_in(styles) { _ => {}
MathSize::Script => glyph.make_script_size(ctx),
MathSize::ScriptScript => glyph.make_script_script_size(ctx),
_ => {}
}
} }
fragments.push(glyph.into()); fragments.push(glyph.into());
} }
@ -164,7 +159,7 @@ fn layout_glyph(
let mut variant = if math_size == MathSize::Display { let mut variant = if math_size == MathSize::Display {
let height = scaled!(ctx, styles, display_operator_min_height) let height = scaled!(ctx, styles, display_operator_min_height)
.max(SQRT_2 * glyph.height()); .max(SQRT_2 * glyph.height());
glyph.stretch_vertical(ctx, height, Abs::zero()) glyph.stretch_vertical(ctx, height)
} else { } else {
glyph.into_variant() glyph.into_variant()
}; };

View File

@ -286,7 +286,7 @@ fn layout_underoverspreader(
let body_class = body.class(); let body_class = body.class();
let body = body.into_fragment(styles); let body = body.into_fragment(styles);
let glyph = GlyphFragment::new(ctx, styles, c, span); let glyph = GlyphFragment::new(ctx, styles, c, span);
let stretched = glyph.stretch_horizontal(ctx, body.width(), Abs::zero()); let stretched = glyph.stretch_horizontal(ctx, body.width());
let mut rows = vec![]; let mut rows = vec![];
let baseline = match position { let baseline = match position {

View File

@ -1,6 +1,6 @@
use typst_library::foundations::StyleChain; use typst_library::foundations::StyleChain;
use typst_library::layout::{Fragment, Frame, FrameItem, HideElem, Point}; use typst_library::layout::{Abs, Fragment, Frame, FrameItem, HideElem, Point, Sides};
use typst_library::model::{Destination, LinkElem}; use typst_library::model::{Destination, LinkElem, ParElem};
/// Frame-level modifications resulting from styles that do not impose any /// Frame-level modifications resulting from styles that do not impose any
/// layout structure. /// layout structure.
@ -52,14 +52,7 @@ pub trait FrameModify {
impl FrameModify for Frame { impl FrameModify for Frame {
fn modify(&mut self, modifiers: &FrameModifiers) { fn modify(&mut self, modifiers: &FrameModifiers) {
if let Some(dest) = &modifiers.dest { modify_frame(self, modifiers, None);
let size = self.size();
self.push(Point::zero(), FrameItem::Link(dest.clone(), size));
}
if modifiers.hidden {
self.hide();
}
} }
} }
@ -82,6 +75,41 @@ where
} }
} }
pub trait FrameModifyText {
/// Resolve and apply [`FrameModifiers`] for this text frame.
fn modify_text(&mut self, styles: StyleChain);
}
impl FrameModifyText for Frame {
fn modify_text(&mut self, styles: StyleChain) {
let modifiers = FrameModifiers::get_in(styles);
let expand_y = 0.5 * ParElem::leading_in(styles);
let outset = Sides::new(Abs::zero(), expand_y, Abs::zero(), expand_y);
modify_frame(self, &modifiers, Some(outset));
}
}
fn modify_frame(
frame: &mut Frame,
modifiers: &FrameModifiers,
link_box_outset: Option<Sides<Abs>>,
) {
if let Some(dest) = &modifiers.dest {
let mut pos = Point::zero();
let mut size = frame.size();
if let Some(outset) = link_box_outset {
pos.y -= outset.top;
pos.x -= outset.left;
size += outset.sum_by_axis();
}
frame.push(pos, FrameItem::Link(dest.clone(), size));
}
if modifiers.hidden {
frame.hide();
}
}
/// Performs layout and modification in one step. /// Performs layout and modification in one step.
/// ///
/// This just runs `layout(styles).modified(&FrameModifiers::get_in(styles))`, /// This just runs `layout(styles).modified(&FrameModifiers::get_in(styles))`,

View File

@ -708,12 +708,13 @@ pub fn fract(
} }
} }
/// Rounds a number to the nearest integer away from zero. /// Rounds a number to the nearest integer.
/// ///
/// Optionally, a number of decimal places can be specified. /// Half-integers are rounded away from zero.
/// ///
/// If the number of digits is negative, its absolute value will indicate the /// Optionally, a number of decimal places can be specified. If negative, its
/// amount of significant integer digits to remove before the decimal point. /// absolute value will indicate the amount of significant integer digits to
/// remove before the decimal point.
/// ///
/// Note that this function will return the same type as the operand. That is, /// Note that this function will return the same type as the operand. That is,
/// applying `round` to a [`float`] will return a `float`, and to a [`decimal`], /// applying `round` to a [`float`] will return a `float`, and to a [`decimal`],

View File

@ -414,7 +414,7 @@ impl Content {
/// Elements produced in `show` rules will not be included in the results. /// Elements produced in `show` rules will not be included in the results.
pub fn query(&self, selector: Selector) -> Vec<Content> { pub fn query(&self, selector: Selector) -> Vec<Content> {
let mut results = Vec::new(); let mut results = Vec::new();
self.traverse(&mut |element| -> ControlFlow<()> { let _ = self.traverse(&mut |element| -> ControlFlow<()> {
if selector.matches(&element, None) { if selector.matches(&element, None) {
results.push(element); results.push(element);
} }
@ -441,7 +441,7 @@ impl Content {
/// Extracts the plain text of this content. /// Extracts the plain text of this content.
pub fn plain_text(&self) -> EcoString { pub fn plain_text(&self) -> EcoString {
let mut text = EcoString::new(); let mut text = EcoString::new();
self.traverse(&mut |element| -> ControlFlow<()> { let _ = self.traverse(&mut |element| -> ControlFlow<()> {
if let Some(textable) = element.with::<dyn PlainText>() { if let Some(textable) = element.with::<dyn PlainText>() {
textable.plain_text(&mut text); textable.plain_text(&mut text);
} }

View File

@ -41,8 +41,23 @@ use crate::layout::{BlockElem, Size};
/// receives the page's dimensions minus its margins. This is mostly useful in /// receives the page's dimensions minus its margins. This is mostly useful in
/// combination with [measurement]($measure). /// combination with [measurement]($measure).
/// ///
/// You can also use this function to resolve [`ratio`] to fixed lengths. This /// To retrieve the _remaining_ height of the page rather than its full size,
/// might come in handy if you're building your own layout abstractions. /// you can wrap your `layout` call in a `{block(height: 1fr)}`. This works
/// because the block automatically grows to fill the remaining space (see the
/// [fraction] documentation for more details).
///
/// ```example
/// #set page(height: 150pt)
///
/// #lorem(20)
///
/// #block(height: 1fr, layout(size => [
/// Remaining height: #size.height
/// ]))
/// ```
///
/// You can also use this function to resolve a [`ratio`] to a fixed length.
/// This might come in handy if you're building your own layout abstractions.
/// ///
/// ```example /// ```example
/// #layout(size => { /// #layout(size => {

View File

@ -1,16 +1,14 @@
use std::borrow::Cow;
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
use std::str::FromStr; use std::str::FromStr;
use comemo::Track;
use typst_utils::{singleton, NonZeroExt, Scalar}; use typst_utils::{singleton, NonZeroExt, Scalar};
use crate::diag::{bail, SourceResult}; use crate::diag::{bail, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, Args, AutoValue, Cast, Construct, Content, Context, Dict, Fold, Func, cast, elem, Args, AutoValue, Cast, Construct, Content, Dict, Fold, NativeElement,
NativeElement, Set, Smart, StyleChain, Value, Set, Smart, Value,
}; };
use crate::introspection::Introspector; use crate::introspection::Introspector;
use crate::layout::{ use crate::layout::{
@ -649,43 +647,6 @@ cast! {
}, },
} }
/// A header, footer, foreground or background definition.
#[derive(Debug, Clone, Hash)]
pub enum Marginal {
/// Bare content.
Content(Content),
/// A closure mapping from a page number to content.
Func(Func),
}
impl Marginal {
/// Resolve the marginal based on the page number.
pub fn resolve(
&self,
engine: &mut Engine,
styles: StyleChain,
page: usize,
) -> SourceResult<Cow<'_, Content>> {
Ok(match self {
Self::Content(content) => Cow::Borrowed(content),
Self::Func(func) => Cow::Owned(
func.call(engine, Context::new(None, Some(styles)).track(), [page])?
.display(),
),
})
}
}
cast! {
Marginal,
self => match self {
Self::Content(v) => v.into_value(),
Self::Func(v) => v.into_value(),
},
v: Content => Self::Content(v),
v: Func => Self::Func(v),
}
/// A list of page ranges to be exported. /// A list of page ranges to be exported.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PageRanges(Vec<PageRange>); pub struct PageRanges(Vec<PageRange>);

View File

@ -80,6 +80,19 @@ impl Accent {
pub fn new(c: char) -> Self { pub fn new(c: char) -> Self {
Self(Self::combine(c).unwrap_or(c)) Self(Self::combine(c).unwrap_or(c))
} }
/// List of bottom accents. Currently just a list of ones included in the
/// Unicode math class document.
const BOTTOM: &[char] = &[
'\u{0323}', '\u{032C}', '\u{032D}', '\u{032E}', '\u{032F}', '\u{0330}',
'\u{0331}', '\u{0332}', '\u{0333}', '\u{033A}', '\u{20E8}', '\u{20EC}',
'\u{20ED}', '\u{20EE}', '\u{20EF}',
];
/// Whether this accent is a bottom accent or not.
pub fn is_bottom(&self) -> bool {
Self::BOTTOM.contains(&self.0)
}
} }
/// This macro generates accent-related functions. /// This macro generates accent-related functions.

View File

@ -259,10 +259,11 @@ impl Show for Packed<EnumElem> {
.spanned(self.span()); .spanned(self.span());
if tight { if tight {
let leading = ParElem::leading_in(styles); let spacing = self
let spacing = .spacing(styles)
VElem::new(leading.into()).with_weak(true).with_attach(true).pack(); .unwrap_or_else(|| ParElem::leading_in(styles).into());
realized = spacing + realized; let v = VElem::new(spacing.into()).with_weak(true).with_attach(true).pack();
realized = v + realized;
} }
Ok(realized) Ok(realized)

View File

@ -125,6 +125,9 @@ pub struct FigureElem {
/// ///
/// ```example /// ```example
/// #set page(height: 200pt) /// #set page(height: 200pt)
/// #show figure: set place(
/// clearance: 1em,
/// )
/// ///
/// = Introduction /// = Introduction
/// #figure( /// #figure(
@ -457,7 +460,7 @@ impl Outlinable for Packed<FigureElem> {
/// customize the appearance of captions for all figures or figures of a /// customize the appearance of captions for all figures or figures of a
/// specific kind. /// specific kind.
/// ///
/// In addition to its `pos` and `body`, the `caption` also provides the /// In addition to its `position` and `body`, the `caption` also provides the
/// figure's `kind`, `supplement`, `counter`, and `numbering` as fields. These /// figure's `kind`, `supplement`, `counter`, and `numbering` as fields. These
/// parts can be used in [`where`]($function.where) selectors and show rules to /// parts can be used in [`where`]($function.where) selectors and show rules to
/// build a completely custom caption. /// build a completely custom caption.

View File

@ -166,10 +166,11 @@ impl Show for Packed<ListElem> {
.spanned(self.span()); .spanned(self.span());
if tight { if tight {
let leading = ParElem::leading_in(styles); let spacing = self
let spacing = .spacing(styles)
VElem::new(leading.into()).with_weak(true).with_attach(true).pack(); .unwrap_or_else(|| ParElem::leading_in(styles).into());
realized = spacing + realized; let v = VElem::new(spacing.into()).with_weak(true).with_attach(true).pack();
realized = v + realized;
} }
Ok(realized) Ok(realized)

View File

@ -9,7 +9,6 @@ use ecow::{eco_format, EcoString, EcoVec};
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{cast, func, Context, Func, Str, Value}; use crate::foundations::{cast, func, Context, Func, Str, Value};
use crate::text::Case;
/// Applies a numbering to a sequence of numbers. /// Applies a numbering to a sequence of numbers.
/// ///
@ -261,9 +260,9 @@ pub enum NumberingKind {
LowerRoman, LowerRoman,
/// Uppercase Roman numerals (I, II, III, etc.). /// Uppercase Roman numerals (I, II, III, etc.).
UpperRoman, UpperRoman,
/// Lowercase Greek numerals (Α, Β, Γ, etc.). /// Lowercase Greek letters (α, β, γ, etc.).
LowerGreek, LowerGreek,
/// Uppercase Greek numerals (α, β, γ, etc.). /// Uppercase Greek letters (Α, Β, Γ, etc.).
UpperGreek, UpperGreek,
/// Paragraph/note-like symbols: *, †, ‡, §, ¶, and ‖. Further items use /// Paragraph/note-like symbols: *, †, ‡, §, ¶, and ‖. Further items use
/// repeated symbols. /// repeated symbols.
@ -381,40 +380,194 @@ impl NumberingKind {
/// Apply the numbering to the given number. /// Apply the numbering to the given number.
pub fn apply(self, n: u64) -> EcoString { pub fn apply(self, n: u64) -> EcoString {
match self { match self {
Self::Arabic => eco_format!("{n}"), Self::Arabic => {
Self::LowerRoman => roman_numeral(n, Case::Lower), numeric(&['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], n)
Self::UpperRoman => roman_numeral(n, Case::Upper),
Self::LowerGreek => greek_numeral(n, Case::Lower),
Self::UpperGreek => greek_numeral(n, Case::Upper),
Self::Symbol => {
if n == 0 {
return '-'.into();
}
const SYMBOLS: &[char] = &['*', '†', '‡', '§', '¶', '‖'];
let n_symbols = SYMBOLS.len() as u64;
let symbol = SYMBOLS[((n - 1) % n_symbols) as usize];
let amount = ((n - 1) / n_symbols) + 1;
std::iter::repeat_n(symbol, amount.try_into().unwrap()).collect()
} }
Self::Hebrew => hebrew_numeral(n), Self::LowerRoman => additive(
&[
Self::LowerLatin => zeroless( ("", 1000000),
[ ("", 500000),
("", 100000),
("", 50000),
("", 10000),
("", 5000),
("i̅v̅", 4000),
("m", 1000),
("cm", 900),
("d", 500),
("cd", 400),
("c", 100),
("xc", 90),
("l", 50),
("xl", 40),
("x", 10),
("ix", 9),
("v", 5),
("iv", 4),
("i", 1),
("n", 0),
],
n,
),
Self::UpperRoman => additive(
&[
("", 1000000),
("", 500000),
("", 100000),
("", 50000),
("", 10000),
("", 5000),
("I̅V̅", 4000),
("M", 1000),
("CM", 900),
("D", 500),
("CD", 400),
("C", 100),
("XC", 90),
("L", 50),
("XL", 40),
("X", 10),
("IX", 9),
("V", 5),
("IV", 4),
("I", 1),
("N", 0),
],
n,
),
Self::LowerGreek => additive(
&[
("͵θ", 9000),
("͵η", 8000),
("͵ζ", 7000),
("͵ϛ", 6000),
("͵ε", 5000),
("͵δ", 4000),
("͵γ", 3000),
("͵β", 2000),
("͵α", 1000),
("ϡ", 900),
("ω", 800),
("ψ", 700),
("χ", 600),
("φ", 500),
("υ", 400),
("τ", 300),
("σ", 200),
("ρ", 100),
("ϟ", 90),
("π", 80),
("ο", 70),
("ξ", 60),
("ν", 50),
("μ", 40),
("λ", 30),
("κ", 20),
("ι", 10),
("θ", 9),
("η", 8),
("ζ", 7),
("ϛ", 6),
("ε", 5),
("δ", 4),
("γ", 3),
("β", 2),
("α", 1),
("𐆊", 0),
],
n,
),
Self::UpperGreek => additive(
&[
("͵Θ", 9000),
("͵Η", 8000),
("͵Ζ", 7000),
("͵Ϛ", 6000),
("͵Ε", 5000),
("͵Δ", 4000),
("͵Γ", 3000),
("͵Β", 2000),
("͵Α", 1000),
("Ϡ", 900),
("Ω", 800),
("Ψ", 700),
("Χ", 600),
("Φ", 500),
("Υ", 400),
("Τ", 300),
("Σ", 200),
("Ρ", 100),
("Ϟ", 90),
("Π", 80),
("Ο", 70),
("Ξ", 60),
("Ν", 50),
("Μ", 40),
("Λ", 30),
("Κ", 20),
("Ι", 10),
("Θ", 9),
("Η", 8),
("Ζ", 7),
("Ϛ", 6),
("Ε", 5),
("Δ", 4),
("Γ", 3),
("Β", 2),
("Α", 1),
("𐆊", 0),
],
n,
),
Self::Hebrew => additive(
&[
("ת", 400),
("ש", 300),
("ר", 200),
("ק", 100),
("צ", 90),
("פ", 80),
("ע", 70),
("ס", 60),
("נ", 50),
("מ", 40),
("ל", 30),
("כ", 20),
("יט", 19),
("יח", 18),
("יז", 17),
("טז", 16),
("טו", 15),
("י", 10),
("ט", 9),
("ח", 8),
("ז", 7),
("ו", 6),
("ה", 5),
("ד", 4),
("ג", 3),
("ב", 2),
("א", 1),
("-", 0),
],
n,
),
Self::LowerLatin => alphabetic(
&[
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
], ],
n, n,
), ),
Self::UpperLatin => zeroless( Self::UpperLatin => alphabetic(
[ &[
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
], ],
n, n,
), ),
Self::HiraganaAiueo => zeroless( Self::HiraganaAiueo => alphabetic(
[ &[
'あ', 'い', 'う', 'え', 'お', 'か', 'き', 'く', 'け', 'こ', 'さ', 'あ', 'い', 'う', 'え', 'お', 'か', 'き', 'く', 'け', 'こ', 'さ',
'し', 'す', 'せ', 'そ', 'た', 'ち', 'つ', 'て', 'と', 'な', 'に', 'し', 'す', 'せ', 'そ', 'た', 'ち', 'つ', 'て', 'と', 'な', 'に',
'ぬ', 'ね', 'の', 'は', 'ひ', 'ふ', 'へ', 'ほ', 'ま', 'み', 'む', 'ぬ', 'ね', 'の', 'は', 'ひ', 'ふ', 'へ', 'ほ', 'ま', 'み', 'む',
@ -423,8 +576,8 @@ impl NumberingKind {
], ],
n, n,
), ),
Self::HiraganaIroha => zeroless( Self::HiraganaIroha => alphabetic(
[ &[
'い', 'ろ', 'は', 'に', 'ほ', 'へ', 'と', 'ち', 'り', 'ぬ', 'る', 'い', 'ろ', 'は', 'に', 'ほ', 'へ', 'と', 'ち', 'り', 'ぬ', 'る',
'を', 'わ', 'か', 'よ', 'た', 'れ', 'そ', 'つ', 'ね', 'な', 'ら', 'を', 'わ', 'か', 'よ', 'た', 'れ', 'そ', 'つ', 'ね', 'な', 'ら',
'む', 'う', 'ゐ', 'の', 'お', 'く', 'や', 'ま', 'け', 'ふ', 'こ', 'む', 'う', 'ゐ', 'の', 'お', 'く', 'や', 'ま', 'け', 'ふ', 'こ',
@ -433,8 +586,8 @@ impl NumberingKind {
], ],
n, n,
), ),
Self::KatakanaAiueo => zeroless( Self::KatakanaAiueo => alphabetic(
[ &[
'ア', 'イ', 'ウ', 'エ', 'オ', 'カ', 'キ', 'ク', 'ケ', 'コ', 'サ', 'ア', 'イ', 'ウ', 'エ', 'オ', 'カ', 'キ', 'ク', 'ケ', 'コ', 'サ',
'シ', 'ス', 'セ', 'ソ', 'タ', 'チ', 'ツ', 'テ', 'ト', 'ナ', 'ニ', 'シ', 'ス', 'セ', 'ソ', 'タ', 'チ', 'ツ', 'テ', 'ト', 'ナ', 'ニ',
'ヌ', 'ネ', '', 'ハ', 'ヒ', 'フ', 'ヘ', 'ホ', 'マ', 'ミ', 'ム', 'ヌ', 'ネ', '', 'ハ', 'ヒ', 'フ', 'ヘ', 'ホ', 'マ', 'ミ', 'ム',
@ -443,8 +596,8 @@ impl NumberingKind {
], ],
n, n,
), ),
Self::KatakanaIroha => zeroless( Self::KatakanaIroha => alphabetic(
[ &[
'イ', 'ロ', 'ハ', 'ニ', 'ホ', 'ヘ', 'ト', 'チ', 'リ', 'ヌ', 'ル', 'イ', 'ロ', 'ハ', 'ニ', 'ホ', 'ヘ', 'ト', 'チ', 'リ', 'ヌ', 'ル',
'ヲ', 'ワ', 'カ', 'ヨ', 'タ', 'レ', 'ソ', 'ツ', 'ネ', 'ナ', 'ラ', 'ヲ', 'ワ', 'カ', 'ヨ', 'タ', 'レ', 'ソ', 'ツ', 'ネ', 'ナ', 'ラ',
'ム', 'ウ', 'ヰ', '', 'オ', 'ク', 'ヤ', 'マ', 'ケ', 'フ', 'コ', 'ム', 'ウ', 'ヰ', '', 'オ', 'ク', 'ヤ', 'マ', 'ケ', 'フ', 'コ',
@ -453,40 +606,40 @@ impl NumberingKind {
], ],
n, n,
), ),
Self::KoreanJamo => zeroless( Self::KoreanJamo => alphabetic(
[ &[
'ㄱ', 'ㄴ', 'ㄷ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅅ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㄱ', 'ㄴ', 'ㄷ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅅ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ',
'ㅌ', 'ㅍ', 'ㅎ', 'ㅌ', 'ㅍ', 'ㅎ',
], ],
n, n,
), ),
Self::KoreanSyllable => zeroless( Self::KoreanSyllable => alphabetic(
[ &[
'가', '나', '다', '라', '마', '바', '사', '아', '자', '차', '카', '가', '나', '다', '라', '마', '바', '사', '아', '자', '차', '카',
'타', '파', '하', '타', '파', '하',
], ],
n, n,
), ),
Self::BengaliLetter => zeroless( Self::BengaliLetter => alphabetic(
[ &[
'ক', 'খ', 'গ', 'ঘ', 'ঙ', 'চ', 'ছ', 'জ', 'ঝ', 'ঞ', 'ট', 'ঠ', 'ড', 'ঢ', 'ক', 'খ', 'গ', 'ঘ', 'ঙ', 'চ', 'ছ', 'জ', 'ঝ', 'ঞ', 'ট', 'ঠ', 'ড', 'ঢ',
'ণ', 'ত', 'থ', 'দ', 'ধ', 'ন', 'প', 'ফ', 'ব', 'ভ', 'ম', 'য', 'র', 'ল', 'ণ', 'ত', 'থ', 'দ', 'ধ', 'ন', 'প', 'ফ', 'ব', 'ভ', 'ম', 'য', 'র', 'ল',
'শ', 'ষ', 'স', 'হ', 'শ', 'ষ', 'স', 'হ',
], ],
n, n,
), ),
Self::CircledNumber => zeroless( Self::CircledNumber => fixed(
[ &[
'①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩', '⑪', '⑫', '⑬', '⑭', '⓪', '①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩', '⑪', '⑫', '⑬',
'⑮', '⑯', '⑰', '⑱', '⑲', '⑳', '㉑', '㉒', '㉓', '㉔', '㉕', '㉖', '⑭', '⑮', '⑯', '⑰', '⑱', '⑲', '⑳', '㉑', '㉒', '㉓', '㉔', '㉕',
'㉗', '㉘', '㉙', '㉚', '㉛', '㉜', '㉝', '㉞', '㉟', '㊱', '㊲', '㉖', '㉗', '㉘', '㉙', '㉚', '㉛', '㉜', '㉝', '㉞', '㉟', '㊱',
'㊳', '㊴', '㊵', '㊶', '㊷', '㊸', '㊹', '㊺', '㊻', '㊼', '㊽', '㊲', '㊳', '㊴', '㊵', '㊶', '㊷', '㊸', '㊹', '㊺', '㊻', '㊼',
'㊾', '㊿', '㊽', '㊾', '㊿',
], ],
n, n,
), ),
Self::DoubleCircledNumber => { Self::DoubleCircledNumber => {
zeroless(['⓵', '⓶', '⓷', '⓸', '⓹', '⓺', '⓻', '⓼', '⓽', '⓾'], n) fixed(&['0', '⓵', '⓶', '⓷', '⓸', '⓹', '⓺', '⓻', '⓼', '⓽', '⓾'], n)
} }
Self::LowerSimplifiedChinese => { Self::LowerSimplifiedChinese => {
@ -502,306 +655,170 @@ impl NumberingKind {
u64_to_chinese(ChineseVariant::Traditional, ChineseCase::Upper, n).into() u64_to_chinese(ChineseVariant::Traditional, ChineseCase::Upper, n).into()
} }
Self::EasternArabic => decimal('\u{0660}', n), Self::EasternArabic => {
Self::EasternArabicPersian => decimal('\u{06F0}', n), numeric(&['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩'], n)
Self::DevanagariNumber => decimal('\u{0966}', n),
Self::BengaliNumber => decimal('\u{09E6}', n),
}
}
}
/// Stringify an integer to a Hebrew number.
fn hebrew_numeral(mut n: u64) -> EcoString {
if n == 0 {
return '-'.into();
}
let mut fmt = EcoString::new();
'outer: for (name, value) in [
('ת', 400),
('ש', 300),
('ר', 200),
('ק', 100),
('צ', 90),
('פ', 80),
('ע', 70),
('ס', 60),
('נ', 50),
('מ', 40),
('ל', 30),
('כ', 20),
('י', 10),
('ט', 9),
('ח', 8),
('ז', 7),
('ו', 6),
('ה', 5),
('ד', 4),
('ג', 3),
('ב', 2),
('א', 1),
] {
while n >= value {
match n {
15 => fmt.push_str("ט״ו"),
16 => fmt.push_str("ט״ז"),
_ => {
let append_geresh = n == value && fmt.is_empty();
if n == value && !fmt.is_empty() {
fmt.push('״');
}
fmt.push(name);
if append_geresh {
fmt.push('׳');
}
n -= value;
continue;
}
} }
break 'outer; Self::EasternArabicPersian => {
} numeric(&['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'], n)
}
fmt
}
/// Stringify an integer to a Roman numeral.
fn roman_numeral(mut n: u64, case: Case) -> EcoString {
if n == 0 {
return match case {
Case::Lower => 'n'.into(),
Case::Upper => 'N'.into(),
};
}
// Adapted from Yann Villessuzanne's roman.rs under the
// Unlicense, at https://github.com/linfir/roman.rs/
let mut fmt = EcoString::new();
for &(name, value) in &[
("", 1000000),
("", 500000),
("", 100000),
("", 50000),
("", 10000),
("", 5000),
("I̅V̅", 4000),
("M", 1000),
("CM", 900),
("D", 500),
("CD", 400),
("C", 100),
("XC", 90),
("L", 50),
("XL", 40),
("X", 10),
("IX", 9),
("V", 5),
("IV", 4),
("I", 1),
] {
while n >= value {
n -= value;
for c in name.chars() {
match case {
Case::Lower => fmt.extend(c.to_lowercase()),
Case::Upper => fmt.push(c),
}
} }
Self::DevanagariNumber => {
numeric(&['', '१', '२', '३', '४', '५', '६', '७', '८', '९'], n)
}
Self::BengaliNumber => {
numeric(&['', '১', '২', '৩', '', '৫', '৬', '', '৮', '৯'], n)
}
Self::Symbol => symbolic(&['*', '†', '‡', '§', '¶', '‖'], n),
} }
} }
fmt
} }
/// Stringify an integer to Greek numbers. /// Stringify a number using symbols representing values. The decimal
/// representation of the number is recovered by summing over the values of the
/// symbols present.
/// ///
/// Greek numbers use the Greek Alphabet to represent numbers; it is based on 10 /// Consider the situation where ['I': 1, 'IV': 4, 'V': 5],
/// (decimal). Here we implement the single digit M power representation from
/// [The Greek Number Converter][convert] and also described in
/// [Greek Numbers][numbers].
///
/// [converter]: https://www.russellcottrell.com/greek/utilities/GreekNumberConverter.htm
/// [numbers]: https://mathshistory.st-andrews.ac.uk/HistTopics/Greek_numbers/
fn greek_numeral(n: u64, case: Case) -> EcoString {
let thousands = [
["͵α", "͵Α"],
["͵β", "͵Β"],
["͵γ", "͵Γ"],
["͵δ", "͵Δ"],
["͵ε", "͵Ε"],
["͵ϛ", "͵Ϛ"],
["͵ζ", "͵Ζ"],
["͵η", "͵Η"],
["͵θ", "͵Θ"],
];
let hundreds = [
["ρ", "Ρ"],
["σ", "Σ"],
["τ", "Τ"],
["υ", "Υ"],
["φ", "Φ"],
["χ", "Χ"],
["ψ", "Ψ"],
["ω", "Ω"],
["ϡ", "Ϡ"],
];
let tens = [
["ι", "Ι"],
["κ", "Κ"],
["λ", "Λ"],
["μ", "Μ"],
["ν", "Ν"],
["ξ", "Ξ"],
["ο", "Ο"],
["π", "Π"],
["ϙ", "Ϟ"],
];
let ones = [
["α", "Α"],
["β", "Β"],
["γ", "Γ"],
["δ", "Δ"],
["ε", "Ε"],
["ϛ", "Ϛ"],
["ζ", "Ζ"],
["η", "Η"],
["θ", "Θ"],
];
if n == 0 {
// Greek Zero Sign
return '𐆊'.into();
}
let mut fmt = EcoString::new();
let case = match case {
Case::Lower => 0,
Case::Upper => 1,
};
// Extract a list of decimal digits from the number
let mut decimal_digits: Vec<usize> = Vec::new();
let mut n = n;
while n > 0 {
decimal_digits.push((n % 10) as usize);
n /= 10;
}
// Pad the digits with leading zeros to ensure we can form groups of 4
while decimal_digits.len() % 4 != 0 {
decimal_digits.push(0);
}
decimal_digits.reverse();
let mut m_power = decimal_digits.len() / 4;
// M are used to represent 10000, M_power = 2 means 10000^2 = 10000 0000
// The prefix of M is also made of Greek numerals but only be single digits, so it is 9 at max. This enables us
// to represent up to (10000)^(9 + 1) - 1 = 10^40 -1 (9,999,999,999,999,999,999,999,999,999,999,999,999,999)
let get_m_prefix = |m_power: usize| {
if m_power == 0 {
None
} else {
assert!(m_power <= 9);
// the prefix of M is a single digit lowercase
Some(ones[m_power - 1][0])
}
};
let mut previous_has_number = false;
for chunk in decimal_digits.chunks_exact(4) {
// chunk must be exact 4 item
assert_eq!(chunk.len(), 4);
m_power = m_power.saturating_sub(1);
// `th`ousan, `h`undred, `t`en and `o`ne
let (th, h, t, o) = (chunk[0], chunk[1], chunk[2], chunk[3]);
if th + h + t + o == 0 {
continue;
}
if previous_has_number {
fmt.push_str(", ");
}
if let Some(m_prefix) = get_m_prefix(m_power) {
fmt.push_str(m_prefix);
fmt.push_str("Μ");
}
if th != 0 {
let thousand_digit = thousands[th - 1][case];
fmt.push_str(thousand_digit);
}
if h != 0 {
let hundred_digit = hundreds[h - 1][case];
fmt.push_str(hundred_digit);
}
if t != 0 {
let ten_digit = tens[t - 1][case];
fmt.push_str(ten_digit);
}
if o != 0 {
let one_digit = ones[o - 1][case];
fmt.push_str(one_digit);
}
// if we do not have thousan, we need to append 'ʹ' at the end.
if th == 0 {
fmt.push_str("ʹ");
}
previous_has_number = true;
}
fmt
}
/// Stringify a number using a base-N counting system with no zero digit.
///
/// This is best explained by example. Suppose our digits are 'A', 'B', and 'C'.
/// We would get the following:
/// ///
/// ```text /// ```text
/// 1 => "A" /// 1 => 'I'
/// 2 => "B" /// 2 => 'II'
/// 3 => "C" /// 3 => 'III'
/// 4 => "AA" /// 4 => 'IV'
/// 5 => "AB" /// 5 => 'V'
/// 6 => "AC" /// 6 => 'VI'
/// 7 => "BA" /// 7 => 'VII'
/// 8 => "BB" /// 8 => 'VIII'
/// 9 => "BC"
/// 10 => "CA"
/// 11 => "CB"
/// 12 => "CC"
/// 13 => "AAA"
/// etc.
/// ``` /// ```
/// ///
/// You might be familiar with this scheme from the way spreadsheet software /// where this is the start of the familiar Roman numeral system.
/// tends to label its columns. fn additive(symbols: &[(&str, u64)], mut n: u64) -> EcoString {
fn zeroless<const N_DIGITS: usize>(alphabet: [char; N_DIGITS], mut n: u64) -> EcoString { if n == 0 {
if let Some(&(symbol, 0)) = symbols.last() {
return symbol.into();
}
return '0'.into();
}
let mut s = EcoString::new();
for (symbol, weight) in symbols {
if *weight == 0 || *weight > n {
continue;
}
let reps = n / weight;
for _ in 0..reps {
s.push_str(symbol);
}
n -= weight * reps;
if n == 0 {
return s;
}
}
s
}
/// Stringify a number using a base-n (where n is the number of provided
/// symbols) system without a zero symbol.
///
/// Consider the situation where ['A', 'B', 'C'] are the provided symbols,
///
/// ```text
/// 1 => 'A'
/// 2 => 'B'
/// 3 => 'C'
/// 4 => 'AA
/// 5 => 'AB'
/// 6 => 'AC'
/// 7 => 'BA'
/// ...
/// ```
///
/// This system is commonly used in spreadsheet software.
fn alphabetic(symbols: &[char], mut n: u64) -> EcoString {
let n_digits = symbols.len() as u64;
if n == 0 { if n == 0 {
return '-'.into(); return '-'.into();
} }
let n_digits = N_DIGITS as u64; let mut s = EcoString::new();
let mut cs = EcoString::new(); while n != 0 {
while n > 0 {
n -= 1; n -= 1;
cs.push(alphabet[(n % n_digits) as usize]); s.push(symbols[(n % n_digits) as usize]);
n /= n_digits; n /= n_digits;
} }
cs.chars().rev().collect() s.chars().rev().collect()
} }
/// Stringify a number using a base-10 counting system with a zero digit. /// Stringify a number using the symbols provided, defaulting to the arabic
/// representation when the number is greater than the number of symbols.
/// ///
/// This function assumes that the digits occupy contiguous codepoints. /// Consider the situation where ['0', 'A', 'B', 'C'] are the provided symbols,
fn decimal(start: char, mut n: u64) -> EcoString { ///
if n == 0 { /// ```text
return start.into(); /// 0 => '0'
/// 1 => 'A'
/// 2 => 'B'
/// 3 => 'C'
/// 4 => '4'
/// ...
/// n => 'n'
/// ```
fn fixed(symbols: &[char], n: u64) -> EcoString {
let n_digits = symbols.len() as u64;
if n < n_digits {
return symbols[(n) as usize].into();
} }
let mut cs = EcoString::new(); eco_format!("{n}")
while n > 0 { }
cs.push(char::from_u32((start as u32) + ((n % 10) as u32)).unwrap());
n /= 10; /// Stringify a number using a base-n (where n is the number of provided
} /// symbols) system with a zero symbol.
cs.chars().rev().collect() ///
/// Consider the situation where ['0', '1', '2'] are the provided symbols,
///
/// ```text
/// 0 => '0'
/// 1 => '1'
/// 2 => '2'
/// 3 => '10'
/// 4 => '11'
/// 5 => '12'
/// 6 => '20'
/// ...
/// ```
///
/// which is the familiar trinary counting system.
fn numeric(symbols: &[char], mut n: u64) -> EcoString {
let n_digits = symbols.len() as u64;
if n == 0 {
return symbols[0].into();
}
let mut s = EcoString::new();
while n != 0 {
s.push(symbols[(n % n_digits) as usize]);
n /= n_digits;
}
s.chars().rev().collect()
}
/// Stringify a number using repeating symbols.
///
/// Consider the situation where ['A', 'B', 'C'] are the provided symbols,
///
/// ```text
/// 0 => '-'
/// 1 => 'A'
/// 2 => 'B'
/// 3 => 'C'
/// 4 => 'AA'
/// 5 => 'BB'
/// 6 => 'CC'
/// 7 => 'AAA'
/// ...
/// ```
fn symbolic(symbols: &[char], n: u64) -> EcoString {
let n_digits = symbols.len() as u64;
if n == 0 {
return '-'.into();
}
EcoString::from(symbols[((n - 1) % n_digits) as usize])
.repeat((n.div_ceil(n_digits)) as usize)
} }

View File

@ -21,9 +21,10 @@ use crate::text::TextElem;
/// ///
/// The default, a `{"normal"}` reference, produces a textual reference to a /// The default, a `{"normal"}` reference, produces a textual reference to a
/// label. For example, a reference to a heading will yield an appropriate /// label. For example, a reference to a heading will yield an appropriate
/// string such as "Section 1" for a reference to the first heading. The /// string such as "Section 1" for a reference to the first heading. The word
/// references are also links to the respective element. Reference syntax can /// "Section" depends on the [`lang`]($text.lang) setting and is localized
/// also be used to [cite] from a bibliography. /// accordingly. The references are also links to the respective element.
/// Reference syntax can also be used to [cite] from a bibliography.
/// ///
/// As the default form requires a supplement and numbering, the label must be /// As the default form requires a supplement and numbering, the label must be
/// attached to a _referenceable element_. Referenceable elements include /// attached to a _referenceable element_. Referenceable elements include

View File

@ -189,13 +189,15 @@ impl Show for Packed<TermsElem> {
.styled(TermsElem::set_within(true)); .styled(TermsElem::set_within(true));
if tight { if tight {
let leading = ParElem::leading_in(styles); let spacing = self
let spacing = VElem::new(leading.into()) .spacing(styles)
.unwrap_or_else(|| ParElem::leading_in(styles).into());
let v = VElem::new(spacing.into())
.with_weak(true) .with_weak(true)
.with_attach(true) .with_attach(true)
.pack() .pack()
.spanned(span); .spanned(span);
realized = spacing + realized; realized = v + realized;
} }
Ok(realized) Ok(realized)

View File

@ -373,6 +373,7 @@ pub struct Decoration {
/// A kind of decorative line. /// A kind of decorative line.
#[derive(Debug, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[allow(clippy::large_enum_variant)]
pub enum DecoLine { pub enum DecoLine {
Underline { Underline {
stroke: Stroke<Abs>, stroke: Stroke<Abs>,

View File

@ -194,6 +194,8 @@ bitflags::bitflags! {
const MONOSPACE = 1 << 0; const MONOSPACE = 1 << 0;
/// Glyphs have short strokes at their stems. /// Glyphs have short strokes at their stems.
const SERIF = 1 << 1; const SERIF = 1 << 1;
/// Font face has a MATH table
const MATH = 1 << 2;
} }
} }
@ -272,6 +274,7 @@ impl FontInfo {
let mut flags = FontFlags::empty(); let mut flags = FontFlags::empty();
flags.set(FontFlags::MONOSPACE, ttf.is_monospaced()); flags.set(FontFlags::MONOSPACE, ttf.is_monospaced());
flags.set(FontFlags::MATH, ttf.tables().math.is_some());
// Determine whether this is a serif or sans-serif font. // Determine whether this is a serif or sans-serif font.
if let Some(panose) = ttf if let Some(panose) = ttf

View File

@ -14,7 +14,7 @@ macro_rules! translation {
}; };
} }
const TRANSLATIONS: [(&str, &str); 39] = [ const TRANSLATIONS: [(&str, &str); 40] = [
translation!("ar"), translation!("ar"),
translation!("bg"), translation!("bg"),
translation!("ca"), translation!("ca"),
@ -36,6 +36,7 @@ const TRANSLATIONS: [(&str, &str); 39] = [
translation!("it"), translation!("it"),
translation!("ja"), translation!("ja"),
translation!("la"), translation!("la"),
translation!("lv"),
translation!("nb"), translation!("nb"),
translation!("nl"), translation!("nl"),
translation!("nn"), translation!("nn"),
@ -87,6 +88,7 @@ impl Lang {
pub const ITALIAN: Self = Self(*b"it ", 2); pub const ITALIAN: Self = Self(*b"it ", 2);
pub const JAPANESE: Self = Self(*b"ja ", 2); pub const JAPANESE: Self = Self(*b"ja ", 2);
pub const LATIN: Self = Self(*b"la ", 2); pub const LATIN: Self = Self(*b"la ", 2);
pub const LATVIAN: Self = Self(*b"lv ", 2);
pub const LOWER_SORBIAN: Self = Self(*b"dsb", 3); pub const LOWER_SORBIAN: Self = Self(*b"dsb", 3);
pub const NYNORSK: Self = Self(*b"nn ", 2); pub const NYNORSK: Self = Self(*b"nn ", 2);
pub const POLISH: Self = Self(*b"pl ", 2); pub const POLISH: Self = Self(*b"pl ", 2);

View File

@ -348,15 +348,17 @@ pub struct TextElem {
/// This can make justification visually more pleasing. /// This can make justification visually more pleasing.
/// ///
/// ```example /// ```example
/// #set page(width: 220pt)
///
/// #set par(justify: true) /// #set par(justify: true)
/// This justified text has a hyphen in /// This justified text has a hyphen in
/// the paragraph's first line. Hanging /// the paragraph's second line. Hanging
/// the hyphen slightly into the margin /// the hyphen slightly into the margin
/// results in a clearer paragraph edge. /// results in a clearer paragraph edge.
/// ///
/// #set text(overhang: false) /// #set text(overhang: false)
/// This justified text has a hyphen in /// This justified text has a hyphen in
/// the paragraph's first line. Hanging /// the paragraph's second line. Hanging
/// the hyphen slightly into the margin /// the hyphen slightly into the margin
/// results in a clearer paragraph edge. /// results in a clearer paragraph edge.
/// ``` /// ```

View File

@ -237,7 +237,7 @@ impl<'s> SmartQuotes<'s> {
"cs" | "da" | "de" | "sk" | "sl" if alternative => ("", "", "»", "«"), "cs" | "da" | "de" | "sk" | "sl" if alternative => ("", "", "»", "«"),
"cs" | "de" | "et" | "is" | "lt" | "lv" | "sk" | "sl" => low_high, "cs" | "de" | "et" | "is" | "lt" | "lv" | "sk" | "sl" => low_high,
"da" => ("", "", "", ""), "da" => ("", "", "", ""),
"fr" | "ru" if alternative => default, "fr" if alternative => default,
"fr" => ("", "", "«\u{202F}", "\u{202F}»"), "fr" => ("", "", "«\u{202F}", "\u{202F}»"),
"fi" | "sv" if alternative => ("", "", "»", "»"), "fi" | "sv" if alternative => ("", "", "»", "»"),
"bs" | "fi" | "sv" => ("", "", "", ""), "bs" | "fi" | "sv" => ("", "", "", ""),
@ -247,7 +247,9 @@ impl<'s> SmartQuotes<'s> {
"es" if matches!(region, Some("ES") | None) => ("", "", "«", "»"), "es" if matches!(region, Some("ES") | None) => ("", "", "«", "»"),
"hu" | "pl" | "ro" => ("", "", "", ""), "hu" | "pl" | "ro" => ("", "", "", ""),
"no" | "nb" | "nn" if alternative => low_high, "no" | "nb" | "nn" if alternative => low_high,
"ru" | "no" | "nb" | "nn" | "uk" => ("", "", "«", "»"), "no" | "nb" | "nn" => ("", "", "«", "»"),
"ru" => ("", "", "«", "»"),
"uk" => ("", "", "«", "»"),
"el" => ("", "", "«", "»"), "el" => ("", "", "«", "»"),
"he" => ("", "", "", ""), "he" => ("", "", "", ""),
"hr" => ("", "", "", ""), "hr" => ("", "", "", ""),

View File

@ -77,8 +77,8 @@ pub struct ImageElem {
/// [`source`]($image.source) (even then, Typst will try to figure out the /// [`source`]($image.source) (even then, Typst will try to figure out the
/// format automatically, but that's not always possible). /// format automatically, but that's not always possible).
/// ///
/// Supported formats are `{"png"}`, `{"jpg"}`, `{"gif"}`, `{"svg"}` as well /// Supported formats are `{"png"}`, `{"jpg"}`, `{"gif"}`, `{"svg"}`,
/// as raw pixel data. Embedding PDFs as images is /// `{"webp"}` as well as raw pixel data. Embedding PDFs as images is
/// [not currently supported](https://github.com/typst/typst/issues/145). /// [not currently supported](https://github.com/typst/typst/issues/145).
/// ///
/// When providing raw pixel data as the `source`, you must specify a /// When providing raw pixel data as the `source`, you must specify a

View File

@ -9,6 +9,7 @@ use ecow::{eco_format, EcoString};
use image::codecs::gif::GifDecoder; use image::codecs::gif::GifDecoder;
use image::codecs::jpeg::JpegDecoder; use image::codecs::jpeg::JpegDecoder;
use image::codecs::png::PngDecoder; use image::codecs::png::PngDecoder;
use image::codecs::webp::WebPDecoder;
use image::{ use image::{
guess_format, DynamicImage, ImageBuffer, ImageDecoder, ImageResult, Limits, Pixel, guess_format, DynamicImage, ImageBuffer, ImageDecoder, ImageResult, Limits, Pixel,
}; };
@ -77,6 +78,7 @@ impl RasterImage {
ExchangeFormat::Jpg => decode(JpegDecoder::new(cursor), icc), ExchangeFormat::Jpg => decode(JpegDecoder::new(cursor), icc),
ExchangeFormat::Png => decode(PngDecoder::new(cursor), icc), ExchangeFormat::Png => decode(PngDecoder::new(cursor), icc),
ExchangeFormat::Gif => decode(GifDecoder::new(cursor), icc), ExchangeFormat::Gif => decode(GifDecoder::new(cursor), icc),
ExchangeFormat::Webp => decode(WebPDecoder::new(cursor), icc),
} }
.map_err(format_image_error)?; .map_err(format_image_error)?;
@ -242,6 +244,8 @@ pub enum ExchangeFormat {
/// Raster format that is typically used for short animated clips. Typst can /// Raster format that is typically used for short animated clips. Typst can
/// load GIFs, but they will become static. /// load GIFs, but they will become static.
Gif, Gif,
/// Raster format that supports both lossy and lossless compression.
Webp,
} }
impl ExchangeFormat { impl ExchangeFormat {
@ -257,6 +261,7 @@ impl From<ExchangeFormat> for image::ImageFormat {
ExchangeFormat::Png => image::ImageFormat::Png, ExchangeFormat::Png => image::ImageFormat::Png,
ExchangeFormat::Jpg => image::ImageFormat::Jpeg, ExchangeFormat::Jpg => image::ImageFormat::Jpeg,
ExchangeFormat::Gif => image::ImageFormat::Gif, ExchangeFormat::Gif => image::ImageFormat::Gif,
ExchangeFormat::Webp => image::ImageFormat::WebP,
} }
} }
} }
@ -269,6 +274,7 @@ impl TryFrom<image::ImageFormat> for ExchangeFormat {
image::ImageFormat::Png => ExchangeFormat::Png, image::ImageFormat::Png => ExchangeFormat::Png,
image::ImageFormat::Jpeg => ExchangeFormat::Jpg, image::ImageFormat::Jpeg => ExchangeFormat::Jpg,
image::ImageFormat::Gif => ExchangeFormat::Gif, image::ImageFormat::Gif => ExchangeFormat::Gif,
image::ImageFormat::WebP => ExchangeFormat::Webp,
_ => bail!("format not yet supported"), _ => bail!("format not yet supported"),
}) })
} }

View File

@ -0,0 +1,8 @@
figure = Attēls
table = Tabula
equation = Vienādojums
bibliography = Literatūra
heading = Sadaļa
outline = Saturs
raw = Saraksts
page = lpp.

View File

@ -185,6 +185,7 @@ struct Cast {
} }
/// A pattern in a cast, e.g.`"ascender"` or `v: i64`. /// A pattern in a cast, e.g.`"ascender"` or `v: i64`.
#[allow(clippy::large_enum_variant)]
enum Pattern { enum Pattern {
Str(syn::LitStr), Str(syn::LitStr),
Ty(syn::Pat, syn::Type), Ty(syn::Pat, syn::Type),

View File

@ -23,6 +23,7 @@ bytemuck = { workspace = true }
comemo = { workspace = true } comemo = { workspace = true }
ecow = { workspace = true } ecow = { workspace = true }
image = { workspace = true } image = { workspace = true }
infer = { workspace = true }
krilla = { workspace = true } krilla = { workspace = true }
krilla-svg = { workspace = true } krilla-svg = { workspace = true }
serde = { workspace = true } serde = { workspace = true }

View File

@ -34,6 +34,8 @@ pub(crate) fn embed_files(
}, },
}; };
let data: Arc<dyn AsRef<[u8]> + Send + Sync> = Arc::new(embed.data.clone()); let data: Arc<dyn AsRef<[u8]> + Send + Sync> = Arc::new(embed.data.clone());
// TODO: update when new krilla version lands (https://github.com/LaurenzV/krilla/pull/203)
let compress = should_compress(&embed.data).unwrap_or(true);
let file = EmbeddedFile { let file = EmbeddedFile {
path, path,
@ -41,7 +43,7 @@ pub(crate) fn embed_files(
description, description,
association_kind, association_kind,
data: data.into(), data: data.into(),
compress: true, compress,
location: Some(span.into_raw().get()), location: Some(span.into_raw().get()),
}; };
@ -52,3 +54,69 @@ pub(crate) fn embed_files(
Ok(()) Ok(())
} }
fn should_compress(data: &[u8]) -> Option<bool> {
let ty = infer::get(data)?;
match ty.matcher_type() {
infer::MatcherType::App => None,
infer::MatcherType::Archive => match ty.mime_type() {
#[rustfmt::skip]
"application/zip"
| "application/vnd.rar"
| "application/gzip"
| "application/x-bzip2"
| "application/vnd.bzip3"
| "application/x-7z-compressed"
| "application/x-xz"
| "application/vnd.ms-cab-compressed"
| "application/vnd.debian.binary-package"
| "application/x-compress"
| "application/x-lzip"
| "application/x-rpm"
| "application/zstd"
| "application/x-lz4"
| "application/x-ole-storage" => Some(false),
_ => None,
},
infer::MatcherType::Audio => match ty.mime_type() {
#[rustfmt::skip]
"audio/mpeg"
| "audio/m4a"
| "audio/opus"
| "audio/ogg"
| "audio/x-flac"
| "audio/amr"
| "audio/aac"
| "audio/x-ape" => Some(false),
_ => None,
},
infer::MatcherType::Book => None,
infer::MatcherType::Doc => None,
infer::MatcherType::Font => None,
infer::MatcherType::Image => match ty.mime_type() {
#[rustfmt::skip]
"image/jpeg"
| "image/jp2"
| "image/png"
| "image/webp"
| "image/vnd.ms-photo"
| "image/heif"
| "image/avif"
| "image/jxl"
| "image/vnd.djvu" => None,
_ => None,
},
infer::MatcherType::Text => None,
infer::MatcherType::Video => match ty.mime_type() {
#[rustfmt::skip]
"video/mp4"
| "video/x-m4v"
| "video/x-matroska"
| "video/webm"
| "video/quicktime"
| "video/x-flv" => Some(false),
_ => None,
},
infer::MatcherType::Custom => None,
}
}

View File

@ -655,6 +655,7 @@ fn visit_grouping_rules<'a>(
let matching = s.rules.iter().find(|&rule| (rule.trigger)(content, &s.kind)); let matching = s.rules.iter().find(|&rule| (rule.trigger)(content, &s.kind));
// Try to continue or finish an existing grouping. // Try to continue or finish an existing grouping.
let mut i = 0;
while let Some(active) = s.groupings.last() { while let Some(active) = s.groupings.last() {
// Start a nested group if a rule with higher priority matches. // Start a nested group if a rule with higher priority matches.
if matching.is_some_and(|rule| rule.priority > active.rule.priority) { if matching.is_some_and(|rule| rule.priority > active.rule.priority) {
@ -670,6 +671,16 @@ fn visit_grouping_rules<'a>(
} }
finish_innermost_grouping(s)?; finish_innermost_grouping(s)?;
i += 1;
if i > 512 {
// It seems like this case is only hit when there is a cycle between
// a show rule and a grouping rule. The show rule produces content
// that is matched by a grouping rule, which is then again processed
// by the show rule, and so on. The two must be at an equilibrium,
// otherwise either the "maximum show rule depth" or "maximum
// grouping depth" errors are triggered.
bail!(content.span(), "maximum grouping depth exceeded");
}
} }
// Start a new grouping. // Start a new grouping.

View File

@ -45,6 +45,7 @@ pub fn convert_image_to_base64_url(image: &Image) -> EcoString {
ExchangeFormat::Png => "png", ExchangeFormat::Png => "png",
ExchangeFormat::Jpg => "jpeg", ExchangeFormat::Jpg => "jpeg",
ExchangeFormat::Gif => "gif", ExchangeFormat::Gif => "gif",
ExchangeFormat::Webp => "webp",
}, },
raster.data(), raster.data(),
), ),

View File

@ -27,7 +27,6 @@
//! [module]: crate::foundations::Module //! [module]: crate::foundations::Module
//! [content]: crate::foundations::Content //! [content]: crate::foundations::Content
//! [laid out]: typst_layout::layout_document //! [laid out]: typst_layout::layout_document
//! [document]: crate::model::Document
//! [frame]: crate::layout::Frame //! [frame]: crate::layout::Frame
pub extern crate comemo; pub extern crate comemo;

View File

@ -12,6 +12,7 @@ Let's start with a broad overview of the directories in this repository:
- `crates/typst-cli`: Typst's command line interface. This is a relatively small - `crates/typst-cli`: Typst's command line interface. This is a relatively small
layer on top of the compiler and the exporters. layer on top of the compiler and the exporters.
- `crates/typst-eval`: The interpreter for the Typst language. - `crates/typst-eval`: The interpreter for the Typst language.
- `crates/typst-html`: The HTML exporter.
- `crates/typst-ide`: Exposes IDE functionality. - `crates/typst-ide`: Exposes IDE functionality.
- `crates/typst-kit`: Contains various default implementation of - `crates/typst-kit`: Contains various default implementation of
functionality used in `typst-cli`. functionality used in `typst-cli`.

View File

@ -69,7 +69,7 @@ the first item of the list above by indenting it.
## Adding a figure { #figure } ## Adding a figure { #figure }
You think that your report would benefit from a figure. Let's add one. Typst You think that your report would benefit from a figure. Let's add one. Typst
supports images in the formats PNG, JPEG, GIF, and SVG. To add an image file to supports images in the formats PNG, JPEG, GIF, SVG, and WebP. To add an image file to
your project, first open the _file panel_ by clicking the box icon in the left your project, first open the _file panel_ by clicking the box icon in the left
sidebar. Here, you can see a list of all files in your project. Currently, there sidebar. Here, you can see a list of all files in your project. Currently, there
is only one: The main Typst file you are writing in. To upload another file, is only one: The main Typst file you are writing in. To upload another file,

6
flake.lock generated
View File

@ -112,13 +112,13 @@
"rust-manifest": { "rust-manifest": {
"flake": false, "flake": false,
"locked": { "locked": {
"narHash": "sha256-irgHsBXecwlFSdmP9MfGP06Cbpca2QALJdbN4cymcko=", "narHash": "sha256-BwfxWd/E8gpnXoKsucFXhMbevMlVgw3l0becLkIcWCU=",
"type": "file", "type": "file",
"url": "https://static.rust-lang.org/dist/channel-rust-1.85.0.toml" "url": "https://static.rust-lang.org/dist/channel-rust-1.87.0.toml"
}, },
"original": { "original": {
"type": "file", "type": "file",
"url": "https://static.rust-lang.org/dist/channel-rust-1.85.0.toml" "url": "https://static.rust-lang.org/dist/channel-rust-1.87.0.toml"
} }
}, },
"systems": { "systems": {

View File

@ -10,7 +10,7 @@
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
rust-manifest = { rust-manifest = {
url = "https://static.rust-lang.org/dist/channel-rust-1.85.0.toml"; url = "https://static.rust-lang.org/dist/channel-rust-1.87.0.toml";
flake = false; flake = false;
}; };
}; };

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 920 B

After

Width:  |  Height:  |  Size: 947 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 841 B

After

Width:  |  Height:  |  Size: 869 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 395 B

After

Width:  |  Height:  |  Size: 417 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 617 B

After

Width:  |  Height:  |  Size: 643 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 833 B

After

Width:  |  Height:  |  Size: 867 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 699 B

After

Width:  |  Height:  |  Size: 739 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 515 B

After

Width:  |  Height:  |  Size: 547 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 749 B

After

Width:  |  Height:  |  Size: 772 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 828 B

After

Width:  |  Height:  |  Size: 850 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 B

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