380 lines
7.7 KiB
JavaScript
380 lines
7.7 KiB
JavaScript
|
|
/**
|
||
|
|
* 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)));
|
||
|
|
}
|