mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
565 lines
15 KiB
Rust
565 lines
15 KiB
Rust
#![allow(non_snake_case)]
|
||
|
||
use std::fmt::Debug;
|
||
|
||
use super::parse;
|
||
use crate::color::RgbaColor;
|
||
use crate::diag::{Diag, Level, Pass};
|
||
use crate::eval::DictKey;
|
||
use crate::geom::Unit;
|
||
use crate::syntax::*;
|
||
|
||
use BinOp::*;
|
||
use SynNode::{Emph, Linebreak, Parbreak, Space, Strong};
|
||
use UnOp::*;
|
||
|
||
macro_rules! t {
|
||
($src:literal
|
||
nodes: [$($node:expr),* $(,)?]
|
||
$(, errors: [$($err:expr),* $(,)?])?
|
||
$(, warnings: [$($warn:expr),* $(,)?])?
|
||
$(, spans: $spans:expr)?
|
||
$(,)?
|
||
) => {{
|
||
#[allow(unused)]
|
||
let mut spans = false;
|
||
$(spans = $spans;)?
|
||
|
||
let Pass { output, feedback } = parse($src);
|
||
check($src, Content![@$($node),*], output, spans);
|
||
check(
|
||
$src,
|
||
vec![
|
||
$($(into!($err).map(|s: &str| Diag::new(Level::Error, s)),)*)?
|
||
$($(into!($warn).map(|s: &str| Diag::new(Level::Warning, s)),)*)?
|
||
],
|
||
feedback.diags,
|
||
true,
|
||
);
|
||
}};
|
||
|
||
($src:literal $($node:expr),* $(,)?) => {
|
||
t!($src nodes: [$($node),*]);
|
||
};
|
||
}
|
||
|
||
/// Assert that expected and found are equal, printing both and the source of
|
||
/// the test case if they aren't.
|
||
///
|
||
/// When `cmp_spans` is false, spans are ignored.
|
||
#[track_caller]
|
||
pub fn check<T>(src: &str, exp: T, found: T, cmp_spans: bool)
|
||
where
|
||
T: Debug + PartialEq,
|
||
{
|
||
Span::set_cmp(cmp_spans);
|
||
|
||
if exp != found {
|
||
println!("source: {:?}", src);
|
||
println!("expected: {:#?}", exp);
|
||
println!("found: {:#?}", found);
|
||
panic!("test failed");
|
||
}
|
||
|
||
Span::set_cmp(true);
|
||
}
|
||
|
||
/// Shorthand for `Spanned::new`.
|
||
fn S<T>(span: impl Into<Span>, v: T) -> Spanned<T> {
|
||
Spanned::new(v, span)
|
||
}
|
||
|
||
// Enables tests to optionally specify spans.
|
||
impl<T> From<T> for Spanned<T> {
|
||
fn from(t: T) -> Self {
|
||
Spanned::zero(t)
|
||
}
|
||
}
|
||
|
||
/// Shorthand for `Into::<Spanned<_>>::into`.
|
||
macro_rules! into {
|
||
($val:expr) => {
|
||
Into::<Spanned<_>>::into($val)
|
||
};
|
||
}
|
||
|
||
fn Text(text: &str) -> SynNode {
|
||
SynNode::Text(text.into())
|
||
}
|
||
|
||
fn Heading(level: impl Into<Spanned<u8>>, contents: SynTree) -> SynNode {
|
||
SynNode::Heading(NodeHeading { level: level.into(), contents })
|
||
}
|
||
|
||
fn Raw(lang: Option<&str>, lines: &[&str], inline: bool) -> SynNode {
|
||
SynNode::Raw(NodeRaw {
|
||
lang: lang.map(|id| Ident(id.into())),
|
||
lines: lines.iter().map(ToString::to_string).collect(),
|
||
inline,
|
||
})
|
||
}
|
||
|
||
fn Id(ident: &str) -> Expr {
|
||
Expr::Lit(Lit::Ident(Ident(ident.to_string())))
|
||
}
|
||
|
||
fn Bool(b: bool) -> Expr {
|
||
Expr::Lit(Lit::Bool(b))
|
||
}
|
||
|
||
fn Int(int: i64) -> Expr {
|
||
Expr::Lit(Lit::Int(int))
|
||
}
|
||
|
||
fn Float(float: f64) -> Expr {
|
||
Expr::Lit(Lit::Float(float))
|
||
}
|
||
|
||
fn Percent(percent: f64) -> Expr {
|
||
Expr::Lit(Lit::Percent(percent))
|
||
}
|
||
|
||
fn Length(val: f64, unit: Unit) -> Expr {
|
||
Expr::Lit(Lit::Length(val, unit))
|
||
}
|
||
|
||
fn Color(color: RgbaColor) -> Expr {
|
||
Expr::Lit(Lit::Color(color))
|
||
}
|
||
|
||
fn Str(string: &str) -> Expr {
|
||
Expr::Lit(Lit::Str(string.to_string()))
|
||
}
|
||
|
||
fn Block(expr: Expr) -> SynNode {
|
||
SynNode::Expr(expr)
|
||
}
|
||
|
||
fn Binary(
|
||
lhs: impl Into<Spanned<Expr>>,
|
||
op: impl Into<Spanned<BinOp>>,
|
||
rhs: impl Into<Spanned<Expr>>,
|
||
) -> Expr {
|
||
Expr::Binary(ExprBinary {
|
||
lhs: Box::new(lhs.into()),
|
||
op: op.into(),
|
||
rhs: Box::new(rhs.into()),
|
||
})
|
||
}
|
||
|
||
fn Unary(op: impl Into<Spanned<UnOp>>, expr: impl Into<Spanned<Expr>>) -> Expr {
|
||
Expr::Unary(ExprUnary {
|
||
op: op.into(),
|
||
expr: Box::new(expr.into()),
|
||
})
|
||
}
|
||
|
||
macro_rules! Dict {
|
||
(@$($a:expr $(=> $b:expr)?),* $(,)?) => {
|
||
LitDict(vec![$(#[allow(unused)] {
|
||
let key: Option<Spanned<DictKey>> = None;
|
||
let expr = $a;
|
||
$(
|
||
let key = Some(into!($a).map(|s: &str| s.into()));
|
||
let expr = $b;
|
||
)?
|
||
LitDictEntry { key, expr: into!(expr) }
|
||
}),*])
|
||
};
|
||
($($tts:tt)*) => (Expr::Lit(Lit::Dict(Dict![@$($tts)*])));
|
||
}
|
||
|
||
macro_rules! Content {
|
||
(@$($node:expr),* $(,)?) => (vec![$(into!($node)),*]);
|
||
($($tts:tt)*) => (Expr::Lit(Lit::Content(Content![@$($tts)*])));
|
||
}
|
||
|
||
macro_rules! Call {
|
||
(@@$name:expr) => {
|
||
Call!(@@$name, Args![])
|
||
};
|
||
(@@$name:expr, $args:expr) => {
|
||
ExprCall {
|
||
name: into!($name).map(|s: &str| Ident(s.into())),
|
||
args: into!($args),
|
||
}
|
||
};
|
||
(@$($tts:tt)*) => (Expr::Call(Call!(@@$($tts)*)));
|
||
($($tts:tt)*) => (SynNode::Expr(Call!(@$($tts)*)));
|
||
}
|
||
|
||
macro_rules! Args {
|
||
($($tts:tt)*) => (Dict![@$($tts)*]);
|
||
}
|
||
|
||
#[test]
|
||
fn test_parse_comments() {
|
||
// In body.
|
||
t!("a// you\nb" Text("a"), Space, Text("b"));
|
||
t!("* // \n /*\n\n*/*" Strong, Space, Space, Strong);
|
||
|
||
// In header.
|
||
t!("[v /*12pt*/]" Call!("v"));
|
||
t!("[v //\n]" Call!("v"));
|
||
t!("[v 12, /*\n*/ size: 14]" Call!("v", Args![Int(12), "size" => Int(14)]));
|
||
|
||
// Error.
|
||
t!("a*/b"
|
||
nodes: [Text("a"), Text("b")],
|
||
errors: [S(1..3, "unexpected end of block comment")]);
|
||
}
|
||
|
||
#[test]
|
||
fn test_parse_simple_nodes() {
|
||
// Basics.
|
||
t!("");
|
||
t!(" " Space);
|
||
t!("hi" Text("hi"));
|
||
t!("🧽" Text("🧽"));
|
||
t!("_" Emph);
|
||
t!("*" Strong);
|
||
t!("~" Text("\u{00A0}"));
|
||
t!(r"\" Linebreak);
|
||
t!("\n\n" Parbreak);
|
||
|
||
// Multiple nodes.
|
||
t!("ab c" Text("ab"), Space, Text("c"));
|
||
t!("a`hi`\r\n\r*" Text("a"), Raw(None, &["hi"], true), Parbreak, Strong);
|
||
|
||
// Spans.
|
||
t!("*🌍*"
|
||
nodes: [S(0..1, Strong), S(1..5, Text("🌍")), S(5..6, Strong)],
|
||
spans: true);
|
||
|
||
// Errors.
|
||
t!("]}"
|
||
nodes: [],
|
||
errors: [S(0..1, "unexpected closing bracket"),
|
||
S(1..2, "unexpected closing brace")]);
|
||
}
|
||
|
||
#[test]
|
||
fn test_parse_headings() {
|
||
// Basics with spans.
|
||
t!("#a"
|
||
nodes: [S(0..2, Heading(S(0..1, 0), Content![@S(1..2, Text("a"))]))],
|
||
spans: true);
|
||
|
||
// Multiple hashtags.
|
||
t!("###three" Heading(2, Content![@Text("three")]));
|
||
t!("###### six" Heading(5, Content![@Space, Text("six")]));
|
||
|
||
// Start of heading.
|
||
t!("/**/#" Heading(0, Content![@]));
|
||
t!("[f][#ok]" Call!("f", Args![Content![Heading(0, Content![@Text("ok")])]]));
|
||
|
||
// End of heading.
|
||
t!("#a\nb" Heading(0, Content![@Text("a")]), Space, Text("b"));
|
||
|
||
// Continued heading.
|
||
t!("#a{\n1\n}b" Heading(0, Content![@Text("a"), Block(Int(1)), Text("b")]));
|
||
t!("#a[f][\n\n]d" Heading(0, Content![@
|
||
Text("a"), Call!("f", Args![Content![Parbreak]]), Text("d"),
|
||
]));
|
||
|
||
// No heading.
|
||
t!(r"\#" Text("#"));
|
||
t!("Nr. #1" Text("Nr."), Space, Text("#"), Text("1"));
|
||
t!("[v]#" Call!("v"), Text("#"));
|
||
|
||
// Too many hashtags.
|
||
t!("####### seven"
|
||
nodes: [Heading(5, Content![@Space, Text("seven")])],
|
||
warnings: [S(0..7, "section depth should not exceed 6")]);
|
||
}
|
||
|
||
#[test]
|
||
fn test_parse_raw() {
|
||
// Basic, mostly tested in tokenizer and resolver.
|
||
t!("`py`" nodes: [S(0..4, Raw(None, &["py"], true))], spans: true);
|
||
t!("`endless"
|
||
nodes: [Raw(None, &["endless"], true)],
|
||
errors: [S(8..8, "expected backtick(s)")]);
|
||
}
|
||
|
||
#[test]
|
||
fn test_parse_escape_sequences() {
|
||
// Basic, mostly tested in tokenizer.
|
||
t!(r"\[" Text("["));
|
||
t!(r"\u{1F3D5}" nodes: [S(0..9, Text("🏕"))], spans: true);
|
||
|
||
// Bad value.
|
||
t!(r"\u{FFFFFF}"
|
||
nodes: [Text(r"\u{FFFFFF}")],
|
||
errors: [S(0..10, "invalid unicode escape sequence")]);
|
||
|
||
// No closing brace.
|
||
t!(r"\u{41*"
|
||
nodes: [Text("A"), Strong],
|
||
errors: [S(5..5, "expected closing brace")]);
|
||
}
|
||
|
||
#[test]
|
||
fn test_parse_groups() {
|
||
// Test paren group.
|
||
t!("{([v 1) + 3}"
|
||
nodes: [Block(Binary(
|
||
Content![Call!("v", Args![Int(1)])],
|
||
Add,
|
||
Int(3),
|
||
))],
|
||
errors: [S(6..6, "expected closing bracket")]);
|
||
|
||
// Test bracket group.
|
||
t!("[)"
|
||
nodes: [Call!("")],
|
||
errors: [S(1..2, "expected function name, found closing paren"),
|
||
S(2..2, "expected closing bracket")]);
|
||
|
||
t!("[v {]}"
|
||
nodes: [Call!("v", Args![Content![]])],
|
||
errors: [S(4..4, "expected closing brace"),
|
||
S(5..6, "unexpected closing brace")]);
|
||
|
||
// Test brace group.
|
||
t!("{1 + [}"
|
||
nodes: [Block(Binary(Int(1), Add, Content![Call!("")]))],
|
||
errors: [S(6..6, "expected function name"),
|
||
S(6..6, "expected closing bracket")]);
|
||
|
||
// Test subheader group.
|
||
t!("[v (|u )]"
|
||
nodes: [Call!("v", Args![Dict![], Content![Call!("u")]])],
|
||
errors: [S(4..4, "expected closing paren"),
|
||
S(7..8, "expected expression, found closing paren")]);
|
||
}
|
||
|
||
#[test]
|
||
fn test_parse_blocks() {
|
||
// Basic with spans.
|
||
t!("{1}" nodes: [S(0..3, Block(Int(1)))], spans: true);
|
||
|
||
// Function calls.
|
||
t!("{f()}" Call!("f"));
|
||
t!("{[f]}" Block(Content![Call!("f")]));
|
||
|
||
// Missing or bad value.
|
||
t!("{}{1u}"
|
||
nodes: [],
|
||
errors: [S(1..1, "expected expression"),
|
||
S(3..5, "expected expression, found invalid token")]);
|
||
}
|
||
|
||
#[test]
|
||
fn test_parse_bracket_funcs() {
|
||
// Basic.
|
||
t!("[function]" Call!("function"));
|
||
t!("[ v ]" Call!("v"));
|
||
|
||
// Body and no body.
|
||
t!("[v][[f]]" Call!("v", Args![Content![Call!("f")]]));
|
||
t!("[v][v][v]" Call!("v", Args![Content![Text("v")]]), Call!("v"));
|
||
t!("[v] [f]" Call!("v"), Space, Call!("f"));
|
||
|
||
// Spans.
|
||
t!("[v 1][📐]"
|
||
nodes: [S(0..11, Call!(S(1..2, "v"), S(3..4, Args![
|
||
S(3..4, Int(1)),
|
||
S(5..11, Content![S(6..10, Text("📐"))]),
|
||
])))],
|
||
spans: true);
|
||
|
||
// No name and no closing bracket.
|
||
t!("["
|
||
nodes: [Call!("")],
|
||
errors: [S(1..1, "expected function name"),
|
||
S(1..1, "expected closing bracket")]);
|
||
|
||
// No name.
|
||
t!("[]"
|
||
nodes: [Call!("")],
|
||
errors: [S(1..1, "expected function name")]);
|
||
|
||
// Bad name.
|
||
t!("[# 1]"
|
||
nodes: [Call!("", Args![Int(1)])],
|
||
errors: [S(1..2, "expected function name, found hex value")]);
|
||
|
||
// String header eats closing bracket.
|
||
t!(r#"[v "]"#
|
||
nodes: [Call!("v", Args![Str("]")])],
|
||
errors: [S(5..5, "expected quote"),
|
||
S(5..5, "expected closing bracket")]);
|
||
|
||
// Raw in body eats closing bracket.
|
||
t!("[v][`a]`"
|
||
nodes: [Call!("v", Args![Content![Raw(None, &["a]"], true)]])],
|
||
errors: [S(8..8, "expected closing bracket")]);
|
||
}
|
||
|
||
#[test]
|
||
fn test_parse_chaining() {
|
||
// Basic.
|
||
t!("[a | b]" Call!("a", Args![Content![Call!("b")]]));
|
||
t!("[a | b | c]" Call!("a", Args![Content![
|
||
Call!("b", Args![Content![Call!("c")]])
|
||
]]));
|
||
|
||
// With body and spans.
|
||
t!("[a|b][💕]"
|
||
nodes: [S(0..11, Call!(S(1..2, "a"), S(2..2, Args![
|
||
S(3..11, Content![S(3..11, Call!(S(3..4, "b"), S(4..4, Args![
|
||
S(5..11, Content![S(6..10, Text("💕"))])
|
||
])))])
|
||
])))],
|
||
spans: true);
|
||
|
||
// No name in second subheader.
|
||
t!("[a 1|]"
|
||
nodes: [Call!("a", Args![Int(1), Content![Call!("")]])],
|
||
errors: [S(5..5, "expected function name")]);
|
||
|
||
// No name in first subheader.
|
||
t!("[|a true]"
|
||
nodes: [Call!("", Args![Content![Call!("a", Args![Bool(true)])]])],
|
||
errors: [S(1..1, "expected function name")]);
|
||
}
|
||
|
||
#[test]
|
||
fn test_parse_arguments() {
|
||
// Bracket functions.
|
||
t!("[v 1]" Call!("v", Args![Int(1)]));
|
||
t!("[v 1,]" Call!("v", Args![Int(1)]));
|
||
t!("[v a]" Call!("v", Args![Id("a")]));
|
||
t!("[v a,]" Call!("v", Args![Id("a")]));
|
||
t!("[v a:2]" Call!("v", Args!["a" => Int(2)]));
|
||
|
||
// Parenthesized function with nested dictionary literal.
|
||
t!(r#"{f(1, a: (2, 3), #004, b: "five")}"# Block(Call!(@"f", Args![
|
||
Int(1),
|
||
"a" => Dict![Int(2), Int(3)],
|
||
Color(RgbaColor::new(0, 0, 0x44, 0xff)),
|
||
"b" => Str("five"),
|
||
])));
|
||
|
||
// Bad expression.
|
||
t!("[v */]"
|
||
nodes: [Call!("v", Args![])],
|
||
errors: [S(3..5, "expected expression, found end of block comment")]);
|
||
|
||
// Missing comma between arguments.
|
||
t!("[v 1 2]"
|
||
nodes: [Call!("v", Args![Int(1), Int(2)])],
|
||
errors: [S(4..4, "expected comma")]);
|
||
|
||
// Missing expression after name.
|
||
t!("[v a:]"
|
||
nodes: [Call!("v", Args![])],
|
||
errors: [S(5..5, "expected expression")]);
|
||
|
||
// Bad expression after name.
|
||
t!("[v a:1:]"
|
||
nodes: [Call!("v", Args!["a" => Int(1)])],
|
||
errors: [S(6..7, "expected expression, found colon")]);
|
||
|
||
// Name has to be identifier. Number parsed as positional argument.
|
||
t!("[v 1:]"
|
||
nodes: [Call!("v", Args![Int(1)])],
|
||
errors: [S(4..5, "expected expression, found colon")]);
|
||
|
||
// Parsed as two positional arguments.
|
||
t!("[v 1:2]"
|
||
nodes: [Call!("v", Args![Int(1), Int(2)])],
|
||
errors: [S(4..5, "expected expression, found colon"),
|
||
S(4..4, "expected comma")]);
|
||
}
|
||
|
||
#[test]
|
||
fn test_parse_dict_literals() {
|
||
// Basic.
|
||
t!("{()}" Block(Dict![]));
|
||
|
||
// With spans.
|
||
t!("{(1, two: 2)}"
|
||
nodes: [S(0..13, Block(Dict![
|
||
S(2..3, Int(1)),
|
||
S(5..8, "two") => S(10..11, Int(2)),
|
||
]))],
|
||
spans: true);
|
||
|
||
// Unclosed.
|
||
t!("{(}"
|
||
nodes: [Block(Dict![])],
|
||
errors: [S(2..2, "expected closing paren")]);
|
||
}
|
||
|
||
#[test]
|
||
fn test_parse_expressions() {
|
||
// Parenthesis.
|
||
t!("{(x)}" Block(Id("x")));
|
||
|
||
// Unary operations.
|
||
t!("{-1}" Block(Unary(Neg, Int(1))));
|
||
t!("{--1}" Block(Unary(Neg, Unary(Neg, Int(1)))));
|
||
|
||
// Binary operations.
|
||
t!(r#"{"x"+"y"}"# Block(Binary(Str("x"), Add, Str("y"))));
|
||
t!("{1-2}" Block(Binary(Int(1), Sub, Int(2))));
|
||
t!("{a * b}" Block(Binary(Id("a"), Mul, Id("b"))));
|
||
t!("{12pt/.4}" Block(Binary(Length(12.0, Unit::Pt), Div, Float(0.4))));
|
||
|
||
// Associativity.
|
||
t!("{1+2+3}" Block(Binary(Binary(Int(1), Add, Int(2)), Add, Int(3))));
|
||
t!("{1/2*3}" Block(Binary(Binary(Int(1), Div, Int(2)), Mul, Int(3))));
|
||
|
||
// Precedence.
|
||
t!("{1+2*-3}" Block(Binary(
|
||
Int(1), Add, Binary(Int(2), Mul, Unary(Neg, Int(3))),
|
||
)));
|
||
|
||
// Confusion with floating-point literal.
|
||
t!("{1e-3-4e+4}" Block(Binary(Float(1e-3), Sub, Float(4e+4))));
|
||
|
||
// Spans + parentheses winning over precedence.
|
||
t!("{(1+2)*3}"
|
||
nodes: [S(0..9, Block(Binary(
|
||
S(1..6, Binary(S(2..3, Int(1)), S(3..4, Add), S(4..5, Int(2)))),
|
||
S(6..7, Mul),
|
||
S(7..8, Int(3)),
|
||
)))],
|
||
spans: true);
|
||
|
||
// Errors.
|
||
t!("{-}{1+}{2*}"
|
||
nodes: [Block(Int(1)), Block(Int(2))],
|
||
errors: [S(2..2, "expected expression"),
|
||
S(6..6, "expected expression"),
|
||
S(10..10, "expected expression")]);
|
||
}
|
||
|
||
#[test]
|
||
fn test_parse_values() {
|
||
// Basics.
|
||
t!("{_}" Block(Id("_")));
|
||
t!("{name}" Block(Id("name")));
|
||
t!("{ke-bab}" Block(Id("ke-bab")));
|
||
t!("{α}" Block(Id("α")));
|
||
t!("{true}" Block(Bool(true)));
|
||
t!("{false}" Block(Bool(false)));
|
||
t!("{1.0e-4}" Block(Float(1e-4)));
|
||
t!("{3.15}" Block(Float(3.15)));
|
||
t!("{50%}" Block(Percent(50.0)));
|
||
t!("{4.5cm}" Block(Length(4.5, Unit::Cm)));
|
||
t!("{12e1pt}" Block(Length(12e1, Unit::Pt)));
|
||
|
||
// Strings.
|
||
t!(r#"{"hi"}"# Block(Str("hi")));
|
||
t!(r#"{"a\n[]\"\u{1F680}string"}"# Block(Str("a\n[]\"🚀string")));
|
||
|
||
// Colors.
|
||
t!("{#f7a20500}" Block(Color(RgbaColor::new(0xf7, 0xa2, 0x05, 0))));
|
||
t!("{#a5}"
|
||
nodes: [Block(Color(RgbaColor::new(0, 0, 0, 0xff)))],
|
||
errors: [S(1..4, "invalid color")]);
|
||
}
|