From 28c554ec2185a15e22f0408ce485ed4afe035e03 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sat, 28 Jan 2023 12:01:05 +0100 Subject: Rework math attachments and accents --- library/src/math/accent.rs | 95 ++++------------- library/src/math/attach.rs | 251 +++++++++++++++++++++++++++++++++++++++++++++ library/src/math/mod.rs | 41 +++++--- library/src/math/op.rs | 8 +- library/src/math/script.rs | 184 --------------------------------- 5 files changed, 306 insertions(+), 273 deletions(-) create mode 100644 library/src/math/attach.rs delete mode 100644 library/src/math/script.rs (limited to 'library/src/math') diff --git a/library/src/math/accent.rs b/library/src/math/accent.rs index 6829554c..90e64b96 100644 --- a/library/src/math/accent.rs +++ b/library/src/math/accent.rs @@ -1,15 +1,17 @@ +use typst::model::combining_accent; + use super::*; /// How much the accent can be shorter than the base. const ACCENT_SHORT_FALL: Em = Em::new(0.5); /// # Accent -/// An accented node. +/// Attach an accent to a base. /// /// ## Example /// ``` -/// $accent(a, ->) != accent(a, ~)$ \ -/// $accent(a, `) = accent(a, grave)$ +/// $arrow(a) = accent(a, arrow)$ \ +/// $grave(a) = accent(a, `)$ /// ``` /// /// ## Parameters @@ -19,30 +21,26 @@ const ACCENT_SHORT_FALL: Em = Em::new(0.5); /// /// ### Example /// ``` -/// $accent(A B C, ->)$ +/// $arrow(A B C)$ /// ``` /// -/// - accent: Content (positional, required) +/// - accent: char (positional, required) /// The accent to apply to the base. /// /// Supported accents include: -/// - Plus: `` + `` -/// - Overline: `` - ``, `‾` -/// - Dot: `.` -/// - Circumflex: `^` -/// - Acute: `´` -/// - Low Line: `_` -/// - Grave: `` ` `` -/// - Tilde: `~` -/// - Diaeresis: `¨` -/// - Macron: `¯` -/// - Acute: `´` -/// - Cedilla: `¸` -/// - Caron: `ˇ` -/// - Breve: `˘` -/// - Double acute: `˝` -/// - Left arrow: `<-` -/// - Right arrow: `->` +/// - Grave: `grave`, `` ` `` +/// - Acute: `acute`, `´` +/// - Circumflex: `circum`, `^` +/// - Tilde: `tilde`, `~` +/// - Macron: `macron`, `¯` +/// - Breve: `breve`, `˘` +/// - Dot: `dot`, `.` +/// - Diaeresis: `diaer` `¨` +/// - Circle: `circle`, `∘` +/// - Double acute: `acute.double`, `˝` +/// - Caron: `caron`, `ˇ` +/// - Right arrow: `arrow`, `->` +/// - Left arrow: `arrow.l`, `<-` /// /// ## Category /// math @@ -53,7 +51,7 @@ pub struct AccentNode { /// The accent base. pub base: Content, /// The accent. - pub accent: Content, + pub accent: char, } #[node] @@ -78,16 +76,9 @@ impl LayoutMath for AccentNode { _ => (base.width() + base.italics_correction()) / 2.0, }; - let Some(c) = extract(&self.accent) else { - ctx.push(base); - if let Some(span) = self.accent.span() { - bail!(span, "not an accent"); - } - return Ok(()); - }; - // Forcing the accent to be at least as large as the base makes it too // wide in many case. + let c = combining_accent(self.accent).unwrap_or(self.accent); let glyph = GlyphFragment::new(ctx, c); let short_fall = ACCENT_SHORT_FALL.scaled(ctx); let variant = glyph.stretch_horizontal(ctx, base.width(), short_fall); @@ -130,45 +121,3 @@ fn attachment(ctx: &MathContext, id: GlyphId, italics_correction: Abs) -> Abs { (advance.scaled(ctx) + italics_correction) / 2.0 }) } - -/// Extract a single character from content. -fn extract(accent: &Content) -> Option { - let atom = accent.to::()?; - let mut chars = atom.0.chars(); - let c = chars.next().filter(|_| chars.next().is_none())?; - Some(combining(c)) -} - -/// Convert from a non-combining accent to a combining one. -/// -/// https://www.w3.org/TR/mathml-core/#combining-character-equivalences -fn combining(c: char) -> char { - match c { - '\u{002b}' => '\u{031f}', - '\u{002d}' => '\u{0305}', - '\u{002e}' => '\u{0307}', - '\u{005e}' => '\u{0302}', - '\u{005f}' => '\u{0332}', - '\u{0060}' => '\u{0300}', - '\u{007e}' => '\u{0303}', - '\u{00a8}' => '\u{0308}', - '\u{00af}' => '\u{0304}', - '\u{00b4}' => '\u{0301}', - '\u{00b8}' => '\u{0327}', - '\u{02c6}' => '\u{0302}', - '\u{02c7}' => '\u{030c}', - '\u{02d8}' => '\u{0306}', - '\u{02d9}' => '\u{0307}', - '\u{02db}' => '\u{0328}', - '\u{02dc}' => '\u{0303}', - '\u{02dd}' => '\u{030b}', - '\u{203e}' => '\u{0305}', - '\u{2190}' => '\u{20d6}', - '\u{2192}' => '\u{20d7}', - '\u{2212}' => '\u{0305}', - '\u{223C}' => '\u{0303}', - '\u{22C5}' => '\u{0307}', - '\u{27f6}' => '\u{20d7}', - _ => c, - } -} diff --git a/library/src/math/attach.rs b/library/src/math/attach.rs new file mode 100644 index 00000000..2205e556 --- /dev/null +++ b/library/src/math/attach.rs @@ -0,0 +1,251 @@ +use super::*; + +/// # Attachment +/// A base with optional attachments. +/// +/// ## Syntax +/// This function also has dedicated syntax: Use the underscore (`_`) to +/// indicate a bottom attachment and the circumflex (`^`) to indicate a top +/// attachment. +/// +/// ## Example +/// ``` +/// $ sum_(i=0)^n a_i = 2^(1+i) $ +/// ``` +/// +/// ## Parameters +/// - base: Content (positional, required) +/// The base to which things are attached. +/// +/// - top: Content (named) +/// The top attachment. +/// +/// - bottom: Content (named) +/// The bottom attachment. +/// +/// ## Category +/// math +#[func] +#[capable(LayoutMath)] +#[derive(Debug, Hash)] +pub struct AttachNode { + /// The base. + pub base: Content, + /// The top attachment. + pub top: Option, + /// The bottom attachment. + pub bottom: Option, +} + +#[node] +impl AttachNode { + fn construct(_: &Vm, args: &mut Args) -> SourceResult { + let base = args.expect("base")?; + let top = args.named("top")?; + let bottom = args.named("bottom")?; + Ok(Self { base, top, bottom }.pack()) + } +} + +impl LayoutMath for AttachNode { + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + let base = ctx.layout_fragment(&self.base)?; + + let mut sub = Frame::new(Size::zero()); + if let Some(node) = &self.top { + ctx.style(ctx.style.for_subscript()); + sub = ctx.layout_frame(node)?; + ctx.unstyle(); + } + + let mut sup = Frame::new(Size::zero()); + if let Some(node) = &self.bottom { + ctx.style(ctx.style.for_superscript()); + sup = ctx.layout_frame(node)?; + ctx.unstyle(); + } + + let render_limits = self.base.is::() + || (!self.base.is::() + && ctx.style.size == MathSize::Display + && base.class() == Some(MathClass::Large) + && match &base { + MathFragment::Variant(variant) => LIMITS.contains(&variant.c), + MathFragment::Frame(fragment) => fragment.limits, + _ => false, + }); + + if render_limits { + limits(ctx, base, sub, sup) + } else { + scripts(ctx, base, sub, sup, self.top.is_some() && self.bottom.is_some()) + } + } +} + +/// # Scripts +/// Force a base to display attachments as scripts. +/// +/// ## Example +/// ``` +/// $ scripts(sum)_1^2 != sum_1^2 $ +/// ``` +/// +/// ## Parameters +/// - base: Content (positional, required) +/// The base to attach the scripts to. +/// +/// ## Category +/// math +#[func] +#[capable(LayoutMath)] +#[derive(Debug, Hash)] +pub struct ScriptsNode(Content); + +#[node] +impl ScriptsNode { + fn construct(_: &Vm, args: &mut Args) -> SourceResult { + Ok(Self(args.expect("base")?).pack()) + } +} + +impl LayoutMath for ScriptsNode { + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + self.0.layout_math(ctx) + } +} + +/// # Limits +/// Force a base to display attachments as limits. +/// +/// ## Example +/// ``` +/// $ limits(A)_1^2 != A_1^2 $ +/// ``` +/// +/// ## Parameters +/// - base: Content (positional, required) +/// The base to attach the limits to. +/// +/// ## Category +/// math +#[func] +#[capable(LayoutMath)] +#[derive(Debug, Hash)] +pub struct LimitsNode(Content); + +#[node] +impl LimitsNode { + fn construct(_: &Vm, args: &mut Args) -> SourceResult { + Ok(Self(args.expect("base")?).pack()) + } +} + +impl LayoutMath for LimitsNode { + fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { + self.0.layout_math(ctx) + } +} + +/// Layout sub- and superscripts. +fn scripts( + ctx: &mut MathContext, + base: MathFragment, + sub: Frame, + sup: Frame, + both: bool, +) -> SourceResult<()> { + let sup_shift_up = if ctx.style.cramped { + scaled!(ctx, superscript_shift_up_cramped) + } else { + scaled!(ctx, superscript_shift_up) + }; + let sup_bottom_min = scaled!(ctx, superscript_bottom_min); + let sup_bottom_max_with_sub = scaled!(ctx, superscript_bottom_max_with_subscript); + let sup_drop_max = scaled!(ctx, superscript_baseline_drop_max); + let gap_min = scaled!(ctx, sub_superscript_gap_min); + let sub_shift_down = scaled!(ctx, subscript_shift_down); + let sub_top_max = scaled!(ctx, subscript_top_max); + let sub_drop_min = scaled!(ctx, subscript_baseline_drop_min); + let space_after = scaled!(ctx, space_after_script); + + let mut shift_up = sup_shift_up + .max(base.ascent() - sup_drop_max) + .max(sup_bottom_min + sup.descent()); + + let mut shift_down = sub_shift_down + .max(base.descent() + sub_drop_min) + .max(sub.ascent() - sub_top_max); + + if both { + let sup_bottom = shift_up - sup.descent(); + let sub_top = sub.ascent() - shift_down; + let gap = sup_bottom - sub_top; + if gap < gap_min { + let increase = gap_min - gap; + let sup_only = + (sup_bottom_max_with_sub - sup_bottom).clamp(Abs::zero(), increase); + let rest = (increase - sup_only) / 2.0; + shift_up += sup_only + rest; + shift_down += rest; + } + } + + let delta = base.italics_correction(); + let ascent = shift_up + sup.ascent(); + let descent = shift_down + sub.descent(); + let height = ascent + descent; + let width = base.width() + sup.width().max(sub.width() - delta) + space_after; + let base_pos = Point::with_y(ascent - base.ascent()); + let sup_pos = Point::with_x(base.width()); + let sub_pos = Point::new(base.width() - delta, height - sub.height()); + let class = base.class().unwrap_or(MathClass::Normal); + + let mut frame = Frame::new(Size::new(width, height)); + frame.set_baseline(ascent); + frame.push_frame(base_pos, base.to_frame(ctx)); + frame.push_frame(sub_pos, sub); + frame.push_frame(sup_pos, sup); + ctx.push(FrameFragment::new(frame).with_class(class)); + + Ok(()) +} + +/// Layout limits. +fn limits( + ctx: &mut MathContext, + base: MathFragment, + sub: Frame, + sup: Frame, +) -> SourceResult<()> { + let upper_gap_min = scaled!(ctx, upper_limit_gap_min); + let upper_rise_min = scaled!(ctx, upper_limit_baseline_rise_min); + let lower_gap_min = scaled!(ctx, lower_limit_gap_min); + let lower_drop_min = scaled!(ctx, lower_limit_baseline_drop_min); + + let sup_gap = upper_gap_min.max(upper_rise_min - sup.descent()); + let sub_gap = lower_gap_min.max(lower_drop_min - sub.ascent()); + + let delta = base.italics_correction() / 2.0; + let width = base.width().max(sup.width()).max(sub.width()); + let height = sup.height() + sup_gap + base.height() + sub_gap + sub.height(); + let base_pos = Point::new((width - base.width()) / 2.0, sup.height() + sup_gap); + let sup_pos = Point::with_x((width - sup.width()) / 2.0 + delta); + let sub_pos = Point::new((width - sub.width()) / 2.0 - delta, height - sub.height()); + let class = base.class().unwrap_or(MathClass::Normal); + + let mut frame = Frame::new(Size::new(width, height)); + frame.set_baseline(base_pos.y + base.ascent()); + frame.push_frame(base_pos, base.to_frame(ctx)); + frame.push_frame(sub_pos, sub); + frame.push_frame(sup_pos, sup); + ctx.push(FrameFragment::new(frame).with_class(class)); + + Ok(()) +} + +/// Codepoints that should have sub- and superscripts attached as limits. +const LIMITS: &[char] = &[ + '\u{2210}', '\u{22C1}', '\u{22C0}', '\u{2A04}', '\u{22C2}', '\u{22C3}', '\u{220F}', + '\u{2211}', '\u{2A02}', '\u{2A01}', '\u{2A00}', '\u{2A06}', +]; diff --git a/library/src/math/mod.rs b/library/src/math/mod.rs index 1d94661a..636affed 100644 --- a/library/src/math/mod.rs +++ b/library/src/math/mod.rs @@ -5,6 +5,7 @@ mod ctx; mod accent; mod align; mod atom; +mod attach; mod braced; mod frac; mod fragment; @@ -13,7 +14,6 @@ mod matrix; mod op; mod root; mod row; -mod script; mod spacing; mod stretch; mod style; @@ -22,13 +22,13 @@ mod symbols; pub use self::accent::*; pub use self::align::*; pub use self::atom::*; +pub use self::attach::*; pub use self::braced::*; pub use self::frac::*; pub use self::lr::*; pub use self::matrix::*; pub use self::op::*; pub use self::root::*; -pub use self::script::*; pub use self::style::*; use ttf_parser::GlyphId; @@ -54,22 +54,33 @@ use crate::text::{families, variant, FallbackList, FontFamily, SpaceNode}; pub fn module(sym: &Module) -> Module { let mut math = Scope::deduplicating(); math.def_func::("formula"); + + // Grouping. math.def_func::("lr"); - math.def_func::("op"); - math.def_func::("floor"); - math.def_func::("ceil"); math.def_func::("abs"); math.def_func::("norm"); + math.def_func::("floor"); + math.def_func::("ceil"); + + // Attachments and accents. + math.def_func::("attach"); + math.def_func::("scripts"); + math.def_func::("limits"); math.def_func::("accent"); + math.def_func::("underbrace"); + math.def_func::("overbrace"); + + // Fractions and matrix-likes. math.def_func::("frac"); math.def_func::("binom"); - math.def_func::("script"); - math.def_func::("sqrt"); - math.def_func::("root"); math.def_func::("vec"); math.def_func::("cases"); - math.def_func::("underbrace"); - math.def_func::("overbrace"); + + // Roots. + math.def_func::("sqrt"); + math.def_func::("root"); + + // Styles. math.def_func::("bold"); math.def_func::("italic"); math.def_func::("serif"); @@ -78,10 +89,16 @@ pub fn module(sym: &Module) -> Module { math.def_func::("frak"); math.def_func::("mono"); math.def_func::("bb"); - spacing::define(&mut math); - symbols::define(&mut math); + + // Text operators. + math.def_func::("op"); op::define(&mut math); + + // Symbols and spacing. + symbols::define(&mut math); + spacing::define(&mut math); math.copy_from(sym.scope()); + Module::new("math").with_scope(math) } diff --git a/library/src/math/op.rs b/library/src/math/op.rs index 22daee65..766d6381 100644 --- a/library/src/math/op.rs +++ b/library/src/math/op.rs @@ -9,9 +9,9 @@ use super::*; /// - text: EcoString (positional, required) /// The operator's text. /// - limits: bool (named) -/// Whether the operator should display sub- and superscripts as limits. +/// Whether the operator should force attachments to display as limits. /// -/// Defaults to `true`. +/// Defaults to `false`. /// /// ## Category /// math @@ -21,7 +21,7 @@ use super::*; pub struct OpNode { /// The operator's text. pub text: EcoString, - /// Whether the operator should display sub- and superscripts as limits. + /// Whether the operator should force attachments to display as limits. pub limits: bool, } @@ -30,7 +30,7 @@ impl OpNode { fn construct(_: &Vm, args: &mut Args) -> SourceResult { Ok(Self { text: args.expect("text")?, - limits: args.named("limits")?.unwrap_or(true), + limits: args.named("limits")?.unwrap_or(false), } .pack()) } diff --git a/library/src/math/script.rs b/library/src/math/script.rs deleted file mode 100644 index 2c765fbf..00000000 --- a/library/src/math/script.rs +++ /dev/null @@ -1,184 +0,0 @@ -use super::*; - -/// # Script -/// A mathematical sub- and/or superscript. -/// -/// ## Syntax -/// This function also has dedicated syntax: Use the underscore (`_`) to -/// indicate a subscript and the circumflex (`^`) to indicate a superscript. -/// -/// ## Example -/// ``` -/// $ a_i = 2^(1+i) $ -/// ``` -/// -/// ## Parameters -/// - base: Content (positional, required) -/// The base to which the applies the sub- and/or superscript. -/// -/// - sub: Content (named) -/// The subscript. -/// -/// - sup: Content (named) -/// The superscript. -/// -/// ## Category -/// math -#[func] -#[capable(LayoutMath)] -#[derive(Debug, Hash)] -pub struct ScriptNode { - /// The base. - pub base: Content, - /// The subscript. - pub sub: Option, - /// The superscript. - pub sup: Option, -} - -#[node] -impl ScriptNode { - fn construct(_: &Vm, args: &mut Args) -> SourceResult { - let base = args.expect("base")?; - let sub = args.named("sub")?; - let sup = args.named("sup")?; - Ok(Self { base, sub, sup }.pack()) - } -} - -impl LayoutMath for ScriptNode { - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - let base = ctx.layout_fragment(&self.base)?; - - let mut sub = Frame::new(Size::zero()); - if let Some(node) = &self.sub { - ctx.style(ctx.style.for_subscript()); - sub = ctx.layout_frame(node)?; - ctx.unstyle(); - } - - let mut sup = Frame::new(Size::zero()); - if let Some(node) = &self.sup { - ctx.style(ctx.style.for_superscript()); - sup = ctx.layout_frame(node)?; - ctx.unstyle(); - } - - let render_limits = ctx.style.size == MathSize::Display - && base.class() == Some(MathClass::Large) - && match &base { - MathFragment::Variant(variant) => LIMITS.contains(&variant.c), - MathFragment::Frame(fragment) => fragment.limits, - _ => false, - }; - - if render_limits { - limits(ctx, base, sub, sup) - } else { - scripts(ctx, base, sub, sup, self.sub.is_some() && self.sup.is_some()) - } - } -} - -/// Layout normal sub- and superscripts. -fn scripts( - ctx: &mut MathContext, - base: MathFragment, - sub: Frame, - sup: Frame, - both: bool, -) -> SourceResult<()> { - let sup_shift_up = if ctx.style.cramped { - scaled!(ctx, superscript_shift_up_cramped) - } else { - scaled!(ctx, superscript_shift_up) - }; - let sup_bottom_min = scaled!(ctx, superscript_bottom_min); - let sup_bottom_max_with_sub = scaled!(ctx, superscript_bottom_max_with_subscript); - let sup_drop_max = scaled!(ctx, superscript_baseline_drop_max); - let gap_min = scaled!(ctx, sub_superscript_gap_min); - let sub_shift_down = scaled!(ctx, subscript_shift_down); - let sub_top_max = scaled!(ctx, subscript_top_max); - let sub_drop_min = scaled!(ctx, subscript_baseline_drop_min); - let space_after = scaled!(ctx, space_after_script); - - let mut shift_up = sup_shift_up - .max(base.ascent() - sup_drop_max) - .max(sup_bottom_min + sup.descent()); - - let mut shift_down = sub_shift_down - .max(base.descent() + sub_drop_min) - .max(sub.ascent() - sub_top_max); - - if both { - let sup_bottom = shift_up - sup.descent(); - let sub_top = sub.ascent() - shift_down; - let gap = sup_bottom - sub_top; - if gap < gap_min { - let increase = gap_min - gap; - let sup_only = - (sup_bottom_max_with_sub - sup_bottom).clamp(Abs::zero(), increase); - let rest = (increase - sup_only) / 2.0; - shift_up += sup_only + rest; - shift_down += rest; - } - } - - let delta = base.italics_correction(); - let ascent = shift_up + sup.ascent(); - let descent = shift_down + sub.descent(); - let height = ascent + descent; - let width = base.width() + sup.width().max(sub.width() - delta) + space_after; - let base_pos = Point::with_y(ascent - base.ascent()); - let sup_pos = Point::with_x(base.width()); - let sub_pos = Point::new(base.width() - delta, height - sub.height()); - let class = base.class().unwrap_or(MathClass::Normal); - - let mut frame = Frame::new(Size::new(width, height)); - frame.set_baseline(ascent); - frame.push_frame(base_pos, base.to_frame(ctx)); - frame.push_frame(sub_pos, sub); - frame.push_frame(sup_pos, sup); - ctx.push(FrameFragment::new(frame).with_class(class)); - - Ok(()) -} - -/// Layout limits. -fn limits( - ctx: &mut MathContext, - base: MathFragment, - sub: Frame, - sup: Frame, -) -> SourceResult<()> { - let upper_gap_min = scaled!(ctx, upper_limit_gap_min); - let upper_rise_min = scaled!(ctx, upper_limit_baseline_rise_min); - let lower_gap_min = scaled!(ctx, lower_limit_gap_min); - let lower_drop_min = scaled!(ctx, lower_limit_baseline_drop_min); - - let sup_gap = upper_gap_min.max(upper_rise_min - sup.descent()); - let sub_gap = lower_gap_min.max(lower_drop_min - sub.ascent()); - - let delta = base.italics_correction() / 2.0; - let width = base.width().max(sup.width()).max(sub.width()); - let height = sup.height() + sup_gap + base.height() + sub_gap + sub.height(); - let base_pos = Point::new((width - base.width()) / 2.0, sup.height() + sup_gap); - let sup_pos = Point::with_x((width - sup.width()) / 2.0 + delta); - let sub_pos = Point::new((width - sub.width()) / 2.0 - delta, height - sub.height()); - let class = base.class().unwrap_or(MathClass::Normal); - - let mut frame = Frame::new(Size::new(width, height)); - frame.set_baseline(base_pos.y + base.ascent()); - frame.push_frame(base_pos, base.to_frame(ctx)); - frame.push_frame(sub_pos, sub); - frame.push_frame(sup_pos, sup); - ctx.push(FrameFragment::new(frame).with_class(class)); - - Ok(()) -} - -/// Codepoints that should have sub- and superscripts attached as limits. -const LIMITS: &[char] = &[ - '\u{2210}', '\u{22C1}', '\u{22C0}', '\u{2A04}', '\u{22C2}', '\u{22C3}', '\u{220F}', - '\u{2211}', '\u{2A02}', '\u{2A01}', '\u{2A00}', '\u{2A06}', -]; -- cgit v1.2.3