diff --git a/CHANGES.md b/CHANGES.md
index efb45e530b..275f9033cc 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -21,6 +21,7 @@ Deprecations:
Language Improvements:
+- fix(groovy) strings are not allowed inside ternary clauses (#2217) [Josh Goebel][]
- fix(typescript) add `readonly` keyword (#2562) [Martin (Lhoerion)][]
- fix(javascript) fix regex inside parens after a non-regex (#2530) [Josh Goebel][]
- enh(typescript) use identifier to match potential keywords, preventing false positivites (#2519) [Josh Goebel][]
diff --git a/src/languages/groovy.js b/src/languages/groovy.js
index 8dfb00767f..20f8662d77 100644
--- a/src/languages/groovy.js
+++ b/src/languages/groovy.js
@@ -5,69 +5,84 @@
Website: https://groovy-lang.org
*/
+import * as regex from "../lib/regex";
+
+function variants(variants, obj = {}) {
+ obj.variants = variants;
+ return obj;
+}
+
export default function(hljs) {
+ const IDENT_RE = '[A-Za-z0-9_$]+';
+ const COMMENT = variants([
+ hljs.C_LINE_COMMENT_MODE,
+ hljs.C_BLOCK_COMMENT_MODE,
+ hljs.COMMENT(
+ '/\\*\\*',
+ '\\*/',
+ {
+ relevance : 0,
+ contains : [
+ {
+ // eat up @'s in emails to prevent them to be recognized as doctags
+ begin: /\w+@/, relevance: 0
+ }, {
+ className : 'doctag',
+ begin : '@[A-Za-z]+'
+ }
+ ]
+ }
+ )
+ ]);
+ const REGEXP = {
+ className: 'regexp',
+ begin: /~?\/[^\/\n]+\//,
+ contains: [
+ hljs.BACKSLASH_ESCAPE
+ ]
+ };
+ const NUMBER = variants([
+ hljs.BINARY_NUMBER_MODE,
+ hljs.C_NUMBER_MODE,
+ ]);
+ const STRING = variants([
+ {
+ begin: /"""/,
+ end: /"""/
+ }, {
+ begin: /'''/,
+ end: /'''/
+ }, {
+ begin: "\\$/",
+ end: "/\\$",
+ relevance: 10
+ },
+ hljs.APOS_STRING_MODE,
+ hljs.QUOTE_STRING_MODE,
+ ],
+ { className: "string" }
+ );
+
return {
name: 'Groovy',
keywords: {
- literal : 'true false null',
+ built_in: 'this super',
+ literal: 'true false null',
keyword:
'byte short char int long boolean float double void ' +
// groovy specific keywords
'def as in assert trait ' +
// common keywords with Java
- 'super this abstract static volatile transient public private protected synchronized final ' +
+ 'abstract static volatile transient public private protected synchronized final ' +
'class interface enum if else for while switch case break default continue ' +
'throw throws try catch finally implements extends new import package return instanceof'
},
-
contains: [
- hljs.COMMENT(
- '/\\*\\*',
- '\\*/',
- {
- relevance : 0,
- contains : [
- {
- // eat up @'s in emails to prevent them to be recognized as doctags
- begin: /\w+@/, relevance: 0
- },
- {
- className : 'doctag',
- begin : '@[A-Za-z]+'
- }
- ]
- }
- ),
- hljs.C_LINE_COMMENT_MODE,
- hljs.C_BLOCK_COMMENT_MODE,
- {
- className: 'string',
- begin: '"""', end: '"""'
- },
- {
- className: 'string',
- begin: "'''", end: "'''"
- },
- {
- className: 'string',
- begin: "\\$/", end: "/\\$",
- relevance: 10
- },
- hljs.APOS_STRING_MODE,
- {
- className: 'regexp',
- begin: /~?\/[^\/\n]+\//,
- contains: [
- hljs.BACKSLASH_ESCAPE
- ]
- },
- hljs.QUOTE_STRING_MODE,
- {
- className: 'meta',
- begin: "^#!/usr/bin/env", end: '$',
- illegal: '\n'
- },
- hljs.BINARY_NUMBER_MODE,
+ hljs.SHEBANG(),
+ COMMENT,
+ STRING,
+ REGEXP,
+ NUMBER,
{
className: 'class',
beginKeywords: 'class interface trait enum', end: '{',
@@ -77,25 +92,35 @@ export default function(hljs) {
hljs.UNDERSCORE_TITLE_MODE
]
},
- hljs.C_NUMBER_MODE,
{
className: 'meta', begin: '@[A-Za-z]+'
},
{
- // highlight map keys and named parameters as strings
- className: 'string', begin: /[^\?]{0}[A-Za-z0-9_$]+ *:/
+ // highlight map keys and named parameters as attrs
+ className: 'attr', begin: IDENT_RE + '[ \t]*:'
},
{
- // catch middle element of the ternary operator
- // to avoid highlight it as a label, named parameter, or map key
- begin: /\?/, end: /\:/
+ // catch middle element of the ternary operator
+ // to avoid highlight it as a label, named parameter, or map key
+ begin: /\?/,
+ end: /:/,
+ contains: [
+ COMMENT,
+ STRING,
+ REGEXP,
+ NUMBER,
+ 'self'
+ ]
},
{
// highlight labeled statements
- className: 'symbol', begin: '^\\s*[A-Za-z0-9_$]+:',
+ className: 'symbol',
+ begin: '^[ \t]*' + regex.lookahead(IDENT_RE + ':'),
+ excludeBegin: true,
+ end: IDENT_RE + ':',
relevance: 0
}
],
illegal: /#|<\//
- }
+ };
}
diff --git a/test/markup/groovy/default.expect.txt b/test/markup/groovy/default.expect.txt
new file mode 100644
index 0000000000..c546cd97d9
--- /dev/null
+++ b/test/markup/groovy/default.expect.txt
@@ -0,0 +1,58 @@
+#!/usr/bin/env groovy
+package model
+
+import groovy.transform.CompileStatic
+import java.util.List as MyList
+
+trait Distributable {
+ void distribute(String version) {}
+}
+
+@CompileStatic
+class Distribution implements Distributable {
+ double number = 1234.234 / 567
+ def otherNumber = 3 / 4
+ boolean archivable = condition ?: true
+ def ternary = a ? b : c
+ String name = "Guillaume"
+ Closure description = null
+ List<DownloadPackage> packages = []
+ String regex = ~/.*foo.*/
+ String multi = '''
+ multi line string
+ ''' + """
+ now with double quotes and ${gstring}
+ """ + $/
+ even with dollar slashy strings
+ /$
+
+
+ void description(Closure cl) { this.description = cl }
+
+ void version(String name, Closure versionSpec) {
+ def closure = { println "hi" } as Runnable
+
+ MyList ml = [1, 2, [a: 1, b:2,c :3]]
+ for (ch in "name") {}
+
+
+ DownloadPackage pkg = new DownloadPackage(version: name)
+
+ check that: true
+
+ label:
+
+ tabbed_label:
+ def clone = versionSpec.rehydrate(pkg, pkg, pkg)
+
+ clone()
+ packages.add(pkg)
+
+ assert 4 / 2 == 2
+ }
+}
diff --git a/test/markup/groovy/default.txt b/test/markup/groovy/default.txt
new file mode 100644
index 0000000000..1ae59076ad
--- /dev/null
+++ b/test/markup/groovy/default.txt
@@ -0,0 +1,58 @@
+#!/usr/bin/env groovy
+package model
+
+import groovy.transform.CompileStatic
+import java.util.List as MyList
+
+trait Distributable {
+ void distribute(String version) {}
+}
+
+@CompileStatic
+class Distribution implements Distributable {
+ double number = 1234.234 / 567
+ def otherNumber = 3 / 4
+ boolean archivable = condition ?: true
+ def ternary = a ? b : c
+ String name = "Guillaume"
+ Closure description = null
+ List packages = []
+ String regex = ~/.*foo.*/
+ String multi = '''
+ multi line string
+ ''' + """
+ now with double quotes and ${gstring}
+ """ + $/
+ even with dollar slashy strings
+ /$
+
+ /**
+ * description method
+ * @param cl the closure
+ */
+ void description(Closure cl) { this.description = cl }
+
+ void version(String name, Closure versionSpec) {
+ def closure = { println "hi" } as Runnable
+
+ MyList ml = [1, 2, [a: 1, b:2,c :3]]
+ for (ch in "name") {}
+
+ // single line comment
+ DownloadPackage pkg = new DownloadPackage(version: name)
+
+ check that: true
+
+ label:
+ // This is purposely tabbed
+ tabbed_label:
+ def clone = versionSpec.rehydrate(pkg, pkg, pkg)
+ /*
+ now clone() in a multiline comment
+ */
+ clone()
+ packages.add(pkg)
+
+ assert 4 / 2 == 2
+ }
+}
diff --git a/test/markup/groovy/oneoffs.expect.txt b/test/markup/groovy/oneoffs.expect.txt
new file mode 100644
index 0000000000..bb4c7ef80d
--- /dev/null
+++ b/test/markup/groovy/oneoffs.expect.txt
@@ -0,0 +1,3 @@
+
+def formattingMsg = label < 0 ? ('The following files need formatting:\n ' +
+ codeStyleFiles.join('\n ')) : 'All files are correctly formatted'
diff --git a/test/markup/groovy/oneoffs.txt b/test/markup/groovy/oneoffs.txt
new file mode 100644
index 0000000000..8751afc948
--- /dev/null
+++ b/test/markup/groovy/oneoffs.txt
@@ -0,0 +1,3 @@
+// ternary can include quotes
+def formattingMsg = label < 0 ? ('The following files need formatting:\n ' +
+ codeStyleFiles.join('\n ')) : 'All files are correctly formatted'