Better primes in math (#1614)

This commit is contained in:
sitandr 2023-07-10 13:09:09 +03:00 committed by GitHub
parent be0f8fe6d7
commit dfe361ec6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 182 additions and 3 deletions

View File

@ -120,6 +120,7 @@ fn items() -> LangItems {
}
elem.pack()
},
math_primes: |count| math::PrimesElem::new(count).pack(),
math_accent: |base, accent| {
math::AccentElem::new(base, math::Accent::new(accent)).pack()
},

View File

@ -84,6 +84,61 @@ impl LayoutMath for AttachElem {
}
}
/// Grouped primes.
///
/// ## Example { #example }
/// ```example
/// $ a'''_b = a^'''_b $
/// ```
///
/// ## Syntax
/// This function has dedicated syntax: use apostrophes instead of primes. They
/// will automatically attach to the previous element, moving superscripts to
/// the next level.
///
/// Display: Attachment
/// Category: math
#[element(LayoutMath)]
pub struct PrimesElem {
/// The number of grouped primes.
#[required]
pub count: usize,
}
impl LayoutMath for PrimesElem {
#[tracing::instrument(skip(ctx))]
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
match self.count() {
count @ 1..=4 => {
let f = ctx.layout_fragment(&TextElem::packed(match count {
1 => '',
2 => '″',
3 => '‴',
4 => '⁗',
_ => unreachable!(),
}))?;
ctx.push(f);
}
count => {
// Custom amount of primes
let prime = ctx.layout_fragment(&TextElem::packed(''))?.into_frame();
let width = prime.width() * (count + 1) as f64 / 2.0;
let mut frame = Frame::new(Size::new(width, prime.height()));
frame.set_baseline(prime.ascent());
for i in 0..count {
frame.push_frame(
Point::new(prime.width() * (i as f64 / 2.0), Abs::zero()),
prime.clone(),
)
}
ctx.push(FrameFragment::new(ctx, frame));
}
}
Ok(())
}
}
/// Forces a base to display attachments as scripts.
///
/// ## Example { #example }

View File

@ -96,6 +96,8 @@ pub struct LangItems {
tr: Option<Content>,
br: Option<Content>,
) -> Content,
/// Grouped primes: `a'''`.
pub math_primes: fn(count: usize) -> Content,
/// A base with an accent: `arrow(x)`.
pub math_accent: fn(base: Content, accent: char) -> Content,
/// A fraction in math: `x/2`.

View File

