summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsitandr <60141933+sitandr@users.noreply.github.com>2023-07-10 13:09:09 +0300
committerGitHub <noreply@github.com>2023-07-10 12:09:09 +0200
commitdfe361ec6a4adfab4140bf480aae3a39b2683165 (patch)
treeb6476dd63047e424d97950eae3414c69c59d9069
parentbe0f8fe6d70bc5919e4351b73a2835e89001b000 (diff)
Better primes in math (#1614)
-rw-r--r--crates/typst-library/src/lib.rs1
-rw-r--r--crates/typst-library/src/math/attach.rs55
-rw-r--r--crates/typst/src/eval/library.rs2
-rw-r--r--crates/typst/src/eval/mod.rs19
-rw-r--r--crates/typst/src/ide/highlight.rs2
-rw-r--r--crates/typst/src/syntax/ast.rs23
-rw-r--r--crates/typst/src/syntax/kind.rs6
-rw-r--r--crates/typst/src/syntax/lexer.rs3
-rw-r--r--crates/typst/src/syntax/parser.rs59
-rw-r--r--tests/ref/math/opticalsize.pngbin6897 -> 11318 bytes
-rw-r--r--tests/ref/math/syntax.pngbin4977 -> 4882 bytes
-rw-r--r--tests/typ/math/opticalsize.typ15
12 files changed, 182 insertions, 3 deletions
diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs
index 0bd88501..f9b95615 100644
--- a/crates/typst-library/src/lib.rs
+++ b/crates/typst-library/src/lib.rs
@@ -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()
},
diff --git a/crates/typst-library/src/math/attach.rs b/crates/typst-library/src/math/attach.rs
index fedeb908..ee65b657 100644
--- a/crates/typst-library/src/math/attach.rs
+++ b/crates/typst-library/src/math/attach.rs
@@ -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 }
diff --git a/crates/typst/src/eval/library.rs b/crates/typst/src/eval/library.rs
index 1b05de83..dcd78b89 100644
--- a/crates/typst/src/eval/library.rs
+++ b/crates/typst/src/eval/library.rs
@@ -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`.
diff --git a/crates/typst/src/eval/mod.rs b/crates/typst/src/eval/mod.rs
index 97cad97d..b76765e6 100644
--- a/crates/typst/src/eval/mod.rs
+++ b/crates/typst/src/eval/mod.rs
@@ -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;
diff --git a/crates/typst/src/ide/highlight.rs b/crates/typst/src/ide/highlight.rs
index 2db636e3..c9748e92 100644
--- a/crates/typst/src/ide/highlight.rs
+++ b/crates/typst/src/ide/highlight.rs
@@ -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,
diff --git a/crates/typst/src/syntax/ast.rs b/crates/typst/src/syntax/ast.rs
index 7d5e2989..4a0de424 100644
--- a/crates/typst/src/syntax/ast.rs
+++ b/crates/typst/src/syntax/ast.rs
@@ -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! {
diff --git a/crates/typst/src/syntax/kind.rs b/crates/typst/src/syntax/kind.rs
index 0c24c667..49119720 100644
--- a/crates/typst/src/syntax/kind.rs
+++ b/crates/typst/src/syntax/kind.rs
@@ -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",
diff --git a/crates/typst/src/syntax/lexer.rs b/crates/typst/src/syntax/lexer.rs
index d95b5b7b..8fe08f4c 100644
--- a/crates/typst/src/syntax/lexer.rs
+++ b/crates/typst/src/syntax/lexer.rs
@@ -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,
diff --git a/crates/typst/src/syntax/parser.rs b/crates/typst/src/syntax/parser.rs
index bd78df20..5cb31e5e 100644
--- a/crates/typst/src/syntax/parser.rs
+++ b/crates/typst/src/syntax/parser.rs
@@ -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();
diff --git a/tests/ref/math/opticalsize.png b/tests/ref/math/opticalsize.png
index 9fec5520..5c338ab6 100644
--- a/tests/ref/math/opticalsize.png
+++ b/tests/ref/math/opticalsize.png
Binary files differ
diff --git a/tests/ref/math/syntax.png b/tests/ref/math/syntax.png
index 028e21d6..d828a478 100644
--- a/tests/ref/math/syntax.png
+++ b/tests/ref/math/syntax.png
Binary files differ
diff --git a/tests/typ/math/opticalsize.typ b/tests/typ/math/opticalsize.typ
index c96e4a32..6edd2419 100644
--- a/tests/typ/math/opticalsize.typ
+++ b/tests/typ/math/opticalsize.typ
@@ -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' $