Par nodes 🧳
This commit is contained in:
parent
dbfb3d2ced
commit
ed4fdcb0ad
BIN
fonts/SegoeUI-Emoji.ttf
Normal file
BIN
fonts/SegoeUI-Emoji.ttf
Normal file
Binary file not shown.
@ -142,8 +142,6 @@ pub enum Command<'a> {
|
||||
|
||||
/// Start a new line.
|
||||
BreakLine,
|
||||
/// Start a new paragraph.
|
||||
BreakParagraph,
|
||||
/// Start a new page, which will be part of the finished layout even if it
|
||||
/// stays empty (since the page break is a _hard_ space break).
|
||||
BreakPage,
|
||||
|
@ -43,26 +43,43 @@ impl<'a> TreeLayouter<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn layout_tree(&mut self, tree: &SyntaxTree) {
|
||||
for node in tree {
|
||||
self.layout_node(node).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(self) -> Pass<MultiLayout> {
|
||||
Pass::new(self.layouter.finish(), self.feedback)
|
||||
}
|
||||
|
||||
fn layout_tree<'t>(&'t mut self, tree: &'t SyntaxTree) -> DynFuture<'t, ()> {
|
||||
Box::pin(async move {
|
||||
for node in tree {
|
||||
self.layout_node(node).await;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn layout_node(&mut self, node: &Spanned<SyntaxNode>) {
|
||||
let decorate = |this: &mut TreeLayouter, deco| {
|
||||
let decorate = |this: &mut Self, deco| {
|
||||
this.feedback.decorations.push(Spanned::new(deco, node.span));
|
||||
};
|
||||
|
||||
match &node.v {
|
||||
SyntaxNode::Space => self.layout_space(),
|
||||
SyntaxNode::Parbreak => self.layout_paragraph(),
|
||||
SyntaxNode::Spacing => {
|
||||
self.layouter.add_primary_spacing(
|
||||
self.style.text.word_spacing(),
|
||||
SpacingKind::WORD,
|
||||
);
|
||||
},
|
||||
|
||||
SyntaxNode::Linebreak => self.layouter.finish_line(),
|
||||
|
||||
SyntaxNode::ToggleItalic => {
|
||||
self.style.text.italic = !self.style.text.italic;
|
||||
decorate(self, Decoration::Italic);
|
||||
}
|
||||
|
||||
SyntaxNode::ToggleBolder => {
|
||||
self.style.text.bolder = !self.style.text.bolder;
|
||||
decorate(self, Decoration::Bold);
|
||||
}
|
||||
|
||||
SyntaxNode::Text(text) => {
|
||||
if self.style.text.italic {
|
||||
decorate(self, Decoration::Italic);
|
||||
@ -75,16 +92,6 @@ impl<'a> TreeLayouter<'a> {
|
||||
self.layout_text(text).await;
|
||||
}
|
||||
|
||||
SyntaxNode::ToggleItalic => {
|
||||
self.style.text.italic = !self.style.text.italic;
|
||||
decorate(self, Decoration::Italic);
|
||||
}
|
||||
|
||||
SyntaxNode::ToggleBolder => {
|
||||
self.style.text.bolder = !self.style.text.bolder;
|
||||
decorate(self, Decoration::Bold);
|
||||
}
|
||||
|
||||
SyntaxNode::Raw(lines) => {
|
||||
// TODO: Make this more efficient.
|
||||
let fallback = self.style.text.fallback.clone();
|
||||
@ -109,14 +116,25 @@ impl<'a> TreeLayouter<'a> {
|
||||
self.style.text.fallback = fallback;
|
||||
}
|
||||
|
||||
SyntaxNode::Par(par) => self.layout_par(par).await,
|
||||
|
||||
SyntaxNode::Dyn(dynamic) => {
|
||||
self.layout_dyn(Spanned::new(dynamic.as_ref(), node.span)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn layout_par(&mut self, par: &SyntaxTree) {
|
||||
self.layouter.add_secondary_spacing(
|
||||
self.style.text.paragraph_spacing(),
|
||||
SpacingKind::PARAGRAPH,
|
||||
);
|
||||
|
||||
self.layout_tree(par).await;
|
||||
}
|
||||
|
||||
async fn layout_dyn(&mut self, dynamic: Spanned<&dyn DynamicNode>) {
|
||||
// Execute the tree's command-generating layout function.
|
||||
// Execute the dynamic node's command-generating layout function.
|
||||
let layouted = dynamic.v.layout(LayoutContext {
|
||||
style: &self.style,
|
||||
spaces: self.layouter.remaining(),
|
||||
@ -131,11 +149,21 @@ impl<'a> TreeLayouter<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_command<'r>(
|
||||
&'r mut self,
|
||||
command: Command<'r>,
|
||||
tree_span: Span,
|
||||
) -> DynFuture<'r, ()> { Box::pin(async move {
|
||||
async fn layout_text(&mut self, text: &str) {
|
||||
self.layouter.add(
|
||||
layout_text(
|
||||
text,
|
||||
TextContext {
|
||||
loader: &self.ctx.loader,
|
||||
style: &self.style.text,
|
||||
dir: self.ctx.axes.primary,
|
||||
align: self.ctx.align,
|
||||
}
|
||||
).await
|
||||
);
|
||||
}
|
||||
|
||||
async fn execute_command(&mut self, command: Command<'_>, span: Span) {
|
||||
use Command::*;
|
||||
|
||||
match command {
|
||||
@ -149,13 +177,12 @@ impl<'a> TreeLayouter<'a> {
|
||||
}
|
||||
|
||||
BreakLine => self.layouter.finish_line(),
|
||||
BreakParagraph => self.layout_paragraph(),
|
||||
BreakPage => {
|
||||
if self.ctx.root {
|
||||
self.layouter.finish_space(true)
|
||||
} else {
|
||||
error!(
|
||||
@self.feedback, tree_span,
|
||||
@self.feedback, span,
|
||||
"page break cannot only be issued from root context",
|
||||
);
|
||||
}
|
||||
@ -183,7 +210,7 @@ impl<'a> TreeLayouter<'a> {
|
||||
], true);
|
||||
} else {
|
||||
error!(
|
||||
@self.feedback, tree_span,
|
||||
@self.feedback, span,
|
||||
"page style cannot only be changed from root context",
|
||||
);
|
||||
}
|
||||
@ -195,33 +222,5 @@ impl<'a> TreeLayouter<'a> {
|
||||
self.ctx.axes = axes;
|
||||
}
|
||||
}
|
||||
}) }
|
||||
|
||||
async fn layout_text(&mut self, text: &str) {
|
||||
self.layouter.add(
|
||||
layout_text(
|
||||
text,
|
||||
TextContext {
|
||||
loader: &self.ctx.loader,
|
||||
style: &self.style.text,
|
||||
dir: self.ctx.axes.primary,
|
||||
align: self.ctx.align,
|
||||
}
|
||||
).await
|
||||
);
|
||||
}
|
||||
|
||||
fn layout_space(&mut self) {
|
||||
self.layouter.add_primary_spacing(
|
||||
self.style.text.word_spacing(),
|
||||
SpacingKind::WORD,
|
||||
);
|
||||
}
|
||||
|
||||
fn layout_paragraph(&mut self) {
|
||||
self.layouter.add_secondary_spacing(
|
||||
self.style.text.paragraph_spacing(),
|
||||
SpacingKind::PARAGRAPH,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,6 @@ pub fn std() -> Scope {
|
||||
std.add::<PageFunc>("page");
|
||||
std.add::<AlignFunc>("align");
|
||||
std.add::<BoxFunc>("box");
|
||||
std.add::<ParBreakFunc>("parbreak");
|
||||
std.add::<PageBreakFunc>("pagebreak");
|
||||
std.add_with_meta::<SpacingFunc>("h", Horizontal);
|
||||
std.add_with_meta::<SpacingFunc>("v", Vertical);
|
||||
|
@ -57,3 +57,12 @@ function! {
|
||||
vec![SetPageStyle(style)]
|
||||
}
|
||||
}
|
||||
|
||||
function! {
|
||||
/// `pagebreak`: Ends the current page.
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct PageBreakFunc;
|
||||
|
||||
parse(default)
|
||||
layout(self, ctx, f) { vec![BreakPage] }
|
||||
}
|
||||
|
@ -2,26 +2,6 @@ use crate::layout::SpacingKind;
|
||||
use crate::length::ScaleLength;
|
||||
use super::*;
|
||||
|
||||
function! {
|
||||
/// `parbreak`: Ends the current paragraph.
|
||||
///
|
||||
/// This has the same effect as two subsequent newlines.
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct ParBreakFunc;
|
||||
|
||||
parse(default)
|
||||
layout(self, ctx, f) { vec![BreakParagraph] }
|
||||
}
|
||||
|
||||
function! {
|
||||
/// `pagebreak`: Ends the current page.
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct PageBreakFunc;
|
||||
|
||||
parse(default)
|
||||
layout(self, ctx, f) { vec![BreakPage] }
|
||||
}
|
||||
|
||||
function! {
|
||||
/// `h` and `v`: Add horizontal or vertical spacing.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
|
@ -75,8 +75,10 @@ impl Default for TextStyle {
|
||||
"monospace" => ["source code pro", "noto sans mono"],
|
||||
"math" => ["latin modern math", "serif"],
|
||||
},
|
||||
base: ["source sans pro", "noto sans",
|
||||
"noto emoji", "latin modern math"],
|
||||
base: [
|
||||
"source sans pro", "noto sans", "segoe ui emoji",
|
||||
"noto emoji", "latin modern math",
|
||||
],
|
||||
},
|
||||
variant: FontVariant {
|
||||
style: FontStyle::Normal,
|
||||
|
@ -96,6 +96,7 @@ pub struct ParseState {
|
||||
/// function's body.
|
||||
pub fn parse(src: &str, offset: Pos, state: &ParseState) -> Pass<SyntaxTree> {
|
||||
let mut tree = SyntaxTree::new();
|
||||
let mut par = SyntaxTree::new();
|
||||
let mut feedback = Feedback::new();
|
||||
|
||||
for token in Tokens::new(src, offset, TokenMode::Body) {
|
||||
@ -103,10 +104,16 @@ pub fn parse(src: &str, offset: Pos, state: &ParseState) -> Pass<SyntaxTree> {
|
||||
let node = match token.v {
|
||||
// Starting from two newlines counts as a paragraph break, a single
|
||||
// newline does not.
|
||||
Token::Space(newlines) => if newlines >= 2 {
|
||||
SyntaxNode::Parbreak
|
||||
Token::Space(newlines) => if newlines < 2 {
|
||||
SyntaxNode::Spacing
|
||||
} else {
|
||||
SyntaxNode::Space
|
||||
// End the current paragraph if it is not empty.
|
||||
if let (Some(first), Some(last)) = (par.first(), par.last()) {
|
||||
let span = Span::merge(first.span, last.span);
|
||||
let node = SyntaxNode::Par(std::mem::take(&mut par));
|
||||
tree.push(Spanned::new(node, span));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
Token::Function { header, body, terminated } => {
|
||||
@ -136,6 +143,12 @@ pub fn parse(src: &str, offset: Pos, state: &ParseState) -> Pass<SyntaxTree> {
|
||||
}
|
||||
};
|
||||
|
||||
par.push(Spanned::new(node, span));
|
||||
}
|
||||
|
||||
if let (Some(first), Some(last)) = (par.first(), par.last()) {
|
||||
let span = Span::merge(first.span, last.span);
|
||||
let node = SyntaxNode::Par(par);
|
||||
tree.push(Spanned::new(node, span));
|
||||
}
|
||||
|
||||
@ -671,8 +684,7 @@ mod tests {
|
||||
use Decoration::*;
|
||||
use Expr::{Bool, Length as Len, Number as Num};
|
||||
use SyntaxNode::{
|
||||
Space as S, Parbreak, Linebreak, ToggleItalic as Italic,
|
||||
ToggleBolder as Bold,
|
||||
Spacing as S, Linebreak, ToggleItalic as Italic, ToggleBolder as Bold,
|
||||
};
|
||||
|
||||
/// Test whether the given string parses into
|
||||
@ -714,10 +726,10 @@ mod tests {
|
||||
};
|
||||
}
|
||||
|
||||
/// Shorthand for `p!("[val: ...]" => func!("val", ...))`.
|
||||
/// Shorthand for `p!("[val: ...]" => par![func!("val", ...)])`.
|
||||
macro_rules! pval {
|
||||
($header:expr => $($tts:tt)*) => {
|
||||
p!(concat!("[val: ", $header, "]") => [func!("val": $($tts)*)]);
|
||||
p!(concat!("[val: ", $header, "]") => [par![func!("val": $($tts)*)]]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -725,7 +737,6 @@ mod tests {
|
||||
fn Str(text: &str) -> Expr { Expr::Str(text.to_string()) }
|
||||
fn Color(r: u8, g: u8, b: u8, a: u8) -> Expr { Expr::Color(RgbaColor::new(r, g, b, a)) }
|
||||
fn ColorStr(color: &str) -> Expr { Expr::Color(RgbaColor::from_str(color).expect("invalid test color")) }
|
||||
fn ColorHealed() -> Expr { Expr::Color(RgbaColor::new_healed(0, 0, 0, 255)) }
|
||||
fn Neg(e1: Expr) -> Expr { Expr::Neg(Box::new(Z(e1))) }
|
||||
fn Add(e1: Expr, e2: Expr) -> Expr { Expr::Add(Box::new(Z(e1)), Box::new(Z(e2))) }
|
||||
fn Sub(e1: Expr, e2: Expr) -> Expr { Expr::Sub(Box::new(Z(e1)), Box::new(Z(e2))) }
|
||||
@ -764,6 +775,12 @@ mod tests {
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! par {
|
||||
($($tts:tt)*) => {
|
||||
SyntaxNode::Par(span_vec![$($tts)*].0)
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! func {
|
||||
($name:tt
|
||||
$(: ($($pos:tt)*) $(, { $($key:tt => $value:expr),* })? )?
|
||||
@ -837,32 +854,32 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_basic_SyntaxNodes() {
|
||||
// Basic SyntaxNodes.
|
||||
fn parse_basic_nodes() {
|
||||
// Basic nodes.
|
||||
p!("" => []);
|
||||
p!("hi" => [T("hi")]);
|
||||
p!("*hi" => [Bold, T("hi")]);
|
||||
p!("hi_" => [T("hi"), Italic]);
|
||||
p!("hi you" => [T("hi"), S, T("you")]);
|
||||
p!("hi// you\nw" => [T("hi"), S, T("w")]);
|
||||
p!("\n\n\nhello" => [Parbreak, T("hello")]);
|
||||
p!("first//\n//\nsecond" => [T("first"), S, S, T("second")]);
|
||||
p!("first//\n \nsecond" => [T("first"), Parbreak, T("second")]);
|
||||
p!("first/*\n \n*/second" => [T("first"), T("second")]);
|
||||
p!(r"a\ b" => [T("a"), Linebreak, S, T("b")]);
|
||||
p!("💜\n\n 🌍" => [T("💜"), Parbreak, T("🌍")]);
|
||||
p!("hi" => [par![T("hi")]]);
|
||||
p!("*hi" => [par![Bold, T("hi")]]);
|
||||
p!("hi_" => [par![T("hi"), Italic]]);
|
||||
p!("hi you" => [par![T("hi"), S, T("you")]]);
|
||||
p!("hi// you\nw" => [par![T("hi"), S, T("w")]]);
|
||||
p!("\n\n\nhello" => [par![T("hello")]]);
|
||||
p!("first//\n//\nsecond" => [par![T("first"), S, S, T("second")]]);
|
||||
p!("first//\n \nsecond" => [par![T("first")], par![T("second")]]);
|
||||
p!("first/*\n \n*/second" => [par![T("first"), T("second")]]);
|
||||
p!(r"a\ b" => [par![T("a"), Linebreak, S, T("b")]]);
|
||||
p!("💜\n\n 🌍" => [par![T("💜")], par![T("🌍")]]);
|
||||
|
||||
// Raw markup.
|
||||
p!("`py`" => [raw!["py"]]);
|
||||
p!("[val][`hi]`]" => [func!("val"; [raw!["hi]"]])]);
|
||||
p!("`hi\nyou" => [raw!["hi", "you"]], [(1:3, 1:3, "expected backtick")]);
|
||||
p!("`hi\\`du`" => [raw!["hi`du"]]);
|
||||
p!("`py`" => [par![raw!["py"]]]);
|
||||
p!("[val][`hi]`]" => [par![func!("val"; [par![raw!["hi]"]]])]]);
|
||||
p!("`hi\nyou" => [par![raw!["hi", "you"]]], [(1:3, 1:3, "expected backtick")]);
|
||||
p!("`hi\\`du`" => [par![raw!["hi`du"]]]);
|
||||
|
||||
// Spanned SyntaxNodes.
|
||||
p!("Hi" => [(0:0, 0:2, T("Hi"))]);
|
||||
p!("*Hi*" => [(0:0, 0:1, Bold), (0:1, 0:3, T("Hi")), (0:3, 0:4, Bold)]);
|
||||
p!("Hi" => [(0:0, 0:2, par![(0:0, 0:2, T("Hi"))])]);
|
||||
p!("*Hi*" => [(0:0, 0:4, par![(0:0, 0:1, Bold), (0:1, 0:3, T("Hi")), (0:3, 0:4, Bold)])]);
|
||||
p!("🌎\n*/[n]" =>
|
||||
[(0:0, 0:1, T("🌎")), (0:1, 1:0, S), (1:2, 1:5, func!((0:1, 0:2, "n")))],
|
||||
[(0:0, 1:5, par![(0:0, 0:1, T("🌎")), (0:1, 1:0, S), (1:2, 1:5, func!((0:1, 0:2, "n")))])],
|
||||
[(1:0, 1:2, "unexpected end of block comment")],
|
||||
[(1:3, 1:4, ResolvedFunc)],
|
||||
);
|
||||
@ -871,52 +888,52 @@ mod tests {
|
||||
#[test]
|
||||
fn parse_function_names() {
|
||||
// No closing bracket.
|
||||
p!("[" => [func!("")], [
|
||||
p!("[" => [par![func!("")]], [
|
||||
(0:1, 0:1, "expected function name"),
|
||||
(0:1, 0:1, "expected closing bracket")
|
||||
]);
|
||||
|
||||
// No name.
|
||||
p!("[]" => [func!("")], [(0:1, 0:1, "expected function name")]);
|
||||
p!("[\"]" => [func!("")], [
|
||||
p!("[]" => [par![func!("")]], [(0:1, 0:1, "expected function name")]);
|
||||
p!("[\"]" => [par![func!("")]], [
|
||||
(0:1, 0:3, "expected function name, found string"),
|
||||
(0:3, 0:3, "expected closing bracket"),
|
||||
]);
|
||||
|
||||
// An unknown name.
|
||||
p!("[hi]" =>
|
||||
[func!("hi")],
|
||||
[par![func!("hi")]],
|
||||
[(0:1, 0:3, "unknown function")],
|
||||
[(0:1, 0:3, UnresolvedFunc)],
|
||||
);
|
||||
|
||||
// A valid name.
|
||||
p!("[f]" => [func!("f")], [], [(0:1, 0:2, ResolvedFunc)]);
|
||||
p!("[ f]" => [func!("f")], [], [(0:3, 0:4, ResolvedFunc)]);
|
||||
p!("[f]" => [par![func!("f")]], [], [(0:1, 0:2, ResolvedFunc)]);
|
||||
p!("[ f]" => [par![func!("f")]], [], [(0:3, 0:4, ResolvedFunc)]);
|
||||
|
||||
// An invalid token for a name.
|
||||
p!("[12]" => [func!("")], [(0:1, 0:3, "expected function name, found number")], []);
|
||||
p!("[🌎]" => [func!("")], [(0:1, 0:2, "expected function name, found invalid token")], []);
|
||||
p!("[ 🌎]" => [func!("")], [(0:3, 0:4, "expected function name, found invalid token")], []);
|
||||
p!("[12]" => [par![func!("")]], [(0:1, 0:3, "expected function name, found number")], []);
|
||||
p!("[🌎]" => [par![func!("")]], [(0:1, 0:2, "expected function name, found invalid token")], []);
|
||||
p!("[ 🌎]" => [par![func!("")]], [(0:3, 0:4, "expected function name, found invalid token")], []);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_colon_starting_function_arguments() {
|
||||
// Valid.
|
||||
p!("[val: true]" =>
|
||||
[func!["val": (Bool(true))]], [],
|
||||
[par![func!["val": (Bool(true))]]], [],
|
||||
[(0:1, 0:4, ResolvedFunc)],
|
||||
);
|
||||
|
||||
// No colon before arg.
|
||||
p!("[val\"s\"]" => [func!("val")], [(0:4, 0:4, "expected colon")]);
|
||||
p!("[val\"s\"]" => [par![func!("val")]], [(0:4, 0:4, "expected colon")]);
|
||||
|
||||
// No colon before valid, but wrong token.
|
||||
p!("[val=]" => [func!("val")], [(0:4, 0:4, "expected colon")]);
|
||||
p!("[val=]" => [par![func!("val")]], [(0:4, 0:4, "expected colon")]);
|
||||
|
||||
// No colon before invalid tokens, which are ignored.
|
||||
p!("[val/🌎:$]" =>
|
||||
[func!("val")],
|
||||
[par![func!("val")]],
|
||||
[(0:4, 0:4, "expected colon")],
|
||||
[(0:1, 0:4, ResolvedFunc)],
|
||||
);
|
||||
@ -924,18 +941,18 @@ mod tests {
|
||||
// String in invalid header without colon still parsed as string
|
||||
// Note: No "expected quote" error because not even the string was
|
||||
// expected.
|
||||
p!("[val/\"]" => [func!("val")], [
|
||||
p!("[val/\"]" => [par![func!("val")]], [
|
||||
(0:4, 0:4, "expected colon"),
|
||||
(0:7, 0:7, "expected closing bracket"),
|
||||
]);
|
||||
|
||||
// Just colon without args.
|
||||
p!("[val:]" => [func!("val")]);
|
||||
p!("[val:/*12pt*/]" => [func!("val")]);
|
||||
p!("[val:]" => [par![func!("val")]]);
|
||||
p!("[val:/*12pt*/]" => [par![func!("val")]]);
|
||||
|
||||
// Whitespace / comments around colon.
|
||||
p!("[val\n:\ntrue]" => [func!("val": (Bool(true)))]);
|
||||
p!("[val/*:*/://\ntrue]" => [func!("val": (Bool(true)))]);
|
||||
p!("[val\n:\ntrue]" => [par![func!("val": (Bool(true)))]]);
|
||||
p!("[val/*:*/://\ntrue]" => [par![func!("val": (Bool(true)))]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -963,24 +980,25 @@ mod tests {
|
||||
pval!("12e-3cm/1pt" => (Div(Len(Length::cm(12e-3)), Len(Length::pt(1.0)))));
|
||||
|
||||
// Span of expression.
|
||||
p!("[val: 1 + 3]" => [(0:0, 0:12, func!((0:1, 0:4, "val"): (
|
||||
(0:6, 0:11, Expr::Add(
|
||||
p!("[val: 1 + 3]" => [(0:0, 0:12, par![(0:0, 0:12, func!(
|
||||
(0:1, 0:4, "val"): ((0:6, 0:11, Expr::Add(
|
||||
Box::new(span_item!((0:6, 0:7, Num(1.0)))),
|
||||
Box::new(span_item!((0:10, 0:11, Num(3.0)))),
|
||||
))
|
||||
)))]);
|
||||
)))
|
||||
))])]);
|
||||
|
||||
// Unclosed string.
|
||||
p!("[val: \"hello]" => [func!("val": (Str("hello]")), {})], [
|
||||
p!("[val: \"hello]" => [par![func!("val": (Str("hello]")), {})]], [
|
||||
(0:13, 0:13, "expected quote"),
|
||||
(0:13, 0:13, "expected closing bracket"),
|
||||
]);
|
||||
|
||||
// Invalid, healed colors.
|
||||
p!("[val: #12345]" => [func!("val": (ColorHealed()))], [(0:6, 0:12, "invalid color")]);
|
||||
p!("[val: #a5]" => [func!("val": (ColorHealed()))], [(0:6, 0:9, "invalid color")]);
|
||||
p!("[val: #14b2ah]" => [func!("val": (ColorHealed()))], [(0:6, 0:13, "invalid color")]);
|
||||
p!("[val: #f075ff011]" => [func!("val": (ColorHealed()))], [(0:6, 0:16, "invalid color")]);
|
||||
let healed = Expr::Color(RgbaColor::new_healed(0, 0, 0, 255));
|
||||
p!("[val: #12345]" => [par![func!("val": (healed.clone()))]], [(0:6, 0:12, "invalid color")]);
|
||||
p!("[val: #a5]" => [par![func!("val": (healed.clone()))]], [(0:6, 0:9, "invalid color")]);
|
||||
p!("[val: #14b2ah]" => [par![func!("val": (healed.clone()))]], [(0:6, 0:13, "invalid color")]);
|
||||
p!("[val: #f075ff011]" => [par![func!("val": (healed.clone()))]], [(0:6, 0:16, "invalid color")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -999,15 +1017,17 @@ mod tests {
|
||||
pval!("3/4*5" => (Mul(Div(Num(3.0), Num(4.0)), Num(5.0))));
|
||||
|
||||
// Span of parenthesized expression contains parens.
|
||||
p!("[val: (1)]" => [(0:0, 0:10, func!((0:1, 0:4, "val"): ((0:6, 0:9, Num(1.0)))))]);
|
||||
p!("[val: (1)]" => [(0:0, 0:10, par![
|
||||
(0:0, 0:10, func!((0:1, 0:4, "val"): ((0:6, 0:9, Num(1.0)))))
|
||||
])]);
|
||||
|
||||
// Invalid expressions.
|
||||
p!("[val: 4pt--]" => [func!("val": (Len(Length::pt(4.0))))], [
|
||||
p!("[val: 4pt--]" => [par![func!("val": (Len(Length::pt(4.0))))]], [
|
||||
(0:10, 0:11, "dangling minus"),
|
||||
(0:6, 0:10, "missing right summand")
|
||||
]);
|
||||
p!("[val: 3mm+4pt*]" =>
|
||||
[func!("val": (Add(Len(Length::mm(3.0)), Len(Length::pt(4.0)))))],
|
||||
[par![func!("val": (Add(Len(Length::mm(3.0)), Len(Length::pt(4.0)))))]],
|
||||
[(0:10, 0:14, "missing right factor")],
|
||||
);
|
||||
}
|
||||
@ -1026,19 +1046,19 @@ mod tests {
|
||||
|
||||
// Invalid value.
|
||||
p!("[val: sound(\x07)]" =>
|
||||
[func!("val": (named_tuple!("sound")), {})],
|
||||
[par![func!("val": (named_tuple!("sound")), {})]],
|
||||
[(0:12, 0:13, "expected value, found invalid token")],
|
||||
);
|
||||
|
||||
// Invalid tuple name.
|
||||
p!("[val: 👠(\"abc\", 13e-5)]" =>
|
||||
[func!("val": (tuple!(Str("abc"), Num(13.0e-5))), {})],
|
||||
[par![func!("val": (tuple!(Str("abc"), Num(13.0e-5))), {})]],
|
||||
[(0:6, 0:7, "expected argument, found invalid token")],
|
||||
);
|
||||
|
||||
// Unclosed tuple.
|
||||
p!("[val: lang(中文]" =>
|
||||
[func!("val": (named_tuple!("lang", Id("中文"))), {})],
|
||||
[par![func!("val": (named_tuple!("lang", Id("中文"))), {})]],
|
||||
[(0:13, 0:13, "expected closing paren")],
|
||||
);
|
||||
|
||||
@ -1059,18 +1079,18 @@ mod tests {
|
||||
|
||||
// Invalid commas.
|
||||
p!("[val: (,)]" =>
|
||||
[func!("val": (tuple!()), {})],
|
||||
[par![func!("val": (tuple!()), {})]],
|
||||
[(0:7, 0:8, "expected value, found comma")],
|
||||
);
|
||||
p!("[val: (true false)]" =>
|
||||
[func!("val": (tuple!(Bool(true), Bool(false))), {})],
|
||||
[par![func!("val": (tuple!(Bool(true), Bool(false))), {})]],
|
||||
[(0:11, 0:11, "expected comma")],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_objects() {
|
||||
let val = || func!("val": (object! {}), {});
|
||||
let val = || par![func!("val": (object! {}), {})];
|
||||
|
||||
// Okay objects.
|
||||
pval!("{}" => (object! {}));
|
||||
@ -1078,11 +1098,11 @@ mod tests {
|
||||
|
||||
// Unclosed object.
|
||||
p!("[val: {hello: world]" =>
|
||||
[func!("val": (object! { "hello" => Id("world") }), {})],
|
||||
[par![func!("val": (object! { "hello" => Id("world") }), {})]],
|
||||
[(0:19, 0:19, "expected closing brace")],
|
||||
);
|
||||
p!("[val: { a]" =>
|
||||
[func!("val": (object! {}), {})],
|
||||
[par![func!("val": (object! {}), {})]],
|
||||
[(0:9, 0:9, "expected colon"), (0:9, 0:9, "expected closing brace")],
|
||||
);
|
||||
|
||||
@ -1098,25 +1118,25 @@ mod tests {
|
||||
(0:12, 0:17, "expected key, found bool"),
|
||||
]);
|
||||
p!("[val: { a b:c }]" =>
|
||||
[func!("val": (object! { "b" => Id("c") }), {})],
|
||||
[par![func!("val": (object! { "b" => Id("c") }), {})]],
|
||||
[(0:9, 0:9, "expected colon")],
|
||||
);
|
||||
|
||||
// Missing value.
|
||||
p!("[val: { key: : }]" => [val()], [(0:13, 0:14, "expected value, found colon")]);
|
||||
p!("[val: { key: , k: \"s\" }]" =>
|
||||
[func!("val": (object! { "k" => Str("s") }), {})],
|
||||
[par![func!("val": (object! { "k" => Str("s") }), {})]],
|
||||
[(0:13, 0:14, "expected value, found comma")],
|
||||
);
|
||||
|
||||
// Missing comma, invalid token.
|
||||
p!("[val: left={ a: 2, b: false 🌎 }]" =>
|
||||
[func!("val": (), {
|
||||
[par![func!("val": (), {
|
||||
"left" => object! {
|
||||
"a" => Num(2.0),
|
||||
"b" => Bool(false),
|
||||
}
|
||||
})],
|
||||
})]],
|
||||
[(0:27, 0:27, "expected comma"),
|
||||
(0:28, 0:29, "expected key, found invalid token")],
|
||||
);
|
||||
@ -1140,19 +1160,19 @@ mod tests {
|
||||
fn parse_one_keyword_argument() {
|
||||
// Correct
|
||||
p!("[val: x=true]" =>
|
||||
[func!("val": (), { "x" => Bool(true) })], [],
|
||||
[par![func!("val": (), { "x" => Bool(true) })]], [],
|
||||
[(0:6, 0:7, ArgumentKey), (0:1, 0:4, ResolvedFunc)],
|
||||
);
|
||||
|
||||
// Spacing around keyword arguments
|
||||
p!("\n [val: \n hi \n = /* //\n */ \"s\n\"]" =>
|
||||
[S, func!("val": (), { "hi" => Str("s\n") })], [],
|
||||
[par![S, func!("val": (), { "hi" => Str("s\n") })]], [],
|
||||
[(2:1, 2:3, ArgumentKey), (1:2, 1:5, ResolvedFunc)],
|
||||
);
|
||||
|
||||
// Missing value
|
||||
p!("[val: x=]" =>
|
||||
[func!("val")],
|
||||
[par![func!("val")]],
|
||||
[(0:8, 0:8, "expected value")],
|
||||
[(0:6, 0:7, ArgumentKey), (0:1, 0:4, ResolvedFunc)],
|
||||
);
|
||||
@ -1161,7 +1181,8 @@ mod tests {
|
||||
#[test]
|
||||
fn parse_multiple_mixed_arguments() {
|
||||
p!("[val: 12pt, key=value]" =>
|
||||
[func!("val": (Len(Length::pt(12.0))), { "key" => Id("value") })], [],
|
||||
[par![func!("val": (Len(Length::pt(12.0))), { "key" => Id("value") })]],
|
||||
[],
|
||||
[(0:12, 0:15, ArgumentKey), (0:1, 0:4, ResolvedFunc)],
|
||||
);
|
||||
pval!("a , x=\"b\" , c" => (Id("a"), Id("c")), { "x" => Str("b") });
|
||||
@ -1169,15 +1190,15 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parse_invalid_values() {
|
||||
p!("[val: )]" => [func!("val")], [(0:6, 0:7, "expected argument, found closing paren")]);
|
||||
p!("[val: }]" => [func!("val")], [(0:6, 0:7, "expected argument, found closing brace")]);
|
||||
p!("[val: :]" => [func!("val")], [(0:6, 0:7, "expected argument, found colon")]);
|
||||
p!("[val: ,]" => [func!("val")], [(0:6, 0:7, "expected argument, found comma")]);
|
||||
p!("[val: =]" => [func!("val")], [(0:6, 0:7, "expected argument, found equals sign")]);
|
||||
p!("[val: 🌎]" => [func!("val")], [(0:6, 0:7, "expected argument, found invalid token")]);
|
||||
p!("[val: 12ept]" => [func!("val")], [(0:6, 0:11, "expected argument, found invalid token")]);
|
||||
p!("[val: )]" => [par![func!("val")]], [(0:6, 0:7, "expected argument, found closing paren")]);
|
||||
p!("[val: }]" => [par![func!("val")]], [(0:6, 0:7, "expected argument, found closing brace")]);
|
||||
p!("[val: :]" => [par![func!("val")]], [(0:6, 0:7, "expected argument, found colon")]);
|
||||
p!("[val: ,]" => [par![func!("val")]], [(0:6, 0:7, "expected argument, found comma")]);
|
||||
p!("[val: =]" => [par![func!("val")]], [(0:6, 0:7, "expected argument, found equals sign")]);
|
||||
p!("[val: 🌎]" => [par![func!("val")]], [(0:6, 0:7, "expected argument, found invalid token")]);
|
||||
p!("[val: 12ept]" => [par![func!("val")]], [(0:6, 0:11, "expected argument, found invalid token")]);
|
||||
p!("[val: [hi]]" =>
|
||||
[func!("val")],
|
||||
[par![func!("val")]],
|
||||
[(0:6, 0:10, "expected argument, found function")],
|
||||
[(0:1, 0:4, ResolvedFunc)],
|
||||
);
|
||||
@ -1187,7 +1208,7 @@ mod tests {
|
||||
fn parse_invalid_key_value_pairs() {
|
||||
// Invalid keys.
|
||||
p!("[val: true=you]" =>
|
||||
[func!("val": (Bool(true), Id("you")), {})],
|
||||
[par![func!("val": (Bool(true), Id("you")), {})]],
|
||||
[(0:10, 0:10, "expected comma"),
|
||||
(0:10, 0:11, "expected argument, found equals sign")],
|
||||
[(0:1, 0:4, ResolvedFunc)],
|
||||
@ -1195,21 +1216,22 @@ mod tests {
|
||||
|
||||
// Unexpected equals.
|
||||
p!("[box: z=y=4]" =>
|
||||
[func!("box": (Num(4.0)), { "z" => Id("y") })],
|
||||
[par![func!("box": (Num(4.0)), { "z" => Id("y") })]],
|
||||
[(0:9, 0:9, "expected comma"),
|
||||
(0:9, 0:10, "expected argument, found equals sign")],
|
||||
);
|
||||
|
||||
// Invalid colon after keyable positional argument.
|
||||
p!("[val: key:12]" =>
|
||||
[func!("val": (Id("key"), Num(12.0)), {})],
|
||||
[par![func!("val": (Id("key"), Num(12.0)), {})]],
|
||||
[(0:9, 0:9, "expected comma"),
|
||||
(0:9, 0:10, "expected argument, found colon")],
|
||||
[(0:1, 0:4, ResolvedFunc)],
|
||||
);
|
||||
|
||||
// Invalid colon after unkeyable positional argument.
|
||||
p!("[val: true:12]" => [func!("val": (Bool(true), Num(12.0)), {})],
|
||||
p!("[val: true:12]" =>
|
||||
[par![func!("val": (Bool(true), Num(12.0)), {})]],
|
||||
[(0:10, 0:10, "expected comma"),
|
||||
(0:10, 0:11, "expected argument, found colon")],
|
||||
[(0:1, 0:4, ResolvedFunc)],
|
||||
@ -1220,33 +1242,33 @@ mod tests {
|
||||
fn parse_invalid_commas() {
|
||||
// Missing commas.
|
||||
p!("[val: 1pt 1]" =>
|
||||
[func!("val": (Len(Length::pt(1.0)), Num(1.0)), {})],
|
||||
[par![func!("val": (Len(Length::pt(1.0)), Num(1.0)), {})]],
|
||||
[(0:9, 0:9, "expected comma")],
|
||||
);
|
||||
p!(r#"[val: _"s"]"# =>
|
||||
[func!("val": (Id("_"), Str("s")), {})],
|
||||
[par![func!("val": (Id("_"), Str("s")), {})]],
|
||||
[(0:7, 0:7, "expected comma")],
|
||||
);
|
||||
|
||||
// Unexpected commas.
|
||||
p!("[val:,]" => [func!("val")], [(0:5, 0:6, "expected argument, found comma")]);
|
||||
p!("[val: key=,]" => [func!("val")], [(0:10, 0:11, "expected value, found comma")]);
|
||||
p!("[val:,]" => [par![func!("val")]], [(0:5, 0:6, "expected argument, found comma")]);
|
||||
p!("[val: key=,]" => [par![func!("val")]], [(0:10, 0:11, "expected value, found comma")]);
|
||||
p!("[val:, true]" =>
|
||||
[func!("val": (Bool(true)), {})],
|
||||
[par![func!("val": (Bool(true)), {})]],
|
||||
[(0:5, 0:6, "expected argument, found comma")],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_bodies() {
|
||||
p!("[val][Hi]" => [func!("val"; [T("Hi")])]);
|
||||
p!("[val][Hi]" => [par![func!("val"; [par![T("Hi")]])]]);
|
||||
p!("[val:*][*Hi*]" =>
|
||||
[func!("val"; [Bold, T("Hi"), Bold])],
|
||||
[par![func!("val"; [par![Bold, T("Hi"), Bold]])]],
|
||||
[(0:5, 0:6, "expected argument, found star")],
|
||||
);
|
||||
// Errors in bodies.
|
||||
p!(" [val][ */ ]" =>
|
||||
[S, func!("val"; [S, S])],
|
||||
[par![S, func!("val"; [par![S, S]])]],
|
||||
[(0:8, 0:10, "unexpected end of block comment")],
|
||||
);
|
||||
}
|
||||
@ -1255,40 +1277,47 @@ mod tests {
|
||||
fn parse_spanned_functions() {
|
||||
// Space before function
|
||||
p!(" [val]" =>
|
||||
[(0:0, 0:1, S), (0:1, 0:6, func!((0:1, 0:4, "val")))], [],
|
||||
[(0:0, 0:6, par![(0:0, 0:1, S), (0:1, 0:6, func!((0:1, 0:4, "val")))])],
|
||||
[],
|
||||
[(0:2, 0:5, ResolvedFunc)],
|
||||
);
|
||||
|
||||
// Newline before function
|
||||
p!(" \n\r\n[val]" =>
|
||||
[(0:0, 2:0, Parbreak), (2:0, 2:5, func!((0:1, 0:4, "val")))], [],
|
||||
p!("a \n\r\n[val]" =>
|
||||
[
|
||||
(0:0, 0:1, par![(0:0, 0:1, T("a"))]),
|
||||
(2:0, 2:5, par![(2:0, 2:5, func!((0:1, 0:4, "val")))]),
|
||||
],
|
||||
[],
|
||||
[(2:1, 2:4, ResolvedFunc)],
|
||||
);
|
||||
|
||||
// Content before function
|
||||
p!("hello [val][world] 🌎" =>
|
||||
[
|
||||
[(0:0, 0:20, par![
|
||||
(0:0, 0:5, T("hello")),
|
||||
(0:5, 0:6, S),
|
||||
(0:6, 0:18, func!((0:1, 0:4, "val"); [(0:6, 0:11, T("world"))])),
|
||||
(0:6, 0:18, func!((0:1, 0:4, "val"); [
|
||||
(0:6, 0:11, par![(0:6, 0:11, T("world"))])
|
||||
])),
|
||||
(0:18, 0:19, S),
|
||||
(0:19, 0:20, T("🌎"))
|
||||
],
|
||||
])],
|
||||
[],
|
||||
[(0:7, 0:10, ResolvedFunc)],
|
||||
);
|
||||
|
||||
// Nested function
|
||||
p!(" [val][\nbody[ box]\n ]" =>
|
||||
[
|
||||
[(0:0, 2:2, par![
|
||||
(0:0, 0:1, S),
|
||||
(0:1, 2:2, func!((0:1, 0:4, "val"); [
|
||||
(0:1, 2:2, func!((0:1, 0:4, "val"); [(0:6, 2:1, par![
|
||||
(0:6, 1:0, S),
|
||||
(1:0, 1:4, T("body")),
|
||||
(1:4, 1:10, func!((0:2, 0:5, "box"))),
|
||||
(1:10, 2:1, S),
|
||||
]))
|
||||
],
|
||||
])]))
|
||||
])],
|
||||
[],
|
||||
[(0:2, 0:5, ResolvedFunc), (1:6, 1:9, ResolvedFunc)],
|
||||
);
|
||||
|
@ -86,22 +86,23 @@ pub trait SpanlessEq<Rhs = Self> {
|
||||
}
|
||||
|
||||
impl SpanlessEq for SyntaxNode {
|
||||
fn spanless_eq(&self, other: &SyntaxNode) -> bool {
|
||||
fn spanless_eq(&self, other: &Self) -> bool {
|
||||
fn downcast<'a>(func: &'a (dyn DynamicNode + 'static)) -> &'a DebugFn {
|
||||
func.downcast::<DebugFn>().expect("not a debug fn")
|
||||
}
|
||||
|
||||
match (self, other) {
|
||||
(SyntaxNode::Dyn(a), SyntaxNode::Dyn(b)) => {
|
||||
(Self::Dyn(a), Self::Dyn(b)) => {
|
||||
downcast(a.as_ref()).spanless_eq(downcast(b.as_ref()))
|
||||
}
|
||||
(Self::Par(a), Self::Par(b)) => a.spanless_eq(b),
|
||||
(a, b) => a == b,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SpanlessEq for DebugFn {
|
||||
fn spanless_eq(&self, other: &DebugFn) -> bool {
|
||||
fn spanless_eq(&self, other: &Self) -> bool {
|
||||
self.header.spanless_eq(&other.header)
|
||||
&& self.body.spanless_eq(&other.body)
|
||||
}
|
||||
@ -132,7 +133,7 @@ impl SpanlessEq for FuncArg {
|
||||
}
|
||||
|
||||
impl SpanlessEq for Expr {
|
||||
fn spanless_eq(&self, other: &Expr) -> bool {
|
||||
fn spanless_eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Expr::Tuple(a), Expr::Tuple(b)) => a.spanless_eq(b),
|
||||
(Expr::NamedTuple(a), Expr::NamedTuple(b)) => a.spanless_eq(b),
|
||||
@ -148,20 +149,20 @@ impl SpanlessEq for Expr {
|
||||
}
|
||||
|
||||
impl SpanlessEq for Tuple {
|
||||
fn spanless_eq(&self, other: &Tuple) -> bool {
|
||||
fn spanless_eq(&self, other: &Self) -> bool {
|
||||
self.0.spanless_eq(&other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl SpanlessEq for NamedTuple {
|
||||
fn spanless_eq(&self, other: &NamedTuple) -> bool {
|
||||
fn spanless_eq(&self, other: &Self) -> bool {
|
||||
self.name.v == other.name.v
|
||||
&& self.tuple.v.spanless_eq(&other.tuple.v)
|
||||
}
|
||||
}
|
||||
|
||||
impl SpanlessEq for Object {
|
||||
fn spanless_eq(&self, other: &Object) -> bool {
|
||||
fn spanless_eq(&self, other: &Self) -> bool {
|
||||
self.0.spanless_eq(&other.0)
|
||||
}
|
||||
}
|
||||
@ -173,20 +174,20 @@ impl SpanlessEq for Pair {
|
||||
}
|
||||
|
||||
impl<T: SpanlessEq> SpanlessEq for Vec<T> {
|
||||
fn spanless_eq(&self, other: &Vec<T>) -> bool {
|
||||
fn spanless_eq(&self, other: &Self) -> bool {
|
||||
self.len() == other.len()
|
||||
&& self.iter().zip(other).all(|(x, y)| x.spanless_eq(&y))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SpanlessEq> SpanlessEq for Spanned<T> {
|
||||
fn spanless_eq(&self, other: &Spanned<T>) -> bool {
|
||||
fn spanless_eq(&self, other: &Self) -> bool {
|
||||
self.v.spanless_eq(&other.v)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SpanlessEq> SpanlessEq for Box<T> {
|
||||
fn spanless_eq(&self, other: &Box<T>) -> bool {
|
||||
fn spanless_eq(&self, other: &Self) -> bool {
|
||||
(&**self).spanless_eq(&**other)
|
||||
}
|
||||
}
|
||||
|
@ -14,20 +14,20 @@ pub type SyntaxTree = SpanVec<SyntaxNode>;
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SyntaxNode {
|
||||
/// Whitespace containing less than two newlines.
|
||||
Space,
|
||||
/// Whitespace with more than two newlines.
|
||||
Parbreak,
|
||||
Spacing,
|
||||
/// A forced line break.
|
||||
Linebreak,
|
||||
/// Plain text.
|
||||
Text(String),
|
||||
/// Lines of raw text.
|
||||
Raw(Vec<String>),
|
||||
/// Italics were enabled / disabled.
|
||||
ToggleItalic,
|
||||
/// Bolder was enabled / disabled.
|
||||
ToggleBolder,
|
||||
/// A dynamic node, create through function invocations in source code.
|
||||
/// Plain text.
|
||||
Text(String),
|
||||
/// Lines of raw text.
|
||||
Raw(Vec<String>),
|
||||
/// A paragraph of child nodes.
|
||||
Par(SyntaxTree),
|
||||
/// A dynamic node, created through function invocations in source code.
|
||||
Dyn(Box<dyn DynamicNode>),
|
||||
}
|
||||
|
||||
@ -35,13 +35,13 @@ impl PartialEq for SyntaxNode {
|
||||
fn eq(&self, other: &SyntaxNode) -> bool {
|
||||
use SyntaxNode::*;
|
||||
match (self, other) {
|
||||
(Space, Space) => true,
|
||||
(Parbreak, Parbreak) => true,
|
||||
(Spacing, Spacing) => true,
|
||||
(Linebreak, Linebreak) => true,
|
||||
(Text(a), Text(b)) => a == b,
|
||||
(Raw(a), Raw(b)) => a == b,
|
||||
(ToggleItalic, ToggleItalic) => true,
|
||||
(ToggleBolder, ToggleBolder) => true,
|
||||
(Text(a), Text(b)) => a == b,
|
||||
(Raw(a), Raw(b)) => a == b,
|
||||
(Par(a), Par(b)) => a == b,
|
||||
(Dyn(a), Dyn(b)) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user