@ -460,6 +460,7 @@ impl Eval for ast::Expr {
Self::MathAlignPoint(v) => v.eval(vm).map(Value::Content),
Self::MathDelimited(v) => v.eval(vm).map(Value::Content),
Self::MathAttach(v) => v.eval(vm).map(Value::Content),
Self::MathPrimes(v) => v.eval(vm).map(Value::Content),
Self::MathFrac(v) => v.eval(vm).map(Value::Content),
Self::MathRoot(v) => v.eval(vm).map(Value::Content),
Self::Ident(v) => v.eval(vm),
@ -733,12 +734,28 @@ impl Eval for ast::MathAttach {
#[tracing::instrument(name = "MathAttach::eval", skip_all)]
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
let base = self.base().eval_display(vm)?;
let top = self.top().map(|expr| expr.eval_display(vm)).transpose()?;
let mut top = self.top().map(|expr| expr.eval_display(vm)).transpose()?;
if top.is_none() {
if let Some(primes) = self.primes() {
top = Some(primes.eval(vm)?);
}
}
let bottom = self.bottom().map(|expr| expr.eval_display(vm)).transpose()?;
Ok((vm.items.math_attach)(base, top, bottom, None, None, None, None))
}
}
impl Eval for ast::MathPrimes {
type Output = Content;
#[tracing::instrument(name = "MathPrimes::eval", skip_all)]
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
Ok((vm.items.math_primes)(self.count()))
}
}
impl Eval for ast::MathFrac {
type Output = Content;

View File

@ -147,6 +147,7 @@ pub fn highlight(node: &LinkedNode) -> Option<Tag> {
SyntaxKind::MathAttach => None,
SyntaxKind::MathFrac => None,
SyntaxKind::MathRoot => None,
SyntaxKind::MathPrimes => None,
SyntaxKind::Hashtag => highlight_hashtag(node),
SyntaxKind::LeftBrace => Some(Tag::Punctuation),
@ -174,6 +175,7 @@ pub fn highlight(node: &LinkedNode) -> Option<Tag> {
_ => Tag::Operator,
}),
SyntaxKind::Hat => Some(Tag::MathOperator),
SyntaxKind::Prime => Some(Tag::MathOperator),
SyntaxKind::Dot => Some(Tag::Punctuation),
SyntaxKind::Eq => match node.parent_kind() {
Some(SyntaxKind::Heading) => None,

View File

@ -124,6 +124,8 @@ pub enum Expr {
MathDelimited(MathDelimited),
/// A base with optional attachments in math: `a_1^2`.
MathAttach(MathAttach),
/// Grouped math primes
MathPrimes(MathPrimes),
/// A fraction in math: `x/2`.
MathFrac(MathFrac),
/// A root in math: `√x`, `∛x` or `∜x`.
@ -224,6 +226,7 @@ impl AstNode for Expr {
SyntaxKind::MathAlignPoint => node.cast().map(Self::MathAlignPoint),
SyntaxKind::MathDelimited => node.cast().map(Self::MathDelimited),
SyntaxKind::MathAttach => node.cast().map(Self::MathAttach),
SyntaxKind::MathPrimes => node.cast().map(Self::MathPrimes),
SyntaxKind::MathFrac => node.cast().map(Self::MathFrac),
SyntaxKind::MathRoot => node.cast().map(Self::MathRoot),
SyntaxKind::Ident => node.cast().map(Self::Ident),
@ -285,6 +288,7 @@ impl AstNode for Expr {
Self::MathAlignPoint(v) => v.as_untyped(),
Self::MathDelimited(v) => v.as_untyped(),
Self::MathAttach(v) => v.as_untyped(),
Self::MathPrimes(v) => v.as_untyped(),
Self::MathFrac(v) => v.as_untyped(),
Self::MathRoot(v) => v.as_untyped(),
Self::Ident(v) => v.as_untyped(),
@ -841,6 +845,25 @@ impl MathAttach {
.skip_while(|node| !matches!(node.kind(), SyntaxKind::Hat))
.find_map(SyntaxNode::cast)
}
/// Extract primes if present.
pub fn primes(&self) -> Option<MathPrimes> {
self.0.cast_first_match()
}
}
node! {
/// Grouped primes in math: `a'''`.
MathPrimes
}
impl MathPrimes {
pub fn count(&self) -> usize {
self.0
.children()
.filter(|node| matches!(node.kind(), SyntaxKind::Prime))
.count()
}
}
node! {

View File

@ -65,6 +65,8 @@ pub enum SyntaxKind {
MathDelimited,
/// A base with optional attachments in math: `a_1^2`.
MathAttach,
/// Grouped primes in math: `a'''`.
MathPrimes,
/// A fraction in math: `x/2`.
MathFrac,
/// A root in math: `√x`, `∛x` or `∜x`.
@ -108,6 +110,8 @@ pub enum SyntaxKind {
Slash,
/// The superscript operator in math: `^`.
Hat,
/// The prime in math: `'`.
Prime,
/// The field access and method call operator: `.`.
Dot,
/// The assignment operator: `=`.
@ -378,6 +382,7 @@ impl SyntaxKind {
Self::MathAttach => "math attachments",
Self::MathFrac => "math fraction",
Self::MathRoot => "math root",
Self::MathPrimes => "math primes",
Self::Hashtag => "hashtag",
Self::LeftBrace => "opening brace",
Self::RightBrace => "closing brace",
@ -395,6 +400,7 @@ impl SyntaxKind {
Self::Minus => "minus",
Self::Slash => "slash",
Self::Hat => "hat",
Self::Prime => "prime",
Self::Dot => "dot",
Self::Eq => "equals sign",
Self::EqEq => "equality operator",

View File

@ -422,13 +422,14 @@ impl Lexer<'_> {
'|' if self.s.eat_if('|') => SyntaxKind::Shorthand,
'~' if self.s.eat_if("~>") => SyntaxKind::Shorthand,
'~' if self.s.eat_if('>') => SyntaxKind::Shorthand,
'*' | '\'' | '-' => SyntaxKind::Shorthand,
'*' | '-' => SyntaxKind::Shorthand,
'#' => SyntaxKind::Hashtag,
'_' => SyntaxKind::Underscore,
'$' => SyntaxKind::Dollar,
'/' => SyntaxKind::Slash,
'^' => SyntaxKind::Hat,
'\'' => SyntaxKind::Prime,
'&' => SyntaxKind::MathAlignPoint,
'√' | '∛' | '∜' => SyntaxKind::Root,

View File

@ -295,6 +295,18 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) {
}
}
SyntaxKind::Prime => {
// Means that there is nothing to attach the prime to.
continuable = true;
while p.at(SyntaxKind::Prime) {
let m2 = p.marker();
p.eat();
// Eat the group until the space.
while p.eat_if_direct(SyntaxKind::Prime) {}
p.wrap(m2, SyntaxKind::MathPrimes);
}
}
_ => p.expected("expression"),
}
@ -306,6 +318,9 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) {
p.wrap(m, SyntaxKind::Math);
}
// Whether there were _any_ primes in the loop.
let mut primed = false;
while !p.eof() && !p.at(stop) {
if p.directly_at(SyntaxKind::Text) && p.current_text() == "!" {
p.eat();
@ -313,10 +328,39 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) {
continue;
}
let prime_marker = p.marker();
if p.eat_if_direct(SyntaxKind::Prime) {
// Eat as many primes as possible.
while p.eat_if_direct(SyntaxKind::Prime) {}
p.wrap(prime_marker, SyntaxKind::MathPrimes);
// Will not be continued, so need to wrap the prime as attachment.
if p.at(stop) {
p.wrap(m, SyntaxKind::MathAttach);
}
primed = true;
continue;
}
// Separate primes and superscripts to different attachments.
if primed && p.current() == SyntaxKind::Hat {
p.wrap(m, SyntaxKind::MathAttach);
}
let Some((kind, stop, assoc, mut prec)) = math_op(p.current()) else {
// No attachments, so we need to wrap primes as attachment.
if primed {
p.wrap(m, SyntaxKind::MathAttach);
}
break;
};
if primed && kind == SyntaxKind::MathFrac {
p.wrap(m, SyntaxKind::MathAttach);
}
if prec < min_prec {
break;
}
@ -335,7 +379,7 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) {
math_expr_prec(p, prec, stop);
math_unparen(p, m2);
if p.eat_if(SyntaxKind::Underscore) || p.eat_if(SyntaxKind::Hat) {
if p.eat_if(SyntaxKind::Underscore) || (!primed && p.eat_if(SyntaxKind::Hat)) {
let m3 = p.marker();
math_expr_prec(p, prec, SyntaxKind::Eof);
math_unparen(p, m3);
@ -1451,6 +1495,10 @@ impl<'s> Parser<'s> {
self.current == kind && self.prev_end == self.current_start
}
/// Eats if at `kind`.
///
/// Note: In math and code mode, this will ignore trivia in front of the
/// `kind`, To forbid skipping trivia, consider using `eat_if_direct`.
fn eat_if(&mut self, kind: SyntaxKind) -> bool {
let at = self.at(kind);
if at {
@ -1459,6 +1507,15 @@ impl<'s> Parser<'s> {
at
}
/// Eats only if currently at the start of `kind`.
fn eat_if_direct(&mut self, kind: SyntaxKind) -> bool {
let at = self.directly_at(kind);
if at {
self.eat();
}
at
}
fn convert(&mut self, kind: SyntaxKind) {
self.current = kind;
self.eat();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -28,3 +28,18 @@ $sum_(k in NN)^prime 1/k^2$
$ 1/(x^A) $
#[#set text(size:18pt); $1/(x^A)$] vs. #[#set text(size:14pt); $x^A$]
---
// Test dedicated syntax for primes
$a'$, $a'''_b$, $'$, $'''''''$
---
// Test spaces between
$a' ' '$, $' ' '$, $a' '/b$
---
// Test complex prime combilnations
$a'_b^c$, $a_b'^c$, $a_b^c'$, $a_b'^c'^d'$
$(a'_b')^(c'_d')$, $a'/b'$, $a_b'/c_d'$
$∫'$, $∑'$, $ ∑'_S' $