diff --git a/CHANGES.md b/CHANGES.md
index 76fa83e573..62cc1846d4 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,10 @@
+## Version 11.11.2
+
+Core Grammars:
+
+- fix(ruby) symbols, string interpolation, class names with underscores
+
+
## Version 11.11.1
- Fixes regression with Rust grammar.
diff --git a/src/languages/ruby.js b/src/languages/ruby.js
index 91b2cb527d..6121071b22 100644
--- a/src/languages/ruby.js
+++ b/src/languages/ruby.js
@@ -11,13 +11,7 @@ export default function(hljs) {
const regex = hljs.regex;
const RUBY_METHOD_RE = '([a-zA-Z_]\\w*[!?=]?|[-+~]@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?)';
// TODO: move concepts like CAMEL_CASE into `modes.js`
- const CLASS_NAME_RE = regex.either(
- /\b([A-Z]+[a-z0-9]+)+/,
- // ends in caps
- /\b([A-Z]+[a-z0-9]+)+[A-Z]+/,
- )
- ;
- const CLASS_NAME_WITH_NAMESPACE_RE = regex.concat(CLASS_NAME_RE, /(::\w+)*/)
+ const CLASS_NAME_RE = /\b([A-Z]+[a-z0-9_]+)+[A-Z]*/;
// very popular ruby built-ins that one might even assume
// are actual keywords (despite that not being the case)
const PSEUDO_KWS = [
@@ -122,17 +116,13 @@ export default function(hljs) {
end: /\}/,
keywords: RUBY_KEYWORDS
};
- const STRING = {
+ const STRING_INTERPOLABLE = {
className: 'string',
contains: [
hljs.BACKSLASH_ESCAPE,
SUBST
],
variants: [
- {
- begin: /'/,
- end: /'/
- },
{
begin: /"/,
end: /"/
@@ -142,45 +132,37 @@ export default function(hljs) {
end: /`/
},
{
- begin: /%[qQwWx]?\(/,
+ begin: /%[QWx]?\(/,
end: /\)/
},
{
- begin: /%[qQwWx]?\[/,
+ begin: /%[QWx]?\[/,
end: /\]/
},
{
- begin: /%[qQwWx]?\{/,
+ begin: /%[QWx]?\{/,
end: /\}/
},
{
- begin: /%[qQwWx]?,
+ begin: /%[QWx]?,
end: />/
},
{
- begin: /%[qQwWx]?\//,
+ begin: /%[QWx]?\//,
end: /\//
},
{
- begin: /%[qQwWx]?%/,
+ begin: /%[QWx]?%/,
end: /%/
},
{
- begin: /%[qQwWx]?-/,
+ begin: /%[QWx]?-/,
end: /-/
},
{
- begin: /%[qQwWx]?\|/,
+ begin: /%[QWx]?\|/,
end: /\|/
},
- // in the following expressions, \B in the beginning suppresses recognition of ?-sequences
- // where ? is the last character of a preceding identifier, as in: `func?4`
- { begin: /\B\?(\\\d{1,3})/ },
- { begin: /\B\?(\\x[A-Fa-f0-9]{1,2})/ },
- { begin: /\B\?(\\u\{?[A-Fa-f0-9]{1,6}\}?)/ },
- { begin: /\B\?(\\M-\\C-|\\M-\\c|\\c\\M-|\\M-|\\C-\\M-)[\x20-\x7e]/ },
- { begin: /\B\?\\(c|C-)[\x20-\x7e]/ },
- { begin: /\B\?\\?\S/ },
// heredocs
{
// this guard makes sure that we have an entire heredoc and not a false
@@ -202,6 +184,55 @@ export default function(hljs) {
}
]
};
+ const STRING_NONINTERPOLABLE = {
+ className: 'string',
+ variants: [
+ {
+ begin: /'/,
+ end: /'/
+ },
+ {
+ begin: /%[qw]?\(/,
+ end: /\)/
+ },
+ {
+ begin: /%[qw]?\[/,
+ end: /\]/
+ },
+ {
+ begin: /%[qw]?\{/,
+ end: /\}/
+ },
+ {
+ begin: /%[qw]?,
+ end: />/
+ },
+ {
+ begin: /%[qw]?\//,
+ end: /\//
+ },
+ {
+ begin: /%[qw]?%/,
+ end: /%/
+ },
+ {
+ begin: /%[qw]?-/,
+ end: /-/
+ },
+ {
+ begin: /%[qw]?\|/,
+ end: /\|/
+ },
+ // in the following expressions, \B in the beginning suppresses recognition of ?-sequences
+ // where ? is the last character of a preceding identifier, as in: `func?4`
+ { begin: /\B\?(\\\d{1,3})/ },
+ { begin: /\B\?(\\x[A-Fa-f0-9]{1,2})/ },
+ { begin: /\B\?(\\u\{?[A-Fa-f0-9]{1,6}\}?)/ },
+ { begin: /\B\?(\\M-\\C-|\\M-\\c|\\c\\M-|\\M-|\\C-\\M-)[\x20-\x7e]/ },
+ { begin: /\B\?\\(c|C-)[\x20-\x7e]/ },
+ { begin: /\B\?\\?\S/ }
+ ]
+ };
// Ruby syntax is underdocumented, but this grammar seems to be accurate
// as of version 2.7.2 (confirmed with (irb and `Ripper.sexp(...)`)
@@ -243,44 +274,9 @@ export default function(hljs) {
]
};
- const INCLUDE_EXTEND = {
- match: [
- /(include|extend)\s+/,
- CLASS_NAME_WITH_NAMESPACE_RE
- ],
- scope: {
- 2: "title.class"
- },
- keywords: RUBY_KEYWORDS
- };
-
- const CLASS_DEFINITION = {
- variants: [
- {
- match: [
- /class\s+/,
- CLASS_NAME_WITH_NAMESPACE_RE,
- /\s+<\s+/,
- CLASS_NAME_WITH_NAMESPACE_RE
- ]
- },
- {
- match: [
- /\b(class|module)\s+/,
- CLASS_NAME_WITH_NAMESPACE_RE
- ]
- }
- ],
- scope: {
- 2: "title.class",
- 4: "title.class.inherited"
- },
- keywords: RUBY_KEYWORDS
- };
-
const UPPER_CASE_CONSTANT = {
relevance: 0,
- match: /\b[A-Z][A-Z_0-9]+\b/,
+ match: /\b[A-Z][A-Z_0-9]*\b/,
className: "variable.constant"
};
@@ -298,15 +294,9 @@ export default function(hljs) {
]
};
- const OBJECT_CREATION = {
- relevance: 0,
- match: [
- CLASS_NAME_WITH_NAMESPACE_RE,
- /\.new[. (]/
- ],
- scope: {
- 1: "title.class"
- }
+ const CLASS_ANCESTOR = {
+ className: "title.class.inherited",
+ match: CLASS_NAME_RE
};
// CamelCase
@@ -316,30 +306,75 @@ export default function(hljs) {
scope: "title.class"
};
+ const CLASS_SEPARATOR = {
+ match: /::/
+ };
+
+ const CLASS_KEYWORD = {
+ match: /class/,
+ scope: "keyword"
+ };
+
+ const CLASS_INHERITANCE = {
+ begin: /\s*class\b/,
+ excludeBegin: true,
+ variants: [
+ {
+ contains: [
+ CLASS_KEYWORD,
+ {
+ begin: /\s/,
+ contains: [ CLASS_REFERENCE, CLASS_SEPARATOR ],
+ },
+ {
+ begin: /<\s*/,
+ contains: [ CLASS_ANCESTOR, CLASS_SEPARATOR ]
+ }
+ ]
+ },
+ {
+ contains: [
+ CLASS_KEYWORD,
+ {
+ begin: /\s/,
+ contains: [ CLASS_REFERENCE, CLASS_SEPARATOR ]
+ },
+ ]
+ }
+ ]
+ };
+
const RUBY_DEFAULT_CONTAINS = [
- STRING,
- CLASS_DEFINITION,
- INCLUDE_EXTEND,
- OBJECT_CREATION,
+ STRING_INTERPOLABLE,
+ STRING_NONINTERPOLABLE,
UPPER_CASE_CONSTANT,
CLASS_REFERENCE,
+ CLASS_INHERITANCE,
METHOD_DEFINITION,
{
// swallow namespace qualifiers before symbols
- begin: hljs.IDENT_RE + '::' },
+ begin: '::'
+ },
{
className: 'symbol',
- begin: hljs.UNDERSCORE_IDENT_RE + '(!|\\?)?:',
+ begin: hljs.UNDERSCORE_IDENT_RE + '(!|\\?)?:(?!:)',
relevance: 0
},
{
className: 'symbol',
begin: ':(?!\\s)',
contains: [
- STRING,
- { begin: RUBY_METHOD_RE }
+ { begin: /'/, end: /'/ },
+ {
+ begin: /"/, end: /"/,
+ contains: [
+ hljs.BACKSLASH_ESCAPE,
+ SUBST
+ ]
+ },
+ { begin: hljs.UNDERSCORE_IDENT_RE }
],
- relevance: 0
+ relevance: 1
},
NUMBER,
{
diff --git a/test/markup/ruby/classes.expect.txt b/test/markup/ruby/classes.expect.txt
new file mode 100644
index 0000000000..1b59166e66
--- /dev/null
+++ b/test/markup/ruby/classes.expect.txt
@@ -0,0 +1,16 @@
+Class
+ClassName
+Class_Name
+ClassNAME
+ClassName::With::Namespace
+ClassName::With.method
+::TopLevel::Class
+
+class Foo::Bar::Baz
+class Foo < Qux
+class Foo < ::Qux
+class Foo<Qux
+class Foo::Bar < Qux
+class Foo < Qux::Quux
+class Foo::Bar::Baz < Qux::Quux
+class Foo::Bar::Baz < Qux::Quux::Corge
\ No newline at end of file
diff --git a/test/markup/ruby/classes.txt b/test/markup/ruby/classes.txt
new file mode 100644
index 0000000000..91cbca4f7f
--- /dev/null
+++ b/test/markup/ruby/classes.txt
@@ -0,0 +1,16 @@
+Class
+ClassName
+Class_Name
+ClassNAME
+ClassName::With::Namespace
+ClassName::With.method
+::TopLevel::Class
+
+class Foo::Bar::Baz
+class Foo < Qux
+class Foo < ::Qux
+class Foo?\u{00AF09}
c = ?\u{0AF09}
c = ?\u{AF9}
c = ?\u{F9}
-c = ?\u{F}
\ No newline at end of file
+c = ?\u{F}
+
+
+"string"
+"string #{var}"
+`string`
+`string #{var}`
+%W[foo bar]
+%W[foo bar #{var}]
+%Q[foo bar]
+%Q[foo bar #{var}]
+%x[foo]
+%x[foo #{var}]
+<<~DOC
+ Multiline heredoc
+ Text #{var}
+DOC
+
+
+'string'
+'string #{var}'
+%q[foo]
+%q[foo #{var}]
+%w[foo]
+%w[foo #{var}]
\ No newline at end of file
diff --git a/test/markup/ruby/strings.txt b/test/markup/ruby/strings.txt
index 43d35d656b..2ec98d2f0c 100644
--- a/test/markup/ruby/strings.txt
+++ b/test/markup/ruby/strings.txt
@@ -27,4 +27,28 @@ c = ?\u{00AF09}
c = ?\u{0AF09}
c = ?\u{AF9}
c = ?\u{F9}
-c = ?\u{F}
\ No newline at end of file
+c = ?\u{F}
+
+# Interpolable Strings
+"string"
+"string #{var}"
+`string`
+`string #{var}`
+%W[foo bar]
+%W[foo bar #{var}]
+%Q[foo bar]
+%Q[foo bar #{var}]
+%x[foo]
+%x[foo #{var}]
+<<~DOC
+ Multiline heredoc
+ Text #{var}
+DOC
+
+# Non-interpolable Strings
+'string'
+'string #{var}'
+%q[foo]
+%q[foo #{var}]
+%w[foo]
+%w[foo #{var}]
diff --git a/test/markup/ruby/symbols.expect.txt b/test/markup/ruby/symbols.expect.txt
new file mode 100644
index 0000000000..df75d4fbcc
--- /dev/null
+++ b/test/markup/ruby/symbols.expect.txt
@@ -0,0 +1,22 @@
+:symbol
+:Symbol
+:_leading
+:trailing_
+:contains_underscore
+:symbol_CAPS
+:"string symbol"
+:"interpolated #{test}"
+:'string symbol'
+:'not interpolated #{test}'
+method :symbol
+method(:symbol)
+method(&:symbol)
+assign=:symbol
+assign = :symbol
+:symbol, others
+:1notasymbol
+:%q[notasymbol]
+
+::notsymbol
+
+hash_symbol: value
\ No newline at end of file
diff --git a/test/markup/ruby/symbols.txt b/test/markup/ruby/symbols.txt
new file mode 100644
index 0000000000..b11c5c04ba
--- /dev/null
+++ b/test/markup/ruby/symbols.txt
@@ -0,0 +1,22 @@
+:symbol
+:Symbol
+:_leading
+:trailing_
+:contains_underscore
+:symbol_CAPS
+:"string symbol"
+:"interpolated #{test}"
+:'string symbol'
+:'not interpolated #{test}'
+method :symbol
+method(:symbol)
+method(&:symbol)
+assign=:symbol
+assign = :symbol
+:symbol, others
+:1notasymbol
+:%q[notasymbol]
+
+::notsymbol
+
+hash_symbol: value
\ No newline at end of file