Compare commits

...

4 Commits

Author SHA1 Message Date
Max
012e14d40c
Unify layout of vec and cases with mat (#5934) 2025-03-31 09:38:04 +00:00
Max
4f0fbfb7e0
Add dotless parameter to math.accent (#5939)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-03-31 09:17:49 +00:00
+merlan #flirora
a64af130dc
Add default parameter for array.{first, last} (#5970) 2025-03-31 09:06:18 +00:00
Malo
1082181a6f
Improve french smartquotes (#5976) 2025-03-31 09:01:01 +00:00
22 changed files with 178 additions and 134 deletions

View File

@ -19,8 +19,10 @@ pub fn layout_accent(
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. // Try to replace a glyph with its dotless variant.
if let MathFragment::Glyph(glyph) = &mut base { if elem.dotless(styles) {
glyph.make_dotless_form(ctx); if let MathFragment::Glyph(glyph) = &mut base {
glyph.make_dotless_form(ctx);
}
} }
// Preserve class to preserve automatic spacing. // Preserve class to preserve automatic spacing.

View File

@ -1,4 +1,4 @@
use typst_library::diag::{bail, SourceResult}; use typst_library::diag::{bail, warning, SourceResult};
use typst_library::foundations::{Content, Packed, Resolve, StyleChain}; use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
use typst_library::layout::{ use typst_library::layout::{
Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, Ratio, Rel, Size, Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, Ratio, Rel, Size,
@ -9,7 +9,7 @@ use typst_library::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape};
use typst_syntax::Span; use typst_syntax::Span;
use super::{ use super::{
alignments, delimiter_alignment, stack, style_for_denominator, AlignmentResult, alignments, delimiter_alignment, style_for_denominator, AlignmentResult,
FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, DELIM_SHORT_FALL, FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, DELIM_SHORT_FALL,
}; };
@ -23,67 +23,23 @@ pub fn layout_vec(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let delim = elem.delim(styles); let span = elem.span();
let frame = layout_vec_body(
let column: Vec<&Content> = elem.children.iter().collect();
let frame = layout_body(
ctx, ctx,
styles, styles,
&elem.children, &[column],
elem.align(styles), elem.align(styles),
elem.gap(styles),
LeftRightAlternator::Right, LeftRightAlternator::Right,
None,
Axes::with_y(elem.gap(styles)),
span,
"elements",
)?; )?;
layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), elem.span())
}
/// Lays out a [`MatElem`].
#[typst_macros::time(name = "math.mat", span = elem.span())]
pub fn layout_mat(
elem: &Packed<MatElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let augment = elem.augment(styles);
let rows = &elem.rows;
if let Some(aug) = &augment {
for &offset in &aug.hline.0 {
if offset == 0 || offset.unsigned_abs() >= rows.len() {
bail!(
elem.span(),
"cannot draw a horizontal line after row {} of a matrix with {} rows",
if offset < 0 { rows.len() as isize + offset } else { offset },
rows.len()
);
}
}
let ncols = rows.first().map_or(0, |row| row.len());
for &offset in &aug.vline.0 {
if offset == 0 || offset.unsigned_abs() >= ncols {
bail!(
elem.span(),
"cannot draw a vertical line after column {} of a matrix with {} columns",
if offset < 0 { ncols as isize + offset } else { offset },
ncols
);
}
}
}
let delim = elem.delim(styles); let delim = elem.delim(styles);
let frame = layout_mat_body( layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), span)
ctx,
styles,
rows,
elem.align(styles),
augment,
Axes::new(elem.column_gap(styles), elem.row_gap(styles)),
elem.span(),
)?;
layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), elem.span())
} }
/// Lays out a [`CasesElem`]. /// Lays out a [`CasesElem`].
@ -93,60 +49,100 @@ pub fn layout_cases(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let delim = elem.delim(styles); let span = elem.span();
let frame = layout_vec_body(
let column: Vec<&Content> = elem.children.iter().collect();
let frame = layout_body(
ctx, ctx,
styles, styles,
&elem.children, &[column],
FixedAlignment::Start, FixedAlignment::Start,
elem.gap(styles),
LeftRightAlternator::None, LeftRightAlternator::None,
None,
Axes::with_y(elem.gap(styles)),
span,
"branches",
)?; )?;
let delim = elem.delim(styles);
let (open, close) = let (open, close) =
if elem.reverse(styles) { (None, delim.close()) } else { (delim.open(), None) }; if elem.reverse(styles) { (None, delim.close()) } else { (delim.open(), None) };
layout_delimiters(ctx, styles, frame, open, close, span)
layout_delimiters(ctx, styles, frame, open, close, elem.span())
} }
/// Layout the inner contents of a vector. /// Lays out a [`MatElem`].
fn layout_vec_body( #[typst_macros::time(name = "math.mat", span = elem.span())]
pub fn layout_mat(
elem: &Packed<MatElem>,
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
column: &[Content], ) -> SourceResult<()> {
align: FixedAlignment, let span = elem.span();
row_gap: Rel<Abs>, let rows = &elem.rows;
alternator: LeftRightAlternator, let ncols = rows.first().map_or(0, |row| row.len());
) -> SourceResult<Frame> {
let gap = row_gap.relative_to(ctx.region.size.y);
let denom_style = style_for_denominator(styles); let augment = elem.augment(styles);
let mut flat = vec![]; if let Some(aug) = &augment {
for child in column { for &offset in &aug.hline.0 {
// We allow linebreaks in cases and vectors, which are functionally if offset == 0 || offset.unsigned_abs() >= rows.len() {
// identical to commas. bail!(
flat.extend(ctx.layout_into_run(child, styles.chain(&denom_style))?.rows()); span,
"cannot draw a horizontal line after row {} of a matrix with {} rows",
if offset < 0 { rows.len() as isize + offset } else { offset },
rows.len()
);
}
}
for &offset in &aug.vline.0 {
if offset == 0 || offset.unsigned_abs() >= ncols {
bail!(
span,
"cannot draw a vertical line after column {} of a matrix with {} columns",
if offset < 0 { ncols as isize + offset } else { offset },
ncols
);
}
}
} }
// We pad ascent and descent with the ascent and descent of the paren
// to ensure that normal vectors are aligned with others unless they are // Transpose rows of the matrix into columns.
// way too big. let mut row_iters: Vec<_> = rows.iter().map(|i| i.iter()).collect();
let paren = let columns: Vec<Vec<_>> = (0..ncols)
GlyphFragment::new(ctx, styles.chain(&denom_style), '(', Span::detached()); .map(|_| row_iters.iter_mut().map(|i| i.next().unwrap()).collect())
Ok(stack(flat, align, gap, 0, alternator, Some((paren.ascent, paren.descent)))) .collect();
let frame = layout_body(
ctx,
styles,
&columns,
elem.align(styles),
LeftRightAlternator::Right,
augment,
Axes::new(elem.column_gap(styles), elem.row_gap(styles)),
span,
"cells",
)?;
let delim = elem.delim(styles);
layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), span)
} }
/// Layout the inner contents of a matrix. /// Layout the inner contents of a matrix, vector, or cases.
fn layout_mat_body( #[allow(clippy::too_many_arguments)]
fn layout_body(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
rows: &[Vec<Content>], columns: &[Vec<&Content>],
align: FixedAlignment, align: FixedAlignment,
alternator: LeftRightAlternator,
augment: Option<Augment<Abs>>, augment: Option<Augment<Abs>>,
gap: Axes<Rel<Abs>>, gap: Axes<Rel<Abs>>,
span: Span, span: Span,
children: &str,
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
let ncols = rows.first().map_or(0, |row| row.len()); let nrows = columns.first().map_or(0, |col| col.len());
let nrows = rows.len(); let ncols = columns.len();
if ncols == 0 || nrows == 0 { if ncols == 0 || nrows == 0 {
return Ok(Frame::soft(Size::zero())); return Ok(Frame::soft(Size::zero()));
} }
@ -178,16 +174,11 @@ fn layout_mat_body(
// Before the full matrix body can be laid out, the // Before the full matrix body can be laid out, the
// individual cells must first be independently laid out // individual cells must first be independently laid out
// so we can ensure alignment across rows and columns. // so we can ensure alignment across rows and columns.
let mut cols = vec![vec![]; ncols];
// This variable stores the maximum ascent and descent for each row. // This variable stores the maximum ascent and descent for each row.
let mut heights = vec![(Abs::zero(), Abs::zero()); nrows]; let mut heights = vec![(Abs::zero(), Abs::zero()); nrows];
// We want to transpose our data layout to columns
// before final layout. For efficiency, the columns
// variable is set up here and newly generated
// individual cells are then added to it.
let mut cols = vec![vec![]; ncols];
let denom_style = style_for_denominator(styles); let denom_style = style_for_denominator(styles);
// We pad ascent and descent with the ascent and descent of the paren // We pad ascent and descent with the ascent and descent of the paren
// to ensure that normal matrices are aligned with others unless they are // to ensure that normal matrices are aligned with others unless they are
@ -195,10 +186,22 @@ fn layout_mat_body(
let paren = let paren =
GlyphFragment::new(ctx, styles.chain(&denom_style), '(', Span::detached()); GlyphFragment::new(ctx, styles.chain(&denom_style), '(', Span::detached());
for (row, (ascent, descent)) in rows.iter().zip(&mut heights) { for (column, col) in columns.iter().zip(&mut cols) {
for (cell, col) in row.iter().zip(&mut cols) { for (cell, (ascent, descent)) in column.iter().zip(&mut heights) {
let cell_span = cell.span();
let cell = ctx.layout_into_run(cell, styles.chain(&denom_style))?; let cell = ctx.layout_into_run(cell, styles.chain(&denom_style))?;
// We ignore linebreaks in the cells as we can't differentiate
// alignment points for the whole body from ones for a specific
// cell, and multiline cells don't quite make sense at the moment.
if cell.is_multiline() {
ctx.engine.sink.warn(warning!(
cell_span,
"linebreaks are ignored in {}", children;
hint: "use commas instead to separate each line"
));
}
ascent.set_max(cell.ascent().max(paren.ascent)); ascent.set_max(cell.ascent().max(paren.ascent));
descent.set_max(cell.descent().max(paren.descent)); descent.set_max(cell.descent().max(paren.descent));
@ -222,7 +225,7 @@ fn layout_mat_body(
let mut y = Abs::zero(); let mut y = Abs::zero();
for (cell, &(ascent, descent)) in col.into_iter().zip(&heights) { for (cell, &(ascent, descent)) in col.into_iter().zip(&heights) {
let cell = cell.into_line_frame(&points, LeftRightAlternator::Right); let cell = cell.into_line_frame(&points, alternator);
let pos = Point::new( let pos = Point::new(
if points.is_empty() { if points.is_empty() {
x + align.position(rcol - cell.width()) x + align.position(rcol - cell.width())

View File

@ -117,7 +117,6 @@ pub fn stack(
gap: Abs, gap: Abs,
baseline: usize, baseline: usize,
alternator: LeftRightAlternator, alternator: LeftRightAlternator,
minimum_ascent_descent: Option<(Abs, Abs)>,
) -> Frame { ) -> Frame {
let AlignmentResult { points, width } = alignments(&rows); let AlignmentResult { points, width } = alignments(&rows);
let rows: Vec<_> = rows let rows: Vec<_> = rows
@ -125,13 +124,9 @@ pub fn stack(
.map(|row| row.into_line_frame(&points, alternator)) .map(|row| row.into_line_frame(&points, alternator))
.collect(); .collect();
let padded_height = |height: Abs| {
height.max(minimum_ascent_descent.map_or(Abs::zero(), |(a, d)| a + d))
};
let mut frame = Frame::soft(Size::new( let mut frame = Frame::soft(Size::new(
width, width,
rows.iter().map(|row| padded_height(row.height())).sum::<Abs>() rows.iter().map(|row| row.height()).sum::<Abs>()
+ rows.len().saturating_sub(1) as f64 * gap, + rows.len().saturating_sub(1) as f64 * gap,
)); ));
@ -142,14 +137,11 @@ pub fn stack(
} else { } else {
Abs::zero() Abs::zero()
}; };
let ascent_padded_part = minimum_ascent_descent let pos = Point::new(x, y);
.map_or(Abs::zero(), |(a, _)| (a - row.ascent()))
.max(Abs::zero());
let pos = Point::new(x, y + ascent_padded_part);
if i == baseline { if i == baseline {
frame.set_baseline(y + row.baseline() + ascent_padded_part); frame.set_baseline(y + row.baseline());
} }
y += padded_height(row.height()) + gap; y += row.height() + gap;
frame.push_frame(pos, row); frame.push_frame(pos, row);
} }

View File

@ -312,14 +312,8 @@ fn layout_underoverspreader(
} }
}; };
let frame = stack( let frame =
rows, stack(rows, FixedAlignment::Center, gap, baseline, LeftRightAlternator::Right);
FixedAlignment::Center,
gap,
baseline,
LeftRightAlternator::Right,
None,
);
ctx.push(FrameFragment::new(styles, frame).with_class(body_class)); ctx.push(FrameFragment::new(styles, frame).with_class(body_class));
Ok(()) Ok(())

View File

@ -172,17 +172,29 @@ impl Array {
} }
/// Returns the first item in the array. May be used on the left-hand side /// Returns the first item in the array. May be used on the left-hand side
/// of an assignment. Fails with an error if the array is empty. /// an assignment. Returns the default value if the array is empty
/// or fails with an error is no default value was specified.
#[func] #[func]
pub fn first(&self) -> StrResult<Value> { pub fn first(
self.0.first().cloned().ok_or_else(array_is_empty) &self,
/// A default value to return if the array is empty.
#[named]
default: Option<Value>,
) -> StrResult<Value> {
self.0.first().cloned().or(default).ok_or_else(array_is_empty)
} }
/// Returns the last item in the array. May be used on the left-hand side of /// Returns the last item in the array. May be used on the left-hand side of
/// an assignment. Fails with an error if the array is empty. /// an assignment. Returns the default value if the array is empty
/// or fails with an error is no default value was specified.
#[func] #[func]
pub fn last(&self) -> StrResult<Value> { pub fn last(
self.0.last().cloned().ok_or_else(array_is_empty) &self,
/// A default value to return if the array is empty.
#[named]
default: Option<Value>,
) -> StrResult<Value> {
self.0.last().cloned().or(default).ok_or_else(array_is_empty)
} }
/// Returns the item at the specified index in the array. May be used on the /// Returns the item at the specified index in the array. May be used on the

View File

@ -13,8 +13,8 @@ use crate::math::Mathy;
/// ``` /// ```
#[elem(Mathy)] #[elem(Mathy)]
pub struct AccentElem { pub struct AccentElem {
/// The base to which the accent is applied. /// The base to which the accent is applied. May consist of multiple
/// May consist of multiple letters. /// letters.
/// ///
/// ```example /// ```example
/// $arrow(A B C)$ /// $arrow(A B C)$
@ -51,9 +51,24 @@ pub struct AccentElem {
pub accent: Accent, pub accent: Accent,
/// The size of the accent, relative to the width of the base. /// The size of the accent, relative to the width of the base.
///
/// ```example
/// $dash(A, size: #150%)$
/// ```
#[resolve] #[resolve]
#[default(Rel::one())] #[default(Rel::one())]
pub size: Rel<Length>, pub size: Rel<Length>,
/// Whether to remove the dot on top of lowercase i and j when adding a top
/// accent.
///
/// This enables the `dtls` OpenType feature.
///
/// ```example
/// $hat(dotless: #false, i)$
/// ```
#[default(true)]
pub dotless: bool,
} }
/// An accent character. /// An accent character.
@ -103,11 +118,18 @@ macro_rules! accents {
/// The size of the accent, relative to the width of the base. /// The size of the accent, relative to the width of the base.
#[named] #[named]
size: Option<Rel<Length>>, size: Option<Rel<Length>>,
/// Whether to remove the dot on top of lowercase i and j when
/// adding a top accent.
#[named]
dotless: Option<bool>,
) -> Content { ) -> Content {
let mut accent = AccentElem::new(base, Accent::new($primary)); let mut accent = AccentElem::new(base, Accent::new($primary));
if let Some(size) = size { if let Some(size) = size {
accent = accent.with_size(size); accent = accent.with_size(size);
} }
if let Some(dotless) = dotless {
accent = accent.with_dotless(dotless);
}
accent.pack() accent.pack()
} }
)+ )+

View File

@ -238,7 +238,7 @@ impl<'s> SmartQuotes<'s> {
"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" | "ru" if alternative => default,
"fr" => ("\u{00A0}", "\u{00A0}", "«\u{00A0}", "\u{00A0}»"), "fr" => ("", "", "«\u{202F}", "\u{202F}»"),
"fi" | "sv" if alternative => ("", "", "»", "»"), "fi" | "sv" if alternative => ("", "", "»", "»"),
"bs" | "fi" | "sv" => ("", "", "", ""), "bs" | "fi" | "sv" => ("", "", "", ""),
"it" if alternative => default, "it" if alternative => default,

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 570 B

After

Width:  |  Height:  |  Size: 506 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 984 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 856 B

After

Width:  |  Height:  |  Size: 651 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 573 B

After

Width:  |  Height:  |  Size: 568 B

View File

@ -179,6 +179,10 @@
#test((2,).last(), 2) #test((2,).last(), 2)
#test((1, 2, 3).first(), 1) #test((1, 2, 3).first(), 1)
#test((1, 2, 3).last(), 3) #test((1, 2, 3).last(), 3)
#test((1, 2).first(default: 99), 1)
#test(().first(default: 99), 99)
#test((1, 2).last(default: 99), 2)
#test(().last(default: 99), 99)
--- array-first-empty --- --- array-first-empty ---
// Error: 2-12 array is empty // Error: 2-12 array is empty

View File

@ -42,3 +42,11 @@ $tilde(U, size: #1.1em), x^tilde(U, size: #1.1em), sscript(tilde(U, size: #1.1em
macron(bb(#c)), dot(cal(#c)), diaer(upright(#c)), breve(bold(#c)), macron(bb(#c)), dot(cal(#c)), diaer(upright(#c)), breve(bold(#c)),
circle(bold(upright(#c))), caron(upright(sans(#c))), arrow(bold(frak(#c)))$ circle(bold(upright(#c))), caron(upright(sans(#c))), arrow(bold(frak(#c)))$
$test(i) \ test(j)$ $test(i) \ test(j)$
--- math-accent-dotless-disabled ---
// Test disabling the dotless glyph variants.
$hat(i), hat(i, dotless: #false), accent(j, tilde), accent(j, tilde, dotless: #false)$
--- math-accent-dotless-set-rule ---
#set math.accent(dotless: false)
$ hat(i) $

View File

@ -17,6 +17,6 @@ $ x = cases(1, 2) $
$ cases(a, b, c) $ $ cases(a, b, c) $
--- math-cases-linebreaks --- --- math-cases-linebreaks ---
// Currently linebreaks are equivalent to commas, though this behaviour may // Warning: 40-49 linebreaks are ignored in branches
// change in the future. // Hint: 40-49 use commas instead to separate each line
$ cases(a, b, c) cases(reverse: #true, a \ b \ c) $ $ cases(a, b, c) cases(reverse: #true, a \ b \ c) $

View File

@ -256,10 +256,17 @@ $ mat(delim: #(none, "["), 1, 2; 3, 4) $
$ mat(delim: #(sym.angle.r, sym.bracket.double.r), 1, 2; 3, 4) $ $ mat(delim: #(sym.angle.r, sym.bracket.double.r), 1, 2; 3, 4) $
--- math-mat-linebreaks --- --- math-mat-linebreaks ---
// Unlike cases and vectors, linebreaks are discarded in matrices. This // Warning: 20-29 linebreaks are ignored in cells
// behaviour may change in the future. // Hint: 20-29 use commas instead to separate each line
$ mat(a; b; c) mat(a \ b \ c) $ $ mat(a; b; c) mat(a \ b \ c) $
--- math-mat-vec-cases-unity ---
// Test that matrices, vectors, and cases are all laid out the same.
$ mat(z_(n_p); a^2)
vec(z_(n_p), a^2)
cases(reverse: #true, delim: \(, z_(n_p), a^2)
cases(delim: \(, z_(n_p), a^2) $
--- issue-1617-mat-align --- --- issue-1617-mat-align ---
#set page(width: auto) #set page(width: auto)
$ mat(a, b; c, d) mat(x; y) $ $ mat(a, b; c, d) mat(x; y) $

View File

@ -51,6 +51,6 @@ $ vec(1, 2) $
#set math.vec(delim: (none, "%")) #set math.vec(delim: (none, "%"))
--- math-vec-linebreaks --- --- math-vec-linebreaks ---
// Currently linebreaks are equivalent to commas, though this behaviour may // Warning: 20-29 linebreaks are ignored in elements
// change in the future. // Hint: 20-29 use commas instead to separate each line
$ vec(a, b, c) vec(a \ b \ c) $ $ vec(a, b, c) vec(a \ b \ c) $

View File

@ -99,7 +99,7 @@ He's told some books contain questionable "example text".
--- smartquote-disabled-temporarily --- --- smartquote-disabled-temporarily ---
// Test changing properties within text. // Test changing properties within text.
"She suddenly started speaking french: #text(lang: "fr")['Je suis une banane.']" Roman told me. "She suddenly started speaking french: #text(lang: "fr", region: "CH")['Je suis une banane.']" Roman told me.
Some people's thought on this would be #[#set smartquote(enabled: false); "strange."] Some people's thought on this would be #[#set smartquote(enabled: false); "strange."]