summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/typst/src/math/attach.rs54
-rw-r--r--crates/typst/src/math/lr.rs59
-rw-r--r--crates/typst/src/math/matrix.rs8
-rw-r--r--crates/typst/src/math/mod.rs11
-rw-r--r--crates/typst/src/math/stretch.rs184
-rw-r--r--docs/reference/groups.yml4
-rw-r--r--tests/ref/math-stretch-attach-nested-equation.pngbin0 -> 180 bytes
-rw-r--r--tests/ref/math-stretch-attach.pngbin0 -> 2245 bytes
-rw-r--r--tests/ref/math-stretch-basic.pngbin0 -> 787 bytes
-rw-r--r--tests/ref/math-stretch-complex.pngbin0 -> 1457 bytes
-rw-r--r--tests/ref/math-stretch-horizontal.pngbin0 -> 591 bytes
-rw-r--r--tests/ref/math-stretch-nested.pngbin0 -> 253 bytes
-rw-r--r--tests/ref/math-stretch-shorthand.pngbin0 -> 375 bytes
-rw-r--r--tests/ref/math-stretch-vertical.pngbin0 -> 481 bytes
-rw-r--r--tests/suite/math/stretch.typ72
15 files changed, 312 insertions, 80 deletions
diff --git a/crates/typst/src/math/attach.rs b/crates/typst/src/math/attach.rs
index 2d491dfd..9eb0c824 100644
--- a/crates/typst/src/math/attach.rs
+++ b/crates/typst/src/math/attach.rs
@@ -1,15 +1,21 @@
use unicode_math_class::MathClass;
use crate::diag::SourceResult;
-use crate::foundations::{elem, Content, Packed, StyleChain};
-use crate::layout::{Abs, Corner, Frame, Point, Size};
+use crate::foundations::{elem, Content, Packed, Smart, StyleChain};
+use crate::layout::{Abs, Axis, Corner, Frame, Length, Point, Rel, Size};
use crate::math::{
- style_for_subscript, style_for_superscript, EquationElem, FrameFragment, LayoutMath,
- MathContext, MathFragment, MathSize, Scaled,
+ stretch_fragment, style_for_subscript, style_for_superscript, EquationElem,
+ FrameFragment, LayoutMath, MathContext, MathFragment, MathSize, Scaled, StretchElem,
};
use crate::text::TextElem;
use crate::utils::OptionExt;
+macro_rules! measure {
+ ($e: ident, $attr: ident) => {
+ $e.as_ref().map(|e| e.$attr()).unwrap_or_default()
+ };
+}
+
/// A base with optional attachments.
///
/// ```example
@@ -55,8 +61,9 @@ impl LayoutMath for Packed<AttachElem> {
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> {
let new_elem = merge_base(self);
let elem = new_elem.as_ref().unwrap_or(self);
+ let stretch = stretch_size(styles, elem);
- let base = ctx.layout_into_fragment(elem.base(), styles)?;
+ let mut base = ctx.layout_into_fragment(elem.base(), styles)?;
let sup_style = style_for_superscript(styles);
let sup_style_chain = styles.chain(&sup_style);
let tl = elem.tl(sup_style_chain);
@@ -86,12 +93,29 @@ impl LayoutMath for Packed<AttachElem> {
};
}
+ // Layout the top and bottom attachments early so we can measure their
+ // widths, in order to calculate what the stretch size is relative to.
+ let t = layout!(t, sup_style_chain)?;
+ let b = layout!(b, sub_style_chain)?;
+ if let Some(stretch) = stretch {
+ let relative_to_width = measure!(t, width).max(measure!(b, width));
+ stretch_fragment(
+ ctx,
+ styles,
+ &mut base,
+ Some(Axis::X),
+ Some(relative_to_width),
+ stretch,
+ Abs::zero(),
+ );
+ }
+
let fragments = [
layout!(tl, sup_style_chain)?,
- layout!(t, sup_style_chain)?,
+ t,
layout!(tr, sup_style_chain)?,
layout!(bl, sub_style_chain)?,
- layout!(b, sub_style_chain)?,
+ b,
layout!(br, sub_style_chain)?,
];
@@ -288,10 +312,18 @@ fn merge_base(elem: &Packed<AttachElem>) -> Option<Packed<AttachElem>> {
None
}
-macro_rules! measure {
- ($e: ident, $attr: ident) => {
- $e.as_ref().map(|e| e.$attr()).unwrap_or_default()
- };
+/// Get the size to stretch the base to, if the attach argument is true.
+fn stretch_size(
+ styles: StyleChain,
+ elem: &Packed<AttachElem>,
+) -> Option<Smart<Rel<Length>>> {
+ // Extract from an EquationElem.
+ let mut base = elem.base();
+ if let Some(equation) = base.to_packed::<EquationElem>() {
+ base = equation.body();
+ }
+
+ base.to_packed::<StretchElem>().map(|stretch| stretch.size(styles))
}
/// Layout the attachments.
diff --git a/crates/typst/src/math/lr.rs b/crates/typst/src/math/lr.rs
index 70fab12d..80ce55eb 100644
--- a/crates/typst/src/math/lr.rs
+++ b/crates/typst/src/math/lr.rs
@@ -1,15 +1,11 @@
use unicode_math_class::MathClass;
use crate::diag::SourceResult;
-use crate::foundations::{
- elem, func, Content, NativeElement, Packed, Resolve, Smart, StyleChain,
-};
-use crate::layout::{Abs, Em, Length, Rel};
-use crate::math::{GlyphFragment, LayoutMath, MathContext, MathFragment, Scaled};
+use crate::foundations::{elem, func, Content, NativeElement, Packed, Smart, StyleChain};
+use crate::layout::{Abs, Axis, Em, Length, Rel};
+use crate::math::{stretch_fragment, LayoutMath, MathContext, MathFragment, Scaled};
use crate::text::TextElem;
-use super::delimiter_alignment;
-
/// How much less high scaled delimiters can be than what they wrap.
pub(super) const DELIM_SHORT_FALL: Em = Em::new(0.1);
@@ -55,18 +51,15 @@ impl LayoutMath for Packed<LrElem> {
.max()
.unwrap_or_default();
- let height = self
- .size(styles)
- .unwrap_or(Rel::one())
- .resolve(styles)
- .relative_to(2.0 * max_extent);
+ let relative_to = 2.0 * max_extent;
+ let height = self.size(styles);
// Scale up fragments at both ends.
match fragments.as_mut_slice() {
- [one] => scale(ctx, styles, one, height, None),
+ [one] => scale(ctx, styles, one, relative_to, height, None),
[first, .., last] => {
- scale(ctx, styles, first, height, Some(MathClass::Opening));
- scale(ctx, styles, last, height, Some(MathClass::Closing));
+ scale(ctx, styles, first, relative_to, height, Some(MathClass::Opening));
+ scale(ctx, styles, last, relative_to, height, Some(MathClass::Closing));
}
_ => {}
}
@@ -76,7 +69,14 @@ impl LayoutMath for Packed<LrElem> {
if let MathFragment::Variant(ref mut variant) = fragment {
if variant.mid_stretched == Some(false) {
variant.mid_stretched = Some(true);
- scale(ctx, styles, fragment, height, Some(MathClass::Large));
+ scale(
+ ctx,
+ styles,
+ fragment,
+ relative_to,
+ height,
+ Some(MathClass::Large),
+ );
}
}
}
@@ -140,26 +140,27 @@ fn scale(
ctx: &mut MathContext,
styles: StyleChain,
fragment: &mut MathFragment,
- height: Abs,
+ relative_to: Abs,
+ height: Smart<Rel<Length>>,
apply: Option<MathClass>,
) {
if matches!(
fragment.class(),
MathClass::Opening | MathClass::Closing | MathClass::Fence
) {
- let glyph = match fragment {
- MathFragment::Glyph(glyph) => glyph.clone(),
- MathFragment::Variant(variant) => {
- GlyphFragment::new(ctx, styles, variant.c, variant.span)
- }
- _ => return,
- };
-
- let short_fall = DELIM_SHORT_FALL.at(glyph.font_size);
- let mut stretched = glyph.stretch_vertical(ctx, height, short_fall);
- stretched.align_on_axis(ctx, delimiter_alignment(stretched.c));
+ // This unwrap doesn't really matter. If it is None, then the fragment
+ // won't be stretchable anyways.
+ let short_fall = DELIM_SHORT_FALL.at(fragment.font_size().unwrap_or_default());
+ stretch_fragment(
+ ctx,
+ styles,
+ fragment,
+ Some(Axis::Y),
+ Some(relative_to),
+ height,
+ short_fall,
+ );
- *fragment = MathFragment::Variant(stretched);
if let Some(class) = apply {
fragment.set_class(class);
}
diff --git a/crates/typst/src/math/matrix.rs b/crates/typst/src/math/matrix.rs
index 5b9b17a7..345c6605 100644
--- a/crates/typst/src/math/matrix.rs
+++ b/crates/typst/src/math/matrix.rs
@@ -11,9 +11,9 @@ use crate::layout::{
Rel, Size,
};
use crate::math::{
- alignments, scaled_font_size, stack, style_for_denominator, AlignmentResult,
- FrameFragment, GlyphFragment, LayoutMath, LeftRightAlternator, MathContext, Scaled,
- DELIM_SHORT_FALL,
+ alignments, delimiter_alignment, scaled_font_size, stack, style_for_denominator,
+ AlignmentResult, FrameFragment, GlyphFragment, LayoutMath, LeftRightAlternator,
+ MathContext, Scaled, DELIM_SHORT_FALL,
};
use crate::symbols::Symbol;
use crate::syntax::{Span, Spanned};
@@ -21,8 +21,6 @@ use crate::text::TextElem;
use crate::utils::Numeric;
use crate::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape, Stroke};
-use super::delimiter_alignment;
-
const DEFAULT_ROW_GAP: Em = Em::new(0.2);
const DEFAULT_COL_GAP: Em = Em::new(0.5);
const VERTICAL_PADDING: Ratio = Ratio::new(0.1);
diff --git a/crates/typst/src/math/mod.rs b/crates/typst/src/math/mod.rs
index ab998975..2c5c32cb 100644
--- a/crates/typst/src/math/mod.rs
+++ b/crates/typst/src/math/mod.rs
@@ -34,6 +34,7 @@ pub use self::lr::*;
pub use self::matrix::*;
pub use self::op::*;
pub use self::root::*;
+pub use self::stretch::*;
pub use self::style::*;
pub use self::underover::*;
@@ -44,7 +45,6 @@ use self::spacing::*;
use crate::diag::SourceResult;
use crate::foundations::{category, Category, Module, Scope, StyleChain};
-use crate::layout::VAlignment;
use crate::text::TextElem;
/// Typst has special [syntax]($syntax/#math) and library functions to typeset
@@ -161,6 +161,7 @@ pub fn module() -> Module {
math.define_elem::<LrElem>();
math.define_elem::<MidElem>();
math.define_elem::<AttachElem>();
+ math.define_elem::<StretchElem>();
math.define_elem::<ScriptsElem>();
math.define_elem::<LimitsElem>();
math.define_elem::<AccentElem>();
@@ -217,11 +218,3 @@ pub trait LayoutMath {
/// Layout the element, producing fragment in the context.
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()>;
}
-
-fn delimiter_alignment(delimiter: char) -> VAlignment {
- match delimiter {
- '\u{231c}' | '\u{231d}' => VAlignment::Top,
- '\u{231e}' | '\u{231f}' => VAlignment::Bottom,
- _ => VAlignment::Horizon,
- }
-}
diff --git a/crates/typst/src/math/stretch.rs b/crates/typst/src/math/stretch.rs
index 749773e3..d316fa9a 100644
--- a/crates/typst/src/math/stretch.rs
+++ b/crates/typst/src/math/stretch.rs
@@ -1,12 +1,137 @@
use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
use ttf_parser::LazyArray16;
-use crate::layout::{Abs, Frame, Point, Size};
-use crate::math::{GlyphFragment, MathContext, Scaled, VariantFragment};
+use crate::diag::SourceResult;
+use crate::foundations::{elem, Content, Packed, Resolve, Smart, StyleChain};
+use crate::layout::{Abs, Axis, Frame, Length, Point, Rel, Size, VAlignment};
+use crate::math::{
+ GlyphFragment, LayoutMath, MathContext, MathFragment, Scaled, VariantFragment,
+};
+use crate::utils::Get;
/// Maximum number of times extenders can be repeated.
const MAX_REPEATS: usize = 1024;
+/// Stretches a glyph.
+///
+/// This function can also be used to automatically stretch the base of an
+/// attachment, so that it fits the top and bottom attachments.
+///
+/// Note that only some glyphs can be stretched, and which ones can depend on
+/// the math font being used. However, most math fonts are the same in this
+/// regard.
+///
+/// ```example
+/// $ H stretch(=)^"define" U + p V $
+/// $ f : X stretch(->>, size: #150%)_"surjective" Y $
+/// $ x stretch(harpoons.ltrb, size: #3em) y
+/// stretch(\[, size: #150%) z $
+/// ```
+#[elem(LayoutMath)]
+pub struct StretchElem {
+ /// The glyph to stretch.
+ #[required]
+ pub body: Content,
+
+ /// The size to stretch to, relative to the glyph's current size.
+ pub size: Smart<Rel<Length>>,
+}
+
+impl LayoutMath for Packed<StretchElem> {
+ #[typst_macros::time(name = "math.stretch", span = self.span())]
+ fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> {
+ let mut fragment = ctx.layout_into_fragment(self.body(), styles)?;
+ stretch_fragment(
+ ctx,
+ styles,
+ &mut fragment,
+ None,
+ None,
+ self.size(styles),
+ Abs::zero(),
+ );
+ ctx.push(fragment);
+ Ok(())
+ }
+}
+
+/// Attempts to stretch the given fragment by/to the amount given in stretch.
+pub(super) fn stretch_fragment(
+ ctx: &mut MathContext,
+ styles: StyleChain,
+ fragment: &mut MathFragment,
+ axis: Option<Axis>,
+ relative_to: Option<Abs>,
+ stretch: Smart<Rel<Length>>,
+ short_fall: Abs,
+) {
+ let glyph = match fragment {
+ MathFragment::Glyph(glyph) => glyph.clone(),
+ MathFragment::Variant(variant) => {
+ GlyphFragment::new(ctx, styles, variant.c, variant.span)
+ }
+ _ => return,
+ };
+
+ let Some(axis) = axis.or_else(|| stretch_axis(ctx, &glyph)) else {
+ return;
+ };
+
+ let relative_to_size = relative_to.unwrap_or_else(|| fragment.size().get(axis));
+
+ let mut variant = stretch_glyph(
+ ctx,
+ glyph,
+ stretch
+ .unwrap_or(Rel::one())
+ .resolve(styles)
+ .relative_to(relative_to_size),
+ short_fall,
+ axis,
+ );
+
+ if axis == Axis::Y {
+ variant.align_on_axis(ctx, delimiter_alignment(variant.c));
+ }
+
+ *fragment = MathFragment::Variant(variant);
+}
+
+pub(super) fn delimiter_alignment(delimiter: char) -> VAlignment {
+ match delimiter {
+ '\u{231c}' | '\u{231d}' => VAlignment::Top,
+ '\u{231e}' | '\u{231f}' => VAlignment::Bottom,
+ _ => VAlignment::Horizon,
+ }
+}
+
+/// Return whether the glyph is stretchable and if it is, along which axis it
+/// can be stretched.
+fn stretch_axis(ctx: &MathContext, base: &GlyphFragment) -> Option<Axis> {
+ let base_id = base.id;
+ let vertical = ctx
+ .table
+ .variants
+ .and_then(|variants| variants.vertical_constructions.get(base_id))
+ .map(|_| Axis::Y);
+ let horizontal = ctx
+ .table
+ .variants
+ .and_then(|variants| variants.horizontal_constructions.get(base_id))
+ .map(|_| Axis::X);
+
+ match (vertical, horizontal) {
+ (vertical, None) => vertical,
+ (None, horizontal) => horizontal,
+ _ => {
+ // As far as we know, there aren't any glyphs that have both
+ // vertical and horizontal constructions. So for the time being, we
+ // will assume that a glyph cannot have both.
+ panic!("glyph {:?} has both vertical and horizontal constructions", base.c);
+ }
+ }
+}
+
impl GlyphFragment {
/// Try to stretch a glyph to a desired height.
pub fn stretch_vertical(
@@ -15,7 +140,7 @@ impl GlyphFragment {
height: Abs,
short_fall: Abs,
) -> VariantFragment {
- stretch_glyph(ctx, self, height, short_fall, false)
+ stretch_glyph(ctx, self, height, short_fall, Axis::Y)
}
/// Try to stretch a glyph to a desired width.
@@ -25,7 +150,7 @@ impl GlyphFragment {
width: Abs,
short_fall: Abs,
) -> VariantFragment {
- stretch_glyph(ctx, self, width, short_fall, true)
+ stretch_glyph(ctx, self, width, short_fall, Axis::X)
}
}
@@ -37,10 +162,13 @@ fn stretch_glyph(
mut base: GlyphFragment,
target: Abs,
short_fall: Abs,
- horizontal: bool,
+ axis: Axis,
) -> VariantFragment {
// If the base glyph is good enough, use it.
- let advance = if horizontal { base.width } else { base.height() };
+ let advance = match axis {
+ Axis::X => base.width,
+ Axis::Y => base.height(),
+ };
let short_target = target - short_fall;
if short_target <= advance {
return base.into_variant();
@@ -52,10 +180,9 @@ fn stretch_glyph(
.variants
.and_then(|variants| {
min_overlap = variants.min_connector_overlap.scaled(ctx, base.font_size);
- if horizontal {
- variants.horizontal_constructions
- } else {
- variants.vertical_constructions
+ match axis {
+ Axis::X => variants.horizontal_constructions,
+ Axis::Y => variants.vertical_constructions,
}
.get(base.id)
})
@@ -80,7 +207,7 @@ fn stretch_glyph(
// Assemble from parts.
let assembly = construction.assembly.unwrap();
- assemble(ctx, base, assembly, min_overlap, target, horizontal)
+ assemble(ctx, base, assembly, min_overlap, target, axis)
}
/// Assemble a glyph from parts.
@@ -90,7 +217,7 @@ fn assemble(
assembly: GlyphAssembly,
min_overlap: Abs,
target: Abs,
- horizontal: bool,
+ axis: Axis,
) -> VariantFragment {
// Determine the number of times the extenders need to be repeated as well
// as a ratio specifying how much to spread the parts apart
@@ -153,15 +280,18 @@ fn assemble(
let size;
let baseline;
- if horizontal {
- let height = base.ascent + base.descent;
- size = Size::new(full, height);
- baseline = base.ascent;
- } else {
- let axis = ctx.constants.axis_height().scaled(ctx, base.font_size);
- let width = selected.iter().map(|(f, _)| f.width).max().unwrap_or_default();
- size = Size::new(width, full);
- baseline = full / 2.0 + axis;
+ match axis {
+ Axis::X => {
+ let height = base.ascent + base.descent;
+ size = Size::new(full, height);
+ baseline = base.ascent;
+ }
+ Axis::Y => {
+ let axis = ctx.constants.axis_height().scaled(ctx, base.font_size);
+ let width = selected.iter().map(|(f, _)| f.width).max().unwrap_or_default();
+ size = Size::new(width, full);
+ baseline = full / 2.0 + axis;
+ }
}
let mut frame = Frame::soft(size);
@@ -170,16 +300,18 @@ fn assemble(
frame.post_process_raw(base.dests, base.hidden);
for (fragment, advance) in selected {
- let pos = if horizontal {
- Point::new(offset, frame.baseline() - fragment.ascent)
- } else {
- Point::with_y(full - offset - fragment.height())
+ let pos = match axis {
+ Axis::X => Point::new(offset, frame.baseline() - fragment.ascent),
+ Axis::Y => Point::with_y(full - offset - fragment.height()),
};
frame.push_frame(pos, fragment.into_frame());
offset += advance;
}
- let accent_attach = if horizontal { frame.width() / 2.0 } else { base.accent_attach };
+ let accent_attach = match axis {
+ Axis::X => frame.width() / 2.0,
+ Axis::Y => base.accent_attach,
+ };
VariantFragment {
c: base.c,
diff --git a/docs/reference/groups.yml b/docs/reference/groups.yml
index 0be0fd5c..3f2bef23 100644
--- a/docs/reference/groups.yml
+++ b/docs/reference/groups.yml
@@ -83,6 +83,10 @@
automatically decides which is more suitable depending on the base, but you
can also control this manually with the `scripts` and `limits` functions.
+ If you want the base to stretch to fit long top and bottom attachments (for
+ example, an arrow with text above it), use the [`stretch`]($math.stretch)
+ function.
+
# Example
```example
$ sum_(i=0)^n a_i = 2^(1+i) $
diff --git a/tests/ref/math-stretch-attach-nested-equation.png b/tests/ref/math-stretch-attach-nested-equation.png
new file mode 100644
index 00000000..5ab4fbb5
--- /dev/null
+++ b/tests/ref/math-stretch-attach-nested-equation.png
Binary files differ
diff --git a/tests/ref/math-stretch-attach.png b/tests/ref/math-stretch-attach.png
new file mode 100644
index 00000000..179281a5
--- /dev/null
+++ b/tests/ref/math-stretch-attach.png
Binary files differ
diff --git a/tests/ref/math-stretch-basic.png b/tests/ref/math-stretch-basic.png
new file mode 100644
index 00000000..3144b11f
--- /dev/null
+++ b/tests/ref/math-stretch-basic.png
Binary files differ
diff --git a/tests/ref/math-stretch-complex.png b/tests/ref/math-stretch-complex.png
new file mode 100644
index 00000000..be694b71
--- /dev/null
+++ b/tests/ref/math-stretch-complex.png
Binary files differ
diff --git a/tests/ref/math-stretch-horizontal.png b/tests/ref/math-stretch-horizontal.png
new file mode 100644
index 00000000..9b30be59
--- /dev/null
+++ b/tests/ref/math-stretch-horizontal.png
Binary files differ
diff --git a/tests/ref/math-stretch-nested.png b/tests/ref/math-stretch-nested.png
new file mode 100644
index 00000000..4583cab9
--- /dev/null
+++ b/tests/ref/math-stretch-nested.png
Binary files differ
diff --git a/tests/ref/math-stretch-shorthand.png b/tests/ref/math-stretch-shorthand.png
new file mode 100644
index 00000000..59db832a
--- /dev/null
+++ b/tests/ref/math-stretch-shorthand.png
Binary files differ
diff --git a/tests/ref/math-stretch-vertical.png b/tests/ref/math-stretch-vertical.png
new file mode 100644
index 00000000..1f60506d
--- /dev/null
+++ b/tests/ref/math-stretch-vertical.png
Binary files differ
diff --git a/tests/suite/math/stretch.typ b/tests/suite/math/stretch.typ
new file mode 100644
index 00000000..e6817ee5
--- /dev/null
+++ b/tests/suite/math/stretch.typ
@@ -0,0 +1,72 @@
+// Test math stretch.
+
+--- math-stretch-basic ---
+// Test basic stretch.
+$ P -> Q stretch(->, size: #200%) R \
+ R stretch(->) S stretch(->, size: #50%)^"epimorphism" T $
+
+--- math-stretch-complex ---
+// Test complex stretch.
+$ H stretch(=)^"define" U + p V \
+ x stretch(harpoons.ltrb, size: #3em) y
+ stretch(\[, size: #150%) z \
+ f : X stretch(arrow.hook, size: #150%)_"injective" Y \
+ V stretch(->, size: #(100% + 1.5em))^("surjection") ZZ $
+
+--- math-stretch-attach ---
+// Test stretch interactions with attachments.
+#set page(width: auto)
+
+$stretch(stretch(=, size: #4em))_A$
+$stretch(arrow.hook, size: #5em)^"injective map"$
+$stretch(arrow.hook, size: #200%)^"injective map"$
+
+$ P = Q
+ stretch(=)^(k = 0)_(forall i) R
+ stretch(=, size: #150%)^(k = 0)_(forall i) S
+ stretch(=, size: #2mm)^(k = 0)_(forall i) T \
+ U stretch(equiv)^(forall i)_"Chern-Weil" V
+ stretch(equiv, size: #(120% + 2mm))^(forall i)_"Chern-Weil" W $
+
+--- math-stretch-horizontal ---
+// Test stretching along horizontal axis.
+#let ext(sym) = math.stretch(sym, size: 2em)
+$ ext(arrow.r) quad ext(arrow.l.double.bar) \
+ ext(harpoon.rb) quad ext(harpoons.ltrb) \
+ ext(paren.t) quad ext(shell.b) \
+ ext(eq) quad ext(equiv) $
+
+--- math-stretch-vertical ---
+// Test stretching along vertical axis.
+#let ext(sym) = math.stretch(sym, size: 2em)
+$ ext(bar.v) quad ext(bar.v.double) quad
+ ext(angle.l) quad ext(angle.r) quad
+ ext(paren.l) quad ext(paren.r) \
+ ext(bracket.l.double) quad ext(bracket.r.double) quad
+ ext(brace.l) quad ext(brace.r) quad
+ ext(bracket.l) quad ext(bracket.r) $
+
+--- math-stretch-shorthand ---
+// Test stretch when base is given with shorthand.
+$stretch(||, size: #2em)$
+$stretch(\(, size: #2em)$
+$stretch("⟧", size: #2em)$
+$stretch("|", size: #2em)$
+$stretch(->, size: #2em)$
+$stretch(↣, size: #2em)$
+
+--- math-stretch-nested ---
+// Test nested stretch calls.
+$ stretch(=, size: #2em) \
+ stretch(stretch(=, size: #4em), size: #50%) $
+
+#let base = math.stretch($=$, size: 4em)
+$ stretch(base, size: #50%) $
+
+#let base = $stretch(=, size: #4em) $
+$ stretch(base, size: #50%) $
+
+--- math-stretch-attach-nested-equation ---
+// Test stretching with attachments when nested in an equation.
+#let body = $stretch(=)$
+$ body^"text" $