@@ -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+
5675module . 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 - 9 a - f A - F ] { 1 , 6 } \} / ) ,
211+ token . immediate ( / \\ \\ | \\ " | \\ \/ | \\ b | \\ f | \\ n | \\ r | \\ t | \\ s | \\ u \{ [ 0 - 9 a - f A - 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 - 9 a - f A - 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 } ,
0 commit comments