/** * 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))); }