pax-lang 0.38.3

Pax language parser
Documentation

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 }