pax-language 0.39.1

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 | timeline_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_transition_binding | attribute_event_binding | class_attribute | (!(("class") ~ "=") ~ identifier ~ "=" ~ any_template_value) }
class_attribute = { "class" ~ "=" ~ class_value }
class_value = { class_string_list | string | expression_wrapped }
class_string_list = { "[" ~ (string ~ (silent_comma? ~ string)* ~ silent_comma?)? ~ "]" }
attribute_transition_binding = {transition_id ~ "=" ~ transition_binding_value}
attribute_event_binding = {event_id ~ "=" ~ literal_function}
transition_id = {"@" ~ ("in" | "out")}
transition_binding_value = {transition_inline_timeline_value | literal_function}
transition_inline_timeline_value = { "@" ~ "timeline" ~ transition_inline_timeline_body }
transition_inline_timeline_body = { "{" ~ (timeline_block_setting | timeline_property_key_value_pair | comment )* ~ "}" }

double_binding = {identifier ~ "=" ~ "bind" ~ ":" ~ literal_function}

//`...=5.0`, `...={...}`
any_template_value = {gradient_inline_value | timeline_inline_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_block_element* ~ "}"}
settings_block_element = _{ settings_event_binding | settings_conditional | selector_block | comment }
settings_conditional = { settings_if_branch ~ settings_else_if_branch* ~ settings_else_branch? }
settings_if_branch = { "if" ~ expression_body ~ "{" ~ settings_conditional_body* ~ "}" }
settings_else_if_branch = { "else" ~ "if" ~ expression_body ~ "{" ~ settings_conditional_body* ~ "}" }
settings_else_branch = { "else" ~ "{" ~ settings_conditional_body* ~ "}" }
settings_conditional_body = _{ settings_conditional | 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 = { gradient_inline_value | timeline_inline_value | literal_value | expression_wrapped }

////// ////// //////
/// BEGIN TIMELINES
//////

timeline_block_declaration = {"@" ~ "timeline" ~ identifier? ~ "{" ~ (timeline_block_setting | timeline_selector_block | comment )* ~ "}"}
timeline_block_setting = { (settings_key ~ timeline_block_setting_value) ~ silent_comma? }
timeline_block_setting_value = { gradient_inline_value | literal_value | expression_wrapped | timeline_symbol }
timeline_selector_block = { timeline_target ~ timeline_selector_body ~ silent_comma? }
timeline_selector_body = { "{" ~ (timeline_property_key_value_pair | comment )* ~ "}" }
timeline_property_key_value_pair = { (settings_key ~ timeline_track) ~ silent_comma? }
timeline_inline_value = { "@" ~ "timeline" ~ timeline_track }
timeline_track = { "{" ~ (timeline_block_setting | timeline_keyframe | comment )* ~ "}" }
timeline_keyframe = { timeline_marker ~ ":" ~ timeline_keyframe_value ~ (silent_comma ~ timeline_easing_curve)? ~ silent_comma? }
timeline_keyframe_value = { gradient_inline_value | literal_value | expression_wrapped | timeline_symbol }
timeline_marker = { timeline_percent | timeline_duration | literal_number_integer }
timeline_percent = { literal_number ~ "%" ~ !"%"}
timeline_duration = { literal_number ~ timeline_duration_unit }
timeline_duration_unit = { "ms" | "s" | "f" }
timeline_easing_curve = { pascal_identifier }
timeline_target = { selector | timeline_local_target }
timeline_local_target = { "self" | "this" }
timeline_symbol = { ("self." | "this.")? ~ identifier }

////// ////// //////
/// BEGIN GRADIENTS
//////

gradient_inline_value = { "@" ~ "gradient" ~ gradient_body }
gradient_body = { "{" ~ (gradient_shape_block | gradient_stop | comment )* ~ "}" }
gradient_shape_block = { gradient_shape_key ~ ":" ~ gradient_shape_settings ~ silent_comma? }
gradient_shape_key = { "linear" | "radial" | "mesh" }
gradient_shape_settings = { "{" ~ (gradient_shape_setting | comment )* ~ "}" }
gradient_shape_setting = { settings_key ~ gradient_shape_setting_value ~ silent_comma? }
gradient_shape_setting_value = { literal_value | expression_wrapped | identifier }
gradient_stop = { gradient_stop_marker ~ ":" ~ gradient_stop_value ~ silent_comma? }
gradient_stop_marker = { literal_number_with_unit }
gradient_stop_value = { literal_value | expression_wrapped | identifier }

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" | "ms" | "s" | "f"}
literal_tuple = {("(") ~ literal_value ~ ("," ~ literal_value)* ~ (")")}
literal_tuple_access = {identifier ~ "." ~ literal_number_integer}
literal_list = {"[" ~ (template_list_value ~ silent_comma?)* ~ "]"}
template_list_value = { gradient_inline_value | literal_value | expression_wrapped | identifier }
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`,
// a short-circuiting fallback expression like `maybe_label ?? "Untitled"`,
// 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 = { expression_ternary }
expression_ternary = { expression_coalesce ~ (xo_tern_then ~ expression_body ~ xo_tern_else ~ expression_body)? }
expression_coalesce = { expression_binary ~ (xo_null_coalesce ~ expression_coalesce)? }
expression_binary = { xo_prefix* ~ xo_primary ~ (xo_infix ~ xo_prefix* ~ xo_primary )* }

expression_wrapped = _{
    "{" ~ comment? ~ expression_body ~ "}"
}

expression_grouped = { "(" ~ expression_body ~ (expression_grouped_unit | ")") }
expression_grouped_unit = ${ ")" ~ 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."

[Option fallback]
maybe_title ?? "Untitled"
*/

//`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_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_null_coalesce = {"??"}
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 = { statement_if_branch ~ statement_else_if_branch* ~ statement_else_branch? }
statement_if_branch = { "if" ~ expression_body ~ "{" ~ inner_nodes ~ "}" }
statement_else_if_branch = { "else" ~ "if" ~ expression_body ~ "{" ~ inner_nodes ~ "}" }
statement_else_branch = { "else" ~ "{" ~ inner_nodes ~ "}" }
statement_for = {"for" ~ statement_for_predicate_declaration ~ "in" ~ statement_for_source ~ statement_for_key? ~ "{" ~ 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 }
statement_for_key = { "key" ~ expression_body }