/** * @file SRF (Simple Record Format) grammar for tree-sitter * @author Emil Lerch * @license MIT * * SRF's field delimiter depends on the format directive: ',' in compact format * (the default) and '\n' after `#!long`. A numeric type hint additionally means * "this value is exactly N bytes", newlines included. Neither is expressible * here, so those tokens come from the external scanner in src/scanner.c. */ /// // @ts-check module.exports = grammar({ name: 'srf', extras: _ => [], externals: $ => [ $._long_marker, $._compact_marker, $._length_hint, $.value, // Never produced by the scanner. Only valid while tree-sitter is in error // recovery, which tells the scanner to keep its hands off. $._error_sentinel, ], rules: { document: $ => repeat($._line), // Leading whitespace is insignificant on every kind of line: the reference // parser trims the start of each line before deciding what it is. _line: $ => seq( optional($._ws), choice( $.magic, $.directive, $.comment, $._field_line, $.blank_line, ), ), _ws: _ => /[ \t]+/, magic: $ => seq( '#!srfv1', optional($.inline_comment), '\n', ), inline_comment: _ => /[ \t]+#[^\n]*/, directive: $ => choice( $.directive_long, $.directive_compact, $.directive_requireeof, $.directive_eof, $.directive_expires, $.directive_created, $.directive_modified, $.directive_unknown, ), // The format directives are scanned externally so that the scanner learns // which delimiter applies to the rest of the document. If both appear, the // last one wins, matching the reference parser. directive_long: $ => seq($._long_marker, optional($.inline_comment), '\n'), directive_compact: $ => seq($._compact_marker, optional($.inline_comment), '\n'), directive_requireeof: $ => seq('#!requireeof', optional($.inline_comment), '\n'), directive_eof: $ => seq('#!eof', optional($.inline_comment), '\n'), directive_expires: $ => seq( '#!expires=', field('value', $.timestamp), optional($.inline_comment), '\n', ), directive_created: $ => seq( '#!created=', field('value', $.timestamp), optional($.inline_comment), '\n', ), directive_modified: $ => seq( '#!modified=', field('value', $.timestamp), optional($.inline_comment), '\n', ), directive_unknown: _ => seq( /#!/, /[a-zA-Z][a-zA-Z0-9_]*/, optional(seq('=', /[^\n]*/)), '\n', ), timestamp: _ => /[0-9-]+/, comment: _ => token(seq('#', optional(seq(/[^!\n]/, /[^\n]*/)), '\n')), blank_line: _ => token(/[ \t]*\n/), // A comma only ever separates fields in compact format: in long format the // scanner has already swallowed it as part of the value, so the repetition // below is unreachable there. Requiring a field after every comma is what // makes a trailing separator (`key::value,`) an error, as the spec demands. _field_line: $ => seq( $.field, repeat(seq(',', $.field)), '\n', ), field: $ => choice( $.typed_field, $.untyped_field, ), typed_field: $ => seq( $.key, ':', optional($._ws), $.type_hint, optional($._ws), ':', optional($.value), ), untyped_field: $ => seq( $.key, '::', optional($.value), ), // Everything up to the first colon, so commas are fair game. A key may not // start with one, which keeps a stray trailing comma in compact format from // being absorbed into the next key. key: _ => /[^#\n:, \t][^:\n]*/, type_hint: $ => choice( 'string', 'num', 'bool', 'null', 'binary', $._length_hint, ), }, });