Skip to content

Commit 1b177bd

Browse files
authored
feat: better structured headings (#134)
Problems: Header titles cannot be extracted easily (e.g., for generating a table of contents). Solution: Expose nodes for separators (`separator`) and actual heading text (`heading`).
1 parent b711df7 commit 1b177bd

File tree

7 files changed

+5325
-5189
lines changed

7 files changed

+5325
-5189
lines changed

grammar.js

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
// - Rule Order: Tree-sitter will prefer the token that appears earlier in the
55
// grammar.
66
//
7-
// https://tree-sitter.github.io/tree-sitter/creating-parsers
7+
// https://github.com/nvim-treesitter/nvim-treesitter/wiki/Parser-Development
8+
// - Visibility: Prefer JS regex (/\n/) over literals ('\n') unless it should be
9+
// exposed to queries as an anonymous node.
810
// - Rules starting with underscore are hidden in the syntax tree.
911

1012
/// <reference types="tree-sitter-cli/dsl" />
@@ -16,6 +18,11 @@ const _li_token = /[-•][ ]+/;
1618
module.exports = grammar({
1719
name: 'vimdoc',
1820

21+
conflicts: $ => [
22+
[$._line_noli, $._column_heading],
23+
[$._column_heading],
24+
],
25+
1926
extras: () => [/[\t ]/],
2027

2128
// inline: ($) => [
@@ -135,14 +142,14 @@ module.exports = grammar({
135142
'>',
136143
choice(
137144
alias(token.immediate(/[a-z0-9]+\n/), $.language),
138-
token.immediate('\n')),
145+
token.immediate(/\n/)),
139146
alias(repeat1(alias($.line_code, $.line)), $.code),
140147
// Codeblock ends if a line starts with non-whitespace.
141148
// Terminating "<" is consumed in other rules.
142149
)),
143150

144151
// Lines.
145-
_blank: () => field('blank', '\n'),
152+
_blank: () => field('blank', /\n/),
146153
line: ($) => choice(
147154
$.column_heading,
148155
$.h1,
@@ -156,18 +163,18 @@ module.exports = grammar({
156163
optional(token.immediate('<')), // Treat codeblock-terminating "<" as whitespace.
157164
_li_token,
158165
choice(
159-
alias(seq(repeat1($._atom), '\n'), $.line),
166+
alias(seq(repeat1($._atom), /\n/), $.line),
160167
seq(alias(repeat1($._atom), $.line), $.codeblock),
161168
),
162169
repeat(alias($._line_noli, $.line)),
163170
)),
164171
// Codeblock lines: must be indented by at least 1 space/tab.
165172
// Line content (incl. whitespace) is captured as a single atom.
166-
line_code: () => choice('\n', /[\t ]+[^\n]+\n/),
173+
line_code: () => choice(/\n/, /[\t ]+[^\n]+\n/),
167174
_line_noli: ($) => seq(
168175
choice($._atom_noli, $._uppercase_words),
169176
repeat($._atom),
170-
choice($.codeblock, '\n')
177+
choice($.codeblock, /\n/)
171178
),
172179

173180
// Modeline: must start with "vim:" (optionally preceded by whitespace)
@@ -177,31 +184,38 @@ module.exports = grammar({
177184
// Intended for table column names per `:help help-writing`.
178185
// TODO: children should be $.word (plaintext), not $.atom.
179186
column_heading: ($) => seq(
180-
field('name', seq(choice($._atom_noli, $._uppercase_words), repeat($._atom))),
181-
'~',
182-
token.immediate('\n'),
187+
alias($._column_heading, $.heading),
188+
alias('~', $.delimiter),
189+
token.immediate(/\n/),
183190
),
191+
// aliasing a seq exposes every item separately: create hidden rule and alias that
192+
_column_heading: $ => prec.dynamic(1, seq(
193+
choice($._atom_noli, $._uppercase_words),
194+
repeat($._atom)
195+
)),
184196

185197
h1: ($) =>
186-
seq(
187-
token.immediate(field('delimiter', /============+[\t ]*\n/)),
188-
repeat1($._atom),
189-
'\n',
190-
),
198+
prec(1, seq(
199+
alias(token.immediate(/============+[\t ]*\n/), $.delimiter),
200+
alias(repeat1($._atom), $.heading),
201+
optional(seq($.tag, repeat($._atom))),
202+
/\n/,
203+
)),
191204

192205
h2: ($) =>
193-
seq(
194-
token.immediate(field('delimiter', /------------+[\t ]*\n/)),
195-
repeat1($._atom),
196-
'\n',
197-
),
206+
prec(1, seq(
207+
alias(token.immediate(/------------+[\t ]*\n/), $.delimiter),
208+
alias(repeat1($._atom), $.heading),
209+
optional(seq($.tag, repeat($._atom))),
210+
/\n/,
211+
)),
198212

199213
// Heading 3: UPPERCASE NAME, followed by optional *tags*.
200214
h3: ($) =>
201215
seq(
202-
field('name', $.uppercase_name),
216+
alias($.uppercase_name, $.heading),
203217
optional(seq($.tag, repeat($._atom))),
204-
'\n',
218+
/\n/,
205219
),
206220

207221
tag: ($) => _word($,

queries/vimdoc/highlights.scm

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1-
(h1) @markup.heading.1
1+
(h1
2+
(delimiter) @markup.heading.1
3+
(heading) @markup.heading.1)
24

3-
(h2) @markup.heading.2
5+
(h2
6+
(delimiter) @markup.heading.2
7+
(heading) @markup.heading.2)
48

5-
(h3) @markup.heading.3
9+
(h3
10+
(heading) @markup.heading.3)
611

7-
(column_heading) @markup.heading.4
12+
(column_heading
13+
(heading) @markup.heading.4)
814

915
(column_heading
10-
"~" @markup.heading.4.marker
16+
(delimiter) @markup.heading.4.marker
1117
(#set! conceal ""))
1218

1319
(tag

0 commit comments

Comments
 (0)