let newline = choice("\n", "\r", "\r\n"); let newline_or_eof = choice("\n", "\r", "\r\n", "\0"); module.exports = grammar({ name: "norg", // Tell treesitter we want to handle whitespace ourselves extras: $ => [$._preceding_whitespace], externals: $ => [ $._preceding_whitespace, $._weak_delimiting_modifier, $._bold_open, $._bold_close, $._italic_open, $._italic_close, $._strikethrough_open, $._strikethrough_close, $._underline_open, $._underline_close, $._spoiler_open, $._spoiler_close, $._verbatim_open, $._verbatim_close, $._superscript_open, $._superscript_close, $._subscript_open, $._subscript_close, $._inline_comment_open, $._inline_comment_close, $._inline_math_open, $._inline_math_close, $._inline_macro_open, $._inline_macro_close, ], conflicts: $ => [ [$.bold, $._attached_modifier_conflict], [$.italic, $._attached_modifier_conflict], [$.strikethrough, $._attached_modifier_conflict], [$.underline, $._attached_modifier_conflict], [$.spoiler, $._attached_modifier_conflict], [$.verbatim, $._attached_modifier_conflict], [$.superscript, $._attached_modifier_conflict], [$.subscript, $._attached_modifier_conflict], [$.inline_comment, $._attached_modifier_conflict], [$.inline_math, $._attached_modifier_conflict], [$.inline_macro, $._attached_modifier_conflict], [$.bold, $._free_form_conflict], [$.italic, $._free_form_conflict], [$.strikethrough, $._free_form_conflict], [$.underline, $._free_form_conflict], [$.spoiler, $._free_form_conflict], [$.verbatim, $._free_form_conflict], [$.superscript, $._free_form_conflict], [$.subscript, $._free_form_conflict], [$.inline_comment, $._free_form_conflict], [$.inline_math, $._free_form_conflict], [$.inline_macro, $._free_form_conflict], [$.bold, $._free_form_conflict, $._attached_modifier_conflict], [$.italic, $._free_form_conflict, $._attached_modifier_conflict], [$.strikethrough, $._free_form_conflict, $._attached_modifier_conflict], [$.underline, $._free_form_conflict, $._attached_modifier_conflict], [$.spoiler, $._free_form_conflict, $._attached_modifier_conflict], [$.verbatim, $._free_form_conflict, $._attached_modifier_conflict], [$.superscript, $._free_form_conflict, $._attached_modifier_conflict], [$.subscript, $._free_form_conflict, $._attached_modifier_conflict], [$.inline_comment, $._free_form_conflict, $._attached_modifier_conflict], [$.inline_math, $._free_form_conflict, $._attached_modifier_conflict], [$.inline_macro, $._free_form_conflict, $._attached_modifier_conflict], [$.paragraph_segment], ], precedences: $ => [ [$.anchor_definition, $.anchor_declaration], [$._free_form_conflict, $._attached_modifier_conflict], ], inlines: $ => [ ], supertypes: $ => [ $.non_structural, $.heading, $.attached_modifier, $.tag, $._attached_modifier_conflict, $.linkable, ], rules: { document: $ => repeat( choice( $.heading, $.strong_delimiting_modifier, $.non_structural, ), ), _character: _ => token(/[^\s]/), _word: $ => prec.right(repeat1($._character)), _whitespace: _ => token(/[\t                 ]+/), escape_sequence: $ => seq( "\\", alias(/./, $.escape_char), ), // TODO: Fix `hello ,*world*` failing` // TODO: Fix `:*hello*` not printing any bold // perhaps do something like `alias(":", $._word)`? paragraph_segment: $ => seq( choice( $._word, $.escape_sequence, $.linkable, $._inline_link_target_conflict_open, $._free_form_conflict, seq( optional( seq( $._word, $.link_modifier, ), ), $.attached_modifier, optional( seq( $.link_modifier, $._word, ), ), ), ), repeat( choice( seq( $._whitespace, choice( $._word, $._whitespace, $.escape_sequence, $.attached_modifier, $._attached_modifier_conflict, $._free_form_conflict, $.linkable, $._inline_link_target_conflict_open, seq( optional( seq( $._word, $.link_modifier, ), ), $.attached_modifier, optional( seq( $.link_modifier, $._word, ), ), ), ), ), choice( $._word, $.escape_sequence, $._attached_modifier_conflict, $._free_form_conflict, $._inline_link_target_conflict_open, ), ), ), ), paragraph: $ => prec.right(seq( $.paragraph_segment, repeat( seq( newline, $.paragraph_segment, ), ), newline_or_eof, // TODO: deal with https://github.com/nvim-neorg/norg-specs/issues/8 )), heading1: $ => heading($, 1), heading2: $ => heading($, 2), heading3: $ => heading($, 3), heading4: $ => heading($, 4), heading5: $ => heading($, 5), heading6: $ => heading($, 6), heading: $ => choice( $.heading1, $.heading2, $.heading3, $.heading4, $.heading5, $.heading6, ), weak_delimiting_modifier: $ => seq( $._weak_delimiting_modifier, newline_or_eof, ), strong_delimiting_modifier: $ => seq( "==", repeat("="), newline_or_eof, ), horizontal_rule: $ => seq( "__", repeat("_"), newline_or_eof, ), unordered_list_item1: $ => nestable_detached_mod($, "unordered_list_item", "-", 1), unordered_list_item2: $ => nestable_detached_mod($, "unordered_list_item", "-", 2), unordered_list_item3: $ => nestable_detached_mod($, "unordered_list_item", "-", 3), unordered_list_item4: $ => nestable_detached_mod($, "unordered_list_item", "-", 4), unordered_list_item5: $ => nestable_detached_mod($, "unordered_list_item", "-", 5), unordered_list_item6: $ => nestable_detached_mod($, "unordered_list_item", "-", 6), ordered_list_item1: $ => nestable_detached_mod($, "ordered_list_item", "~", 1), ordered_list_item2: $ => nestable_detached_mod($, "ordered_list_item", "~", 2), ordered_list_item3: $ => nestable_detached_mod($, "ordered_list_item", "~", 3), ordered_list_item4: $ => nestable_detached_mod($, "ordered_list_item", "~", 4), ordered_list_item5: $ => nestable_detached_mod($, "ordered_list_item", "~", 5), ordered_list_item6: $ => nestable_detached_mod($, "ordered_list_item", "~", 6), list: $ => prec.right( repeat1( choice( $.unordered_list_item1, $.unordered_list_item2, $.unordered_list_item3, $.unordered_list_item4, $.unordered_list_item5, $.unordered_list_item6, $.ordered_list_item1, $.ordered_list_item2, $.ordered_list_item3, $.ordered_list_item4, $.ordered_list_item5, $.ordered_list_item6, ), ), ), quote_item1: $ => nestable_detached_mod($, "quote_item", ">", 1), quote_item2: $ => nestable_detached_mod($, "quote_item", ">", 2), quote_item3: $ => nestable_detached_mod($, "quote_item", ">", 3), quote_item4: $ => nestable_detached_mod($, "quote_item", ">", 4), quote_item5: $ => nestable_detached_mod($, "quote_item", ">", 5), quote_item6: $ => nestable_detached_mod($, "quote_item", ">", 6), quote: $ => prec.right( repeat1( choice( $.quote_item1, $.quote_item2, $.quote_item3, $.quote_item4, $.quote_item5, $.quote_item6, ), ), ), non_structural: $ => choice( $.paragraph, newline, $.list, $.quote, $.horizontal_rule, $.tag, $.footnote_list, $.definition_list, $.table, ), footnote_list: $ => prec.right( repeat1( choice( $.single_footnote, $.multi_footnote, ), ), ), single_footnote: $ => rangeable_single_detached_mod($, "^"), multi_footnote: $ => rangeable_multi_detached_mod($, "^^"), definition_list: $ => prec.right( repeat1( choice( $.single_definition, $.multi_definition, ), ), ), single_definition: $ => rangeable_single_detached_mod($, "$"), multi_definition: $ => rangeable_multi_detached_mod($, "$$"), table: $ => prec.right( repeat1( choice( $.single_table, $.multi_table, ), ), ), single_table: $ => rangeable_single_detached_mod($, ":"), multi_table: $ => rangeable_multi_detached_mod($, "::"), undone: _ => " ", done: _ => "x", pending: _ => "-", uncertain: _ => "?", on_hold: _ => "=", cancelled: _ => "_", urgent: _ => "!", recurring: _ => "+", date: _ => "@", priority: _ => "#", due: _ => "<", start: _ => ">", _detached_modifier_extension_parameter: $ => repeat1(choice($._word, $._whitespace)), detached_modifier_extension: $ => choice( $.undone, $.done, $.pending, $.uncertain, $.on_hold, $.cancelled, $.urgent, prec.right(seq( $.recurring, optional( seq( $._whitespace, alias($._detached_modifier_extension_parameter, $.timestamp) ), ), )), // TODO(vhyrro): Abstract this prec.right(seq( $.date, $._whitespace, alias($._detached_modifier_extension_parameter, $.timestamp), )), prec.right(seq( $.due, $._whitespace, alias($._detached_modifier_extension_parameter, $.timestamp), )), prec.right(seq( $.start, $._whitespace, alias($._detached_modifier_extension_parameter, $.timestamp), )), prec.right(seq( $.priority, $._whitespace, alias($._detached_modifier_extension_parameter, $.priorities), )), ), detached_modifier_extensions: $ => seq( "(", $.detached_modifier_extension, repeat( seq( // NOTE(vhyrro): Yes, this is the only real way to represent this. // It works somehow! optional($._whitespace), optional(newline), optional($._whitespace), "|", optional($._whitespace), optional(newline), optional($._whitespace), $.detached_modifier_extension, ), ), ")", ), attached_modifier: $ => choice( $.bold, $.italic, $.strikethrough, $.underline, $.spoiler, $.verbatim, $.superscript, $.subscript, $.inline_comment, $.inline_math, $.inline_macro, ), bold: $ => prec.dynamic(2, attached_mod($, "bold", false)), italic: $ => prec.dynamic(2, attached_mod($, "italic", false)), strikethrough: $ => prec.dynamic(2, attached_mod($, "strikethrough", false)), underline: $ => prec.dynamic(2, attached_mod($, "underline", false)), spoiler: $ => prec.dynamic(2, attached_mod($, "spoiler", false)), verbatim: $ => prec.dynamic(3, attached_mod($, "verbatim", true)), superscript: $ => prec.dynamic(2, attached_mod($, "superscript", false)), subscript: $ => prec.dynamic(2, attached_mod($, "subscript", false)), inline_comment: $ => prec.dynamic(2, attached_mod($, "inline_comment", false)), inline_math: $ => prec.dynamic(3, attached_mod($, "inline_math", true)), inline_macro: $ => prec.dynamic(3, attached_mod($, "inline_macro", true)), _attached_modifier_conflict: $ => choice( $._bold_open, $._italic_open, $._strikethrough_open, $._underline_open, $._verbatim_open, $._spoiler_open, $._superscript_open, $._subscript_open, $._inline_comment_open, $._inline_math_open, $._inline_macro_open, ), _free_form_conflict: $ => prec.dynamic(1, seq( choice( $._bold_open, $._italic_open, $._strikethrough_open, $._underline_open, $._verbatim_open, $._spoiler_open, $._superscript_open, $._subscript_open, $._inline_comment_open, $._inline_math_open, $._inline_macro_open, ), "|", )), attribute_identifier: $ => seq( alias($._word, $.attribute_name), repeat( seq( ":", alias($._word, $.attribute_name), ), ), ), attached_modifier_extension: $ => seq( "(", $.attribute_identifier, repeat( seq( // NOTE(vhyrro): Yes, this is the only real way to represent this. // It works somehow! optional($._whitespace), optional(newline), optional($._whitespace), "|", optional($._whitespace), optional(newline), optional($._whitespace), $.attribute_identifier, ), ), ")", ), link_modifier: _ => ":", tag: $ => choice( $.ranged_verbatim_tag, $.infirm_tag, $.standard_ranged_tag, $.strong_carryover_tag, $.weak_carryover_tag, $.macro_tag, ), tag_name: $ => seq( alias($._word, $.word), repeat( seq( ".", alias($._word, $.word), ), ), ), tag_parameter: $ => prec.right(repeat1( choice( $._word, $.escape_sequence, ), )), tag_parameters: $ => prec.right(seq( $.tag_parameter, repeat( seq( $._whitespace, $.tag_parameter, ), ), )), ranged_verbatim_tag: $ => prec.right(seq( ...tag($, "@"), alias(repeat( choice( $._word, $._whitespace, newline, ), ), $.ranged_verbatim_content), seq( token("@end"), newline_or_eof, ), )), infirm_tag: $ => seq( ...tag($, ".") ), standard_ranged_tag: $ => seq( ...tag($, "|"), alias(repeat( choice( $.heading, $.strong_delimiting_modifier, $.non_structural, ), ), $.ranged_tag_content), seq( token("|end"), newline_or_eof, ), ), macro_tag: $ => seq( ...tag($, "="), alias(repeat( choice( $.heading, $.strong_delimiting_modifier, $.non_structural, ), ), $.ranged_tag_content), seq( token("=end"), newline_or_eof, ), ), strong_carryover_tag: $ => prec.right(seq( ...tag($, "#"), repeat(prec(1, newline)), )), weak_carryover_tag: $ => prec.right(seq( ...tag($, "+"), repeat(prec(1, newline)), )), linkable: $ => choice( $.inline_link_target, $.link, $.anchor_declaration, $.anchor_definition, ), _inline_link_target_conflict_open: $ => seq("<", newline_or_eof), inline_link_target: $ => seq( "<", /[^\n\r>]/, // TODO: Try to convert this to just an array of paragraph segments like in the // link description node? repeat( choice( /[^\s\n\r>]/, newline, $._whitespace, $.escape_sequence, $.attached_modifier, $._attached_modifier_conflict, $._free_form_conflict, seq( optional( seq( /[^\s\n\r>]/, $.link_modifier, ), ), $.attached_modifier, optional( seq( $.link_modifier, /[^\s\n\r>]/, ), ), ), ), ), ">", ), weak_paragraph: $ => seq( $.paragraph_segment, repeat( seq( newline, $.paragraph_segment, ), ), ), _link_target: $ => seq( choice( link_target($, /\*+/, "heading"), link_target($, "?", "wiki"), link_target($, "#", "generic"), link_target($, "/", "external_file"), link_target($, "@", "timestamp"), link_target($, "$", "definition"), link_target($, "^", "footnote"), link_target($, "=", "extendable"), field("target", alias(token(prec(-1, /[^}]+/)), $.url)), field("target", alias(/\d+/, $.line_number)), ), optional( seq( alias(/\s+:\s+/, $.scope), $._link_target, ), ), ), link_location: $ => seq( "{", choice( seq( ":", alias(/[^\n\r:]+/, $.filepath), ":", optional($._link_target), ), $._link_target, ), "}", ), link_description: $ => seq( "[", $.weak_paragraph, "]", ), link: $ => prec.right(seq( $.link_location, optional($.link_description), optional($.attached_modifier_extension) )), anchor_declaration: $ => prec.right(seq( $.link_description, optional($.link_description), optional($.attached_modifier_extension) )), anchor_definition: $ => seq( $.link_description, $.link_location, optional($.attached_modifier_extension) ), }, }); function heading($, level) { return prec.right( seq( nestable_detached_mod_prefix($, "*", level), $._whitespace, optional( seq( $.detached_modifier_extensions, $._whitespace, ), ), field("title", $.paragraph_segment), choice( prec.dynamic(1, seq( $._whitespace, alias(token(prec(1, ":")), $.intersection), $._whitespace, $.paragraph_segment, newline_or_eof, )), newline_or_eof, ), repeat( choice( $.non_structural, ...lower_level_items($, "heading", level), ), ), optional($.weak_delimiting_modifier), ) ); } function nestable_detached_mod($, type, chr, level) { return prec.right( seq( nestable_detached_mod_prefix($, chr, level), $._whitespace, optional( seq( $.detached_modifier_extensions, $._whitespace, ), ), field("content", $.paragraph), repeat( choice( ...lower_level_items($, type, level), ), ), ), ); } function nestable_detached_mod_prefix($, chr, level) { return alias(chr.repeat(level), $.prefix); } function rangeable_single_detached_mod($, chr) { return seq( chr, $._whitespace, optional( seq( $.detached_modifier_extensions, $._whitespace, ), ), field("title", $.paragraph_segment), choice( prec.dynamic(1, seq( $._whitespace, alias(token(prec(1, ":")), $.intersection), $._whitespace, field("content", $.paragraph_segment), newline_or_eof, )), seq( newline, field("content", $.paragraph), ), ), ); } function rangeable_multi_detached_mod($, chr) { return seq( chr, $._whitespace, optional( seq( $.detached_modifier_extensions, $._whitespace, ), ), field("title", $.paragraph_segment), choice( prec.dynamic(1, seq( $._whitespace, alias(token(prec(1, ":")), $.intersection), $._whitespace, field("content", $.paragraph_segment), newline_or_eof, )), newline_or_eof, ), field("content", repeat($.non_structural)), token(seq(chr, newline_or_eof)), ); } function lower_level_items($, type, level) { let lower_level = [] for (let i = level + 1; i <= 6; i++) { lower_level.push($[type + i]); } return lower_level; } // TODO: Handle the following case: `/hello /` function attached_mod($, name, verbatim) { let modifiers = [ "bold", "italic", "strikethrough", "underline", "verbatim", "spoiler", "superscript", "subscript", "inline_comment", "inline_math", "inline_macro", ].filter(n => n !== name); if (verbatim) { modifiers = modifiers.map(n => alias($[n], "_word")); return seq( $["_" + name + "_open"], choice( seq( alias("|", "free_form_open"), repeat1( choice( $._word, $._whitespace, $._attached_modifier_conflict, $._free_form_conflict, ...modifiers, alias($[name], "_word"), "|", newline, ), ), alias("|", "free_form_close"), $["_" + name + "_close"], ), // TODO: Disallow whitespace to be the first element in a verbatim attached mod seq( repeat1( choice( $._word, $._whitespace, $.escape_sequence, $._attached_modifier_conflict, newline, ), ), $["_" + name + "_close"], ), ), optional($.attached_modifier_extension), ); } modifiers = modifiers.map(n => $[n]); return seq( $["_" + name + "_open"], choice( seq( alias("|", "free_form_open"), repeat1( choice( $._word, $._whitespace, $.escape_sequence, ...modifiers, alias($[name], "_word"), $._attached_modifier_conflict, $._free_form_conflict, "|", $.linkable, $._inline_link_target_conflict_open, newline, ), ), alias("|", "free_form_close"), $["_" + name + "_close"], ), seq( choice( $._word, $.escape_sequence, ...modifiers, alias($[name], "_word"), $._attached_modifier_conflict, $._free_form_conflict, $.linkable, $._inline_link_target_conflict_open, ), repeat( choice( $._word, $._whitespace, $.escape_sequence, ...modifiers, alias($[name], "_word"), $._attached_modifier_conflict, $._free_form_conflict, $.linkable, $._inline_link_target_conflict_open, newline, ), ), $["_" + name + "_close"], ), ), optional($.attached_modifier_extension), ); } function tag($, char) { return [ char, $.tag_name, choice( seq( optional($._whitespace), newline_or_eof, ), seq($._whitespace, $.tag_parameters, optional($._whitespace), newline_or_eof), ) ]; } function link_target($, identifier, name) { return seq( field("target", alias(identifier, $[name])), $._whitespace, $.weak_paragraph, ); }