Better primes in math (#1614)
This commit is contained in:
parent
be0f8fe6d7
commit
dfe361ec6a
@ -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()
|
||||
},
|
||||
|
@ -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 }
|
||||
|
@ -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`.
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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! {
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
||||
|
@ -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 |
@ -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' $
|
||||
|
Loading…
Reference in New Issue
Block a user