More capable math calls

This commit is contained in:
Laurenz 2023-01-28 15:35:56 +01:00
parent 28c554ec21
commit 4809e685a2
29 changed files with 375 additions and 267 deletions

View File

@ -191,12 +191,11 @@ fn items() -> LangItems {
layout::ListItem::Term(basics::TermItem { term, description }).pack()
},
formula: |body, block| math::FormulaNode { body, block }.pack(),
math_atom: |atom| math::AtomNode(atom).pack(),
math_align_point: || math::AlignPointNode.pack(),
math_delimited: |open, body, close| math::LrNode(open + body + close).pack(),
math_attach: |base, sub, sup| {
math::AttachNode { base, top: sub, bottom: sup }.pack()
math_delimited: |open, body, close| {
math::LrNode { body: open + body + close, size: None }.pack()
},
math_attach: |base, bottom, top| math::AttachNode { base, bottom, top }.pack(),
math_accent: |base, accent| math::AccentNode { base, accent }.pack(),
math_frac: |num, denom| math::FracNode { num, denom }.pack(),
}

View File

@ -10,8 +10,9 @@ const ACCENT_SHORT_FALL: Em = Em::new(0.5);
///
/// ## Example
/// ```
/// $grave(a) = accent(a, `)$ \
/// $arrow(a) = accent(a, arrow)$ \
/// $grave(a) = accent(a, `)$
/// $tilde(a) = accent(a, \u{0303})$
/// ```
///
/// ## Parameters
@ -58,11 +59,22 @@ pub struct AccentNode {
impl AccentNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
let base = args.expect("base")?;
let accent = args.expect("accent")?;
let accent = args.expect::<Accent>("accent")?.0;
Ok(Self { base, accent }.pack())
}
}
struct Accent(char);
castable! {
Accent,
v: char => Self(v),
v: Content => match v.to::<TextNode>() {
Some(text) => Self(Value::Str(text.0.clone().into()).cast()?),
None => Err("expected text")?,
},
}
impl LayoutMath for AccentNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
ctx.style(ctx.style.with_cramped(true));

View File

@ -1,58 +0,0 @@
use super::*;
/// # Atom
/// An atom in a math formula: `x`, `+`, `12`.
///
/// ## Parameters
/// - text: EcoString (positional, required)
/// The atom's text.
///
/// ## Category
/// math
#[func]
#[capable(LayoutMath)]
#[derive(Debug, Hash)]
pub struct AtomNode(pub EcoString);
#[node]
impl AtomNode {
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
Ok(Self(args.expect("text")?).pack())
}
}
impl LayoutMath for AtomNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
let mut chars = self.0.chars();
if let Some(glyph) = chars
.next()
.filter(|_| chars.next().is_none())
.and_then(|c| GlyphFragment::try_new(ctx, c))
{
// A single letter that is available in the math font.
if ctx.style.size == MathSize::Display
&& glyph.class == Some(MathClass::Large)
{
let height = scaled!(ctx, display_operator_min_height);
ctx.push(glyph.stretch_vertical(ctx, height, Abs::zero()));
} else {
ctx.push(glyph);
}
} else if self.0.chars().all(|c| c.is_ascii_digit()) {
// A number that should respect math styling and can therefore
// not fall back to the normal text layout.
let mut vec = vec![];
for c in self.0.chars() {
vec.push(GlyphFragment::new(ctx, c).into());
}
let frame = MathRow(vec).to_frame(ctx);
ctx.push(frame);
} else {
// Anything else is handled by Typst's standard text layout.
let frame = ctx.layout_non_math(&TextNode(self.0.clone()).pack())?;
ctx.push(FrameFragment::new(frame).with_class(MathClass::Alphabetic));
}
Ok(())
}
}

View File

