From 23742e237a114eb28d605257b936c54087dbb180 Mon Sep 17 00:00:00 2001 From: Dmitry Sharabin Date: Thu, 18 Dec 2025 16:52:41 +0100 Subject: [PATCH 01/11] First pass on adding the Dotenv language --- src/languages/dotenv.js | 102 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 src/languages/dotenv.js diff --git a/src/languages/dotenv.js b/src/languages/dotenv.js new file mode 100644 index 0000000000..7d0c90ae8b --- /dev/null +++ b/src/languages/dotenv.js @@ -0,0 +1,102 @@ +/** @type {LanguageProto<'dotenv'>} */ +export default { + id: 'dotenv', + optional: 'bash', + grammar () { + /** + * @param {RegExp} prefix + * @returns {Array} + */ + const commonPatterns = prefix => { + return [ + { + pattern: RegExp( + prefix.source + + /(?:-?[1-9]\d*|0)(?:\\.\d+)?(?=\s*\}|\s[^}]+$|\s*$|"$)/.source + ), + alias: 'number', + }, + { + pattern: RegExp(prefix.source + /(?:false|true)(?=\s*\}|\s[^}]+$|\s*$)/.source), + alias: 'boolean', + }, + { + // Ignore leading and trailing whitespace characters + pattern: RegExp(prefix.source + /(\S[^}]*?\S)(?=\s*\})/.source), + alias: 'string', + }, + ]; + }; + + // Based on https://dotenvx.com/docs/env-file + return { + 'comment': /(?:(?<=^)|(?<=[\s"'`]))#(?![^\n"'`]*["'`])[^\r\n]*?(?=[ \t]*(?:\r?\n|$))/, + 'keyword': /^export(?=\s)/m, + 'key': { + // Allow bare keys (without values) + pattern: /[a-z_]\w*(?=\s*=|(? Date: Fri, 19 Dec 2025 09:31:31 +0100 Subject: [PATCH 02/11] Register the Dotenv language --- src/components.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components.json b/src/components.json index 7dfe6d098d..5cacba8d26 100644 --- a/src/components.json +++ b/src/components.json @@ -387,6 +387,10 @@ "title": "DOT (Graphviz)", "owner": "RunDevelopment" }, + "dotenv": { + "title": "Dotenv", + "owner": "dmitrysharabin" + }, "ebnf": { "title": "EBNF", "owner": "RunDevelopment" From 93da66174f7bb9500a130c620da756a8ab34fc2d Mon Sep 17 00:00:00 2001 From: Dmitry Sharabin Date: Fri, 19 Dec 2025 09:29:50 +0100 Subject: [PATCH 03/11] Add tests --- .../languages/dotenv+bash/bash_inclusion.test | 44 +++ .../dotenv/command_substitution_feature.test | 115 ++++++ tests/languages/dotenv/comment_feature.test | 90 +++++ .../dotenv/export_keyword_feature.test | 14 + .../dotenv/key_value_pair_feature.test | 122 +++++++ .../dotenv/variable_expansion_feature.test | 343 ++++++++++++++++++ 6 files changed, 728 insertions(+) create mode 100644 tests/languages/dotenv+bash/bash_inclusion.test create mode 100644 tests/languages/dotenv/command_substitution_feature.test create mode 100644 tests/languages/dotenv/comment_feature.test create mode 100644 tests/languages/dotenv/export_keyword_feature.test create mode 100644 tests/languages/dotenv/key_value_pair_feature.test create mode 100644 tests/languages/dotenv/variable_expansion_feature.test diff --git a/tests/languages/dotenv+bash/bash_inclusion.test b/tests/languages/dotenv+bash/bash_inclusion.test new file mode 100644 index 0000000000..e84e7b3a51 --- /dev/null +++ b/tests/languages/dotenv+bash/bash_inclusion.test @@ -0,0 +1,44 @@ +SHORT_HASH=$(git rev-parse --short HEAD) +PWD_MESSAGE="Running from $(pwd)" +DATABASE_URL=postgres://$(whoami)@localhost/my_database + +---------------------------------------------------- + +[ + ["assign-left", ["SHORT_HASH"]], + ["operator", ["="]], + ["variable", [ + ["variable", "$("], + ["function", "git"], + " rev-parse ", + ["parameter", "--short"], + " HEAD", + ["variable", ")"] + ]], + + ["assign-left", ["PWD_MESSAGE"]], + ["operator", ["="]], + ["string", [ + "\"Running from ", + ["variable", [ + ["variable", "$("], + ["builtin", "pwd"], + ["variable", ")"] + ]], + "\"" + ]], + + ["assign-left", ["DATABASE_URL"]], + ["operator", ["="]], + "postgres://", + ["variable", [ + ["variable", "$("], + ["function", "whoami"], + ["variable", ")"] + ]], + "@localhost/my_database" +] + +---------------------------------------------------- + +Check for bash inclusion. diff --git a/tests/languages/dotenv/command_substitution_feature.test b/tests/languages/dotenv/command_substitution_feature.test new file mode 100644 index 0000000000..e28a91ab7d --- /dev/null +++ b/tests/languages/dotenv/command_substitution_feature.test @@ -0,0 +1,115 @@ +DATE=$(date) +SHORT_HASH=$(git rev-parse --short HEAD) +LONG_HASH=`git rev-parse HEAD` +BUILD_ID="build-$(uuidgen)" +PWD_MESSAGE="Running from $(pwd)" + +DATABASE_URL="postgres://$(whoami)@localhost/my_database" +WITH_SUBSTITUTION=postgres://$(whoami)@localhost/my_database +NO_SUBSTITUTION='postgres://$(whoami)@localhost/my_database' +AND_HERE=`Some text... +postgres://$(whoami)@localhost/my_database` + +---------------------------------------------------- + +[ + ["key", "DATE"], + ["assignment-operator", "="], + ["value", [ + ["command-substitution", [ + ["command-substitution-punctuation", "$("], + ["shell-command", "date"], + ["command-substitution-punctuation", ")"] + ]] + ]], + + ["key", "SHORT_HASH"], + ["assignment-operator", "="], + ["value", [ + ["command-substitution", [ + ["command-substitution-punctuation", "$("], + ["shell-command", "git rev-parse --short HEAD"], + ["command-substitution-punctuation", ")"] + ]] + ]], + + ["key", "LONG_HASH"], + ["assignment-operator", "="], + ["value", [ + ["punctuation", "`"], + "git rev-parse HEAD", + ["punctuation", "`"] + ]], + + ["key", "BUILD_ID"], + ["assignment-operator", "="], + ["value", [ + ["punctuation", "\""], + "build-", + ["command-substitution", [ + ["command-substitution-punctuation", "$("], + ["shell-command", "uuidgen"], + ["command-substitution-punctuation", ")"] + ]], + ["punctuation", "\""] + ]], + + ["key", "PWD_MESSAGE"], + ["assignment-operator", "="], + ["value", [ + ["punctuation", "\""], + "Running from ", + ["command-substitution", [ + ["command-substitution-punctuation", "$("], + ["shell-command", "pwd"], + ["command-substitution-punctuation", ")"] + ]], + ["punctuation", "\""] + ]], + + ["key", "DATABASE_URL"], + ["assignment-operator", "="], + ["value", [ + ["punctuation", "\""], + "postgres://", + ["command-substitution", [ + ["command-substitution-punctuation", "$("], + ["shell-command", "whoami"], + ["command-substitution-punctuation", ")"] + ]], + "@localhost/my_database", + ["punctuation", "\""] + ]], + + ["key", "WITH_SUBSTITUTION"], + ["assignment-operator", "="], + ["value", [ + "postgres://", + ["command-substitution", [ + ["command-substitution-punctuation", "$("], + ["shell-command", "whoami"], + ["command-substitution-punctuation", ")"] + ]], + "@localhost/my_database" + ]], + + ["key", "NO_SUBSTITUTION"], + ["assignment-operator", "="], + ["value", [ + ["punctuation", "'"], + "postgres://$(whoami)@localhost/my_database", + ["punctuation", "'"] + ]], + + ["key", "AND_HERE"], + ["assignment-operator", "="], + ["value", [ + ["punctuation", "`"], + "Some text...\r\npostgres://$(whoami)@localhost/my_database", + ["punctuation", "`"] + ]] +] + +---------------------------------------------------- + +Check for command substitution. diff --git a/tests/languages/dotenv/comment_feature.test b/tests/languages/dotenv/comment_feature.test new file mode 100644 index 0000000000..5a39d6f675 --- /dev/null +++ b/tests/languages/dotenv/comment_feature.test @@ -0,0 +1,90 @@ +# This is a comment + +KEY=VAL # inline comment +KEY="VAL"# and another one +KEY='VAL'# and another one +KEY=`VAL`# and another one + +KEY="String with a hash # this is not a comment" +KEY='String with a hash # this is not a comment' +KEY=`String with a hash # this is not a comment` +KEY=VAL# also not a comment +KEY=Escaped \# hash is not a comment as well + +---------------------------------------------------- + +[ + ["comment", "# This is a comment"], + + ["key", "KEY"], + ["assignment-operator", "="], + ["value", ["VAL"]], + ["comment", "# inline comment"], + + ["key", "KEY"], + ["assignment-operator", "="], + ["value", [ + ["punctuation", "\""], + "VAL", + ["punctuation", "\""] + ]], + ["comment", "# and another one"], + + ["key", "KEY"], + ["assignment-operator", "="], + ["value", [ + ["punctuation", "'"], + "VAL", + ["punctuation", "'"] + ]], + ["comment", "# and another one"], + + ["key", "KEY"], + ["assignment-operator", "="], + ["value", [ + ["punctuation", "`"], + "VAL", + ["punctuation", "`"] + ]], + ["comment", "# and another one"], + + ["key", "KEY"], + ["assignment-operator", "="], + ["value", [ + ["punctuation", "\""], + "String with a hash # this is not a comment", + ["punctuation", "\""] + ]], + + ["key", "KEY"], + ["assignment-operator", "="], + ["value", [ + ["punctuation", "'"], + "String with a hash # this is not a comment", + ["punctuation", "'"] + ]], + + ["key", "KEY"], + ["assignment-operator", "="], + ["value", [ + ["punctuation", "`"], + "String with a hash # this is not a comment", + ["punctuation", "`"] + ]], + + ["key", "KEY"], + ["assignment-operator", "="], + ["value", ["VAL# also not a comment"]], + + ["key", "KEY"], + ["assignment-operator", "="], + ["value", [ + "Escaped ", + ["escape-sequence", "\\#"], + " hash is not a comment as well" + ]] +] + +---------------------------------------------------- + +Check for comments. diff --git a/tests/languages/dotenv/export_keyword_feature.test b/tests/languages/dotenv/export_keyword_feature.test new file mode 100644 index 0000000000..255a038f39 --- /dev/null +++ b/tests/languages/dotenv/export_keyword_feature.test @@ -0,0 +1,14 @@ +export EXPORTED_VAR=value + +---------------------------------------------------- + +[ + ["keyword", "export"], + ["key", "EXPORTED_VAR"], + ["assignment-operator", "="], + ["value", ["value"]] +] + +---------------------------------------------------- + +Check for the export keyword. diff --git a/tests/languages/dotenv/key_value_pair_feature.test b/tests/languages/dotenv/key_value_pair_feature.test new file mode 100644 index 0000000000..2ffe210f44 --- /dev/null +++ b/tests/languages/dotenv/key_value_pair_feature.test @@ -0,0 +1,122 @@ +APP_ENV=production +API_VERSION=1.2.3 +LOG=true +USERS_COUNT=42 +TIMEOUT=30.5 + +BARE_KEY +EMPTY_STRING= +#DISABLED_VAR=value + +APP_NAME="My App" +WELCOME="Hello\nWorld" +PASSWORD='literal#string' +ESCAPED="He said: \"Hello\"" + +HOST=example.com +PORT=8080 +ENABLE=true +SPECIAL=some\#literal + +MULTILINE=`line 1 \ +line 2 \ +line 3` + +SPACEY = spaced-value +TABS = value-with-tabs +TABS_AND_SPACES = value-with-tabs-and-spaces + +---------------------------------------------------- + +[ + ["key", "APP_ENV"], ["assignment-operator", "="], ["value", ["production"]], + ["key", "API_VERSION"], ["assignment-operator", "="], ["value", ["1.2.3"]], + ["key", "LOG"], ["assignment-operator", "="], ["value", "true"], + ["key", "USERS_COUNT"], ["assignment-operator", "="], ["value", "42"], + ["key", "TIMEOUT"], ["assignment-operator", "="], ["value", "30.5"], + + ["key", "BARE_KEY"], + ["key", "EMPTY_STRING"], ["assignment-operator", "="], ["value", ""], + ["comment", "#DISABLED_VAR=value"], + + ["key", "APP_NAME"], + ["assignment-operator", "="], + ["value", [ + ["punctuation", "\""], + "My App", + ["punctuation", "\""] + ]], + + ["key", "WELCOME"], + ["assignment-operator", "="], + ["value", [ + ["punctuation", "\""], + "Hello", + ["escape-sequence", "\\n"], + "World", + ["punctuation", "\""] + ]], + + ["key", "PASSWORD"], + ["assignment-operator", "="], + ["value", [ + ["punctuation", "'"], + "literal#string", + ["punctuation", "'"] + ]], + + ["key", "ESCAPED"], + ["assignment-operator", "="], + ["value", [ + ["punctuation", "\""], + "He said: ", + ["escape-sequence", "\\\""], + "Hello", + ["escape-sequence", "\\\""], + ["punctuation", "\""] + ]], + + ["key", "HOST"], + ["assignment-operator", "="], + ["value", ["example.com"]], + + ["key", "PORT"], + ["assignment-operator", "="], + ["value", "8080"], + + ["key", "ENABLE"], + ["assignment-operator", "="], + ["value", "true"], + + ["key", "SPECIAL"], + ["assignment-operator", "="], + ["value", [ + "some", + ["escape-sequence", "\\#"], + "literal" + ]], + + ["key", "MULTILINE"], + ["assignment-operator", "="], + ["value", [ + ["punctuation", "`"], + "line 1 \\\r\nline 2 \\\r\nline 3", + ["punctuation", "`"] + ]], + + ["key", "SPACEY"], + ["assignment-operator", "="], + ["value", ["spaced-value"]], + + ["key", "TABS"], + ["assignment-operator", "="], + ["value", ["value-with-tabs"]], + + ["key", "TABS_AND_SPACES"], + ["assignment-operator", "="], + ["value", ["value-with-tabs-and-spaces"]] +] + +---------------------------------------------------- + +Check for key-value pairs. diff --git a/tests/languages/dotenv/variable_expansion_feature.test b/tests/languages/dotenv/variable_expansion_feature.test new file mode 100644 index 0000000000..8c486c3328 --- /dev/null +++ b/tests/languages/dotenv/variable_expansion_feature.test @@ -0,0 +1,343 @@ +HELLO="Hi, ${NAME}" +FULL="${HELLO}, welcome to ${APP_ENV}" + +INTERPOLATED=Multiple $FOO:+false ${BAR-main}\nLines of text +ALSO_INTERPOLATED="Multiple\nLines with a var ${BAZ+42} and an escape sequence \" " +NON_INTERPOLATED='Text without variable (like, $YOLO) interpolation' +MULTILINE_NON_INTERPOLATED=`Long text here (with escape sequences, like \` +or similar), +e.g. a private SSH key (without $VAR interpolation)` + +KEY=${FOO:-default} +KEY=${FOO:-false} +KEY=${FOO:-true value} +KEY=$FOO:-false +KEY=$FOO:-false value +KEY=${FOO--42} +KEY=$FOO-42 years +KEY=${FOO:--15} + +KEY=${BAR:+alternative} +KEY=${BAR+true} +KEY=${BAR+false value} +KEY=$BAR+true +KEY=$BAR+true value +KEY=${BAR:+42} +KEY=${BAR:+-42} +KEY=${BAR:+42 years} +KEY=$BAR:+42 years +KEY="$FOO:+42" + +---------------------------------------------------- + +[ + ["key", "HELLO"], + ["assignment-operator", "="], + ["value", [ + ["punctuation", "\""], + "Hi, ", + ["variable-expansion", [ + ["variable-expansion-punctuation", "${"], + ["variable", "NAME"], + ["variable-expansion-punctuation", "}"] + ]], + ["punctuation", "\""] + ]], + + ["key", "FULL"], + ["assignment-operator", "="], + ["value", [ + ["punctuation", "\""], + ["variable-expansion", [ + ["variable-expansion-punctuation", "${"], + ["variable", "HELLO"], + ["variable-expansion-punctuation", "}"] + ]], + ", welcome to ", + ["variable-expansion", [ + ["variable-expansion-punctuation", "${"], + ["variable", "APP_ENV"], + ["variable-expansion-punctuation", "}"] + ]], + ["punctuation", "\""] + ]], + + ["key", "INTERPOLATED"], + ["assignment-operator", "="], + ["value", [ + "Multiple ", + ["variable-expansion", [ + ["variable-expansion-punctuation", "$"], + ["variable", "FOO"], + ["variable-expansion-punctuation", ":+"], + ["alternative-value", "false"] + ]], + ["variable-expansion", [ + ["variable-expansion-punctuation", "${"], + ["variable", "BAR"], + ["variable-expansion-punctuation", "-"], + ["default-value", "main"], + ["variable-expansion-punctuation", "}"] + ]], + ["escape-sequence", "\\n"], + "Lines of text" + ]], + + ["key", "ALSO_INTERPOLATED"], + ["assignment-operator", "="], + ["value", [ + ["punctuation", "\""], + "Multiple", + ["escape-sequence", "\\n"], + "Lines with a var ", + ["variable-expansion", [ + ["variable-expansion-punctuation", "${"], + ["variable", "BAZ"], + ["variable-expansion-punctuation", "+"], + ["alternative-value", "42"], + ["variable-expansion-punctuation", "}"] + ]], + " and an escape sequence ", + ["escape-sequence", "\\\""], + ["punctuation", "\""] + ]], + + ["key", "NON_INTERPOLATED"], + ["assignment-operator", "="], + ["value", [ + ["punctuation", "'"], + "Text without variable (like, $YOLO) interpolation", + ["punctuation", "'"] + ]], + + ["key", "MULTILINE_NON_INTERPOLATED"], + ["assignment-operator", "="], + ["value", [ + ["punctuation", "`"], + "Long text here (with escape sequences, like ", + ["escape-sequence", "\\`"], + + "\r\nor similar),\r\ne.g. a private SSH key (without $VAR interpolation)", + ["punctuation", "`"] + ]], + + ["key", "KEY"], + ["assignment-operator", "="], + ["value", [ + ["variable-expansion", [ + ["variable-expansion-punctuation", "${"], + ["variable", "FOO"], + ["variable-expansion-punctuation", ":-"], + ["default-value", "default"], + ["variable-expansion-punctuation", "}"] + ]] + ]], + + ["key", "KEY"], + ["assignment-operator", "="], + ["value", [ + ["variable-expansion", [ + ["variable-expansion-punctuation", "${"], + ["variable", "FOO"], + ["variable-expansion-punctuation", ":-"], + ["default-value", "false"], + ["variable-expansion-punctuation", "}"] + ]] + ]], + + ["key", "KEY"], + ["assignment-operator", "="], + ["value", [ + ["variable-expansion", [ + ["variable-expansion-punctuation", "${"], + ["variable", "FOO"], + ["variable-expansion-punctuation", ":-"], + ["default-value", "true value"], + ["variable-expansion-punctuation", "}"] + ]] + ]], + + ["key", "KEY"], + ["assignment-operator", "="], + ["value", [ + ["variable-expansion", [ + ["variable-expansion-punctuation", "$"], + ["variable", "FOO"], + ["variable-expansion-punctuation", ":-"], + ["default-value", "false"] + ]] + ]], + + ["key", "KEY"], + ["assignment-operator", "="], + ["value", [ + ["variable-expansion", [ + ["variable-expansion-punctuation", "$"], + ["variable", "FOO"], + ["variable-expansion-punctuation", ":-"], + ["default-value", "false"] + ]], + " value" + ]], + + ["key", "KEY"], + ["assignment-operator", "="], + ["value", [ + ["variable-expansion", [ + ["variable-expansion-punctuation", "${"], + ["variable", "FOO"], + ["variable-expansion-punctuation", "-"], + ["default-value", "-42"], + ["variable-expansion-punctuation", "}"] + ]] + ]], + + ["key", "KEY"], + ["assignment-operator", "="], + ["value", [ + ["variable-expansion", [ + ["variable-expansion-punctuation", "$"], + ["variable", "FOO"], + ["variable-expansion-punctuation", "-"], + ["default-value", "42"] + ]], + " years" + ]], + + ["key", "KEY"], + ["assignment-operator", "="], + ["value", [ + ["variable-expansion", [ + ["variable-expansion-punctuation", "${"], + ["variable", "FOO"], + ["variable-expansion-punctuation", ":-"], + ["default-value", "-15"], + ["variable-expansion-punctuation", "}"] + ]] + ]], + + ["key", "KEY"], + ["assignment-operator", "="], + ["value", [ + ["variable-expansion", [ + ["variable-expansion-punctuation", "${"], + ["variable", "BAR"], + ["variable-expansion-punctuation", ":+"], + ["alternative-value", "alternative"], + ["variable-expansion-punctuation", "}"] + ]] + ]], + + ["key", "KEY"], + ["assignment-operator", "="], + ["value", [ + ["variable-expansion", [ + ["variable-expansion-punctuation", "${"], + ["variable", "BAR"], + ["variable-expansion-punctuation", "+"], + ["alternative-value", "true"], + ["variable-expansion-punctuation", "}"] + ]] + ]], + + ["key", "KEY"], + ["assignment-operator", "="], + ["value", [ + ["variable-expansion", [ + ["variable-expansion-punctuation", "${"], + ["variable", "BAR"], + ["variable-expansion-punctuation", "+"], + ["alternative-value", "false value"], + ["variable-expansion-punctuation", "}"] + ]] + ]], + + ["key", "KEY"], + ["assignment-operator", "="], + ["value", [ + ["variable-expansion", [ + ["variable-expansion-punctuation", "$"], + ["variable", "BAR"], + ["variable-expansion-punctuation", "+"], + ["alternative-value", "true"] + ]] + ]], + + ["key", "KEY"], + ["assignment-operator", "="], + ["value", [ + ["variable-expansion", [ + ["variable-expansion-punctuation", "$"], + ["variable", "BAR"], + ["variable-expansion-punctuation", "+"], + ["alternative-value", "true"] + ]], + " value" + ]], + + ["key", "KEY"], + ["assignment-operator", "="], + ["value", [ + ["variable-expansion", [ + ["variable-expansion-punctuation", "${"], + ["variable", "BAR"], + ["variable-expansion-punctuation", ":+"], + ["alternative-value", "42"], + ["variable-expansion-punctuation", "}"] + ]] + ]], + + ["key", "KEY"], + ["assignment-operator", "="], + ["value", [ + ["variable-expansion", [ + ["variable-expansion-punctuation", "${"], + ["variable", "BAR"], + ["variable-expansion-punctuation", ":+"], + ["variable-expansion-punctuation", "-"], + ["default-value", "42"], + ["variable-expansion-punctuation", "}"] + ]] + ]], + + ["key", "KEY"], + ["assignment-operator", "="], + ["value", [ + ["variable-expansion", [ + ["variable-expansion-punctuation", "${"], + ["variable", "BAR"], + ["variable-expansion-punctuation", ":+"], + ["alternative-value", "42 years"], + ["variable-expansion-punctuation", "}"] + ]] + ]], + + ["key", "KEY"], + ["assignment-operator", "="], + ["value", [ + ["variable-expansion", [ + ["variable-expansion-punctuation", "$"], + ["variable", "BAR"], + ["variable-expansion-punctuation", ":+"], + ["alternative-value", "42"] + ]], + " years" + ]], + + ["key", "KEY"], + ["assignment-operator", "="], + ["value", [ + ["punctuation", "\""], + ["variable-expansion", [ + ["variable-expansion-punctuation", "$"], + ["variable", "FOO"], + ["variable-expansion-punctuation", ":+"], + ["alternative-value", "42"] + ]], + ["punctuation", "\""] + ]] +] + +---------------------------------------------------- + +Check for interpolation (also known as variable expansion). From 9bfb6bc9f9645f6b589c0cb4646e56e20a33e207 Mon Sep 17 00:00:00 2001 From: Dmitry Sharabin Date: Thu, 18 Dec 2025 16:40:27 +0100 Subject: [PATCH 04/11] Simplify regexes --- src/languages/dotenv.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/languages/dotenv.js b/src/languages/dotenv.js index 7d0c90ae8b..13bce0305b 100644 --- a/src/languages/dotenv.js +++ b/src/languages/dotenv.js @@ -30,7 +30,7 @@ export default { // Based on https://dotenvx.com/docs/env-file return { - 'comment': /(?:(?<=^)|(?<=[\s"'`]))#(?![^\n"'`]*["'`])[^\r\n]*?(?=[ \t]*(?:\r?\n|$))/, + 'comment': /(?:^|(?<=[\s"'`]))#(?![^\n"'`]*["'`])[^\r\n]*?(?=[ \t]*(?:\r?\n|$))/, 'keyword': /^export(?=\s)/m, 'key': { // Allow bare keys (without values) @@ -53,7 +53,7 @@ export default { }, { pattern: - /(?<==\s*)(?:(['"`])((?:\\.|(?!\1)[\s\S])*?)\1|(?:\S.*?\S))(?=(?:\s+#.*)?\s*$)/m, + /(?<==\s*)(?:(['"`])((?:\\.|(?!\1)[\s\S])*?)\1|\S.*?\S)(?=(?:\s+#.*)?\s*$)/m, alias: 'string', inside: { 'command-substitution': { @@ -74,7 +74,7 @@ export default { // Variable expansion is disabled in strings enclosed in "'" (single quotes) and "`" (backticks) pattern: /(? Date: Thu, 18 Dec 2025 17:10:47 +0100 Subject: [PATCH 05/11] Fix possible backtracking in the `comment` pattern --- src/languages/dotenv.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/languages/dotenv.js b/src/languages/dotenv.js index 13bce0305b..f9b5a125c5 100644 --- a/src/languages/dotenv.js +++ b/src/languages/dotenv.js @@ -30,7 +30,8 @@ export default { // Based on https://dotenvx.com/docs/env-file return { - 'comment': /(?:^|(?<=[\s"'`]))#(?![^\n"'`]*["'`])[^\r\n]*?(?=[ \t]*(?:\r?\n|$))/, + 'comment': + /(?:^|(?<=[\s"'`]))#(?![^\n"'`]*["'`])[^\r\n \t]*(?:[ \t]+[^\r\n \t]+)*[ \t]*/, 'keyword': /^export(?=\s)/m, 'key': { // Allow bare keys (without values) From 909128d645a3a4454d379079a686318f2fc287e3 Mon Sep 17 00:00:00 2001 From: Dmitry Sharabin Date: Thu, 18 Dec 2025 17:26:49 +0100 Subject: [PATCH 06/11] Partially fix pattern tests --- src/languages/dotenv.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/languages/dotenv.js b/src/languages/dotenv.js index f9b5a125c5..9c4f3d242e 100644 --- a/src/languages/dotenv.js +++ b/src/languages/dotenv.js @@ -22,7 +22,7 @@ export default { }, { // Ignore leading and trailing whitespace characters - pattern: RegExp(prefix.source + /(\S[^}]*?\S)(?=\s*\})/.source), + pattern: RegExp(prefix.source + /\S[^}]*?\S(?=\s*\})/.source), alias: 'string', }, ]; @@ -54,7 +54,7 @@ export default { }, { pattern: - /(?<==\s*)(?:(['"`])((?:\\.|(?!\1)[\s\S])*?)\1|\S.*?\S)(?=(?:\s+#.*)?\s*$)/m, + /(?<==\s*)(?:(['"`])(?:\\.|(?!\1)[\s\S])*?\1|\S.*?\S)(?=(?:\s+#.*)?\s*$)/m, alias: 'string', inside: { 'command-substitution': { @@ -73,7 +73,7 @@ export default { }, 'variable-expansion': { // Variable expansion is disabled in strings enclosed in "'" (single quotes) and "`" (backticks) - pattern: /(? Date: Thu, 18 Dec 2025 18:05:45 +0100 Subject: [PATCH 07/11] Fix the `key` pattern --- src/languages/dotenv.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/dotenv.js b/src/languages/dotenv.js index 9c4f3d242e..ac447cfe6a 100644 --- a/src/languages/dotenv.js +++ b/src/languages/dotenv.js @@ -35,7 +35,7 @@ export default { 'keyword': /^export(?=\s)/m, 'key': { // Allow bare keys (without values) - pattern: /[a-z_]\w*(?=\s*=|(? Date: Fri, 19 Dec 2025 08:46:54 +0100 Subject: [PATCH 08/11] Don't match the empty string --- src/languages/dotenv.js | 5 ----- tests/languages/dotenv/key_value_pair_feature.test | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/languages/dotenv.js b/src/languages/dotenv.js index ac447cfe6a..48bb42ba29 100644 --- a/src/languages/dotenv.js +++ b/src/languages/dotenv.js @@ -39,11 +39,6 @@ export default { alias: 'constant', }, 'value': [ - { - // Some implementations support empty values that are interpreted as empty strings - pattern: /(?<==)(?=\s*$)/, - alias: 'empty', - }, { pattern: /(?<==\s*)(?:-?[1-9]\d*|0)(?:\.\d+)?(?=\s*$)/, alias: 'number', diff --git a/tests/languages/dotenv/key_value_pair_feature.test b/tests/languages/dotenv/key_value_pair_feature.test index 2ffe210f44..de6b993f16 100644 --- a/tests/languages/dotenv/key_value_pair_feature.test +++ b/tests/languages/dotenv/key_value_pair_feature.test @@ -36,7 +36,7 @@ TABS_AND_SPACES = value-with-tabs-and-spaces ["key", "TIMEOUT"], ["assignment-operator", "="], ["value", "30.5"], ["key", "BARE_KEY"], - ["key", "EMPTY_STRING"], ["assignment-operator", "="], ["value", ""], + ["key", "EMPTY_STRING"], ["assignment-operator", "="], ["comment", "#DISABLED_VAR=value"], ["key", "APP_NAME"], From ae060de24d82cfe80439652a2baad0f00189bae1 Mon Sep 17 00:00:00 2001 From: Dmitry Sharabin Date: Fri, 19 Dec 2025 08:56:26 +0100 Subject: [PATCH 09/11] Fix polynomial backtracking --- src/languages/dotenv.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/dotenv.js b/src/languages/dotenv.js index 48bb42ba29..e5787e10b3 100644 --- a/src/languages/dotenv.js +++ b/src/languages/dotenv.js @@ -49,7 +49,7 @@ export default { }, { pattern: - /(?<==\s*)(?:(['"`])(?:\\.|(?!\1)[\s\S])*?\1|\S.*?\S)(?=(?:\s+#.*)?\s*$)/m, + /(?<==\s*)(?:(['"`])(?:\\.|(?!\1)[\s\S])*?\1|\S.*?\S)(?=\s*$|\s+#.*$)/m, alias: 'string', inside: { 'command-substitution': { From d013aab137d6bedeb23f5760b51a7eac4ee799ae Mon Sep 17 00:00:00 2001 From: Dmitry Sharabin Date: Fri, 19 Dec 2025 09:20:00 +0100 Subject: [PATCH 10/11] Fix exponential backtracking --- src/languages/dotenv.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/dotenv.js b/src/languages/dotenv.js index e5787e10b3..aa6ce87307 100644 --- a/src/languages/dotenv.js +++ b/src/languages/dotenv.js @@ -49,7 +49,7 @@ export default { }, { pattern: - /(?<==\s*)(?:(['"`])(?:\\.|(?!\1)[\s\S])*?\1|\S.*?\S)(?=\s*$|\s+#.*$)/m, + /(?<==\s*)(?:(['"`])(?:\\[\s\S]|(?!\1)[^\\])*?\1|\S(?:.*?\S)?)(?=\s*$|\s+#.*$)/m, alias: 'string', inside: { 'command-substitution': { From e1397c46fee22d57b974adcd47cf789702d7b7f2 Mon Sep 17 00:00:00 2001 From: Dmitry Sharabin Date: Fri, 19 Dec 2025 09:31:50 +0100 Subject: [PATCH 11/11] Fix components order + tweak the language title --- src/components.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components.json b/src/components.json index 5cacba8d26..6a6246c4be 100644 --- a/src/components.json +++ b/src/components.json @@ -383,14 +383,14 @@ "title": "Docker", "owner": "JustinBeckwith" }, + "dotenv": { + "title": "Dotenv (.env)", + "owner": "DmitrySharabin" + }, "dot": { "title": "DOT (Graphviz)", "owner": "RunDevelopment" }, - "dotenv": { - "title": "Dotenv", - "owner": "dmitrysharabin" - }, "ebnf": { "title": "EBNF", "owner": "RunDevelopment"