srf-tree-sitter/grammar.js
2026-09-01 10:07:02 -07:00

150 lines
3.9 KiB
JavaScript

/**
* @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.
*/
/// <reference types="tree-sitter-cli/dsl" />
// @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,
),
},
});