Files
storybook/tree-sitter-storybook/grammar.js
Sienna Meridian Satterwhite c49b00a2dc feat(grammar): add type system nodes to tree-sitter
Added grammar rules for v0.3 type system declarations:
- concept_declaration: semicolon-terminated base type
- sub_concept: enum and record forms with dot notation
- concept_comparison: nested pattern matching with variant_pattern
- any_type: named node for the 'any' keyword
- template species extension: optional ': Species' syntax

Updated query files:
- highlights.scm: new keywords and named field highlights
- outline.scm: concept/sub_concept/concept_comparison in outline
- indents.scm: indentation for new brace-delimited nodes

Fixed pre-existing query issues:
- Replaced invalid repeat_node reference with decorator_node
- Removed invalid '?' punctuation reference

Added comprehensive test corpus (type_system.txt) covering:
- concept declaration (simple and multiple)
- sub_concept enum and record forms
- concept_comparison with any/is conditions
- template with species extension
- full combined example
2026-02-14 14:29:29 +00:00

524 lines
11 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],
[$.sub_concept_enum_body, $.sub_concept_record_body]
],
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,
$.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: $ => 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(seq(':', field('species', $.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]+[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: $ => 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,
$.condition_node,
$.if_decorator_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),
'}'
),
// Decorator node: repeat/retry/timeout/etc { child }
decorator_node: $ => seq(
field('decorator', $.decorator_keyword),
optional(field('params', $.decorator_params)),
'{',
field('child', $.behavior_node),
'}'
),
decorator_keyword: $ => choice(
'repeat',
'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: $ => seq(
'institution',
field('name', $.identifier),
$.block
),
// Relationship declaration
relationship: $ => seq(
'relationship',
field('name', $.identifier),
'{',
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: $ => 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),
'}'
),
// 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: Type, field_name: any
sub_concept_record_body: $ => commaSep1($.sub_concept_field),
sub_concept_field: $ => seq(
field('name', $.identifier),
':',
field('type', choice($.identifier, $.any_type))
),
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),
'}'
),
// A condition on a sub_concept field
field_condition: $ => seq(
field('sub_concept', $.dotted_path),
':',
field('condition', $.condition_expr)
),
// Condition: either 'any' or 'Type is Value or Type is Value2'
condition_expr: $ => choice(
$.any_type,
$.is_condition
),
// Pattern: DottedPath is Ident [or DottedPath is Ident]*
is_condition: $ => seq(
$.dotted_path,
'is',
$.identifier,
repeat(seq('or', $.dotted_path, 'is', $.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)));
}