mirror of
https://github.com/typst/typst
synced 2025-05-13 20:46:23 +08:00
More capable math calls
This commit is contained in:
parent
28c554ec21
commit
4809e685a2
@ -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(),
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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(())
|
||||
}
|
||||
}
|
@ -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(())
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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(),
|
||||
))
|
||||
}
|
||||
|
@ -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)?;
|
||||
|
@ -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" => '∜',
|
||||
|
@ -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};
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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>()
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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)?;
|
||||
|
@ -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)]
|
||||
|
@ -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);
|
||||
|
@ -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)],
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
|
@ -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 }
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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) {
|
||||
|
@ -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 |
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user