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: /%[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]?/ + }, + { + 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} + +# 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}] \ 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