Files
storybook/tree-sitter-storybook/grammar.js

659 lines
14 KiB
JavaScript
Raw Normal View History

/**
* Tree-sitter grammar for Storybook DSL
*
* This grammar defines the syntax for the Storybook narrative DSL,
* including characters, templates, relationships, life arcs, behaviors, etc.
*/
module.exports = grammar({
name: 'storybook',
// externals: $ => [
// $._prose_block_content,
// $._prose_block_end
// ],
extras: $ => [
/\s/, // Whitespace
$.line_comment,
$.block_comment
],
conflicts: $ => [
[$.path_segments],
[$.sub_concept_enum_body, $.sub_concept_record_body],
[$.uses_behaviors]
],
word: $ => $.identifier,
rules: {
// Top-level structure
source_file: $ => repeat($.declaration),
// Comments
line_comment: $ => token(seq('//', /.*/)),
block_comment: $ => token(seq('/*', /[^*]*\*+([^/*][^*]*\*+)*/, '/')),
// Declarations
declaration: $ => choice(
$.use_declaration,
$.character_declaration,
$.template_declaration,
$.life_arc_declaration,
$.schedule_declaration,
$.behavior_declaration,
$.institution_declaration,
$.relationship_declaration,
$.location_declaration,
$.species_declaration,
$.enum_declaration,
$.concept_declaration,
$.sub_concept,
$.concept_comparison
),
// Use declarations
use_declaration: $ => seq(
'use',
$.path_segments,
optional(choice(
seq('::', '{', commaSep1($.identifier), '}'),
seq('::', '*')
)),
';'
),
path: $ => $.path_segments,
path_segments: $ => sep1($.identifier, token('::')),
// Character declaration
character_declaration: $ => seq(
'character',
field('name', $.identifier),
optional(seq(':', field('species', $.identifier))),
optional(field('template', $.template_clause)),
field('body', $.character_body)
),
character_body: $ => seq(
'{',
repeat($.field),
'}'
),
template_clause: $ => seq('from', commaSep1($.identifier)),
// Template declaration
template_declaration: $ => seq(
'template',
field('name', $.identifier),
optional(seq(':', field('species', $.identifier))),
optional('strict'),
field('body', $.template_body)
),
template_body: $ => seq(
'{',
repeat(choice(
$.include,
$.uses_behaviors,
$.uses_schedule,
$.field
)),
'}'
),
uses_behaviors: $ => seq(
'uses',
'behaviors',
':',
commaSep1($.identifier)
),
uses_schedule: $ => seq(
'uses',
choice('schedule', 'schedules'),
':',
choice(
$.identifier,
seq('[', commaSep1($.identifier), ']')
)
),
include: $ => seq('include', $.identifier),
// Fields (key: value pairs)
field: $ => choice(
seq(
field('name', $.dotted_path),
':',
field('value', $.value)
),
$.prose_block
),
dotted_path: $ => sep1($.identifier, '.'),
// Values
value: $ => choice(
$.integer,
$.float,
$.string,
$.boolean,
$.range,
$.time,
$.duration,
$.path,
$.prose_block,
$.list,
$.object,
$.override
),
integer: $ => /-?[0-9]+/,
float: $ => /-?[0-9]+\.[0-9]+/,
string: $ => /"([^"\\]|\\.)*"/,
boolean: $ => choice('true', 'false'),
range: $ => choice(
seq($.integer, '..', $.integer),
seq($.float, '..', $.float)
),
time: $ => /[0-9]{2}:[0-9]{2}(:[0-9]{2})?/,
duration: $ => /[0-9]+[dhms]([0-9]+[dhms])*/,
list: $ => seq('[', commaSep($.value), ']'),
object: $ => $.block,
block: $ => seq('{', repeat($.field), '}'),
// Override (@base { remove field, field: value })
override: $ => seq(
'@',
$.path,
'{',
repeat($.override_op),
'}'
),
override_op: $ => choice(
seq('remove', $.identifier),
seq('append', $.field),
$.field
),
// Prose blocks (---tag content ---)
prose_block: $ => seq(
field('marker', $.prose_marker),
field('tag', $.identifier),
optional(/[^\n]*/), // Rest of opening line
field('content', $.prose_content),
field('end', $.prose_marker)
),
prose_marker: $ => '---',
// Capture prose content as a single token for markdown injection
prose_content: $ => token(prec(-1, repeat1(choice(
/[^\-]+/, // Any non-dash characters
/-[^\-]/, // Single dash not followed by another dash
/-\-[^\-]/, // Two dashes not followed by another dash
)))),
// Life arc declaration
life_arc_declaration: $ => seq(
'life_arc',
field('name', $.identifier),
optional(field('requires', $.requires_clause)),
'{',
repeat($.field),
repeat($.state_block),
'}'
),
requires_clause: $ => seq(
'requires',
'{',
commaSep1($.required_field),
'}'
),
required_field: $ => seq(
field('name', $.identifier),
':',
field('type', $.identifier)
),
state_block: $ => seq(
'state',
field('name', $.identifier),
field('body', $.state_body)
),
state_body: $ => seq(
'{',
optional($.on_enter),
repeat($.field),
repeat($.transition),
'}'
),
on_enter: $ => seq('on', 'enter', $.block),
transition: $ => seq(
'on',
field('condition', $.expression),
'->',
field('target', $.identifier)
),
// Schedule declaration
schedule_declaration: $ => seq(
'schedule',
field('name', $.identifier),
optional(seq('extends', field('extends', $.identifier))),
field('body', $.schedule_body)
),
schedule_body: $ => seq(
'{',
repeat(choice(
$.field,
$.schedule_block,
$.override_block,
$.recurrence_block
)),
'}'
),
// Named block: block name { time range, action, fields }
schedule_block: $ => seq(
'block',
field('name', $.identifier),
'{',
field('time_range', $.time_range),
repeat($.block_field),
'}'
),
// Override block: override name { time range, action, fields }
override_block: $ => seq(
'override',
field('name', $.identifier),
'{',
field('time_range', $.time_range),
repeat($.block_field),
'}'
),
// Recurrence: recurrence Name on DayOfWeek { blocks }
recurrence_block: $ => seq(
'recurrence',
field('name', $.identifier),
'on',
field('day', $.identifier),
'{',
repeat1($.schedule_block),
'}'
),
time_range: $ => seq(
field('start', $.time),
'->',
field('end', $.time),
optional(',')
),
block_field: $ => seq(
field('name', $.identifier),
':',
field('value', choice($.identifier, $.string, $.integer, $.float))
),
// Behavior tree declaration
behavior_declaration: $ => seq(
'behavior',
field('name', $.identifier),
field('body', $.behavior_body)
),
behavior_body: $ => seq(
'{',
repeat($.field),
field('root', $.behavior_node),
'}'
),
behavior_node: $ => choice(
$.selector_node,
$.sequence_node,
$.condition_node,
$.if_decorator_node,
$.repeat_node,
$.decorator_node,
$.action_node,
$.subtree_node
),
// Selector node: choose { ... }
selector_node: $ => seq(
'choose',
optional(field('label', $.identifier)),
'{',
repeat1($.behavior_node),
'}'
),
// Sequence node: then { ... }
sequence_node: $ => seq(
'then',
optional(field('label', $.identifier)),
'{',
repeat1($.behavior_node),
'}'
),
// Condition node: if(expr) or when(expr) - NO BRACES
condition_node: $ => seq(
choice('if', 'when'),
'(',
field('condition', $.expression),
')'
),
// If decorator: if(expr) { child } - WITH BRACES
if_decorator_node: $ => seq(
'if',
'(',
field('condition', $.expression),
')',
'{',
field('child', $.behavior_node),
'}'
),
// Repeat node: repeat { child } or repeat(N) { child } or repeat(min..max) { child }
repeat_node: $ => seq(
'repeat',
optional(field('params', choice(
seq('(', $.integer, ')'),
seq('(', $.integer, '..', $.integer, ')'),
seq('(', $.duration, ')')
))),
'{',
field('child', $.behavior_node),
'}'
),
// Decorator node: retry/timeout/etc { child }
decorator_node: $ => seq(
field('decorator', $.decorator_keyword),
optional(field('params', $.decorator_params)),
'{',
field('child', $.behavior_node),
'}'
),
decorator_keyword: $ => choice(
'invert',
'retry',
'timeout',
'cooldown',
'succeed_always',
'fail_always'
),
decorator_params: $ => seq(
'(',
choice(
// min..max range (for repeat)
seq($.integer, '..', $.integer),
// N (for repeat, retry)
$.integer,
// duration (for timeout, cooldown)
$.duration
),
')'
),
// Action node: action_name or action_name(params)
action_node: $ => choice(
seq($.identifier, '(', commaSep($.action_param), ')'),
$.identifier
),
action_param: $ => choice(
// Named parameter: name: value
seq($.dotted_path, ':', $.value),
// Positional parameter: just value
$.value
),
// Subtree node: include path::to::subtree
subtree_node: $ => seq('include', $.path),
// Institution declaration
institution_declaration: $ => seq(
'institution',
field('name', $.identifier),
field('body', $.block)
),
// Relationship declaration
relationship_declaration: $ => seq(
'relationship',
field('name', $.identifier),
field('body', $.relationship_body)
),
relationship_body: $ => seq(
'{',
repeat1($.participant),
repeat($.field),
'}'
),
participant: $ => choice(
// name as role { fields } (role optional)
seq($.path, 'as', $.identifier, $.block),
// name { fields } (block required)
seq($.path, $.block)
),
// Location declaration
location_declaration: $ => seq(
'location',
field('name', $.identifier),
field('body', $.block)
),
// Species declaration
species_declaration: $ => seq(
'species',
field('name', $.identifier),
field('body', $.species_body)
),
species_body: $ => seq(
'{',
repeat($.include),
repeat($.species_field),
'}'
),
species_field: $ => choice(
// Field with range: name: min..max
seq(
field('name', $.identifier),
':',
field('value', $.range)
),
// Field with single value: name: value
seq(
field('name', $.identifier),
':',
field('value', choice($.integer, $.float, $.boolean, $.string))
),
$.prose_block
),
// Enum declaration
enum_declaration: $ => seq(
'enum',
field('name', $.identifier),
'{',
commaSep1($.identifier),
'}'
),
// Concept declaration - base type with no structure
concept_declaration: $ => seq(
'concept',
field('name', $.identifier)
),
// Sub-concept declaration - enum or record subtype with dot notation
sub_concept: $ => seq(
'sub_concept',
field('parent', $.identifier),
'.',
field('name', $.identifier),
'{',
field('body', choice(
$.sub_concept_record_body,
$.sub_concept_enum_body
)),
'}'
),
// Enum form: Variant1, Variant2, Variant3
sub_concept_enum_body: $ => commaSep1($.identifier),
// Record form: field_name: value, field_name: value
sub_concept_record_body: $ => commaSep1($.sub_concept_field),
sub_concept_field: $ => seq(
field('name', $.identifier),
':',
field('value', choice($.integer, $.float, $.string, $.boolean))
),
any_type: $ => 'any',
// Concept comparison - compile-time pattern matching
concept_comparison: $ => seq(
'concept_comparison',
field('name', $.identifier),
'{',
commaSep1($.variant_pattern),
'}'
),
// A named variant with field conditions
variant_pattern: $ => seq(
field('name', $.identifier),
':',
'{',
commaSep1($.field_condition),
'}'
),
// Field condition: field_name: any OR field_name: Type is Value [or Type is Value]*
field_condition: $ => seq(
field('name', $.identifier),
':',
field('condition', choice(
$.any_type,
$.is_condition
))
),
// Is condition: Type is Value [or Type is Value]*
is_condition: $ => seq(
$.is_value,
repeat(seq('or', $.is_value))
),
is_value: $ => seq(
field('field', $.identifier),
'is',
field('value', $.identifier)
),
// Expressions (for conditions in life arcs)
expression: $ => choice(
$.or_expression,
$.and_expression,
$.not_expression,
$.comparison,
$.field_access,
$.primary_expression
),
or_expression: $ => prec.left(1, seq(
$.expression,
'or',
$.expression
)),
and_expression: $ => prec.left(2, seq(
$.expression,
'and',
$.expression
)),
not_expression: $ => prec(3, seq('not', $.expression)),
comparison: $ => prec.left(4, seq(
$.expression,
field('operator', choice('is', '>', '>=', '<', '<=')),
$.expression
)),
field_access: $ => prec.left(5, seq(
$.expression,
'.',
$.identifier
)),
primary_expression: $ => choice(
'self',
'other',
$.integer,
$.float,
$.string,
$.boolean,
$.path
),
// Identifiers
identifier: $ => /[a-zA-Z_][a-zA-Z0-9_]*/,
}
});
/**
* Helper to create comma-separated lists with optional trailing comma
*/
function commaSep(rule) {
return optional(commaSep1(rule));
}
function commaSep1(rule) {
return seq(rule, repeat(seq(',', rule)), optional(','));
}
/**
* Helper to create separator-based lists (e.g., :: or .)
*/
function sep1(rule, separator) {
return seq(rule, repeat(seq(separator, rule)));
}