summaryrefslogtreecommitdiff
path: root/crates/typst-layout/src/math
diff options
context:
space:
mode:
Diffstat (limited to 'crates/typst-layout/src/math')
-rw-r--r--crates/typst-layout/src/math/mod.rs6
-rw-r--r--crates/typst-layout/src/math/text.rs240
2 files changed, 142 insertions, 104 deletions
diff --git a/crates/typst-layout/src/math/mod.rs b/crates/typst-layout/src/math/mod.rs
index 905e159a..702816ee 100644
--- a/crates/typst-layout/src/math/mod.rs
+++ b/crates/typst-layout/src/math/mod.rs
@@ -538,11 +538,7 @@ fn layout_realized(
} else if let Some(elem) = elem.to_packed::<TextElem>() {
self::text::layout_text(elem, ctx, styles)?;
} else if let Some(elem) = elem.to_packed::<SymbolElem>() {
- // This is a hack to avoid affecting layout that will be replaced in a
- // later commit.
- let text_elem = TextElem::new(elem.text.to_string().into());
- let packed = Packed::new(text_elem);
- self::text::layout_text(&packed, ctx, styles)?;
+ self::text::layout_symbol(elem, ctx, styles)?;
} else if let Some(elem) = elem.to_packed::<BoxElem>() {
layout_box(elem, ctx, styles)?;
} else if elem.is::<AlignPointElem>() {
diff --git a/crates/typst-layout/src/math/text.rs b/crates/typst-layout/src/math/text.rs
index 7e849c46..6b9703aa 100644
--- a/crates/typst-layout/src/math/text.rs
+++ b/crates/typst-layout/src/math/text.rs
@@ -2,7 +2,7 @@ use std::f64::consts::SQRT_2;
use ecow::{eco_vec, EcoString};
use typst_library::diag::SourceResult;
-use typst_library::foundations::{Packed, StyleChain, StyleVec};
+use typst_library::foundations::{Packed, StyleChain, StyleVec, SymbolElem};
use typst_library::layout::{Abs, Size};
use typst_library::math::{EquationElem, MathSize, MathVariant};
use typst_library::text::{
@@ -22,54 +22,66 @@ pub fn layout_text(
) -> SourceResult<()> {
let text = &elem.text;
let span = elem.span();
- let mut chars = text.chars();
- let math_size = EquationElem::size_in(styles);
- let mut dtls = ctx.dtls_table.is_some();
- let fragment: MathFragment = if let Some(mut glyph) = chars
- .next()
- .filter(|_| chars.next().is_none())
- .map(|c| dtls_char(c, &mut dtls))
- .map(|c| styled_char(styles, c, true))
- .and_then(|c| GlyphFragment::try_new(ctx, styles, c, span))
- {
- // A single letter that is available in the math font.
- if dtls {
- glyph.make_dotless_form(ctx);
- }
+ let fragment = if text.contains(is_newline) {
+ layout_text_lines(text.split(is_newline), span, ctx, styles)?
+ } else {
+ layout_inline_text(text, span, ctx, styles)?
+ };
+ ctx.push(fragment);
+ Ok(())
+}
- match math_size {
- MathSize::Script => {
- glyph.make_script_size(ctx);
- }
- MathSize::ScriptScript => {
- glyph.make_script_script_size(ctx);
- }
- _ => (),
+/// Layout multiple lines of text.
+fn layout_text_lines<'a>(
+ lines: impl Iterator<Item = &'a str>,
+ span: Span,
+ ctx: &mut MathContext,
+ styles: StyleChain,
+) -> SourceResult<FrameFragment> {
+ let mut fragments = vec![];
+ for (i, line) in lines.enumerate() {
+ if i != 0 {
+ fragments.push(MathFragment::Linebreak);
}
-
- if glyph.class == MathClass::Large {
- let mut variant = if math_size == MathSize::Display {
- let height = scaled!(ctx, styles, display_operator_min_height)
- .max(SQRT_2 * glyph.height());
- glyph.stretch_vertical(ctx, height, Abs::zero())
- } else {
- glyph.into_variant()
- };
- // TeXbook p 155. Large operators are always vertically centered on the axis.
- variant.center_on_axis(ctx);
- variant.into()
- } else {
- glyph.into()
+ if !line.is_empty() {
+ fragments.push(layout_inline_text(line, span, ctx, styles)?.into());
}
- } else if text.chars().all(|c| c.is_ascii_digit() || c == '.') {
- // Numbers aren't that difficult.
+ }
+ let mut frame = MathRun::new(fragments).into_frame(styles);
+ let axis = scaled!(ctx, styles, axis_height);
+ frame.set_baseline(frame.height() / 2.0 + axis);
+ Ok(FrameFragment::new(styles, frame))
+}
+
+/// Layout the given text string into a [`FrameFragment`] after styling all
+/// characters for the math font (without auto-italics).
+fn layout_inline_text(
+ text: &str,
+ span: Span,
+ ctx: &mut MathContext,
+ styles: StyleChain,
+) -> SourceResult<FrameFragment> {
+ if text.chars().all(|c| c.is_ascii_digit() || c == '.') {
+ // Small optimization for numbers. Note that this lays out slightly
+ // differently to normal text and is worth re-evaluating in the future.
let mut fragments = vec![];
- for c in text.chars() {
- let c = styled_char(styles, c, false);
- fragments.push(GlyphFragment::new(ctx, styles, c, span).into());
+ let is_single = text.chars().count() == 1;
+ for unstyled_c in text.chars() {
+ let c = styled_char(styles, unstyled_c, false);
+ let mut glyph = GlyphFragment::new(ctx, styles, c, span);
+ if is_single {
+ // Duplicate what `layout_glyph` does exactly even if it's
+ // probably incorrect here.
+ match EquationElem::size_in(styles) {
+ MathSize::Script => glyph.make_script_size(ctx),
+ MathSize::ScriptScript => glyph.make_script_script_size(ctx),
+ _ => {}
+ }
+ }
+ fragments.push(glyph.into());
}
let frame = MathRun::new(fragments).into_frame(styles);
- FrameFragment::new(styles, frame).with_text_like(true).into()
+ Ok(FrameFragment::new(styles, frame).with_text_like(true))
} else {
let local = [
TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds)),
@@ -77,64 +89,97 @@ pub fn layout_text(
]
.map(|p| p.wrap());
- // Anything else is handled by Typst's standard text layout.
let styles = styles.chain(&local);
- let text: EcoString =
+ let styled_text: EcoString =
text.chars().map(|c| styled_char(styles, c, false)).collect();
- if text.contains(is_newline) {
- let mut fragments = vec![];
- for (i, piece) in text.split(is_newline).enumerate() {
- if i != 0 {
- fragments.push(MathFragment::Linebreak);
- }
- if !piece.is_empty() {
- fragments.push(layout_complex_text(piece, ctx, span, styles)?.into());
- }
- }
- let mut frame = MathRun::new(fragments).into_frame(styles);
- let axis = scaled!(ctx, styles, axis_height);
- frame.set_baseline(frame.height() / 2.0 + axis);
- FrameFragment::new(styles, frame).into()
- } else {
- layout_complex_text(&text, ctx, span, styles)?.into()
+
+ let spaced = styled_text.graphemes(true).nth(1).is_some();
+ let elem = TextElem::packed(styled_text).spanned(span);
+
+ // There isn't a natural width for a paragraph in a math environment;
+ // because it will be placed somewhere probably not at the left margin
+ // it will overflow. So emulate an `hbox` instead and allow the
+ // paragraph to extend as far as needed.
+ let frame = (ctx.engine.routines.layout_inline)(
+ ctx.engine,
+ &StyleVec::wrap(eco_vec![elem]),
+ ctx.locator.next(&span),
+ styles,
+ false,
+ Size::splat(Abs::inf()),
+ false,
+ )?
+ .into_frame();
+
+ Ok(FrameFragment::new(styles, frame)
+ .with_class(MathClass::Alphabetic)
+ .with_text_like(true)
+ .with_spaced(spaced))
+ }
+}
+
+/// Layout a single character in the math font with the correct styling applied
+/// (includes auto-italics).
+pub fn layout_symbol(
+ elem: &Packed<SymbolElem>,
+ ctx: &mut MathContext,
+ styles: StyleChain,
+) -> SourceResult<()> {
+ // Switch dotless char to normal when we have the dtls OpenType feature.
+ // This should happen before the main styling pass.
+ let (unstyled_c, dtls) = match try_dotless(elem.text) {
+ Some(c) if ctx.dtls_table.is_some() => (c, true),
+ _ => (elem.text, false),
+ };
+ let c = styled_char(styles, unstyled_c, true);
+ let fragment = match GlyphFragment::try_new(ctx, styles, c, elem.span()) {
+ Some(glyph) => layout_glyph(glyph, dtls, ctx, styles),
+ None => {
+ // Not in the math font, fallback to normal inline text layout.
+ layout_inline_text(c.encode_utf8(&mut [0; 4]), elem.span(), ctx, styles)?
+ .into()
}
};
-
ctx.push(fragment);
Ok(())
}
-/// Layout the given text string into a [`FrameFragment`].
-fn layout_complex_text(
- text: &str,
+/// Layout a [`GlyphFragment`].
+fn layout_glyph(
+ mut glyph: GlyphFragment,
+ dtls: bool,
ctx: &mut MathContext,
- span: Span,
styles: StyleChain,
-) -> SourceResult<FrameFragment> {
- // There isn't a natural width for a paragraph in a math environment;
- // because it will be placed somewhere probably not at the left margin
- // it will overflow. So emulate an `hbox` instead and allow the paragraph
- // to extend as far as needed.
- let spaced = text.graphemes(true).nth(1).is_some();
- let elem = TextElem::packed(text).spanned(span);
- let frame = (ctx.engine.routines.layout_inline)(
- ctx.engine,
- &StyleVec::wrap(eco_vec![elem]),
- ctx.locator.next(&span),
- styles,
- false,
- Size::splat(Abs::inf()),
- false,
- )?
- .into_frame();
-
- Ok(FrameFragment::new(styles, frame)
- .with_class(MathClass::Alphabetic)
- .with_text_like(true)
- .with_spaced(spaced))
+) -> MathFragment {
+ if dtls {
+ glyph.make_dotless_form(ctx);
+ }
+ let math_size = EquationElem::size_in(styles);
+ match math_size {
+ MathSize::Script => glyph.make_script_size(ctx),
+ MathSize::ScriptScript => glyph.make_script_script_size(ctx),
+ _ => {}
+ }
+
+ if glyph.class == MathClass::Large {
+ let mut variant = if math_size == MathSize::Display {
+ let height = scaled!(ctx, styles, display_operator_min_height)
+ .max(SQRT_2 * glyph.height());
+ glyph.stretch_vertical(ctx, height, Abs::zero())
+ } else {
+ glyph.into_variant()
+ };
+ // TeXbook p 155. Large operators are always vertically centered on the
+ // axis.
+ variant.center_on_axis(ctx);
+ variant.into()
+ } else {
+ glyph.into()
+ }
}
-/// Select the correct styled math letter.
+/// Style the character by selecting the unicode codepoint for italic, bold,
+/// caligraphic, etc.
///
/// <https://www.w3.org/TR/mathml-core/#new-text-transform-mappings>
/// <https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols>
@@ -353,15 +398,12 @@ fn greek_exception(
})
}
-/// Switch dotless character to non dotless character for use of the dtls
-/// OpenType feature.
-pub fn dtls_char(c: char, dtls: &mut bool) -> char {
- match (c, *dtls) {
- ('ı', true) => 'i',
- ('ȷ', true) => 'j',
- _ => {
- *dtls = false;
- c
- }
+/// The non-dotless version of a dotless character that can be used with the
+/// `dtls` OpenType feature.
+pub fn try_dotless(c: char) -> Option<char> {
+ match c {
+ 'ı' => Some('i'),
+ 'ȷ' => Some('j'),
+ _ => None,
}
}