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 apt update
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:
targets: ${{ matrix.bits == 32 && 'i686-unknown-linux-gnu' || '' }}
- uses: Swatinem/rust-cache@v2
@ -73,7 +73,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.85.0
- uses: dtolnay/rust-toolchain@1.87.0
with:
components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2

View File

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

14
Cargo.lock generated
View File

@ -508,9 +508,9 @@ dependencies = [
[[package]]
name = "crossbeam-channel"
version = "0.5.14"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
@ -1215,6 +1215,7 @@ dependencies = [
"byteorder-lite",
"color_quant",
"gif",
"image-webp",
"num-traits",
"png",
"zune-core",
@ -1259,6 +1260,12 @@ dependencies = [
"serde",
]
[[package]]
name = "infer"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7"
[[package]]
name = "inotify"
version = "0.11.0"
@ -2857,7 +2864,7 @@ dependencies = [
[[package]]
name = "typst-assets"
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]]
name = "typst-cli"
@ -3127,6 +3134,7 @@ dependencies = [
"comemo",
"ecow",
"image",
"infer",
"krilla",
"krilla-svg",
"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-timing = { path = "crates/typst-timing", 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" }
arrayvec = "0.7.4"
az = "1.2"
@ -69,8 +69,9 @@ icu_provider_adapters = "1.4"
icu_provider_blob = "1.4"
icu_segmenter = { version = "1.4", features = ["serde"] }
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"] }
infer = { version = "0.19.0", default-features = false }
kamadak-exif = "0.6"
krilla = { version = "0.4.0", default-features = false, features = ["raster-images", "comemo", "rayon"] }
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> {
let span = self.span();
let callee = self.callee();
let in_math = in_math(callee);
let callee_span = callee.span();
let args = self.args();
let trailing_comma = args.trailing_comma();
vm.engine.route.check_call_depth().at(span)?;
// 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 field = access.field();
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),
}
} else {
@ -45,9 +48,15 @@ impl Eval for ast::FuncCall<'_> {
(callee.eval(vm)?, args.eval(vm)?.spanned(span))
};
let func_result = callee.clone().cast::<Func>();
if in_math && func_result.is_err() {
return wrap_args_in_math(callee, callee_span, args, trailing_comma);
let func_result = callee_value.clone().cast::<Func>();
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
@ -56,8 +65,11 @@ impl Eval for ast::FuncCall<'_> {
let point = || Tracepoint::Call(func.name().map(Into::into));
let f = || {
func.call(&mut vm.engine, vm.context, args)
.trace(vm.world(), point, span)
func.call(&mut vm.engine, vm.context, args_value).trace(
vm.world(),
point,
span,
)
};
// Stacker is broken on WASM.
@ -404,12 +416,14 @@ fn wrap_args_in_math(
if trailing_comma {
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(')'))
.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.

View File

@ -15,7 +15,7 @@ use typst::syntax::{
ast, is_id_continue, is_id_start, is_ident, FileId, LinkedNode, Side, Source,
SyntaxKind,
};
use typst::text::RawElem;
use typst::text::{FontFlags, RawElem};
use typst::visualize::Color;
use unscanny::Scanner;
@ -841,7 +841,9 @@ fn param_value_completions<'a>(
/// Returns which file extensions to complete for the given parameter if any.
fn path_completion(func: &Func, param: &ParamInfo) -> Option<&'static [&'static str]> {
Some(match (func.name(), param.name) {
(Some("image"), "source") => &["png", "jpg", "jpeg", "gif", "svg", "svgz"],
(Some("image"), "source") => {
&["png", "jpg", "jpeg", "gif", "svg", "svgz", "webp"]
}
(Some("csv"), "source") => &["csv"],
(Some("plugin"), "source") => &["wasm"],
(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.
struct CompletionContext<'a> {
world: &'a (dyn IdeWorld + 'a),
@ -1152,10 +1172,12 @@ impl<'a> CompletionContext<'a> {
/// Add completions for all font families.
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() {
let detail = summarize_font_family(iter);
if !equation || family.contains("Math") {
let variants: Vec<_> = iter.collect();
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(
family,
Some(CompletionKind::Font),
@ -1790,4 +1812,21 @@ mod tests {
.must_include(["r", "dashed"])
.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;
continue;
} else if *count > 1 {
write!(pieces.last_mut().unwrap(), " (x{count})").unwrap();
write!(pieces.last_mut().unwrap(), " (×{count})").unwrap();
}
}
pieces.push(value.repr());
@ -95,7 +95,7 @@ fn expr_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option<Tooltip> {
if let Some((_, count)) = last {
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());
then {
let detail = summarize_font_family(iter);
let detail = summarize_font_family(iter.collect());
return Some(Tooltip::Text(detail));
}
};
@ -371,4 +371,11 @@ mod tests {
test(&world, -2, Side::Before).must_be_none();
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.
pub fn summarize_font_family<'a>(
variants: impl Iterator<Item = &'a FontInfo>,
) -> EcoString {
let mut infos: Vec<_> = variants.collect();
infos.sort_by_key(|info| info.variant);
pub fn summarize_font_family(mut variants: Vec<&FontInfo>) -> EcoString {
variants.sort_by_key(|info| info.variant);
let mut has_italic = false;
let mut min_weight = u16::MAX;
let mut max_weight = 0;
for info in &infos {
for info in &variants {
let weight = info.variant.weight.to_number();
has_italic |= info.variant.style == FontStyle::Italic;
min_weight = min_weight.min(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" });
if min_weight == max_weight {

View File

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

View File

@ -11,7 +11,7 @@ use typst_library::layout::{
use typst_library::text::TextElem;
use typst_library::visualize::Geometry;
use typst_syntax::Span;
use typst_utils::{MaybeReverseIter, Numeric};
use typst_utils::Numeric;
use super::{
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.
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();
for row in rows {
// We want to only draw the fill starting at the parent
@ -643,18 +643,13 @@ impl<'a> GridLayouter<'a> {
.sum()
};
let width = self.cell_spanned_width(cell, x);
// In the grid, cell colspans expand to the right,
// so we're at the leftmost (lowest 'x') column
// spanned by the cell. However, in RTL, cells
// expand to the left. Therefore, without the
// offset below, cell fills would start at the
// rightmost visual position of a cell and extend
// 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 mut pos = Point::new(dx, dy);
if self.is_rtl {
// In RTL cells expand to the left, thus the
// position must additionally be offset by the
// cell's width.
pos.x = self.width - (dx + width);
}
let size = Size::new(width, height);
let rect = Geometry::Rect(size).filled(fill);
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 pos = Point::zero();
let mut offset = Point::zero();
// Reverse the column order when using RTL.
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) {
// Rowspans have a separate layout step
if cell.rowspan.get() == 1 {
@ -1257,25 +1251,17 @@ impl<'a> GridLayouter<'a> {
let frame =
layout_cell(cell, engine, disambiguator, self.styles, pod)?
.into_frame();
let mut pos = pos;
let mut pos = offset;
if self.is_rtl {
// In the grid, cell colspans expand to the right,
// so we're at the leftmost (lowest 'x') column
// spanned by the cell. However, in RTL, cells
// 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;
// In RTL cells expand to the left, thus the position
// must additionally be offset by the cell's width.
pos.x = self.width - (pos.x + width);
}
output.push_frame(pos, frame);
}
}
pos.x += rcol;
offset.x += rcol;
}
Ok(output)
@ -1302,8 +1288,8 @@ impl<'a> GridLayouter<'a> {
pod.backlog = &heights[1..];
// Layout the row.
let mut pos = Point::zero();
for (x, &rcol) in self.rcols.iter().enumerate().rev_if(self.is_rtl) {
let mut offset = Point::zero();
for (x, &rcol) in self.rcols.iter().enumerate() {
if let Some(cell) = self.grid.cell(x, y) {
// Rowspans have a separate layout step
if cell.rowspan.get() == 1 {
@ -1314,17 +1300,19 @@ impl<'a> GridLayouter<'a> {
let fragment =
layout_cell(cell, engine, disambiguator, self.styles, pod)?;
for (output, frame) in outputs.iter_mut().zip(fragment) {
let mut pos = pos;
let mut pos = offset;
if self.is_rtl {
let offset = -width + rcol;
pos.x += offset;
// In RTL cells expand to the left, thus the
// position must additionally be offset by the
// cell's width.
pos.x = self.width - (offset.x + width);
}
output.push_frame(pos, frame);
}
}
}
pos.x += rcol;
offset.x += rcol;
}
Ok(Fragment::frames(outputs))

View File

@ -3,7 +3,6 @@ use typst_library::engine::Engine;
use typst_library::foundations::Resolve;
use typst_library::layout::grid::resolve::Repeatable;
use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing};
use typst_utils::MaybeReverseIter;
use super::layouter::{in_last_with_offset, points, Row, RowPiece};
use super::{layout_cell, Cell, GridLayouter};
@ -23,6 +22,10 @@ pub struct Rowspan {
/// specified for the parent cell's `breakable` field.
pub is_effectively_unbreakable: bool,
/// 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,
/// The vertical offset of this rowspan in the first region.
pub dy: Abs,
@ -118,10 +121,11 @@ impl GridLayouter<'_> {
// Nothing to layout.
return Ok(());
};
let first_column = self.rcols[x];
let cell = self.grid.cell(x, y).unwrap();
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.
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.
/// If so, adds them to the rowspans vector.
pub fn check_for_rowspans(&mut self, disambiguator: usize, y: usize) {
// We will compute the horizontal offset of each rowspan in advance.
// For that reason, we must reverse the column order when using RTL.
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 offsets = points(self.rcols.iter().copied());
for (x, dx) in (0..self.rcols.len()).zip(offsets) {
let Some(cell) = self.grid.cell(x, y) else {
continue;
};

View File

@ -147,6 +147,7 @@ fn determine_format(source: &DataSource, data: &Bytes) -> StrResult<ImageFormat>
"jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()),
"gif" => return Ok(ExchangeFormat::Gif.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() {
Breakpoint::Mandatory
} else {
const OBJ_REPLACE: char = '\u{FFFC}';
match lb.get(c) {
// Fix for: https://github.com/unicode-org/icu4x/issues/4146
LineBreak::Glue | LineBreak::WordJoiner | LineBreak::ZWJ => continue,
LineBreak::MandatoryBreak
| LineBreak::CarriageReturn
| LineBreak::LineFeed
| 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,
}
};

View File

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

View File

@ -1,7 +1,7 @@
use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain};
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};
@ -18,8 +18,11 @@ pub fn layout_accent(
let cramped = style_cramped();
let mut base = ctx.layout_into_fragment(&elem.base, styles.chain(&cramped))?;
// Try to replace a glyph with its dotless variant.
if elem.dotless(styles) {
let accent = elem.accent;
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 {
glyph.make_dotless_form(ctx);
}
@ -29,41 +32,54 @@ pub fn layout_accent(
let base_class = base.class();
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;
let mut glyph = GlyphFragment::new(ctx, styles, c, elem.span());
// Try to replace accent glyph with flattened variant.
// Try to replace accent glyph with its flattened variant.
if top_accent {
let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height);
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
// wide in many case.
let width = elem.size(styles).relative_to(base.width());
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_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.
// 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 accent_pos = Point::with_x(base_attach - accent_attach);
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)
};
let size = Size::new(base.width(), accent.height() + gap + base.height());
let baseline = base_pos.y + base.ascent();
let base_italics_correction = base.italics_correction();
let base_text_like = base.is_text_like();
let base_ascent = match &base {
MathFragment::Frame(frame) => frame.base_ascent,
_ => base.ascent(),
};
let base_descent = match &base {
MathFragment::Frame(frame) => frame.base_descent,
_ => base.descent(),
};
let mut frame = Frame::soft(size);
frame.set_baseline(baseline);
@ -73,6 +89,7 @@ pub fn layout_accent(
FrameFragment::new(styles, frame)
.with_class(base_class)
.with_base_ascent(base_ascent)
.with_base_descent(base_descent)
.with_italics_correction(base_italics_correction)
.with_accent_attach(base_attach)
.with_text_like(base_text_like),

View File

@ -434,9 +434,13 @@ fn compute_script_shifts(
}
if bl.is_some() || br.is_some() {
let descent = match &base {
MathFragment::Frame(frame) => frame.base_descent,
_ => base.descent(),
};
shift_down = 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!(br, ascent) - sub_top_max);
}

View File

@ -110,12 +110,12 @@ fn layout_frac_like(
if binom {
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);
ctx.push(left);
ctx.push(FrameFragment::new(styles, frame));
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);
ctx.push(right);
} else {

View File

@ -11,7 +11,7 @@ use typst_library::layout::{
};
use typst_library::math::{EquationElem, MathSize};
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_utils::default_math_class;
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 {
Self::Glyph(glyph) => glyph.accent_attach,
Self::Variant(variant) => variant.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 region: Option<Region>,
pub fill: Paint,
pub stroke: Option<FixedStroke>,
pub shift: Abs,
pub width: Abs,
pub ascent: Abs,
pub descent: Abs,
pub italics_correction: Abs,
pub accent_attach: Abs,
pub accent_attach: (Abs, Abs),
pub font_size: Abs,
pub class: MathClass,
pub math_size: MathSize,
@ -286,6 +287,7 @@ impl GlyphFragment {
lang: TextElem::lang_in(styles),
region: TextElem::region_in(styles),
fill: TextElem::fill_in(styles).as_decoration(),
stroke: TextElem::stroke_in(styles).map(|s| s.unwrap_or_default()),
shift: TextElem::baseline_in(styles),
font_size: TextElem::size_in(styles),
math_size: EquationElem::size_in(styles),
@ -294,7 +296,7 @@ impl GlyphFragment {
descent: Abs::zero(),
limits: Limits::for_char(c),
italics_correction: Abs::zero(),
accent_attach: Abs::zero(),
accent_attach: (Abs::zero(), Abs::zero()),
class,
span,
modifiers: FrameModifiers::get_in(styles),
@ -326,8 +328,14 @@ impl GlyphFragment {
});
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);
let bottom_accent_attach = (width - italics) / 2.0;
let extended_shape = is_extended_shape(ctx, id);
if !extended_shape {
@ -339,7 +347,7 @@ impl GlyphFragment {
self.ascent = bbox.y_max.scaled(ctx, self.font_size);
self.descent = -bbox.y_min.scaled(ctx, self.font_size);
self.italics_correction = italics;
self.accent_attach = accent_attach;
self.accent_attach = (top_accent_attach, bottom_accent_attach);
self.extended_shape = extended_shape;
}
@ -368,10 +376,10 @@ impl GlyphFragment {
font: self.font.clone(),
size: self.font_size,
fill: self.fill,
stroke: self.stroke,
lang: self.lang,
region: self.region,
text: self.c.into(),
stroke: None,
glyphs: vec![Glyph {
id: self.id.0,
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.
pub fn stretch_vertical(
self,
ctx: &mut MathContext,
height: Abs,
short_fall: Abs,
) -> VariantFragment {
stretch_glyph(ctx, self, height, short_fall, Axis::Y)
pub fn stretch_vertical(self, ctx: &mut MathContext, height: Abs) -> VariantFragment {
stretch_glyph(ctx, self, height, Axis::Y)
}
/// Try to stretch a glyph to a desired width.
@ -441,9 +444,8 @@ impl GlyphFragment {
self,
ctx: &mut MathContext,
width: Abs,
short_fall: Abs,
) -> 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 c: char,
pub italics_correction: Abs,
pub accent_attach: Abs,
pub accent_attach: (Abs, Abs),
pub frame: Frame,
pub font_size: Abs,
pub class: MathClass,
@ -499,8 +501,9 @@ pub struct FrameFragment {
pub limits: Limits,
pub spaced: bool,
pub base_ascent: Abs,
pub base_descent: Abs,
pub italics_correction: Abs,
pub accent_attach: Abs,
pub accent_attach: (Abs, Abs),
pub text_like: bool,
pub ignorant: bool,
}
@ -508,6 +511,7 @@ pub struct FrameFragment {
impl FrameFragment {
pub fn new(styles: StyleChain, frame: Frame) -> Self {
let base_ascent = frame.ascent();
let base_descent = frame.descent();
let accent_attach = frame.width() / 2.0;
Self {
frame: frame.modified(&FrameModifiers::get_in(styles)),
@ -517,8 +521,9 @@ impl FrameFragment {
limits: Limits::Never,
spaced: false,
base_ascent,
base_descent,
italics_correction: Abs::zero(),
accent_attach,
accent_attach: (accent_attach, accent_attach),
text_like: false,
ignorant: false,
}
@ -540,11 +545,15 @@ impl FrameFragment {
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 {
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 }
}

View File

@ -314,7 +314,7 @@ fn layout_delimiters(
if let Some(left) = left {
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));
ctx.push(left);
}
@ -323,7 +323,7 @@ fn layout_delimiters(
if let Some(right) = right {
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));
ctx.push(right);
}

View File

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

View File

@ -278,6 +278,9 @@ impl MathRun {
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> {
let mut items = vec![];
@ -295,22 +298,25 @@ impl MathRun {
let mut space_is_visible = false;
let is_relation = |f: &MathFragment| matches!(f.class(), MathClass::Relation);
let is_space = |f: &MathFragment| {
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();
while let Some(fragment) = iter.next() {
if space_is_visible {
match fragment {
MathFragment::Space(width) | MathFragment::Spacing(width, _) => {
items.push(InlineItem::Space(width, true));
if space_is_visible && is_space(&fragment) {
items.push(InlineItem::Space(fragment.width(), true));
continue;
}
_ => {}
}
}
let class = fragment.class();
let y = fragment.ascent();
@ -323,10 +329,9 @@ impl MathRun {
frame.push_frame(pos, fragment.into_frame());
empty = false;
if class == MathClass::Binary
|| (class == MathClass::Relation
&& !iter.peek().map(is_relation).unwrap_or_default())
{
// Split our current frame when we encounter a binary operator or
// relation so that there is a line-breaking opportunity.
if is_line_break_opportunity(class, iter.peek().map(|f| f.class())) {
let mut frame_prev =
std::mem::replace(&mut frame, Frame::soft(Size::zero()));

View File

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

View File

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

View File

@ -286,7 +286,7 @@ fn layout_underoverspreader(
let body_class = body.class();
let body = body.into_fragment(styles);
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 baseline = match position {

View File

@ -1,6 +1,6 @@
use typst_library::foundations::StyleChain;
use typst_library::layout::{Fragment, Frame, FrameItem, HideElem, Point};
use typst_library::model::{Destination, LinkElem};
use typst_library::layout::{Abs, Fragment, Frame, FrameItem, HideElem, Point, Sides};
use typst_library::model::{Destination, LinkElem, ParElem};
/// Frame-level modifications resulting from styles that do not impose any
/// layout structure.
@ -52,14 +52,7 @@ pub trait FrameModify {
impl FrameModify for Frame {
fn modify(&mut self, modifiers: &FrameModifiers) {
if let Some(dest) = &modifiers.dest {
let size = self.size();
self.push(Point::zero(), FrameItem::Link(dest.clone(), size));
}
if modifiers.hidden {
self.hide();
}
modify_frame(self, modifiers, None);
}
}
@ -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.
///
/// 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
/// amount of significant integer digits to remove before the decimal point.
/// Optionally, a number of decimal places can be specified. If negative, its
/// 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,
/// 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.
pub fn query(&self, selector: Selector) -> Vec<Content> {
let mut results = Vec::new();
self.traverse(&mut |element| -> ControlFlow<()> {
let _ = self.traverse(&mut |element| -> ControlFlow<()> {
if selector.matches(&element, None) {
results.push(element);
}
@ -441,7 +441,7 @@ impl Content {
/// Extracts the plain text of this content.
pub fn plain_text(&self) -> EcoString {
let mut text = EcoString::new();
self.traverse(&mut |element| -> ControlFlow<()> {
let _ = self.traverse(&mut |element| -> ControlFlow<()> {
if let Some(textable) = element.with::<dyn PlainText>() {
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
/// combination with [measurement]($measure).
///
/// You can also use this function to resolve [`ratio`] to fixed lengths. This
/// might come in handy if you're building your own layout abstractions.
/// To retrieve the _remaining_ height of the page rather than its full size,
/// 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
/// #layout(size => {

View File

@ -1,16 +1,14 @@
use std::borrow::Cow;
use std::num::NonZeroUsize;
use std::ops::RangeInclusive;
use std::str::FromStr;
use comemo::Track;
use typst_utils::{singleton, NonZeroExt, Scalar};
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, Args, AutoValue, Cast, Construct, Content, Context, Dict, Fold, Func,
NativeElement, Set, Smart, StyleChain, Value,
cast, elem, Args, AutoValue, Cast, Construct, Content, Dict, Fold, NativeElement,
Set, Smart, Value,
};
use crate::introspection::Introspector;
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.
#[derive(Debug, Clone)]
pub struct PageRanges(Vec<PageRange>);

View File

@ -80,6 +80,19 @@ impl Accent {
pub fn new(c: char) -> Self {
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.

View File

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

View File

@ -125,6 +125,9 @@ pub struct FigureElem {
///
/// ```example
/// #set page(height: 200pt)
/// #show figure: set place(
/// clearance: 1em,
/// )
///
/// = Introduction
/// #figure(
@ -457,7 +460,7 @@ impl Outlinable for Packed<FigureElem> {
/// customize the appearance of captions for all figures or figures of a
/// 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
/// parts can be used in [`where`]($function.where) selectors and show rules to
/// build a completely custom caption.

View File

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

View File

@ -9,7 +9,6 @@ use ecow::{eco_format, EcoString, EcoVec};
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{cast, func, Context, Func, Str, Value};
use crate::text::Case;
/// Applies a numbering to a sequence of numbers.
///
@ -261,9 +260,9 @@ pub enum NumberingKind {
LowerRoman,
/// Uppercase Roman numerals (I, II, III, etc.).
UpperRoman,
/// Lowercase Greek numerals (Α, Β, Γ, etc.).
/// Lowercase Greek letters (α, β, γ, etc.).
LowerGreek,
/// Uppercase Greek numerals (α, β, γ, etc.).
/// Uppercase Greek letters (Α, Β, Γ, etc.).
UpperGreek,
/// Paragraph/note-like symbols: *, †, ‡, §, ¶, and ‖. Further items use
/// repeated symbols.
@ -381,202 +380,37 @@ impl NumberingKind {
/// Apply the numbering to the given number.
pub fn apply(self, n: u64) -> EcoString {
match self {
Self::Arabic => eco_format!("{n}"),
Self::LowerRoman => roman_numeral(n, Case::Lower),
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();
Self::Arabic => {
numeric(&['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], n)
}
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::LowerLatin => zeroless(
[
'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',
Self::LowerRoman => 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::UpperLatin => zeroless(
[
'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',
],
n,
),
Self::HiraganaAiueo => zeroless(
[
'あ', 'い', 'う', 'え', 'お', 'か', 'き', 'く', 'け', 'こ', 'さ',
'し', 'す', 'せ', 'そ', 'た', 'ち', 'つ', 'て', 'と', 'な', 'に',
'ぬ', 'ね', 'の', 'は', 'ひ', 'ふ', 'へ', 'ほ', 'ま', 'み', 'む',
'め', 'も', 'や', 'ゆ', 'よ', 'ら', 'り', 'る', 'れ', 'ろ', 'わ',
'を', 'ん',
],
n,
),
Self::HiraganaIroha => zeroless(
[
'い', 'ろ', 'は', 'に', 'ほ', 'へ', 'と', 'ち', 'り', 'ぬ', 'る',
'を', 'わ', 'か', 'よ', 'た', 'れ', 'そ', 'つ', 'ね', 'な', 'ら',
'む', 'う', 'ゐ', 'の', 'お', 'く', 'や', 'ま', 'け', 'ふ', 'こ',
'え', 'て', 'あ', 'さ', 'き', 'ゆ', 'め', 'み', 'し', 'ゑ', 'ひ',
'も', 'せ', 'す',
],
n,
),
Self::KatakanaAiueo => zeroless(
[
'ア', 'イ', 'ウ', 'エ', 'オ', 'カ', 'キ', 'ク', 'ケ', 'コ', 'サ',
'シ', 'ス', 'セ', 'ソ', 'タ', 'チ', 'ツ', 'テ', 'ト', 'ナ', 'ニ',
'ヌ', 'ネ', '', 'ハ', 'ヒ', 'フ', 'ヘ', 'ホ', 'マ', 'ミ', 'ム',
'メ', 'モ', 'ヤ', 'ユ', 'ヨ', 'ラ', 'リ', 'ル', 'レ', 'ロ', 'ワ',
'ヲ', 'ン',
],
n,
),
Self::KatakanaIroha => zeroless(
[
'イ', 'ロ', 'ハ', 'ニ', 'ホ', 'ヘ', 'ト', 'チ', 'リ', 'ヌ', 'ル',
'ヲ', 'ワ', 'カ', 'ヨ', 'タ', 'レ', 'ソ', 'ツ', 'ネ', 'ナ', 'ラ',
'ム', 'ウ', 'ヰ', '', 'オ', 'ク', 'ヤ', 'マ', 'ケ', 'フ', 'コ',
'エ', 'テ', 'ア', 'サ', 'キ', 'ユ', 'メ', 'ミ', 'シ', 'ヱ', 'ヒ',
'モ', 'セ', 'ス',
],
n,
),
Self::KoreanJamo => zeroless(
[
'ㄱ', 'ㄴ', 'ㄷ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅅ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ',
'ㅌ', 'ㅍ', 'ㅎ',
],
n,
),
Self::KoreanSyllable => zeroless(
[
'가', '나', '다', '라', '마', '바', '사', '아', '자', '차', '카',
'타', '파', '하',
],
n,
),
Self::BengaliLetter => zeroless(
[
'ক', 'খ', 'গ', 'ঘ', 'ঙ', 'চ', 'ছ', 'জ', 'ঝ', 'ঞ', 'ট', 'ঠ', 'ড', 'ঢ',
'ণ', 'ত', 'থ', 'দ', 'ধ', 'ন', 'প', 'ফ', 'ব', 'ভ', 'ম', 'য', 'র', 'ল',
'শ', 'ষ', 'স', 'হ',
],
n,
),
Self::CircledNumber => zeroless(
[
'①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩', '⑪', '⑫', '⑬', '⑭',
'⑮', '⑯', '⑰', '⑱', '⑲', '⑳', '㉑', '㉒', '㉓', '㉔', '㉕', '㉖',
'㉗', '㉘', '㉙', '㉚', '㉛', '㉜', '㉝', '㉞', '㉟', '㊱', '㊲',
'㊳', '㊴', '㊵', '㊶', '㊷', '㊸', '㊹', '㊺', '㊻', '㊼', '㊽',
'㊾', '㊿',
],
n,
),
Self::DoubleCircledNumber => {
zeroless(['⓵', '⓶', '⓷', '⓸', '⓹', '⓺', '⓻', '⓼', '⓽', '⓾'], n)
}
Self::LowerSimplifiedChinese => {
u64_to_chinese(ChineseVariant::Simple, ChineseCase::Lower, n).into()
}
Self::UpperSimplifiedChinese => {
u64_to_chinese(ChineseVariant::Simple, ChineseCase::Upper, n).into()
}
Self::LowerTraditionalChinese => {
u64_to_chinese(ChineseVariant::Traditional, ChineseCase::Lower, n).into()
}
Self::UpperTraditionalChinese => {
u64_to_chinese(ChineseVariant::Traditional, ChineseCase::Upper, n).into()
}
Self::EasternArabic => decimal('\u{0660}', n),
Self::EasternArabicPersian => decimal('\u{06F0}', 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;
}
}
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 &[
Self::UpperRoman => additive(
&[
("", 1000000),
("", 500000),
("", 100000),
@ -597,211 +431,394 @@ fn roman_numeral(mut n: u64, case: Case) -> EcoString {
("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),
("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',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
],
n,
),
Self::UpperLatin => alphabetic(
&[
'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',
],
n,
),
Self::HiraganaAiueo => alphabetic(
&[
'あ', 'い', 'う', 'え', 'お', 'か', 'き', 'く', 'け', 'こ', 'さ',
'し', 'す', 'せ', 'そ', 'た', 'ち', 'つ', 'て', 'と', 'な', 'に',
'ぬ', 'ね', 'の', 'は', 'ひ', 'ふ', 'へ', 'ほ', 'ま', 'み', 'む',
'め', 'も', 'や', 'ゆ', 'よ', 'ら', 'り', 'る', 'れ', 'ろ', 'わ',
'を', 'ん',
],
n,
),
Self::HiraganaIroha => alphabetic(
&[
'い', 'ろ', 'は', 'に', 'ほ', 'へ', 'と', 'ち', 'り', 'ぬ', 'る',
'を', 'わ', 'か', 'よ', 'た', 'れ', 'そ', 'つ', 'ね', 'な', 'ら',
'む', 'う', 'ゐ', 'の', 'お', 'く', 'や', 'ま', 'け', 'ふ', 'こ',
'え', 'て', 'あ', 'さ', 'き', 'ゆ', 'め', 'み', 'し', 'ゑ', 'ひ',
'も', 'せ', 'す',
],
n,
),
Self::KatakanaAiueo => alphabetic(
&[
'ア', 'イ', 'ウ', 'エ', 'オ', 'カ', 'キ', 'ク', 'ケ', 'コ', 'サ',
'シ', 'ス', 'セ', 'ソ', 'タ', 'チ', 'ツ', 'テ', 'ト', 'ナ', 'ニ',
'ヌ', 'ネ', '', 'ハ', 'ヒ', 'フ', 'ヘ', 'ホ', 'マ', 'ミ', 'ム',
'メ', 'モ', 'ヤ', 'ユ', 'ヨ', 'ラ', 'リ', 'ル', 'レ', 'ロ', 'ワ',
'ヲ', 'ン',
],
n,
),
Self::KatakanaIroha => alphabetic(
&[
'イ', 'ロ', 'ハ', 'ニ', 'ホ', 'ヘ', 'ト', 'チ', 'リ', 'ヌ', 'ル',
'ヲ', 'ワ', 'カ', 'ヨ', 'タ', 'レ', 'ソ', 'ツ', 'ネ', 'ナ', 'ラ',
'ム', 'ウ', 'ヰ', '', 'オ', 'ク', 'ヤ', 'マ', 'ケ', 'フ', 'コ',
'エ', 'テ', 'ア', 'サ', 'キ', 'ユ', 'メ', 'ミ', 'シ', 'ヱ', 'ヒ',
'モ', 'セ', 'ス',
],
n,
),
Self::KoreanJamo => alphabetic(
&[
'ㄱ', 'ㄴ', 'ㄷ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅅ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ',
'ㅌ', 'ㅍ', 'ㅎ',
],
n,
),
Self::KoreanSyllable => alphabetic(
&[
'가', '나', '다', '라', '마', '바', '사', '아', '자', '차', '카',
'타', '파', '하',
],
n,
),
Self::BengaliLetter => alphabetic(
&[
'ক', 'খ', 'গ', 'ঘ', 'ঙ', 'চ', 'ছ', 'জ', 'ঝ', 'ঞ', 'ট', 'ঠ', 'ড', 'ঢ',
'ণ', 'ত', 'থ', 'দ', 'ধ', 'ন', 'প', 'ফ', 'ব', 'ভ', 'ম', 'য', 'র', 'ল',
'শ', 'ষ', 'স', 'হ',
],
n,
),
Self::CircledNumber => fixed(
&[
'⓪', '①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩', '⑪', '⑫', '⑬',
'⑭', '⑮', '⑯', '⑰', '⑱', '⑲', '⑳', '㉑', '㉒', '㉓', '㉔', '㉕',
'㉖', '㉗', '㉘', '㉙', '㉚', '㉛', '㉜', '㉝', '㉞', '㉟', '㊱',
'㊲', '㊳', '㊴', '㊵', '㊶', '㊷', '㊸', '㊹', '㊺', '㊻', '㊼',
'㊽', '㊾', '㊿',
],
n,
),
Self::DoubleCircledNumber => {
fixed(&['0', '⓵', '⓶', '⓷', '⓸', '⓹', '⓺', '⓻', '⓼', '⓽', '⓾'], n)
}
Self::LowerSimplifiedChinese => {
u64_to_chinese(ChineseVariant::Simple, ChineseCase::Lower, n).into()
}
Self::UpperSimplifiedChinese => {
u64_to_chinese(ChineseVariant::Simple, ChineseCase::Upper, n).into()
}
Self::LowerTraditionalChinese => {
u64_to_chinese(ChineseVariant::Traditional, ChineseCase::Lower, n).into()
}
Self::UpperTraditionalChinese => {
u64_to_chinese(ChineseVariant::Traditional, ChineseCase::Upper, n).into()
}
Self::EasternArabic => {
numeric(&['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩'], n)
}
Self::EasternArabicPersian => {
numeric(&['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'], n)
}
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
/// (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:
/// Consider the situation where ['I': 1, 'IV': 4, 'V': 5],
///
/// ```text
/// 1 => "A"
/// 2 => "B"
/// 3 => "C"
/// 4 => "AA"
/// 5 => "AB"
/// 6 => "AC"
/// 7 => "BA"
/// 8 => "BB"
/// 9 => "BC"
/// 10 => "CA"
/// 11 => "CB"
/// 12 => "CC"
/// 13 => "AAA"
/// etc.
/// 1 => 'I'
/// 2 => 'II'
/// 3 => 'III'
/// 4 => 'IV'
/// 5 => 'V'
/// 6 => 'VI'
/// 7 => 'VII'
/// 8 => 'VIII'
/// ```
///
/// You might be familiar with this scheme from the way spreadsheet software
/// tends to label its columns.
fn zeroless<const N_DIGITS: usize>(alphabet: [char; N_DIGITS], mut n: u64) -> EcoString {
/// where this is the start of the familiar Roman numeral system.
fn additive(symbols: &[(&str, u64)], 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 {
return '-'.into();
}
let n_digits = N_DIGITS as u64;
let mut cs = EcoString::new();
while n > 0 {
let mut s = EcoString::new();
while n != 0 {
n -= 1;
cs.push(alphabet[(n % n_digits) as usize]);
s.push(symbols[(n % n_digits) as usize]);
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.
fn decimal(start: char, mut n: u64) -> EcoString {
/// Consider the situation where ['0', 'A', 'B', 'C'] are the provided symbols,
///
/// ```text
/// 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();
}
eco_format!("{n}")
}
/// Stringify a number using a base-n (where n is the number of provided
/// symbols) system with a zero symbol.
///
/// 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 start.into();
return symbols[0].into();
}
let mut cs = EcoString::new();
while n > 0 {
cs.push(char::from_u32((start as u32) + ((n % 10) as u32)).unwrap());
n /= 10;
let mut s = EcoString::new();
while n != 0 {
s.push(symbols[(n % n_digits) as usize]);
n /= n_digits;
}
cs.chars().rev().collect()
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
/// 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
/// references are also links to the respective element. Reference syntax can
/// also be used to [cite] from a bibliography.
/// string such as "Section 1" for a reference to the first heading. The word
/// "Section" depends on the [`lang`]($text.lang) setting and is localized
/// 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
/// attached to a _referenceable element_. Referenceable elements include

View File

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

View File

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

View File

@ -194,6 +194,8 @@ bitflags::bitflags! {
const MONOSPACE = 1 << 0;
/// Glyphs have short strokes at their stems.
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();
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.
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!("bg"),
translation!("ca"),
@ -36,6 +36,7 @@ const TRANSLATIONS: [(&str, &str); 39] = [
translation!("it"),
translation!("ja"),
translation!("la"),
translation!("lv"),
translation!("nb"),
translation!("nl"),
translation!("nn"),
@ -87,6 +88,7 @@ impl Lang {
pub const ITALIAN: Self = Self(*b"it ", 2);
pub const JAPANESE: Self = Self(*b"ja ", 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 NYNORSK: Self = Self(*b"nn ", 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.
///
/// ```example
/// #set page(width: 220pt)
///
/// #set par(justify: true)
/// 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
/// results in a clearer paragraph edge.
///
/// #set text(overhang: false)
/// 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
/// results in a clearer paragraph edge.
/// ```

View File

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

View File

@ -77,8 +77,8 @@ pub struct ImageElem {
/// [`source`]($image.source) (even then, Typst will try to figure out the
/// format automatically, but that's not always possible).
///
/// Supported formats are `{"png"}`, `{"jpg"}`, `{"gif"}`, `{"svg"}` as well
/// as raw pixel data. Embedding PDFs as images is
/// Supported formats are `{"png"}`, `{"jpg"}`, `{"gif"}`, `{"svg"}`,
/// `{"webp"}` as well as raw pixel data. Embedding PDFs as images is
/// [not currently supported](https://github.com/typst/typst/issues/145).
///
/// 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::jpeg::JpegDecoder;
use image::codecs::png::PngDecoder;
use image::codecs::webp::WebPDecoder;
use image::{
guess_format, DynamicImage, ImageBuffer, ImageDecoder, ImageResult, Limits, Pixel,
};
@ -77,6 +78,7 @@ impl RasterImage {
ExchangeFormat::Jpg => decode(JpegDecoder::new(cursor), icc),
ExchangeFormat::Png => decode(PngDecoder::new(cursor), icc),
ExchangeFormat::Gif => decode(GifDecoder::new(cursor), icc),
ExchangeFormat::Webp => decode(WebPDecoder::new(cursor), icc),
}
.map_err(format_image_error)?;
@ -242,6 +244,8 @@ pub enum ExchangeFormat {
/// Raster format that is typically used for short animated clips. Typst can
/// load GIFs, but they will become static.
Gif,
/// Raster format that supports both lossy and lossless compression.
Webp,
}
impl ExchangeFormat {
@ -257,6 +261,7 @@ impl From<ExchangeFormat> for image::ImageFormat {
ExchangeFormat::Png => image::ImageFormat::Png,
ExchangeFormat::Jpg => image::ImageFormat::Jpeg,
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::Jpeg => ExchangeFormat::Jpg,
image::ImageFormat::Gif => ExchangeFormat::Gif,
image::ImageFormat::WebP => ExchangeFormat::Webp,
_ => 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`.
#[allow(clippy::large_enum_variant)]
enum Pattern {
Str(syn::LitStr),
Ty(syn::Pat, syn::Type),

View File

@ -23,6 +23,7 @@ bytemuck = { workspace = true }
comemo = { workspace = true }
ecow = { workspace = true }
image = { workspace = true }
infer = { workspace = true }
krilla = { workspace = true }
krilla-svg = { 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());
// 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 {
path,
@ -41,7 +43,7 @@ pub(crate) fn embed_files(
description,
association_kind,
data: data.into(),
compress: true,
compress,
location: Some(span.into_raw().get()),
};
@ -52,3 +54,69 @@ pub(crate) fn embed_files(
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));
// Try to continue or finish an existing grouping.
let mut i = 0;
while let Some(active) = s.groupings.last() {
// Start a nested group if a rule with higher priority matches.
if matching.is_some_and(|rule| rule.priority > active.rule.priority) {
@ -670,6 +671,16 @@ fn visit_grouping_rules<'a>(
}
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.

View File

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

View File

@ -27,7 +27,6 @@
//! [module]: crate::foundations::Module
//! [content]: crate::foundations::Content
//! [laid out]: typst_layout::layout_document
//! [document]: crate::model::Document
//! [frame]: crate::layout::Frame
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
layer on top of the compiler and the exporters.
- `crates/typst-eval`: The interpreter for the Typst language.
- `crates/typst-html`: The HTML exporter.
- `crates/typst-ide`: Exposes IDE functionality.
- `crates/typst-kit`: Contains various default implementation of
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 }
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
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,

6
flake.lock generated
View File

@ -112,13 +112,13 @@
"rust-manifest": {
"flake": false,
"locked": {
"narHash": "sha256-irgHsBXecwlFSdmP9MfGP06Cbpca2QALJdbN4cymcko=",
"narHash": "sha256-BwfxWd/E8gpnXoKsucFXhMbevMlVgw3l0becLkIcWCU=",
"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": {
"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": {

View File

@ -10,7 +10,7 @@
inputs.nixpkgs.follows = "nixpkgs";
};
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;
};
};

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