Fix math spacing bugs
@ -31,7 +31,7 @@ impl LayoutMath for AtomNode {
|
||||
{
|
||||
// A single letter that is available in the math font.
|
||||
if ctx.style.size == MathSize::Display
|
||||
&& glyph.class() == Some(MathClass::Large)
|
||||
&& glyph.class == Some(MathClass::Large)
|
||||
{
|
||||
let height = scaled!(ctx, display_operator_min_height);
|
||||
ctx.push(glyph.stretch_vertical(ctx, height, Abs::zero()));
|
||||
@ -49,7 +49,8 @@ impl LayoutMath for AtomNode {
|
||||
ctx.push(frame);
|
||||
} else {
|
||||
// Anything else is handled by Typst's standard text layout.
|
||||
TextNode(self.0.clone()).pack().layout_math(ctx)?;
|
||||
let frame = ctx.layout_non_math(&TextNode(self.0.clone()).pack())?;
|
||||
ctx.push(FrameFragment::new(frame).with_class(MathClass::Alphabetic));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -30,6 +30,7 @@ pub(super) struct MathContext<'a, 'b, 'v> {
|
||||
pub ttf: &'a ttf_parser::Face<'a>,
|
||||
pub table: ttf_parser::math::Table<'a>,
|
||||
pub constants: ttf_parser::math::Constants<'a>,
|
||||
pub space_width: Em,
|
||||
pub fill: Paint,
|
||||
pub lang: Lang,
|
||||
pub row: MathRow,
|
||||
@ -50,6 +51,14 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
|
||||
let table = font.ttf().tables().math.unwrap();
|
||||
let constants = table.constants.unwrap();
|
||||
let size = styles.get(TextNode::SIZE);
|
||||
|
||||
let ttf = font.ttf();
|
||||
let space_width = ttf
|
||||
.glyph_index(' ')
|
||||
.and_then(|id| ttf.glyph_hor_advance(id))
|
||||
.map(|advance| font.to_em(advance))
|
||||
.unwrap_or(THICK);
|
||||
|
||||
Self {
|
||||
vt,
|
||||
outer: styles,
|
||||
@ -71,6 +80,7 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
|
||||
ttf: font.ttf(),
|
||||
table,
|
||||
constants,
|
||||
space_width,
|
||||
row: MathRow::new(),
|
||||
base_size: size,
|
||||
scaled_size: size,
|
||||
@ -79,7 +89,16 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
|
||||
}
|
||||
|
||||
pub fn push(&mut self, fragment: impl Into<MathFragment>) {
|
||||
self.row.push(self.scaled_size, self.style, fragment);
|
||||
self.row
|
||||
.push(self.scaled_size, self.space_width, self.style, fragment);
|
||||
}
|
||||
|
||||
pub fn extend(&mut self, row: MathRow) {
|
||||
let mut iter = row.0.into_iter();
|
||||
if let Some(first) = iter.next() {
|
||||
self.push(first);
|
||||
}
|
||||
self.row.0.extend(iter);
|
||||
}
|
||||
|
||||
pub fn layout_non_math(&mut self, content: &Content) -> SourceResult<Frame> {
|
||||
|
@ -6,8 +6,9 @@ pub(super) enum MathFragment {
|
||||
Variant(VariantFragment),
|
||||
Frame(FrameFragment),
|
||||
Spacing(Abs),
|
||||
Align,
|
||||
Space,
|
||||
Linebreak,
|
||||
Align,
|
||||
}
|
||||
|
||||
impl MathFragment {
|
||||
@ -54,13 +55,26 @@ impl MathFragment {
|
||||
|
||||
pub fn class(&self) -> Option<MathClass> {
|
||||
match self {
|
||||
Self::Glyph(glyph) => glyph.class(),
|
||||
Self::Variant(variant) => variant.class(),
|
||||
Self::Glyph(glyph) => glyph.class,
|
||||
Self::Variant(variant) => variant.class,
|
||||
Self::Frame(fragment) => Some(fragment.class),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_class(&mut self, class: MathClass) {
|
||||
match self {
|
||||
Self::Glyph(glyph) => glyph.class = Some(class),
|
||||
Self::Variant(variant) => variant.class = Some(class),
|
||||
Self::Frame(fragment) => fragment.class = class,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn participating(&self) -> bool {
|
||||
!matches!(self, Self::Space | Self::Spacing(_) | Self::Align)
|
||||
}
|
||||
|
||||
pub fn italics_correction(&self) -> Abs {
|
||||
match self {
|
||||
Self::Glyph(glyph) => glyph.italics_correction,
|
||||
@ -99,7 +113,7 @@ impl From<FrameFragment> for MathFragment {
|
||||
|
||||
impl From<Frame> for MathFragment {
|
||||
fn from(frame: Frame) -> Self {
|
||||
Self::Frame(FrameFragment { frame, class: MathClass::Normal, limits: false })
|
||||
Self::Frame(FrameFragment::new(frame))
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,6 +126,7 @@ pub(super) struct GlyphFragment {
|
||||
pub ascent: Abs,
|
||||
pub descent: Abs,
|
||||
pub italics_correction: Abs,
|
||||
pub class: Option<MathClass>,
|
||||
}
|
||||
|
||||
impl GlyphFragment {
|
||||
@ -144,6 +159,10 @@ impl GlyphFragment {
|
||||
ascent: bbox.y_max.scaled(ctx),
|
||||
descent: -bbox.y_min.scaled(ctx),
|
||||
italics_correction: italics,
|
||||
class: match c {
|
||||
':' => Some(MathClass::Relation),
|
||||
_ => unicode_math_class::class(c),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -151,16 +170,13 @@ impl GlyphFragment {
|
||||
self.ascent + self.descent
|
||||
}
|
||||
|
||||
pub fn class(&self) -> Option<MathClass> {
|
||||
unicode_math_class::class(self.c)
|
||||
}
|
||||
|
||||
pub fn to_variant(&self, ctx: &MathContext) -> VariantFragment {
|
||||
VariantFragment {
|
||||
c: self.c,
|
||||
id: Some(self.id),
|
||||
frame: self.to_frame(ctx),
|
||||
italics_correction: self.italics_correction,
|
||||
class: self.class,
|
||||
}
|
||||
}
|
||||
|
||||
@ -191,12 +207,7 @@ pub struct VariantFragment {
|
||||
pub id: Option<GlyphId>,
|
||||
pub frame: Frame,
|
||||
pub italics_correction: Abs,
|
||||
}
|
||||
|
||||
impl VariantFragment {
|
||||
pub fn class(&self) -> Option<MathClass> {
|
||||
unicode_math_class::class(self.c)
|
||||
}
|
||||
pub class: Option<MathClass>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -204,6 +215,30 @@ pub struct FrameFragment {
|
||||
pub frame: Frame,
|
||||
pub class: MathClass,
|
||||
pub limits: bool,
|
||||
pub spaced: bool,
|
||||
}
|
||||
|
||||
impl FrameFragment {
|
||||
pub fn new(frame: Frame) -> Self {
|
||||
Self {
|
||||
frame,
|
||||
class: MathClass::Normal,
|
||||
limits: false,
|
||||
spaced: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_class(self, class: MathClass) -> Self {
|
||||
Self { class, ..self }
|
||||
}
|
||||
|
||||
pub fn with_limits(self, limits: bool) -> Self {
|
||||
Self { limits, ..self }
|
||||
}
|
||||
|
||||
pub fn with_spaced(self, spaced: bool) -> Self {
|
||||
Self { spaced, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
/// Look up the italics correction for a glyph.
|
||||
|
@ -69,9 +69,7 @@ impl LayoutMath for LrNode {
|
||||
}
|
||||
}
|
||||
|
||||
for fragment in row.0 {
|
||||
ctx.push(fragment);
|
||||
}
|
||||
ctx.extend(row);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ use self::row::*;
|
||||
use self::spacing::*;
|
||||
use crate::layout::HNode;
|
||||
use crate::layout::ParNode;
|
||||
use crate::layout::Spacing;
|
||||
use crate::prelude::*;
|
||||
use crate::text::LinebreakNode;
|
||||
use crate::text::TextNode;
|
||||
@ -222,6 +223,7 @@ impl LayoutMath for FormulaNode {
|
||||
impl LayoutMath for Content {
|
||||
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
||||
if self.is::<SpaceNode>() {
|
||||
ctx.push(MathFragment::Space);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@ -230,6 +232,17 @@ impl LayoutMath for Content {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(node) = self.to::<HNode>() {
|
||||
if let Spacing::Relative(rel) = node.amount {
|
||||
if rel.rel.is_zero() {
|
||||
ctx.push(MathFragment::Spacing(
|
||||
rel.abs.resolve(ctx.outer.chain(&ctx.map)),
|
||||
));
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(node) = self.to::<SequenceNode>() {
|
||||
for child in &node.0 {
|
||||
child.layout_math(ctx)?;
|
||||
@ -242,7 +255,7 @@ impl LayoutMath for Content {
|
||||
}
|
||||
|
||||
let frame = ctx.layout_non_math(self)?;
|
||||
ctx.push(frame);
|
||||
ctx.push(FrameFragment::new(frame).with_spaced(true));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -39,11 +39,11 @@ impl OpNode {
|
||||
impl LayoutMath for OpNode {
|
||||
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
||||
let frame = ctx.layout_non_math(&TextNode(self.text.clone()).pack())?;
|
||||
ctx.push(FrameFragment {
|
||||
frame,
|
||||
class: MathClass::Large,
|
||||
limits: self.limits,
|
||||
});
|
||||
ctx.push(
|
||||
FrameFragment::new(frame)
|
||||
.with_class(MathClass::Large)
|
||||
.with_limits(self.limits),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -15,36 +15,60 @@ impl MathRow {
|
||||
pub fn push(
|
||||
&mut self,
|
||||
font_size: Abs,
|
||||
space_width: Em,
|
||||
style: MathStyle,
|
||||
fragment: impl Into<MathFragment>,
|
||||
) {
|
||||
let fragment = fragment.into();
|
||||
if let Some(fragment_class) = fragment.class() {
|
||||
for (i, prev) in self.0.iter().enumerate().rev() {
|
||||
if matches!(prev, MathFragment::Align) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut amount = Abs::zero();
|
||||
if let MathFragment::Glyph(glyph) = *prev {
|
||||
if !glyph.italics_correction.is_zero()
|
||||
&& fragment_class != MathClass::Alphabetic
|
||||
{
|
||||
amount += glyph.italics_correction;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(prev_class) = prev.class() {
|
||||
amount += spacing(prev_class, fragment_class, style).at(font_size);
|
||||
}
|
||||
|
||||
if !amount.is_zero() {
|
||||
self.0.insert(i + 1, MathFragment::Spacing(amount));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
let mut fragment = fragment.into();
|
||||
if !fragment.participating() {
|
||||
self.0.push(fragment);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut space = false;
|
||||
for (i, prev) in self.0.iter().enumerate().rev() {
|
||||
if !prev.participating() {
|
||||
space |= matches!(prev, MathFragment::Space);
|
||||
if matches!(prev, MathFragment::Spacing(_)) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if fragment.class() == Some(MathClass::Vary) {
|
||||
if matches!(
|
||||
prev.class(),
|
||||
Some(
|
||||
MathClass::Normal
|
||||
| MathClass::Alphabetic
|
||||
| MathClass::Binary
|
||||
| MathClass::Closing
|
||||
| MathClass::Fence
|
||||
| MathClass::Relation
|
||||
)
|
||||
) {
|
||||
fragment.set_class(MathClass::Binary);
|
||||
}
|
||||
}
|
||||
|
||||
let mut amount = Abs::zero();
|
||||
if let MathFragment::Glyph(glyph) = *prev {
|
||||
if !glyph.italics_correction.is_zero()
|
||||
&& fragment.class() != Some(MathClass::Alphabetic)
|
||||
{
|
||||
amount += glyph.italics_correction;
|
||||
}
|
||||
}
|
||||
|
||||
amount += spacing(prev, &fragment, style, space, space_width).at(font_size);
|
||||
|
||||
if !amount.is_zero() {
|
||||
self.0.insert(i + 1, MathFragment::Spacing(amount));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
self.0.push(fragment);
|
||||
}
|
||||
|
||||
|
@ -139,7 +139,7 @@ fn scripts(
|
||||
frame.push_frame(base_pos, base.to_frame(ctx));
|
||||
frame.push_frame(sub_pos, sub);
|
||||
frame.push_frame(sup_pos, sup);
|
||||
ctx.push(FrameFragment { frame, class, limits: false });
|
||||
ctx.push(FrameFragment::new(frame).with_class(class));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -172,7 +172,7 @@ fn limits(
|
||||
frame.push_frame(base_pos, base.to_frame(ctx));
|
||||
frame.push_frame(sub_pos, sub);
|
||||
frame.push_frame(sup_pos, sup);
|
||||
ctx.push(FrameFragment { frame, class, limits: false });
|
||||
ctx.push(FrameFragment::new(frame).with_class(class));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
use super::*;
|
||||
|
||||
const ZERO: Em = Em::zero();
|
||||
const THIN: Em = Em::new(1.0 / 6.0);
|
||||
const MEDIUM: Em = Em::new(2.0 / 9.0);
|
||||
const THICK: Em = Em::new(5.0 / 18.0);
|
||||
const QUAD: Em = Em::new(1.0);
|
||||
pub(super) const ZERO: Em = Em::zero();
|
||||
pub(super) const THIN: Em = Em::new(1.0 / 6.0);
|
||||
pub(super) const MEDIUM: Em = Em::new(2.0 / 9.0);
|
||||
pub(super) const THICK: Em = Em::new(5.0 / 18.0);
|
||||
pub(super) const QUAD: Em = Em::new(1.0);
|
||||
|
||||
/// Hook up all spacings.
|
||||
pub(super) fn define(math: &mut Scope) {
|
||||
@ -15,10 +15,20 @@ pub(super) fn define(math: &mut Scope) {
|
||||
}
|
||||
|
||||
/// Determine the spacing between two fragments in a given style.
|
||||
pub(super) fn spacing(left: MathClass, right: MathClass, style: MathStyle) -> Em {
|
||||
pub(super) fn spacing(
|
||||
left: &MathFragment,
|
||||
right: &MathFragment,
|
||||
style: MathStyle,
|
||||
space: bool,
|
||||
space_width: Em,
|
||||
) -> Em {
|
||||
use MathClass::*;
|
||||
let script = style.size <= MathSize::Script;
|
||||
match (left, right) {
|
||||
let (Some(l), Some(r)) = (left.class(), right.class()) else {
|
||||
return ZERO;
|
||||
};
|
||||
|
||||
match (l, r) {
|
||||
// No spacing before punctuation; thin spacing after punctuation, unless
|
||||
// in script size.
|
||||
(_, Punctuation) => ZERO,
|
||||
@ -33,12 +43,23 @@ pub(super) fn spacing(left: MathClass, right: MathClass, style: MathStyle) -> Em
|
||||
(Relation, _) | (_, Relation) if !script => THICK,
|
||||
|
||||
// Medium spacing around binary operators, unless in script size.
|
||||
(Vary | Binary, _) | (_, Vary | Binary) if !script => MEDIUM,
|
||||
(Binary, _) | (_, Binary) if !script => MEDIUM,
|
||||
|
||||
// Thin spacing around large operators, unless next to a delimiter.
|
||||
(Large, Opening | Fence) | (Closing | Fence, Large) => ZERO,
|
||||
(Large, _) | (_, Large) => THIN,
|
||||
|
||||
// Spacing around spaced frames.
|
||||
_ if space && (is_spaced(left) || is_spaced(right)) => space_width,
|
||||
|
||||
_ => ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this fragment should react to adjacent spaces.
|
||||
fn is_spaced(fragment: &MathFragment) -> bool {
|
||||
match fragment {
|
||||
MathFragment::Frame(frame) => frame.spaced,
|
||||
_ => fragment.class() == Some(MathClass::Fence),
|
||||
}
|
||||
}
|
||||
|
@ -178,6 +178,7 @@ fn assemble(
|
||||
id: None,
|
||||
frame,
|
||||
italics_correction: Abs::zero(),
|
||||
class: base.class,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -209,7 +209,12 @@ pub fn highlight(node: &LinkedNode) -> Option<Category> {
|
||||
SyntaxKind::Unary => None,
|
||||
SyntaxKind::Binary => None,
|
||||
SyntaxKind::FieldAccess => match node.parent_kind() {
|
||||
Some(SyntaxKind::Markup | SyntaxKind::Math) => Some(Category::Interpolated),
|
||||
Some(
|
||||
SyntaxKind::Markup
|
||||
| SyntaxKind::Math
|
||||
| SyntaxKind::MathFrac
|
||||
| SyntaxKind::MathScript,
|
||||
) => Some(Category::Interpolated),
|
||||
Some(SyntaxKind::FieldAccess) => node.parent().and_then(highlight),
|
||||
_ => None,
|
||||
},
|
||||
|
@ -9,7 +9,7 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use super::{
|
||||
methods, ops, Arg, Args, Array, CapturesVisitor, Closure, Content, Dict, Func, Label,
|
||||
LangItems, Module, Recipe, Scopes, Selector, StyleMap, Transform, Value,
|
||||
LangItems, Module, Recipe, Scopes, Selector, StyleMap, Symbol, Transform, Value,
|
||||
};
|
||||
use crate::diag::{
|
||||
bail, error, At, SourceError, SourceResult, StrResult, Trace, Tracepoint,
|
||||
@ -421,9 +421,7 @@ impl Eval for ast::Escape {
|
||||
type Output = Value;
|
||||
|
||||
fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> {
|
||||
// This can be in markup and math, going through a string ensure
|
||||
// that either text or atom is picked.
|
||||
Ok(Value::Str(self.get().into()))
|
||||
Ok(Value::Symbol(Symbol::new(self.get())))
|
||||
}
|
||||
}
|
||||
|
||||
@ -431,9 +429,7 @@ impl Eval for ast::Shorthand {
|
||||
type Output = Value;
|
||||
|
||||
fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> {
|
||||
// This can be in markup and math, going through a string ensure
|
||||
// that either text or atom is picked.
|
||||
Ok(Value::Str(self.get().into()))
|
||||
Ok(Value::Symbol(Symbol::new(self.get())))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -151,7 +151,6 @@ impl Value {
|
||||
Self::Int(v) => item!(math_atom)(format_eco!("{}", v)),
|
||||
Self::Float(v) => item!(math_atom)(format_eco!("{}", v)),
|
||||
Self::Symbol(v) => item!(math_atom)(v.get().into()),
|
||||
Self::Str(v) => item!(math_atom)(v.into()),
|
||||
_ => self.display(),
|
||||
}
|
||||
}
|
||||
|
@ -308,7 +308,12 @@ fn math_delimited(p: &mut Parser, stop: MathClass) {
|
||||
p.eat();
|
||||
let m2 = p.marker();
|
||||
while !p.eof() && !p.at(SyntaxKind::Dollar) {
|
||||
if math_class(p.current_text()) == Some(stop) {
|
||||
let class = math_class(p.current_text());
|
||||
if stop == MathClass::Fence && class == Some(MathClass::Closing) {
|
||||
break;
|
||||
}
|
||||
|
||||
if class == Some(stop) {
|
||||
p.wrap(m2, SyntaxKind::Math);
|
||||
p.eat();
|
||||
p.wrap(m, SyntaxKind::MathDelimited);
|
||||
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 53 KiB |
@ -12,8 +12,8 @@
|
||||
```
|
||||
Let $x in NN$ be ...
|
||||
$ (1 + x/2)^2 $
|
||||
$ x arrow:l y $
|
||||
$ sum_(n=1)^mu 1 + (2pi (5 + n)) / k $
|
||||
$ x arrow.l y $
|
||||
$ sum_(n=1)^mu 1 + (2pi(5 + n)) / k $
|
||||
$ { x in RR | x "is natural" and x < 10 } $
|
||||
$ sqrt(x^2) = frac(x, 1) $
|
||||
$ "profit" = "income" - "expenses" $
|
||||
|