From a8440d7f6b9e809dfeec08b12d28808e9191a928 Mon Sep 17 00:00:00 2001 From: Jacob Rothstein Date: Thu, 28 May 2026 11:49:28 -0700 Subject: [PATCH] Add KDL --- src/components.json | 4 + src/languages/kdl.js | 84 +++++++++++++++++++ tests/languages/kdl/boolean_feature.test | 38 +++++++++ tests/languages/kdl/comment_feature.test | 15 ++++ tests/languages/kdl/null_feature.test | 24 ++++++ tests/languages/kdl/number_feature.test | 35 ++++++++ tests/languages/kdl/property_feature.test | 37 ++++++++ tests/languages/kdl/raw-string_feature.test | 27 ++++++ tests/languages/kdl/slashdash_feature.test | 36 ++++++++ tests/languages/kdl/string_feature.test | 27 ++++++ tests/languages/kdl/tag_feature.test | 27 ++++++ .../kdl/type-annotation_feature.test | 42 ++++++++++ 12 files changed, 396 insertions(+) create mode 100644 src/languages/kdl.js create mode 100644 tests/languages/kdl/boolean_feature.test create mode 100644 tests/languages/kdl/comment_feature.test create mode 100644 tests/languages/kdl/null_feature.test create mode 100644 tests/languages/kdl/number_feature.test create mode 100644 tests/languages/kdl/property_feature.test create mode 100644 tests/languages/kdl/raw-string_feature.test create mode 100644 tests/languages/kdl/slashdash_feature.test create mode 100644 tests/languages/kdl/string_feature.test create mode 100644 tests/languages/kdl/tag_feature.test create mode 100644 tests/languages/kdl/type-annotation_feature.test diff --git a/src/components.json b/src/components.json index 3ac7282612..2c94a09b73 100644 --- a/src/components.json +++ b/src/components.json @@ -665,6 +665,10 @@ "title": "Julia", "owner": "cdagnino" }, + "kdl": { + "title": "KDL", + "owner": "jbr" + }, "keepalived": { "title": "Keepalived Configure", "owner": "dev-itsheng" diff --git a/src/languages/kdl.js b/src/languages/kdl.js new file mode 100644 index 0000000000..6820bbfd1e --- /dev/null +++ b/src/languages/kdl.js @@ -0,0 +1,84 @@ +/** @type {import('../types.d.ts').LanguageProto<'kdl'>} */ +export default { + id: 'kdl', + grammar () { + // Characters that are NOT valid inside a bare identifier (KDL v2 set, + // which is the more restrictive of v1/v2 in the ways that matter for + // highlighting). Used as a negated character class. + const NON_ID = '\\s\\\\/(){}\\[\\]"#;='; + const IDENT_BODY = `[^${NON_ID}]`; + + // Bare identifier. The dotted/signed first-character rules from the spec + // are folded into three alternatives, longest-form first so the regex + // engine doesn't commit to a one-char match when more is available. + const IDENT = + '(?:' + + // optional sign, dot, then a non-digit body + `[+\\-]?\\.(?:[^${NON_ID}0-9]${IDENT_BODY}*)?` + + '|' + + // sign, then a non-digit, non-dot body + `[+\\-](?:[^${NON_ID}0-9.]${IDENT_BODY}*)?` + + '|' + + // plain start: non-digit, non-sign, non-dot, non-special + `[^${NON_ID}0-9+\\-.]${IDENT_BODY}*` + + ')'; + + // Single-line quoted string. Permissive escape handling: a backslash plus + // any single character is treated as an escape, no further structure. + // (Being more specific about `\u{...}` causes regex-pattern ambiguity.) + const QUOTED = '"(?:\\\\[\\s\\S]|[^"\\\\\\r\\n])*"'; + + // Anything that can fill an identifier slot (node name, property key, + // type annotation name). + const STRINGY = `(?:${QUOTED}|${IDENT})`; + + return { + 'comment': { + pattern: /\/\/.*|\/\*[\s\S]*?\*\//, + greedy: true, + }, + 'slashdash': { + pattern: /\/-/, + alias: 'comment', + }, + 'raw-string': { + // v2 raw strings: one or more `#`s, then `"..."` or `"""..."""`. + // v1 raw strings: `r`, then zero or more `#`s, then quotes. + // Backreferences enforce matching hash counts on both sides. + pattern: /(#+)"""[\s\S]*?"""\1|(#+)"[\s\S]*?"\2|r(#*)"""[\s\S]*?"""\3|r(#*)"[\s\S]*?"\4/, + greedy: true, + alias: 'string', + }, + 'property': { + // Must come before `string` so quoted-string keys win over the + // generic string pattern. + pattern: RegExp(`${STRINGY}(?=\\s*=[^=])`), + greedy: true, + alias: 'attr-name', + }, + 'string': { + pattern: /"""[\s\S]*?"""|"(?:\\[\s\S]|[^"\\\r\n])*"/, + greedy: true, + }, + 'type-annotation': { + // Must come before `number`/`keyword` so digits or keyword-shaped + // type names (like `u8` or `true`) aren't shredded by those rules. + pattern: RegExp(`\\(\\s*${STRINGY}\\s*\\)`), + inside: { + 'class-name': RegExp(STRINGY), + 'punctuation': /[()]/, + }, + }, + 'keyword': /#(?:true|false|null|-inf|inf|nan)\b|\b(?:true|false|null)\b/, + 'number': /[+-]?(?:0x[\da-fA-F][\da-fA-F_]*|0o[0-7][0-7_]*|0b[01][01_]*|\d[\d_]*(?:\.\d[\d_]*)?(?:[eE][+-]?\d[\d_]*)?)/, + 'tag': { + // First identifier on a line, or right after `{`, `;`, or a + // type annotation's closing `)`. Handles whitespace in between. + pattern: RegExp(`(^[\\t ]*|[{;)][\\t ]*)${STRINGY}`, 'm'), + lookbehind: true, + greedy: true, + }, + 'punctuation': /[{};=\\]/, + }; + }, +}; diff --git a/tests/languages/kdl/boolean_feature.test b/tests/languages/kdl/boolean_feature.test new file mode 100644 index 0000000000..e483a9494e --- /dev/null +++ b/tests/languages/kdl/boolean_feature.test @@ -0,0 +1,38 @@ +node #true +node #false +node true +node false +node a=#true b=#false +node a=true b=false + +---------------------------------------------------- + +[ + ["tag", "node"], + ["keyword", "#true"], + + ["tag", "node"], + ["keyword", "#false"], + + ["tag", "node"], + ["keyword", "true"], + + ["tag", "node"], + ["keyword", "false"], + + ["tag", "node"], + ["property", "a"], + ["punctuation", "="], + ["keyword", "#true"], + ["property", "b"], + ["punctuation", "="], + ["keyword", "#false"], + + ["tag", "node"], + ["property", "a"], + ["punctuation", "="], + ["keyword", "true"], + ["property", "b"], + ["punctuation", "="], + ["keyword", "false"] +] diff --git a/tests/languages/kdl/comment_feature.test b/tests/languages/kdl/comment_feature.test new file mode 100644 index 0000000000..a5df3fa30f --- /dev/null +++ b/tests/languages/kdl/comment_feature.test @@ -0,0 +1,15 @@ +// single-line comment +/* multi + line + comment */ +node // trailing comment +node /* inline */ arg + +---------------------------------------------------- + +[ + ["comment", "// single-line comment"], + ["comment", "/* multi\r\n line\r\n comment */"], + ["tag", "node"], ["comment", "// trailing comment"], + ["tag", "node"], ["comment", "/* inline */"], " arg" +] diff --git a/tests/languages/kdl/null_feature.test b/tests/languages/kdl/null_feature.test new file mode 100644 index 0000000000..a13479a81e --- /dev/null +++ b/tests/languages/kdl/null_feature.test @@ -0,0 +1,24 @@ +node #null +node null +node key=#null +node key=null + +---------------------------------------------------- + +[ + ["tag", "node"], + ["keyword", "#null"], + + ["tag", "node"], + ["keyword", "null"], + + ["tag", "node"], + ["property", "key"], + ["punctuation", "="], + ["keyword", "#null"], + + ["tag", "node"], + ["property", "key"], + ["punctuation", "="], + ["keyword", "null"] +] diff --git a/tests/languages/kdl/number_feature.test b/tests/languages/kdl/number_feature.test new file mode 100644 index 0000000000..82fb45671a --- /dev/null +++ b/tests/languages/kdl/number_feature.test @@ -0,0 +1,35 @@ +node 0 +node 42 +node -42 +node +42 +node 3.14 +node -3.14 +node 1.5e10 +node 2.5E-3 +node 1_000_000 +node 0xDEAD_BEEF +node 0o755 +node 0b1010_0101 +node #inf +node #-inf +node #nan + +---------------------------------------------------- + +[ + ["tag", "node"], ["number", "0"], + ["tag", "node"], ["number", "42"], + ["tag", "node"], ["number", "-42"], + ["tag", "node"], ["number", "+42"], + ["tag", "node"], ["number", "3.14"], + ["tag", "node"], ["number", "-3.14"], + ["tag", "node"], ["number", "1.5e10"], + ["tag", "node"], ["number", "2.5E-3"], + ["tag", "node"], ["number", "1_000_000"], + ["tag", "node"], ["number", "0xDEAD_BEEF"], + ["tag", "node"], ["number", "0o755"], + ["tag", "node"], ["number", "0b1010_0101"], + ["tag", "node"], ["keyword", "#inf"], + ["tag", "node"], ["keyword", "#-inf"], + ["tag", "node"], ["keyword", "#nan"] +] diff --git a/tests/languages/kdl/property_feature.test b/tests/languages/kdl/property_feature.test new file mode 100644 index 0000000000..6b1f4a8c7b --- /dev/null +++ b/tests/languages/kdl/property_feature.test @@ -0,0 +1,37 @@ +node key=1 +node key="value" +node a=1 b=2 +node key = 1 +node "quoted key"="value" + +---------------------------------------------------- + +[ + ["tag", "node"], + ["property", "key"], + ["punctuation", "="], + ["number", "1"], + + ["tag", "node"], + ["property", "key"], + ["punctuation", "="], + ["string", "\"value\""], + + ["tag", "node"], + ["property", "a"], + ["punctuation", "="], + ["number", "1"], + ["property", "b"], + ["punctuation", "="], + ["number", "2"], + + ["tag", "node"], + ["property", "key"], + ["punctuation", "="], + ["number", "1"], + + ["tag", "node"], + ["property", "\"quoted key\""], + ["punctuation", "="], + ["string", "\"value\""] +] diff --git a/tests/languages/kdl/raw-string_feature.test b/tests/languages/kdl/raw-string_feature.test new file mode 100644 index 0000000000..d2cc98132a --- /dev/null +++ b/tests/languages/kdl/raw-string_feature.test @@ -0,0 +1,27 @@ +node #"v2 raw with \n literal"# +node ##"v2 raw with "# inside"## +node #""" + multi + line raw + """# +node r"v1 raw \n literal" +node r##"v1 raw with "# inside"## + +---------------------------------------------------- + +[ + ["tag", "node"], + ["raw-string", "#\"v2 raw with \\n literal\"#"], + + ["tag", "node"], + ["raw-string", "##\"v2 raw with \"# inside\"##"], + + ["tag", "node"], + ["raw-string", "#\"\"\"\r\n multi\r\n line raw\r\n \"\"\"#"], + + ["tag", "node"], + ["raw-string", "r\"v1 raw \\n literal\""], + + ["tag", "node"], + ["raw-string", "r##\"v1 raw with \"# inside\"##"] +] diff --git a/tests/languages/kdl/slashdash_feature.test b/tests/languages/kdl/slashdash_feature.test new file mode 100644 index 0000000000..c79b91cd77 --- /dev/null +++ b/tests/languages/kdl/slashdash_feature.test @@ -0,0 +1,36 @@ +/- commented-node +node /- 1 2 3 +node /- key=value +node { + /- commented-child + real-child +} + +---------------------------------------------------- + +[ + ["slashdash", "/-"], + " commented-node\r\n", + + ["tag", "node"], + ["slashdash", "/-"], + ["number", "1"], + ["number", "2"], + ["number", "3"], + + ["tag", "node"], + ["slashdash", "/-"], + ["property", "key"], + ["punctuation", "="], + "value\r\n", + + ["tag", "node"], + ["punctuation", "{"], + + ["slashdash", "/-"], + " commented-child\r\n ", + + ["tag", "real-child"], + + ["punctuation", "}"] +] diff --git a/tests/languages/kdl/string_feature.test b/tests/languages/kdl/string_feature.test new file mode 100644 index 0000000000..e3f04f7b1f --- /dev/null +++ b/tests/languages/kdl/string_feature.test @@ -0,0 +1,27 @@ +node "hello" +node "with \"escaped\" quotes" +node "newline\nhere" +node "unicode \u{1F4A9}" +node """ + multi-line + string + """ + +---------------------------------------------------- + +[ + ["tag", "node"], + ["string", "\"hello\""], + + ["tag", "node"], + ["string", "\"with \\\"escaped\\\" quotes\""], + + ["tag", "node"], + ["string", "\"newline\\nhere\""], + + ["tag", "node"], + ["string", "\"unicode \\u{1F4A9}\""], + + ["tag", "node"], + ["string", "\"\"\"\r\n multi-line\r\n string\r\n \"\"\""] +] diff --git a/tests/languages/kdl/tag_feature.test b/tests/languages/kdl/tag_feature.test new file mode 100644 index 0000000000..2a7a7c4149 --- /dev/null +++ b/tests/languages/kdl/tag_feature.test @@ -0,0 +1,27 @@ +node +my-node +node1 +parent { + child1 + child2 +} +parent { child1; child2 } + +---------------------------------------------------- + +[ + ["tag", "node"], + ["tag", "my-node"], + ["tag", "node1"], + ["tag", "parent"], + ["punctuation", "{"], + ["tag", "child1"], + ["tag", "child2"], + ["punctuation", "}"], + ["tag", "parent"], + ["punctuation", "{"], + ["tag", "child1"], + ["punctuation", ";"], + ["tag", "child2"], + ["punctuation", "}"] +] diff --git a/tests/languages/kdl/type-annotation_feature.test b/tests/languages/kdl/type-annotation_feature.test new file mode 100644 index 0000000000..bdf0a3ab52 --- /dev/null +++ b/tests/languages/kdl/type-annotation_feature.test @@ -0,0 +1,42 @@ +node (u8)42 +node (regex)".*" +(published)date "1970-01-01" +node prop=(i32)100 + +---------------------------------------------------- + +[ + ["tag", "node"], + ["type-annotation", [ + ["punctuation", "("], + ["class-name", "u8"], + ["punctuation", ")"] + ]], + ["number", "42"], + + ["tag", "node"], + ["type-annotation", [ + ["punctuation", "("], + ["class-name", "regex"], + ["punctuation", ")"] + ]], + ["string", "\".*\""], + + ["type-annotation", [ + ["punctuation", "("], + ["class-name", "published"], + ["punctuation", ")"] + ]], + ["tag", "date"], + ["string", "\"1970-01-01\""], + + ["tag", "node"], + ["property", "prop"], + ["punctuation", "="], + ["type-annotation", [ + ["punctuation", "("], + ["class-name", "i32"], + ["punctuation", ")"] + ]], + ["number", "100"] +]