WHITESPACE = _{ " " | "\t" | "\r" | NEWLINE }
// Comment rule is explicit so parser doesn't accept them anywhere vs. built-ins are accepted anywhere
comment = @{ ( "/*" ~ (!"*/" ~ ANY)* ~ "*/" ) | ("//" ~ (!(NEWLINE) ~ ANY)* ~ NEWLINE) | ("<!--" ~ (!"-->" ~ ANY)* ~ "-->") }
////// ////// //////
/// BEGIN TEMPLATE
//////
//A template is expressed as an XML-like document with support for
//property binding, control-flow (if, for) and {}-wrapped embedded expressions
//A component definition requires at least one element in its template; a `@settings` block may also be included, and any future relevant blocks like `@defaults`
//The parser will willingly _parse_ multiple @settings/@template blocks per component definition, but the compiler won't presently support them
pax_component_definition = { SOI ~ (root_tag_pair | settings_block_declaration )* ~ EOI }
root_tag_pair = { any_tag_pair }
any_tag_pair = _{statement_control_flow | matched_tag | self_closing_tag | comment }
//This duo describes an XML-style open-tag, like <SomeElement id="...">
//and matching close-tag, like </SomeElement>. Note the use of Pest's stack feature, `PUSH`
//and `POP`, to match closing & opening tags
open_tag = {"<" ~ PUSH(pascal_identifier) ~ attribute_key_value_pair * ~ ">"}
closing_tag = {"<" ~ "/" ~ POP ~ ">"}
//Describes a (leaf-node) self-closing element, like <SomeElement />
self_closing_tag = {"<" ~ pascal_identifier ~ attribute_key_value_pair* ~ "/" ~ ">"}
//Describes an XML subtree surrounded by a pair of matching tags, like
//<SomeElement>...</SomeElement>
matched_tag = {open_tag ~ inner_nodes ~ closing_tag}
inner_nodes = { node_inner_content | any_tag_pair* }
//Describes an atomic symbolic identifier, like `id`, `Engine`, or `some_thing`
identifier = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_" | "-")* }
//Describes a symbolic identifier with an Uppercase first letter, a la PascalCase
//Does not enforce the presence of any other uppercase letters.
//Intended as convention for symbolic ids in expressions, e.g. `Engine`, specification
//of explicit types in polymorphic/enum contexts (e.g. `fill: Color {...}`), and
//for namespaced access of symbolic ids, like `Orientation::Vertical`
pascal_identifier = @{ ASCII_ALPHA_UPPER ~ (ASCII_ALPHANUMERIC | "_")*}
//Describes the ID of an event to which a handler may be bound, e.g. `@pre_render`
event_id = {"@" ~ identifier}
//Describes an attribute k/v pair like `id="some_element"` or `@click=self.handle_click`. Supports expressions.
attribute_key_value_pair = {double_binding | attribute_event_binding | (identifier ~ "=" ~ any_template_value) }
attribute_event_binding = {event_id ~ "=" ~ literal_function}
double_binding = {identifier ~ "=" ~ "bind" ~ ":" ~ literal_function}
//`...=5.0`, `...={...}`
any_template_value = {literal_value | expression_wrapped | identifier}
//For example: <Text>"This is my inner content"</Text>
//Presumably this content can be bare literal values other than strings like Color::hlca(...)
//It could also be an `{...}` expression
node_inner_content = { literal_value | expression_wrapped }
//string/inner/char from https://pest.rs/book/examples/json.html
string = ${ ("\"" ~ inner ~ "\"") | ("'" ~ inner ~ "'") | ("`" ~ inner ~ "`") }
inner = @{ char* }
char = {
!("\"" | "\\") ~ ANY
| "\\" ~ ("\"" | "\\" | "/" | "b" | "f" | "n" | "r" | "t")
| "\\" ~ ("u" ~ ASCII_HEX_DIGIT{4})
}
////// ////// //////
/// BEGIN SETTINGS
//////
settings_block_declaration = {"@" ~ "settings" ~ "{" ~ (settings_event_binding | selector_block | comment )* ~ "}"}
selector_block = {selector ~ literal_object ~ silent_comma? }
literal_object = { pascal_identifier? ~ "{" ~ (settings_key_value_pair | comment)* ~ "}" }
//Describes a CSS-style selector, used for joining settings to elements
//Note: only basic `id` and `class` syntax supported for now; could be extended
//Example: `#some-element`
selector = {("." | "#") ~ identifier}
//Describes a key-value pair in a settings block, which supports a number of formats,
//included recursive nesting via `property_block`
settings_key_value_pair = { (settings_key ~ settings_value) ~ silent_comma? }
settings_event_binding = {event_id ~ ":" ~ literal_function ~ silent_comma? }
settings_key = { identifier ~ (":" | "=") } //Offer some grace here, since our borrowing of HTML/CSS semantics means we inherit the mismatch between xml-like `=` and json-like `:`. Let's allow both and let linters deal with cleaning up mismatches.
settings_value = { literal_value | expression_wrapped }
literal_function = { ("self." | "this.")? ~ identifier }
silent_comma = _{","}
function_list = {"[" ~ literal_function* ~ "]"}
literal_value = { literal_list | literal_color | literal_number_with_unit | literal_number | literal_tuple | literal_option | literal_enum_value | literal_boolean | literal_object | string }
literal_boolean = {("true" | "false")}
literal_some = {("Some" ~ "(" ~ literal_value ~ ")")}
literal_none = {"None"}
literal_option = {"Option::"? ~ literal_some | literal_none}
literal_number_with_unit = { literal_number ~ literal_number_unit }
literal_number = {literal_number_float | literal_number_integer}
literal_number_integer = {"-"? ~ (!(".") ~ ASCII_DIGIT)+ }
literal_number_float = {"-"? ~ ASCII_DIGIT* ~ "." ~ ASCII_DIGIT+ }
literal_number_unit = {("%" ~ !"%") | "px" | "deg" | "rad"}
literal_tuple = {("(") ~ literal_value ~ ("," ~ literal_value)* ~ (")")}
literal_tuple_access = {identifier ~ "." ~ literal_number_integer}
literal_list = {"[" ~ (literal_value ~ silent_comma?)* ~ "]"}
literal_list_access = {identifier ~ "[" ~ literal_number_integer ~ "]"}
//Enums like Orientation::Vertical
//Note that this is parsed separately from expression enums, `xo_enum*`
literal_enum_value = {identifier ~ ("::" ~ identifier)+ ~ ("("~literal_enum_args_list~")")?}
literal_enum_args_list = {literal_value ~ ("," ~ literal_value)* ~ silent_comma?}
////// ////// //////
/// BEGIN COLORS
//////
//Notes on gradients:
// - when we support object literal syntax for gradients, can we just rely on the vanilla Rust structs instead of re-inventing
// an object literal syntax in the grammar? in particular, it would be nice to support the sugared syntax of number+percentage as keys,
// but perhaps we could do that e.g. by desugaring `{0%: "bar", 100%: "foo" }` into `[(0%, "bar"),(100%, "foo")]`, akin to how
// you can construct a HashMap from a vec of k/v tuples in Rust. Thus, you could express a LinearGradient as
// LinearGradient {start: (0,0), stops: {0%: RED, 100%: BLUE}, end: (100%,100%)}
//Literal colors
literal_color = {literal_color_space_func | literal_color_const}
literal_color_space_func = {
(("rgb(" | "hsl(") ~ literal_color_channel ~ "," ~ literal_color_channel ~ "," ~ literal_color_channel ~ ","? ~ ")") |
(("rgba(" | "hsla(") ~ literal_color_channel ~ "," ~ literal_color_channel ~ "," ~ literal_color_channel ~ "," ~ literal_color_channel ~ ","? ~ ")")
}
// Valid units include integers 0-255, 0-100%, and arbitrary numeric deg/rad (for hue, which is expressed as an angle)
literal_color_channel = {literal_number_with_unit | literal_number_integer}
//Expression-friendly colors. note that literal variants are covered under the xo_literal tree
xo_color_space_func = {
(("rgb(" | "hsl(") ~ expression_body ~ "," ~ expression_body ~ "," ~ expression_body ~ ","? ~ ")") |
(("rgba(" | "hsla(") ~ expression_body ~ "," ~ expression_body ~ "," ~ expression_body ~ "," ~ expression_body ~ ","? ~ ")")
}
// Color constants inspired by Tailwind
literal_color_const = {
"SLATE" |
"GRAY" |
"ZINC" |
"NEUTRAL" |
"STONE" |
"RED" |
"ORANGE" |
"AMBER" |
"YELLOW" |
"LIME" |
"GREEN" |
"EMERALD" |
"TEAL" |
"CYAN" |
"SKY" |
"BLUE" |
"INDIGO" |
"VIOLET" |
"PURPLE" |
"FUCHSIA" |
"PINK" |
"ROSE" |
"BLACK" |
"WHITE" |
"TRANSPARENT" |
"NONE"
}
////// ////// //////
/// BEGIN EXPRESSIONS
/// This sub-grammar describes PAXEL, the Pax Expression Language
//////
// Expression body may be a binary operation like `x + 5` or `num_clicks % 2 == 0`
// or a literal returned value like `Color { ... }` or `5`
//
// If we wish to include postfix operators, or e.g. refactor `px` and `%` to be treated as postfix operators,
// the following is the order of xo that the Pratt parser expects
// expr_with_postfix = { xo_prefix* ~ xo_primary ~ xo_postfix* ~ (xo_infix ~ xo_prefix* ~ xo_primary ~ xo_postfix* )* }
expression_body = { xo_prefix* ~ xo_primary ~ (xo_infix ~ xo_prefix* ~ xo_primary )* }
expression_wrapped = _{
"{" ~ comment? ~ expression_body ~ "}"
}
expression_grouped = { "(" ~ expression_body ~ ")" ~ literal_number_unit? }
/*
Some examples of valid expressions:
[Object construction]
Color {h: 360, s: 1, l: 1, a: 1}
[Object construction with implicit type (type enforced by downstream compiler)
{h: 360, s: 1, l: 1, a: 1}
[Boolean statements]
num_clicks % 2 == 0
[Complex statements including ternaries, grouping, logical operators, and object construction]
(num_clicks % 2 == 0 && is_selected) ?
{r: 255 * color_intensity, g: 0, b: 0, a: 1} :
{r: 0, g: 255 * color_intensity, b: 0, a: 1}
[String literals + operations]
"Is " + (is_selected ? "" : "not ") + "selected."
*/
//`xo` is short for both "expression operator" and "expression operand", collectively all symbols
//that can be expressed inside expressions
xo_primary = _{ expression_grouped | xo_color_space_func | xo_enum_or_function_call | xo_object | xo_range | xo_tuple | xo_list | xo_literal | xo_symbol }
xo_prefix = _{xo_neg | xo_bool_not}
xo_neg = {"-"}
xo_bool_not = {"!"}
xo_infix = _{
xo_add |
xo_bool_and |
xo_bool_or |
xo_div |
xo_exp |
xo_mod |
xo_mul |
xo_rel_eq |
xo_rel_gte |
xo_rel_gt |
xo_rel_lte |
xo_rel_lt |
xo_rel_neq |
xo_sub |
xo_tern_then |
xo_tern_else
}
xo_add = {"+"}
xo_bool_and = {"&&"}
xo_bool_or = {"||"}
xo_div = {"/"}
xo_exp = {"^"}
xo_mod = {"%%"}
xo_mul = {"*"}
xo_rel_eq = {"=="}
xo_rel_gt = {">"}
xo_rel_gte = {">="}
xo_rel_lt = {"<"}
xo_rel_lte = {"<="}
xo_rel_neq = {"!="}
xo_sub = {"-"}
xo_tern_then = {"?"}
xo_tern_else = {":"}
xo_range = { (xo_literal | xo_symbol) ~ (xo_range_exclusive) ~ (xo_literal | xo_symbol)}
xo_range_exclusive = @{".."}
// xo_range_inclusive = @{"..="}
xo_literal = {literal_value | literal_tuple_access | literal_list_access }
//objects may recurse into arbitrary expressions for any value -- consider the `key_2` in:
// `some_prop={ TypedReturn {key_0: 0, key_1: "one", key_2: 1.0 + 1.0} }`
xo_object = { identifier? ~ "{" ~ xo_object_settings_key_value_pair* ~ "}" }
xo_object_settings_key_value_pair = { settings_key ~ expression_body ~ silent_comma? }
xo_symbol = { "$"? ~ identifier ~ (("." ~ identifier) | ("[" ~ expression_body ~ "]") )* }
xo_tuple = { "(" ~ expression_body ~ ("," ~ expression_body)* ~ ")"}
xo_list = { "[" ~ (expression_body ~ ("," ~ expression_body)*)? ~ silent_comma? ~ "]" }
xo_enum_or_function_call = {identifier ~ (("::") ~ identifier)+ ~ ("("~xo_enum_or_function_args_list~")")?}
xo_enum_or_function_args_list = {(expression_body ~ ("," ~ expression_body)* ~ silent_comma? )?}
////// ////// //////
/// BEGIN CONTROL FLOW
//////
//Control flow statements are NOT embeddable all places that expressions are. That is, control-flow statements
//can only sit alongside elements in a template and cannot be bound to properties. As a result,
//and to foster clarity of nomenclature, we call these `statements` rather than `expressions`.
//These statements work as syntactic sugar for built-in primitives: Conditional, Repeat, and Slot.
statement_control_flow = {(statement_if | statement_for | statement_slot)}
statement_if = {"if" ~ expression_body ~ "{" ~ inner_nodes ~ "}"} //FUTURE: support else, else if
statement_for = {"for" ~ statement_for_predicate_declaration ~ "in" ~ statement_for_source ~ "{" ~ inner_nodes ~ "}"}
statement_slot = {"slot" ~ ("(" ~ expression_body ~ ")")}
//Examples:
//for i | for (elem, i)
statement_for_predicate_declaration = {
identifier |
("(" ~ identifier ~ ","~ identifier ~")")
}
//Examples:
// in some_symbol
// in self.some_symbol
// in this.Pascal_snake-kebab
// in 0..5
// in this.some_symbol..25
// in 25..some_symbol
statement_for_source = { xo_range | xo_symbol }