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

380 lines
7.7 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]
],
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,
$.template,
$.life_arc,
$.schedule,
$.behavior,
$.institution,
$.relationship,
$.location,
$.species,
$.enum_declaration
),
// 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: $ => seq(
'character',
field('name', $.identifier),
optional(seq(':', field('species', $.identifier))),
optional(field('template', $.template_clause)),
field('body', $.block)
),
template_clause: $ => seq('from', commaSep1($.identifier)),
// Template declaration
template: $ => seq(
'template',
field('name', $.identifier),
optional('strict'),
'{',
repeat($.include),
repeat($.field),
'}'
),
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]+[hms]([0-9]+[hms])*/,
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: $ => seq(
'life_arc',
field('name', $.identifier),
'{',
repeat($.field),
repeat($.arc_state),
'}'
),
arc_state: $ => seq(
'state',
field('name', $.identifier),
'{',
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: $ => seq(
'schedule',
field('name', $.identifier),
'{',
repeat($.field),
repeat($.schedule_block),
'}'
),
schedule_block: $ => seq(
field('start', $.time),
'->',
field('end', $.time),
':',
field('activity', $.identifier),
$.block
),
// Behavior tree declaration
behavior: $ => seq(
'behavior',
field('name', $.identifier),
'{',
repeat($.field),
field('root', $.behavior_node),
'}'
),
behavior_node: $ => choice(
$.selector_node,
$.sequence_node,
$.repeat_node,
$.action_node,
$.subtree_node
),
selector_node: $ => seq('?', '{', repeat1($.behavior_node), '}'),
sequence_node: $ => seq('>', '{', repeat1($.behavior_node), '}'),
repeat_node: $ => seq('*', '{', $.behavior_node, '}'),
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: $ => seq('@', $.path),
// Institution declaration
institution: $ => seq(
'institution',
field('name', $.identifier),
$.block
),
// Relationship declaration
relationship: $ => seq(
'relationship',
field('name', $.identifier),
'{',
repeat1($.participant),
repeat($.field),
'}'
),
participant: $ => choice(
// name { fields }
seq($.path, $.block),
// name as role { fields }
seq($.path, 'as', $.identifier, $.block),
// bare name
$.path
),
// Location declaration
location: $ => seq(
'location',
field('name', $.identifier),
$.block
),
// Species declaration
species: $ => seq(
'species',
field('name', $.identifier),
'{',
repeat($.include),
repeat($.field),
'}'
),
// Enum declaration
enum_declaration: $ => seq(
'enum',
field('name', $.identifier),
'{',
commaSep1($.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)));
}