/** * 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, $.definition ), // 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('modifies', field('modifies', $.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', // Definition - compile-time pattern matching definition: $ => seq( 'definition', 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))); }