Skip to content

Commit 723c6dd

Browse files
committed
feat: add KDL v2 compatibility to grammar and scanner
1 parent b37e3d5 commit 723c6dd

7 files changed

Lines changed: 15957 additions & 6615 deletions

File tree

grammar.js

Lines changed: 64 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,39 @@ const ANNOTATION_BUILTINS = [
5353
'base64',
5454
];
5555

56+
// Support KDL v1, v2.
57+
const BOOLEAN_KEYWORDS = ['true', 'false', '#true', '#false'];
58+
const NULL_KEYWORDS = ['null', '#null'];
59+
const KEYWORD_NUMBERS = ['#inf', '#-inf', '#nan'];
60+
61+
function linespacedNodes($) {
62+
return seq(
63+
repeat($._linespace),
64+
optional(seq(
65+
$.node,
66+
repeat(seq(
67+
repeat($._linespace),
68+
$.node,
69+
)),
70+
)),
71+
repeat($._linespace),
72+
);
73+
}
74+
5675
module.exports = grammar({
5776
name: 'kdl',
5877

5978
conflicts: $ => [
6079
[$.document],
6180
[$._node_space],
6281
[$.node_children],
82+
[$.identifier, $.value],
6383
],
6484

6585
externals: $ => [
6686
$._eof,
6787
$.multi_line_comment,
88+
$.multi_line_string,
6889
$._raw_string,
6990
],
7091

@@ -74,24 +95,17 @@ module.exports = grammar({
7495

7596
rules: {
7697
// nodes := linespace* (node nodes?)? linespace*
77-
document: $ =>
78-
seq(
79-
repeat($._linespace),
80-
optional(seq(
81-
$.node,
82-
repeat(seq(
83-
repeat($._linespace),
84-
$.node,
85-
)),
86-
)),
87-
repeat($._linespace),
88-
),
98+
document: $ => linespacedNodes($),
99+
89100

90101
// node := ('/-' node-space*)? type? identifier (node-space+ node-prop-or-arg)* (node-space* node-children ws*)? node-space* node-terminator
102+
// Compatibility grammar: node names stay under `identifier` to preserve the
103+
// existing AST shape, even though `identifier` itself already includes
104+
// quoted/raw string forms via `$.string`.
91105
node: $ => prec(1,
92106
seq(
93107
alias(optional(seq('/-', repeat($._node_space))), $.node_comment),
94-
optional($.type),
108+
optional(seq($.type, repeat($._node_space))),
95109
$.identifier,
96110
repeat(seq(repeat1($._node_space), $.node_field)),
97111
optional(seq(repeat($._node_space), field('children', $.node_children), repeat($._ws))),
@@ -133,6 +147,10 @@ module.exports = grammar({
133147

134148
// identifier := string | bare-identifier
135149
identifier: $ => choice($.string, $._bare_identifier),
150+
// Compatibility rule: exclude `#` from the first character so KDL v2 tokens
151+
// like #true / #null / #inf do not get consumed as identifiers, but keep
152+
// legacy/editor-friendly support for `#` in later positions.
153+
136154
// bare-identifier := ((identifier-char - digit - sign) identifier-char* | sign ((identifier-char - digit) identifier-char*)?) - keyword
137155
_bare_identifier: $ =>
138156
choice(
@@ -143,7 +161,7 @@ module.exports = grammar({
143161
// _normal_bare_identifier: $ => $.__identifier_char_no_digit_sign,
144162
_normal_bare_identifier: _ => token(
145163
seq(
146-
/[\u4E00-\u9FFF\p{L}\p{M}\p{N}\p{Emoji}_~!@#\$%\^&\*.:'\|\?&&[^\s\d\/(){}<>;\[\]=,"]]/,
164+
/[\u4E00-\u9FFF\p{L}\p{M}\p{N}\p{Emoji}_~!@\$%\^&\*.:'\|\?&&[^\s\d\/(){}<>;\[\]=,"]]/,
147165
/[\u4E00-\u9FFF\p{L}\p{M}\p{N}\p{Emoji}\-_~!@#\$%\^&\*.:'\|\?+&&[^\s\/(){}<>;\[\]=,"]]*/,
148166
),
149167
),
@@ -154,40 +172,49 @@ module.exports = grammar({
154172

155173
// can't start with a digit
156174
__identifier_char_no_digit: _ => token(
157-
/[\u4E00-\u9FFF\p{L}\p{M}\p{N}\-_~!@#\$%\^&\*.:'\|\?+&&[^\s\d\/(){}<>;\[\]=,"]]/,
175+
/[\u4E00-\u9FFF\p{L}\p{M}\p{N}\-_~!@\$%\^&\*.:'\|\?+&&[^\s\d\/(){}<>;\[\]=,"]]/,
158176
),
159177

160178
// can't start with a digit or sign
161179
__identifier_char_no_digit_sign: _ => token(
162-
/[\u4E00-\u9FFF\p{L}\p{M}\p{N}\-_~!@#\$%\^&\*.:'\|\?&&[^\s\d\+\-\/(){}<>;\[\]=,"]]/,
180+
/[\u4E00-\u9FFF\p{L}\p{M}\p{N}\-_~!@\$%\^&\*.:'\|\?&&[^\s\d\+\-\/(){}<>;\[\]=,"]]/,
163181
),
164182

165-
// keyword := boolean | 'null'
166-
keyword: $ => choice($.boolean, 'null'),
183+
// Intentionally accept both v1 and v2 keywords here.
184+
// If we ever add strict parsing modes, this is one of the first places to split.
185+
keyword: $ => choice($.boolean, ...NULL_KEYWORDS),
186+
167187
// type annotations
168188
annotation_type: _ => choice(...ANNOTATION_BUILTINS),
169-
// prop := identifier '=' value
170-
prop: $ => seq($.identifier, '=', $.value),
189+
190+
prop: $ => seq($.identifier, repeat($._node_space), '=', repeat($._node_space), $.value),
191+
171192
// value := type? (string | number | keyword)
172-
value: $ => seq(optional($.type), choice($.string, $.number, $.keyword)),
193+
value: $ => seq(optional(seq($.type, repeat($._node_space))), choice($.string, $.number, $.keyword)),
194+
173195
// type := '(' identifier ')'
174-
type: $ => seq('(', choice($.identifier, $.annotation_type), ')'),
196+
type: $ => seq('(', repeat($._node_space), choice($.identifier, $.annotation_type), repeat($._node_space), ')'),
175197

176198
// String
177-
// string := raw-string | escaped-string
178-
string: $ => choice($._raw_string, $._escaped_string),
199+
// `string` intentionally covers multiple concrete syntaxes:
200+
// - v1 raw strings: r#"..."#
201+
// - v2 raw strings: #"..."#
202+
// - v2 multiline strings: """..."""
203+
// The tricky delimiter matching lives in the external scanner.
204+
string: $ => choice($._raw_string, $.multi_line_string, $._escaped_string),
179205
// escaped-string := '"' character* '"'
180-
_escaped_string: $ => seq('"', alias(repeat(choice($.escape, /[^"]/)), $.string_fragment), '"'),
206+
_escaped_string: $ => seq('"', alias(repeat(choice($.escape, $.escaped_whitespace, /[^"]/)), $.string_fragment), '"'),
181207
// character := '\' escape | [^\"]
182-
_character: $ => choice($.escape, /[^"]/),
183-
// escape := ["\\/bfnrt] | 'u{' hex-digit{1, 6} '}'
208+
_character: $ => choice($.escape, $.escaped_whitespace, /[^"]/),
209+
// escape := ["\\/bfnrts] | 'u{' hex-digit{1, 6} '}'
184210
escape: _ =>
185-
token.immediate(/\\\\|\\"|\\\/|\\b|\\f|\\n|\\r|\\t|\\u\{[0-9a-fA-F]{1,6}\}/),
211+
token.immediate(/\\\\|\\"|\\\/|\\b|\\f|\\n|\\r|\\t|\\s|\\u\{[0-9a-fA-F]{1,6}\}/),
212+
escaped_whitespace: _ => token.immediate(/\\(?:\r\n|[\u0009\u0020\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000\r\n\u0085\u000B\u000C\u2028\u2029])+/),
186213
// hex-digit := [0-9a-fA-F]
187214
_hex_digit: _ => /[0-9a-fA-F]/,
188215

189-
// number := decimal | hex | octal | binary
190-
number: $ => choice($._decimal, $._hex, $._octal, $._binary),
216+
// v2 adds keyword-like numeric literals such as #inf / #-inf / #nan.
217+
number: $ => choice($.keyword_number, $._decimal, $._hex, $._octal, $._binary),
191218

192219
// decimal := sign? integer ('.' integer)? exponent?
193220
_decimal: $ =>
@@ -214,8 +241,9 @@ module.exports = grammar({
214241
// binary := sign? '0b' ('0' | '1') ('0' | '1' | '_')*
215242
_binary: $ => seq(optional($._sign), '0b', choice('0', '1'), repeat(choice('0', '1', '_'))),
216243

217-
// boolean := 'true' | 'false'
218-
boolean: _ => choice('true', 'false'),
244+
keyword_number: _ => choice(...KEYWORD_NUMBERS),
245+
246+
boolean: _ => choice(...BOOLEAN_KEYWORDS),
219247

220248
// escline := '\\' ws* (single-line-comment | newline)
221249
_escline: $ => seq('\\', repeat($._ws), choice($.single_line_comment, $._newline)),
@@ -238,9 +266,10 @@ module.exports = grammar({
238266
// │ PS Paragraph Separator U+2029 │
239267
// ╰──────────────────────────────────────────────────────────╯
240268
// Note that for the purpose of new lines, CRLF is considered a single newline.
241-
_newline: _ => choice(/\r'/, /\n/, /\r\n/, /\u0085/, /\u000C/, /\u2028/, /\u2029/),
269+
_newline: _ => choice(/\r\n/, /\r/, /\n/, /\u0085/, /\u000B/, /\u000C/, /\u2028/, /\u2029/),
242270

243-
// ws := bom | unicode-space | multi-line-comment
271+
// `ws` is non-newline whitespace.
272+
// The long Unicode list is copied from the KDL spec on purpose.
244273
_ws: $ => choice($._bom, $._unicode_space, $.multi_line_comment),
245274

246275
// bom := '\u{FEFF}'
@@ -278,7 +307,7 @@ module.exports = grammar({
278307
single_line_comment: $ =>
279308
seq(
280309
'//',
281-
repeat(/[^\r\n\u0085\u000C\u2028\u2029]/),
310+
repeat(/[^\r\n\u0085\u000B\u000C\u2028\u2029]/),
282311
choice($._newline, $._eof),
283312
),
284313
},

queries/highlights.scm

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
; Types
22

33
(node (identifier) @type)
4+
(node (identifier (string) @string))
45

56
(type) @type
67

@@ -25,16 +26,21 @@
2526

2627
(string) @string
2728

29+
(multi_line_string) @string
30+
2831
(escape) @string.escape
2932

3033
(number) @number
3134

35+
(keyword_number) @number
36+
3237
(number (decimal) @float)
3338
(number (exponent) @float)
3439

3540
(boolean) @boolean
3641

3742
"null" @constant.builtin
43+
"#null" @constant.builtin
3844

3945
; Punctuation
4046

0 commit comments

Comments
 (0)