@ -51,17 +51,17 @@ 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());
let mut top = Frame::new(Size::zero());
if let Some(node) = &self.top {
ctx.style(ctx.style.for_subscript());
sub = ctx.layout_frame(node)?;
top = ctx.layout_frame(node)?;
ctx.unstyle();
}
let mut sup = Frame::new(Size::zero());
let mut bottom = Frame::new(Size::zero());
if let Some(node) = &self.bottom {
ctx.style(ctx.style.for_superscript());
sup = ctx.layout_frame(node)?;
bottom = ctx.layout_frame(node)?;
ctx.unstyle();
}
@ -76,9 +76,9 @@ impl LayoutMath for AttachNode {
});
if render_limits {
limits(ctx, base, sub, sup)
limits(ctx, base, top, bottom)
} else {
scripts(ctx, base, sub, sup, self.top.is_some() && self.bottom.is_some())
scripts(ctx, base, top, bottom, self.top.is_some() && self.bottom.is_some())
}
}
}
@ -151,8 +151,8 @@ impl LayoutMath for LimitsNode {
fn scripts(
ctx: &mut MathContext,
base: MathFragment,
sub: Frame,
sup: Frame,
sub: Frame,
both: bool,
) -> SourceResult<()> {
let sup_shift_up = if ctx.style.cramped {
@ -191,21 +191,28 @@ fn scripts(
}
}
let delta = base.italics_correction();
let italics = base.italics_correction();
let top_delta = match base.class() {
Some(MathClass::Large) => Abs::zero(),
_ => italics,
};
let bottom_delta = -italics;
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 width = base.width()
+ (sup.width() + top_delta).max(sub.width() + bottom_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 sup_pos = Point::with_x(base.width() + top_delta);
let sub_pos = Point::new(base.width() + bottom_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);
frame.push_frame(sub_pos, sub);
ctx.push(FrameFragment::new(frame).with_class(class));
Ok(())
@ -215,30 +222,31 @@ fn scripts(
fn limits(
ctx: &mut MathContext,
base: MathFragment,
sub: Frame,
sup: Frame,
top: Frame,
bottom: 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 top_gap = upper_gap_min.max(upper_rise_min - top.descent());
let bottom_gap = lower_gap_min.max(lower_drop_min - bottom.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 width = base.width().max(top.width()).max(bottom.width());
let height = top.height() + top_gap + base.height() + bottom_gap + bottom.height();
let base_pos = Point::new((width - base.width()) / 2.0, top.height() + top_gap);
let sup_pos = Point::with_x((width - top.width()) / 2.0 + delta);
let sub_pos =
Point::new((width - bottom.width()) / 2.0 - delta, height - bottom.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);
frame.push_frame(sub_pos, bottom);
frame.push_frame(sup_pos, top);
ctx.push(FrameFragment::new(frame).with_class(class));
Ok(())

View File

@ -1,4 +1,5 @@
use ttf_parser::math::MathValue;
use unicode_segmentation::UnicodeSegmentation;
use super::*;
@ -129,6 +130,45 @@ impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
Ok(self.layout_fragment(node)?.to_frame(self))
}
pub fn layout_text(&mut self, text: &EcoString) -> SourceResult<()> {
let mut chars = text.chars();
if let Some(glyph) = chars
.next()
.filter(|_| chars.next().is_none())
.and_then(|c| GlyphFragment::try_new(self, c))
{
// A single letter that is available in the math font.
if self.style.size == MathSize::Display
&& glyph.class == Some(MathClass::Large)
{
let height = scaled!(self, display_operator_min_height);
self.push(glyph.stretch_vertical(self, height, Abs::zero()));
} else {
self.push(glyph);
}
} else if text.chars().all(|c| c.is_ascii_digit()) {
// A number that should respect math styling and can therefore
// not fall back to the normal text layout.
let mut vec = vec![];
for c in text.chars() {
vec.push(GlyphFragment::new(self, c).into());
}
let frame = MathRow(vec).to_frame(self);
self.push(frame);
} else {
// Anything else is handled by Typst's standard text layout.
let spaced = text.graphemes(true).count() > 1;
let frame = self.layout_non_math(&TextNode::packed(text.clone()))?;
self.push(
FrameFragment::new(frame)
.with_class(MathClass::Alphabetic)
.with_spaced(spaced),
);
}
Ok(())
}
pub fn size(&self) -> Abs {
self.scaled_size
}

View File

@ -12,18 +12,29 @@ pub(super) const DELIM_SHORT_FALL: Em = Em::new(0.1);
/// ## Example
/// ```
/// $ lr(]a, b/2]) $
/// $ lr(]sum_(x=1)^n] x, #size: 50%) $
/// ```
///
/// ## Parameters
/// - body: Content (positional, variadic)
/// The delimited content, including the delimiters.
///
/// - size: Rel<Length> (named)
/// The size of the brackets, relative to the height of the wrapped content.
///
/// Defaults to `{100%}`.
///
/// ## Category
/// math
#[func]
#[capable(LayoutMath)]
#[derive(Debug, Hash)]
pub struct LrNode(pub Content);
pub struct LrNode {
/// The delimited content, including the delimiters.
pub body: Content,
/// The size of the brackets.
pub size: Option<Rel<Length>>,
}
#[node]
impl LrNode {
@ -31,17 +42,18 @@ impl LrNode {
let mut body = Content::empty();
for (i, arg) in args.all::<Content>()?.into_iter().enumerate() {
if i > 0 {
body += AtomNode(','.into()).pack();
body += TextNode::packed(',');
}
body += arg;
}
Ok(Self(body).pack())
let size = args.named("size")?;
Ok(Self { body, size }.pack())
}
}
impl LayoutMath for LrNode {
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
let mut row = ctx.layout_row(&self.0)?;
let mut row = ctx.layout_row(&self.body)?;
let axis = scaled!(ctx, axis_height);
let max_extent = row
@ -51,22 +63,19 @@ impl LayoutMath for LrNode {
.max()
.unwrap_or_default();
let height = 2.0 * max_extent;
if let [first, .., last] = row.0.as_mut_slice() {
for fragment in [first, last] {
if !matches!(
fragment.class(),
Some(MathClass::Opening | MathClass::Closing | MathClass::Fence)
) {
continue;
}
let height = self
.size
.unwrap_or(Rel::one())
.resolve(ctx.outer.chain(&ctx.map))
.relative_to(2.0 * max_extent);
let MathFragment::Glyph(glyph) = *fragment else { continue };
let short_fall = DELIM_SHORT_FALL.scaled(ctx);
*fragment = MathFragment::Variant(
glyph.stretch_vertical(ctx, height, short_fall),
);
match row.0.as_mut_slice() {
[one] => scale(ctx, one, height, None),
[first, .., last] => {
scale(ctx, first, height, Some(MathClass::Opening));
scale(ctx, last, height, Some(MathClass::Closing));
}
_ => {}
}
ctx.extend(row);
@ -75,6 +84,28 @@ impl LayoutMath for LrNode {
}
}
/// Scale a math fragment to a height.
fn scale(
ctx: &mut MathContext,
fragment: &mut MathFragment,
height: Abs,
apply: Option<MathClass>,
) {
if matches!(
fragment.class(),
Some(MathClass::Opening | MathClass::Closing | MathClass::Fence)
) {
let MathFragment::Glyph(glyph) = *fragment else { return };
let short_fall = DELIM_SHORT_FALL.scaled(ctx);
*fragment =
MathFragment::Variant(glyph.stretch_vertical(ctx, height, short_fall));
if let Some(class) = apply {
fragment.set_class(class);
}
}
}
/// # Floor
/// Floor an expression.
///
@ -153,11 +184,14 @@ pub fn norm(args: &mut Args) -> SourceResult<Value> {
fn delimited(args: &mut Args, left: char, right: char) -> SourceResult<Value> {
Ok(Value::Content(
LrNode(Content::sequence(vec![
AtomNode(left.into()).pack(),
LrNode {
body: Content::sequence(vec![
TextNode::packed(left),
args.expect::<Content>("body")?,
AtomNode(right.into()).pack(),
]))
TextNode::packed(right),
]),
size: None,
}
.pack(),
))
}

View File

@ -4,7 +4,6 @@
mod ctx;
mod accent;
mod align;
mod atom;
mod attach;
mod braced;
mod frac;
@ -21,7 +20,6 @@ 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::*;
@ -263,6 +261,11 @@ impl LayoutMath for Content {
return Ok(());
}
if let Some(node) = self.to::<TextNode>() {
ctx.layout_text(&node.0)?;
return Ok(());
}
if let Some(node) = self.to::<SequenceNode>() {
for child in &node.0 {
child.layout_math(ctx)?;

View File

@ -155,7 +155,7 @@ fn layout(
/// Select a precomposed radical, if the font has it.
fn precomposed(ctx: &MathContext, index: Option<&Content>, target: Abs) -> Option<Frame> {
let node = index?.to::<AtomNode>()?;
let node = index?.to::<TextNode>()?;
let c = match node.0.as_str() {
"3" => '∛',
"4" => '∜',

View File

@ -18,7 +18,7 @@ pub use typst::model::{
array, capability, capable, castable, dict, format_str, func, node, Args, Array,
AutoValue, Cast, CastInfo, Content, Dict, Finalize, Fold, Func, Introspector, Label,
Node, NodeId, NoneValue, Prepare, Resolve, Selector, Show, StabilityProvider, Str,
StyleChain, StyleMap, StyleVec, Unlabellable, Value, Vm, Vt,
StyleChain, StyleMap, StyleVec, Symbol, Unlabellable, Value, Vm, Vt,
};
#[doc(no_inline)]
pub use typst::syntax::{Span, Spanned};

View File

@ -68,7 +68,6 @@ impl SourceError {
/// Create a new, bare error.
#[track_caller]
pub fn new(span: Span, message: impl Into<EcoString>) -> Self {
assert!(!span.is_detached());
Self {
span,
pos: ErrorPos::Full,

View File

@ -7,7 +7,9 @@ use crate::World;
/// Try to determine a set of possible values for an expression.
pub fn analyze(world: &(dyn World + 'static), node: &LinkedNode) -> Vec<Value> {
match node.cast::<ast::Expr>() {
Some(ast::Expr::Ident(_) | ast::Expr::MathIdent(_)) => {
Some(
ast::Expr::Ident(_) | ast::Expr::MathIdent(_) | ast::Expr::MethodCall(_),
) => {
if let Some(parent) = node.parent() {
if parent.kind() == SyntaxKind::FieldAccess && node.index() > 0 {
return analyze(world, parent);

View File

@ -229,7 +229,7 @@ fn complete_math(ctx: &mut CompletionContext) -> bool {
}
// Behind existing atom or identifier: "$a|$" or "$abc|$".
if matches!(ctx.leaf.kind(), SyntaxKind::MathAtom | SyntaxKind::MathIdent) {
if matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathIdent) {
ctx.from = ctx.leaf.offset();
math_completions(ctx);
return true;
@ -274,7 +274,7 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool {
// Behind an expression plus dot: "emoji.|".
if_chain! {
if ctx.leaf.kind() == SyntaxKind::Dot
|| (matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathAtom)
|| (ctx.leaf.kind() == SyntaxKind::Text
&& ctx.leaf.text() == ".");
if ctx.leaf.range().end == ctx.cursor;
if let Some(prev) = ctx.leaf.prev_sibling();
@ -326,11 +326,15 @@ fn field_access_completions(ctx: &mut CompletionContext, value: &Value) {
}
}
_ => {
for &method in methods_on(value.type_name()) {
for &(method, args) in methods_on(value.type_name()) {
ctx.completions.push(Completion {
kind: CompletionKind::Func,
label: method.into(),
apply: Some(format_eco!("{method}(${{}})")),
apply: Some(if args {
format_eco!("{method}(${{}})")
} else {
format_eco!("{method}()${{}}")
}),
detail: None,
})
}

View File

@ -1,4 +1,4 @@
use crate::syntax::{LinkedNode, SyntaxKind};
use crate::syntax::{ast, LinkedNode, SyntaxKind};
/// Syntax highlighting categories.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
@ -115,19 +115,17 @@ pub fn highlight(node: &LinkedNode) -> Option<Category> {
SyntaxKind::Formula => None,
SyntaxKind::Math => None,
SyntaxKind::MathAtom => None,
SyntaxKind::MathIdent => highlight_ident(node),
SyntaxKind::MathDelimited => None,
SyntaxKind::MathAttach => None,
SyntaxKind::MathFrac => None,
SyntaxKind::MathAlignPoint => Some(Category::MathOperator),
SyntaxKind::Hashtag if node.before_error() => None,
SyntaxKind::Hashtag => node
.next_leaf()
.filter(|node| node.kind() != SyntaxKind::Dollar)
.as_ref()
.and_then(highlight),
.next_sibling()
.filter(|node| node.cast::<ast::Expr>().map_or(false, |e| e.hashtag()))
.and_then(|node| node.leftmost_leaf())
.and_then(|node| highlight(&node)),
SyntaxKind::LeftBrace => Some(Category::Punctuation),
SyntaxKind::RightBrace => Some(Category::Punctuation),
@ -248,12 +246,6 @@ pub fn highlight(node: &LinkedNode) -> Option<Category> {
/// Highlight an identifier based on context.
fn highlight_ident(node: &LinkedNode) -> Option<Category> {
match node.parent_kind() {
Some(
SyntaxKind::Markup
| SyntaxKind::Math
| SyntaxKind::MathFrac
| SyntaxKind::MathAttach,
) => Some(Category::Interpolated),
Some(SyntaxKind::FuncCall) => Some(Category::Function),
Some(SyntaxKind::FieldAccess)
if node.parent().and_then(|p| p.parent_kind())
@ -287,6 +279,13 @@ fn highlight_ident(node: &LinkedNode) -> Option<Category> {
{
Some(Category::Function)
}
Some(
SyntaxKind::Markup
| SyntaxKind::Math
| SyntaxKind::MathFrac
| SyntaxKind::MathAttach,
) => Some(Category::Interpolated),
_ if node.kind() == SyntaxKind::MathIdent => Some(Category::Interpolated),
_ => None,
}
}

View File

@ -183,12 +183,18 @@ impl Content {
}
/// Whether the contained node is of type `T`.
pub fn is<T: 'static>(&self) -> bool {
pub fn is<T>(&self) -> bool
where
T: Capable + 'static,
{
(*self.obj).as_any().is::<T>()
}
/// Cast to `T` if the contained node is of type `T`.
pub fn to<T: 'static>(&self) -> Option<&T> {
pub fn to<T>(&self) -> Option<&T>
where
T: Capable + 'static,
{
(*self.obj).as_any().downcast_ref::<T>()
}

View File

@ -3,8 +3,8 @@ use std::fmt::{self, Debug, Formatter, Write};
use std::ops::{Add, AddAssign};
use std::sync::Arc;
use super::{Args, Array, Func, Str, Value, Vm};
use crate::diag::{bail, SourceResult, StrResult};
use super::{array, Array, Str, Value};
use crate::diag::StrResult;
use crate::syntax::is_ident;
use crate::util::{format_eco, ArcExt, EcoString};
@ -104,17 +104,12 @@ impl Dict {
self.0.values().cloned().collect()
}
/// Transform each pair in the dictionary with a function.
pub fn map(&self, vm: &mut Vm, func: Func) -> SourceResult<Array> {
if func.argc().map_or(false, |count| count != 2) {
bail!(func.span(), "function must have exactly two parameters");
}
self.iter()
.map(|(key, value)| {
let args =
Args::new(func.span(), [Value::Str(key.clone()), value.clone()]);
func.call(vm, args)
})
/// Return the values of the dictionary as an array of pairs (arrays of
/// length two).
pub fn pairs(&self) -> Array {
self.0
.iter()
.map(|(k, v)| Value::Array(array![k.clone(), v.clone()]))
.collect()
}

View File

@ -344,7 +344,6 @@ impl Eval for ast::Expr {
Self::Term(v) => v.eval(vm).map(Value::Content),
Self::Formula(v) => v.eval(vm).map(Value::Content),
Self::Math(v) => v.eval(vm).map(Value::Content),
Self::MathAtom(v) => v.eval(vm).map(Value::Content),
Self::MathIdent(v) => v.eval(vm),
Self::MathAlignPoint(v) => v.eval(vm).map(Value::Content),
Self::MathDelimited(v) => v.eval(vm).map(Value::Content),
@ -552,21 +551,13 @@ impl Eval for ast::Math {
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
Ok(Content::sequence(
self.exprs()
.map(|expr| Ok(expr.eval(vm)?.display_in_math()))
.map(|expr| Ok(expr.eval(vm)?.display()))
.collect::<SourceResult<_>>()?,
)
.spanned(self.span()))
}
}
impl Eval for ast::MathAtom {
type Output = Content;
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
Ok((vm.items.math_atom)(self.get().clone()))
}
}
impl Eval for ast::MathIdent {
type Output = Value;
@ -587,9 +578,9 @@ impl Eval for ast::MathDelimited {
type Output = Content;
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
let open = self.open().eval(vm)?.display_in_math();
let open = self.open().eval(vm)?.display();
let body = self.body().eval(vm)?;
let close = self.close().eval(vm)?.display_in_math();
let close = self.close().eval(vm)?.display();
Ok((vm.items.math_delimited)(open, body, close))
}
}
@ -598,16 +589,13 @@ impl Eval for ast::MathAttach {
type Output = Content;
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
let base = self.base().eval(vm)?.display_in_math();
let sub = self
let base = self.base().eval(vm)?.display();
let bottom = self
.bottom()
.map(|expr| expr.eval(vm).map(Value::display_in_math))
.map(|expr| expr.eval(vm).map(Value::display))
.transpose()?;
let sup = self
.top()
.map(|expr| expr.eval(vm).map(Value::display_in_math))
.transpose()?;
Ok((vm.items.math_attach)(base, sub, sup))
let top = self.top().map(|expr| expr.eval(vm).map(Value::display)).transpose()?;
Ok((vm.items.math_attach)(base, bottom, top))
}
}
@ -615,8 +603,8 @@ impl Eval for ast::MathFrac {
type Output = Content;
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
let num = self.num().eval(vm)?.display_in_math();
let denom = self.denom().eval(vm)?.display_in_math();
let num = self.num().eval(vm)?.display();
let denom = self.denom().eval(vm)?.display();
Ok((vm.items.math_frac)(num, denom))
}
}
@ -945,15 +933,15 @@ impl Eval for ast::FuncCall {
}
}
let mut body = (vm.items.math_atom)('('.into());
let mut body = (vm.items.text)('('.into());
for (i, arg) in args.all::<Content>()?.into_iter().enumerate() {
if i > 0 {
body += (vm.items.math_atom)(','.into());
body += (vm.items.text)(','.into());
}
body += arg;
}
body += (vm.items.math_atom)(')'.into());
return Ok(Value::Content(callee.display_in_math() + body));
body += (vm.items.text)(')'.into());
return Ok(Value::Content(callee.display() + body));
}
let callee = callee.cast::<Func>().at(callee_span)?;

View File

@ -389,6 +389,7 @@ impl<'a> CapturesVisitor<'a> {
// actually bind a new name are handled below (individually through
// the expressions that contain them).
Some(ast::Expr::Ident(ident)) => self.capture(ident),
Some(ast::Expr::MathIdent(ident)) => self.capture_in_math(ident),
// Code and content blocks create a scope.
Some(ast::Expr::Code(_) | ast::Expr::Content(_)) => {
@ -483,6 +484,15 @@ impl<'a> CapturesVisitor<'a> {
}
}
}
/// Capture a variable in math mode if it isn't internal.
fn capture_in_math(&mut self, ident: ast::MathIdent) {
if self.internal.get(&ident).is_err() {
if let Ok(value) = self.external.get_in_math(&ident) {
self.captures.define_captured(ident.take(), value.clone());
}
}
}
}
#[cfg(test)]

View File

@ -67,8 +67,6 @@ pub struct LangItems {
pub term_item: fn(term: Content, description: Content) -> Content,
/// A mathematical formula: `$x$`, `$ x^2 $`.
pub formula: fn(body: Content, block: bool) -> Content,
/// An atom in a formula: `x`, `+`, `12`.
pub math_atom: fn(atom: EcoString) -> Content,
/// An alignment point in a formula: `&`.
pub math_align_point: fn() -> Content,
/// Matched delimiters surrounding math in a formula: `[x + y]`.
@ -110,7 +108,6 @@ impl Hash for LangItems {
self.enum_item.hash(state);
self.term_item.hash(state);
self.formula.hash(state);
self.math_atom.hash(state);
self.math_align_point.hash(state);
self.math_delimited.hash(state);
self.math_attach.hash(state);

View File

@ -107,7 +107,7 @@ pub fn call(
"at" => dict.at(&args.expect::<Str>("key")?).cloned().at(span)?,
"keys" => Value::Array(dict.keys()),
"values" => Value::Array(dict.values()),
"pairs" => Value::Array(dict.map(vm, args.expect("function")?)?),
"pairs" => Value::Array(dict.pairs()),
_ => return missing(),
},
@ -211,35 +211,61 @@ fn missing_method(type_name: &str, method: &str) -> String {
format!("type {type_name} has no method `{method}`")
}
/// List the available methods for a type.
pub fn methods_on(type_name: &str) -> &[&'static str] {
/// List the available methods for a type and whether they take arguments.
pub fn methods_on(type_name: &str) -> &[(&'static str, bool)] {
match type_name {
"color" => &["lighten", "darken", "negate"],
"color" => &[("lighten", true), ("darken", true), ("negate", false)],
"string" => &[
"len",
"at",
"contains",
"ends-with",
"find",
"first",
"last",
"match",
"matches",
"position",
"replace",
"slice",
"split",
"starts-with",
"trim",
("len", false),
("at", true),
("contains", true),
("ends-with", true),
("find", true),
("first", false),
("last", false),
("match", true),
("matches", true),
("position", true),
("replace", true),
("slice", true),
("split", true),
("starts-with", true),
("trim", true),
],
"array" => &[
"all", "any", "at", "contains", "filter", "find", "first", "flatten", "fold",
"insert", "join", "last", "len", "map", "pop", "position", "push", "remove",
"rev", "slice", "sorted",
("all", true),
("any", true),
("at", true),
("contains", true),
("filter", true),
("find", true),
("first", false),
("flatten", false),
("fold", true),
("insert", true),
("join", true),
("last", false),
("len", false),
("map", true),
("pop", false),
("position", true),
("push", true),
("remove", true),
("rev", false),
("slice", true),
("sorted", false),
],
"dictionary" => &["at", "insert", "keys", "len", "pairs", "remove", "values"],
"function" => &["where", "with"],
"arguments" => &["named", "pos"],
"dictionary" => &[
("at", true),
("insert", true),
("keys", false),
("len", false),
("pairs", false),
("remove", true),
("values", false),
],
"function" => &[("where", true), ("with", true)],
"arguments" => &[("named", false), ("pos", false)],
_ => &[],
}
}

View File

@ -145,16 +145,6 @@ impl Value {
}
}
/// Return the display representation of the value in math mode.
pub fn display_in_math(self) -> Content {
match self {
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.display(),
}
}
/// Try to extract documentation for the value.
pub fn docs(&self) -> Option<&'static str> {
match self {
@ -447,8 +437,8 @@ primitive! { Label: "label", Label }
primitive! { Content: "content",
Content,
None => Content::empty(),
Symbol(symbol) => item!(text)(symbol.get().into()),
Str(text) => item!(text)(text.into())
Symbol(v) => item!(text)(v.get().into()),
Str(v) => item!(text)(v.into())
}
primitive! { Array: "array", Array }
primitive! { Dict: "dictionary", Dict }

View File

@ -115,8 +115,6 @@ pub enum Expr {
Formula(Formula),
/// A math formula: `$x$`, `$ x^2 $`.
Math(Math),
/// An atom in a math formula: `x`, `+`, `12`.
MathAtom(MathAtom),
/// An identifier in a math formula: `pi`.
MathIdent(MathIdent),
/// An alignment point in a math formula: `&`.
@ -219,7 +217,6 @@ impl AstNode for Expr {
SyntaxKind::TermItem => node.cast().map(Self::Term),
SyntaxKind::Formula => node.cast().map(Self::Formula),
SyntaxKind::Math => node.cast().map(Self::Math),
SyntaxKind::MathAtom => node.cast().map(Self::MathAtom),
SyntaxKind::MathIdent => node.cast().map(Self::MathIdent),
SyntaxKind::MathAlignPoint => node.cast().map(Self::MathAlignPoint),
SyntaxKind::MathDelimited => node.cast().map(Self::MathDelimited),
@ -280,7 +277,6 @@ impl AstNode for Expr {
Self::Term(v) => v.as_untyped(),
Self::Formula(v) => v.as_untyped(),
Self::Math(v) => v.as_untyped(),
Self::MathAtom(v) => v.as_untyped(),
Self::MathIdent(v) => v.as_untyped(),
Self::MathAlignPoint(v) => v.as_untyped(),
Self::MathDelimited(v) => v.as_untyped(),
@ -320,6 +316,42 @@ impl AstNode for Expr {
}
}
impl Expr {
/// Can this expression be embedded into markup with a hashtag?
pub fn hashtag(&self) -> bool {
match self {
Self::Ident(_) => true,
Self::None(_) => true,
Self::Auto(_) => true,
Self::Bool(_) => true,
Self::Int(_) => true,
Self::Float(_) => true,
Self::Numeric(_) => true,
Self::Str(_) => true,
Self::Code(_) => true,
Self::Content(_) => true,
Self::Array(_) => true,
Self::Dict(_) => true,
Self::Parenthesized(_) => true,
Self::FieldAccess(_) => true,
Self::FuncCall(_) => true,
Self::MethodCall(_) => true,
Self::Let(_) => true,
Self::Set(_) => true,
Self::Show(_) => true,
Self::Conditional(_) => true,
Self::While(_) => true,
Self::For(_) => true,
Self::Import(_) => true,
Self::Include(_) => true,
Self::Break(_) => true,
Self::Continue(_) => true,
Self::Return(_) => true,
_ => false,
}
}
}
impl Default for Expr {
fn default() -> Self {
Expr::Space(Space::default())
@ -393,18 +425,23 @@ impl Shorthand {
"..." => '…',
"*" => '',
"!=" => '≠',
"<<" => '≪',
"<<<" => '⋘',
">>" => '≫',
">>>" => '⋙',
"<=" => '≤',
">=" => '≥',
"<-" => '←',
"->" => '→',
"=>" => '⇒',
"|->" => '↦',
"|=>" => '⤇',
"<->" => '↔',
"<=>" => '⇔',
":=" => '≔',
"[|" => '⟦',
"|]" => '⟧',
"||" => '‖',
"|->" => '↦',
"<->" => '↔',
"<=>" => '⇔',
_ => char::default(),
}
}
@ -660,18 +697,6 @@ impl Math {
}
}
node! {
/// A atom in a formula: `x`, `+`, `12`.
MathAtom
}
impl MathAtom {
/// Get the atom's text.
pub fn get(&self) -> &EcoString {
self.0.text()
}
}
node! {
/// An identifier in a math formula: `pi`.
MathIdent

View File

@ -59,8 +59,6 @@ pub enum SyntaxKind {
/// Mathematical markup.
Math,
/// An atom in math: `x`, `+`, `12`.
MathAtom,
/// An identifier in math: `pi`.
MathIdent,
/// An alignment point in math: `&`.
@ -345,7 +343,6 @@ impl SyntaxKind {
Self::Formula => "math formula",
Self::Math => "math",
Self::MathIdent => "math identifier",
Self::MathAtom => "math atom",
Self::MathAlignPoint => "math alignment point",
Self::MathDelimited => "delimited math",
Self::MathAttach => "math attachments",

View File

@ -380,21 +380,28 @@ impl Lexer<'_> {
'\\' => self.backslash(),
'"' => self.string(),
'*' => SyntaxKind::Shorthand,
'.' if self.s.eat_if("..") => SyntaxKind::Shorthand,
'|' if self.s.eat_if("->") => SyntaxKind::Shorthand,
'<' if self.s.eat_if("->") => SyntaxKind::Shorthand,
'<' if self.s.eat_if("=>") => SyntaxKind::Shorthand,
'|' if self.s.eat_if("=>") => SyntaxKind::Shorthand,
'!' if self.s.eat_if('=') => SyntaxKind::Shorthand,
'<' if self.s.eat_if("<<") => SyntaxKind::Shorthand,
'<' if self.s.eat_if('<') => SyntaxKind::Shorthand,
'>' if self.s.eat_if(">>") => SyntaxKind::Shorthand,
'>' if self.s.eat_if('>') => SyntaxKind::Shorthand,
'<' if self.s.eat_if("=>") => SyntaxKind::Shorthand,
'<' if self.s.eat_if("->") => SyntaxKind::Shorthand,
'<' if self.s.eat_if('=') => SyntaxKind::Shorthand,
'>' if self.s.eat_if('=') => SyntaxKind::Shorthand,
'<' if self.s.eat_if('-') => SyntaxKind::Shorthand,
'-' if self.s.eat_if('>') => SyntaxKind::Shorthand,
'=' if self.s.eat_if('>') => SyntaxKind::Shorthand,
':' if self.s.eat_if('=') => SyntaxKind::Shorthand,
'[' if self.s.eat_if('|') => SyntaxKind::Shorthand,
'|' if self.s.eat_if(']') => SyntaxKind::Shorthand,
'|' if self.s.eat_if('|') => SyntaxKind::Shorthand,
'*' => SyntaxKind::Shorthand,
'#' if !self.s.at(char::is_whitespace) => SyntaxKind::Hashtag,
'_' => SyntaxKind::Underscore,
@ -410,11 +417,11 @@ impl Lexer<'_> {
}
// Other math atoms.
_ => self.atom(start, c),
_ => self.math_text(start, c),
}
}
fn atom(&mut self, start: usize, c: char) -> SyntaxKind {
fn math_text(&mut self, start: usize, c: char) -> SyntaxKind {
// Keep numbers and grapheme clusters together.
if c.is_numeric() {
self.s.eat_while(char::is_numeric);
@ -427,7 +434,7 @@ impl Lexer<'_> {
.map_or(0, str::len);
self.s.jump(start + len);
}
SyntaxKind::MathAtom
SyntaxKind::Text
}
}

View File

@ -713,14 +713,6 @@ impl<'a> LinkedNode<'a> {
Some(next)
}
}
/// Whether an error follows directly after the node.
pub fn before_error(&self) -> bool {
let Some(parent) = self.parent() else { return false };
let Some(index) = self.index.checked_add(1) else { return false };
let Some(node) = parent.node.children().nth(index) else { return false };
node.kind().is_error()
}
}
/// Access to leafs.

View File

@ -234,24 +234,24 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) {
SyntaxKind::Hashtag => embedded_code_expr(p),
SyntaxKind::MathIdent => {
p.eat();
while p.directly_at(SyntaxKind::MathAtom)
while p.directly_at(SyntaxKind::Text)
&& p.current_text() == "."
&& matches!(
p.lexer.clone().next(),
SyntaxKind::MathIdent | SyntaxKind::MathAtom
SyntaxKind::MathIdent | SyntaxKind::Text
)
{
p.convert(SyntaxKind::Dot);
p.convert(SyntaxKind::Ident);
p.wrap(m, SyntaxKind::FieldAccess);
}
if p.directly_at(SyntaxKind::MathAtom) && p.current_text() == "(" {
if p.directly_at(SyntaxKind::Text) && p.current_text() == "(" {
math_args(p);
p.wrap(m, SyntaxKind::FuncCall);
}
}
SyntaxKind::MathAtom | SyntaxKind::Shorthand => {
SyntaxKind::Text | SyntaxKind::Shorthand => {
if math_class(p.current_text()) == Some(MathClass::Fence) {
math_delimited(p, MathClass::Fence)
} else if math_class(p.current_text()) == Some(MathClass::Opening) {
@ -374,16 +374,32 @@ fn math_op(kind: SyntaxKind) -> Option<(SyntaxKind, SyntaxKind, ast::Assoc, usiz
}
fn math_args(p: &mut Parser) {
p.assert(SyntaxKind::MathAtom);
p.assert(SyntaxKind::Text);
let m = p.marker();
let mut m2 = p.marker();
let mut arg = p.marker();
let mut namable = true;
let mut named = None;
while !p.eof() && !p.at(SyntaxKind::Dollar) {
if namable
&& (p.at(SyntaxKind::MathIdent) || p.at(SyntaxKind::Text))
&& p.text[p.current_end()..].starts_with(':')
{
p.convert(SyntaxKind::Ident);
p.convert(SyntaxKind::Colon);
named = Some(arg);
arg = p.marker();
}
match p.current_text() {
")" => break,
"," => {
p.wrap(m2, SyntaxKind::Math);
maybe_wrap_in_math(p, arg, named);
p.convert(SyntaxKind::Comma);
m2 = p.marker();
arg = p.marker();
namable = true;
named = None;
continue;
}
_ => {}
@ -394,12 +410,30 @@ fn math_args(p: &mut Parser) {
if !p.progress(prev) {
p.unexpected();
}
namable = false;
}
if m2 != p.marker() {
p.wrap(m2, SyntaxKind::Math);
if arg != p.marker() {
maybe_wrap_in_math(p, arg, named);
}
p.wrap(m, SyntaxKind::Args);
p.expect(SyntaxKind::MathAtom);
if !p.eat_if(SyntaxKind::Text) {
p.expected("closing paren");
p.balanced = false;
}
}
fn maybe_wrap_in_math(p: &mut Parser, arg: Marker, named: Option<Marker>) {
let exprs = p.post_process(arg).filter(|node| node.is::<ast::Expr>()).count();
if exprs != 1 {
p.wrap(arg, SyntaxKind::Math);
}
if let Some(m) = named {
p.wrap(m, SyntaxKind::Named);
}
}
fn code(p: &mut Parser, mut stop: impl FnMut(SyntaxKind) -> bool) {

View File

@ -99,9 +99,9 @@ fn try_reparse(
&& (parent_kind.is_none() || parent_kind == Some(SyntaxKind::ContentBlock))
&& !overlap.is_empty()
{
// Add one node of slack in both directions.
// Add slack in both directions.
let children = node.children_mut();
let mut start = overlap.start.saturating_sub(1);
let mut start = overlap.start.saturating_sub(2);
let mut end = (overlap.end + 1).min(children.len());
// Expand to the left.
@ -242,7 +242,7 @@ mod tests {
#[test]
fn test_reparse_markup() {
test("abc~def~ghi", 5..6, "+", true);
test("abc~def~gh~", 5..6, "+", true);
test("~~~~~~~", 3..4, "A", true);
test("abc~~", 1..2, "", true);
test("#var. hello", 5..6, " ", false);
@ -264,7 +264,6 @@ mod tests {
test("#show f: a => b..", 16..16, "c", false);
test("#for", 4..4, "//", false);
test("a\n#let \nb", 7..7, "i", true);
test("#let x = (1, 2 + ;~ Five\r\n\r", 20..23, "2.", true);
test(r"#{{let x = z}; a = 1} b", 7..7, "//", false);
test(r#"a ```typst hello```"#, 16..17, "", false);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -49,7 +49,7 @@
#test("c" in dict, true)
#test(dict.len(), 3)
#test(dict.values(), (3, 1, 2))
#test(dict.pairs((k, v) => k + str(v)).join(), "a3b1c2")
#test(dict.pairs().map(p => p.first() + str(p.last())).join(), "a3b1c2")
#{ dict.remove("c") }
#test("c" in dict, false)

View File

@ -34,7 +34,7 @@
// Map captured arguments.
#let f1(..args) = args.pos().map(repr)
#let f2(..args) = args.named().pairs((k, v) => repr(k) + ": " + repr(v))
#let f2(..args) = args.named().pairs().map(p => repr(p.first()) + ": " + repr(p.last()))
#let f(..args) = (f1(..args) + f2(..args)).join(", ")
#f(1, a: 2)