150 lines
3.9 KiB
JavaScript
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,
|
|
),
|
|
},
|
|
});
|