const ESCAPE_SEQUENCE = token(/\\([nrt"\\]|(\r?\n))/);
const SHEBANG_ENV_FLAG = token(/-\S*/);
function comma_sep1(rule) {
return seq(rule, repeat(seq(",", rule)));
}
function array(rule) {
const item = field("element", rule);
return field(
"array",
seq(
"[",
optional(field("content", seq(comma_sep1(item), optional(item)))),
"]",
),
);
}
module.exports = grammar({
name: "just",
externals: ($) => [
$._indent,
$._dedent,
$._newline,
$.text,
$.error_recovery,
],
extras: ($) => [$.comment, /\\(\n|\r\n)\s*/, /\s/],
inline: ($) => [
$._string,
$._string_indented,
$._raw_string_indented,
$._expression_recurse,
],
word: ($) => $.identifier,
rules: {
source_file: ($) =>
seq(optional(seq($.shebang, $._newline)), repeat($._item)),
_item: ($) =>
choice(
$.recipe,
$.alias,
$.assignment,
$.export,
$.import,
$.module,
$.setting,
),
alias: ($) =>
seq(
repeat($.attribute),
"alias",
field("left", $.identifier),
":=",
field("right", $.identifier),
),
assignment: ($) =>
seq(
field("left", $.identifier),
":=",
field("right", $.expression),
$._newline,
),
export: ($) => seq("export", $.assignment),
import: ($) => seq("import", optional("?"), $.string),
module: ($) =>
seq(
"mod",
optional("?"),
field("name", $.identifier),
optional($.string),
),
setting: ($) =>
choice(
seq(
"set",
field("left", $.identifier),
field(
"right",
optional(seq(":=", choice($.boolean, $.string, array($.string)))),
),
$._newline,
),
seq("set", "shell", ":=", field("right", array($.string)), $._newline),
),
boolean: (_) => choice("true", "false"),
expression: ($) => seq(optional("/"), $._expression_inner),
_expression_inner: ($) =>
choice(
$.if_expression,
prec.left(2, seq($._expression_recurse, "+", $._expression_recurse)),
prec.left(1, seq($._expression_recurse, "/", $._expression_recurse)),
$.value,
),
_expression_recurse: ($) => alias($._expression_inner, "expression"),
if_expression: ($) =>
seq(
"if",
$.condition,
field("consequence", $._braced_expr),
repeat(field("alternative", $.else_if_clause)),
optional(field("alternative", $.else_clause)),
),
else_if_clause: ($) => seq("else", "if", $.condition, $._braced_expr),
else_clause: ($) => seq("else", $._braced_expr),
_braced_expr: ($) => seq("{", field("body", $.expression), "}"),
condition: ($) =>
choice(
seq($.expression, "==", $.expression),
seq($.expression, "!=", $.expression),
seq($.expression, "=~", choice($.regex_literal, $.expression)),
$.expression,
),
regex_literal: ($) => prec(1, $.string),
value: ($) =>
prec.left(
choice(
$.function_call,
$.external_command,
$.identifier,
$.string,
$.numeric_error,
seq("(", $.expression, ")"),
),
),
function_call: ($) =>
seq(
field("name", $.identifier),
"(",
optional(field("arguments", $.sequence)),
")",
),
external_command: ($) =>
choice(seq($._backticked), seq($._indented_backticked)),
sequence: ($) => comma_sep1($.expression),
attribute: ($) =>
seq(
"[",
comma_sep1(
choice(
$.identifier,
seq(
$.identifier,
"(",
field("argument", comma_sep1($.string)),
")",
),
seq($.identifier, ":", field("argument", $.string)),
),
),
"]",
$._newline,
),
recipe: ($) =>
seq(
repeat($.attribute),
$.recipe_header,
$._newline,
optional($.recipe_body),
),
recipe_header: ($) =>
seq(
optional("@"),
field("name", $.identifier),
optional($.parameters),
":",
optional($.dependencies),
),
parameters: ($) =>
seq(repeat($.parameter), choice($.parameter, $.variadic_parameter)),
parameter: ($) =>
seq(
optional("$"),
field("name", $.identifier),
optional(seq("=", field("default", $.value))),
),
variadic_parameter: ($) =>
seq(field("kleene", choice("*", "+")), $.parameter),
dependencies: ($) => repeat1(seq(optional("&&"), $.dependency)),
dependency: ($) =>
choice(field("name", $.identifier), $.dependency_expression),
dependency_expression: ($) =>
seq("(", field("name", $.identifier), repeat($.expression), ")"),
recipe_body: ($) =>
seq(
$._indent,
optional(seq(field("shebang", $.shebang), $._newline)),
repeat(choice(seq($.recipe_line, $._newline), $._newline)),
$._dedent,
),
recipe_line: ($) =>
seq(
optional($.recipe_line_prefix),
repeat1(choice($.text, $.interpolation)),
),
recipe_line_prefix: (_) => choice("@-", "-@", "@", "-"),
shebang: ($) =>
seq(/#![ \t]*/, choice($._shebang_with_lang, $._opaque_shebang)),
_shebang_with_lang: ($) =>
seq(
/\S*\//,
optional(seq("env", repeat(SHEBANG_ENV_FLAG))),
alias($.identifier, $.language),
/.*/,
),
_opaque_shebang: (_) => /[^/\n]+/,
string: ($) =>
choice(
$._string_indented,
$._raw_string_indented,
$._string,
/'[^']*'/,
),
_raw_string_indented: (_) => seq("'''", repeat(/./), "'''"),
_string: ($) => seq('"', repeat(choice($.escape_sequence, /[^\\"]+/)), '"'),
_string_indented: ($) =>
seq('"""', repeat(choice($.escape_sequence, /[^\\]?[^\\"]+/)), '"""'),
escape_sequence: (_) => ESCAPE_SEQUENCE,
_backticked: ($) => seq("`", optional($.command_body), "`"),
_indented_backticked: ($) => seq("```", optional($.command_body), "```"),
command_body: ($) => repeat1(choice($.interpolation, /./)),
interpolation: ($) => seq("{{", $.expression, "}}"),
identifier: (_) => /[a-zA-Z_][a-zA-Z0-9_-]*/,
numeric_error: (_) => /(\d+\.\d*|\d+)/,
comment: (_) => token(prec(-1, /#.*/)),
},
});