From 8cea942683c0c2f19ca809d70c226121afd0cc24 Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Wed, 2 Mar 2022 20:26:23 -0500 Subject: [PATCH] (enh) lots of small Ruby improvements (#3491) * (enh) lots of small Ruby improvements * do not scope () as params * better camel case regex --- CHANGES.md | 1 + src/highlight.js | 4 +- src/languages/ruby.js | 185 +++++++++++++++++++++------- test/markup/erb/default.expect.txt | 2 +- test/markup/haml/default.expect.txt | 2 +- test/markup/ruby/heredoc.expect.txt | 4 +- test/markup/ruby/prompt.expect.txt | 6 +- 7 files changed, 148 insertions(+), 56 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2ad4fc51f6..78adafe977 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,7 @@ New Grammars: Grammars: +- enh(ruby) lots of small Ruby cleanups/improvements [Josh Goebel][] - enh(objectivec) add `type` and `variable.language` scopes [Josh Goebel][] - enh(xml) support processing instructions (#3492) [Josh Goebel][] - enh(ruby ) better support multi-line IRB prompts diff --git a/src/highlight.js b/src/highlight.js index ed0e187fd5..f81cc458e9 100644 --- a/src/highlight.js +++ b/src/highlight.js @@ -277,8 +277,8 @@ const HLJS = function(hljs) { */ function emitMultiClass(scope, match) { let i = 1; - // eslint-disable-next-line no-undefined - while (match[i] !== undefined) { + const max = match.length - 1; + while (i <= max) { if (!scope._emit[i]) { i++; continue; } const klass = language.classNameAliases[scope[i]] || scope[i]; const text = match[i]; diff --git a/src/languages/ruby.js b/src/languages/ruby.js index 59e77edec9..0504039a0a 100644 --- a/src/languages/ruby.js +++ b/src/languages/ruby.js @@ -10,15 +10,71 @@ Category: common 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 RUBY_KEYWORDS = { - keyword: - 'and then defined module in return redo if BEGIN retry end for self when ' - + 'next until do begin unless END rescue else break undef not super class case ' - + 'require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor ' - + '__FILE__', - built_in: 'proc lambda', - literal: - 'true false nil' + "variable.constant": [ + "__FILE__", + "__LINE__" + ], + "variable.language": [ + "self", + "super", + ], + keyword: [ + "alias", + "and", + "attr_accessor", + "attr_reader", + "attr_writer", + "begin", + "BEGIN", + "break", + "case", + "class", + "defined", + "do", + "else", + "elsif", + "end", + "END", + "ensure", + "for", + "if", + "in", + "include", + "module", + "next", + "not", + "or", + "redo", + "require", + "rescue", + "retry", + "return", + "then", + "undef", + "unless", + "until", + "when", + "while", + "yield", + ], + built_in: [ + "proc", + "lambda" + ], + literal: [ + "true", + "false", + "nil" + ] }; const YARDOCTAG = { className: 'doctag', @@ -42,7 +98,7 @@ export default function(hljs) { relevance: 10 } ), - hljs.COMMENT('^__END__', '\\n$') + hljs.COMMENT('^__END__', hljs.MATCH_NOTHING_RE) ]; const SUBST = { className: 'subst', @@ -156,49 +212,82 @@ export default function(hljs) { }; const PARAMS = { - className: 'params', - begin: '\\(', - end: '\\)', - endsParent: true, + variants: [ + { + match: /\(\)/, + }, + { + className: 'params', + begin: /\(/, + end: /(?=\))/, + excludeBegin: true, + endsParent: true, + keywords: RUBY_KEYWORDS, + } + ] + }; + + const CLASS_DEFINITION = { + variants: [ + { + match: [ + /class\s+/, + CLASS_NAME_WITH_NAMESPACE_RE, + /\s+<\s+/, + CLASS_NAME_WITH_NAMESPACE_RE + ] + }, + { + match: [ + /class\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/, + className: "variable.constant" + }; + + const METHOD_DEFINITION = { + match: [ + /def/, /\s+/, + RUBY_METHOD_RE + ], + scope: { + 1: "keyword", + 3: "title.function" + }, + contains: [ + PARAMS + ] + }; + + const OBJECT_CREATION = { + relevance: 0, + match: [ + CLASS_NAME_WITH_NAMESPACE_RE, + /\.new[ (]/ + ], + scope: { + 1: "title.class" + } + }; + const RUBY_DEFAULT_CONTAINS = [ STRING, - { - className: 'class', - beginKeywords: 'class module', - end: '$|;', - illegal: /=/, - contains: [ - hljs.inherit(hljs.TITLE_MODE, { begin: '[A-Za-z_]\\w*(::\\w+)*(\\?|!)?' }), - { - begin: '<\\s*', - contains: [ - { - begin: '(' + hljs.IDENT_RE + '::)?' + hljs.IDENT_RE, - // we already get points for <, we don't need poitns - // for the name also - relevance: 0 - } - ] - } - ].concat(COMMENT_MODES) - }, - { - className: 'function', - // def method_name( - // def method_name; - // def method_name (end of line) - begin: regex.concat(/def\s+/, regex.lookahead(RUBY_METHOD_RE + "\\s*(\\(|;|$)")), - relevance: 0, // relevance comes from kewords - keywords: "def", - end: '$|;', - contains: [ - hljs.inherit(hljs.TITLE_MODE, { begin: RUBY_METHOD_RE }), - PARAMS - ].concat(COMMENT_MODES) - }, + CLASS_DEFINITION, + OBJECT_CREATION, + UPPER_CASE_CONSTANT, + METHOD_DEFINITION, { // swallow namespace qualifiers before symbols begin: hljs.IDENT_RE + '::' }, @@ -227,6 +316,8 @@ export default function(hljs) { className: 'params', begin: /\|/, end: /\|/, + excludeBegin: true, + excludeEnd: true, relevance: 0, // this could be a lot of things (in other languages) other than params keywords: RUBY_KEYWORDS }, diff --git a/test/markup/erb/default.expect.txt b/test/markup/erb/default.expect.txt index 47a8cf878d..2473ac1b61 100644 --- a/test/markup/erb/default.expect.txt +++ b/test/markup/erb/default.expect.txt @@ -1,6 +1,6 @@ <%# this is a comment %> -<% @posts.each do |post| %> +<% @posts.each do |post| %> <p><%= link_to post.title, post %></p> <% end %> diff --git a/test/markup/haml/default.expect.txt b/test/markup/haml/default.expect.txt index ccb3a210f4..7a422eb11f 100644 --- a/test/markup/haml/default.expect.txt +++ b/test/markup/haml/default.expect.txt @@ -5,7 +5,7 @@ /html comment -# ignore this line %ul(style='margin: 0') - -items.each do |i| + -items.each do |i| %i= i = variable =variable2 diff --git a/test/markup/ruby/heredoc.expect.txt b/test/markup/ruby/heredoc.expect.txt index 3f9379314e..cb3fd9f31b 100644 --- a/test/markup/ruby/heredoc.expect.txt +++ b/test/markup/ruby/heredoc.expect.txt @@ -13,7 +13,7 @@ message = <<-MESSAGE.chomp This looks good MESSAGE -def foo() +def foo() msg = <<-HTML <div> <h4>#{bar}</h4> @@ -21,7 +21,7 @@ MESSAGE HTML end -def baz() +def baz() msg = <<~FOO <div> <h4>#{bar}</h4> diff --git a/test/markup/ruby/prompt.expect.txt b/test/markup/ruby/prompt.expect.txt index 0d6baee8c5..bef182b866 100644 --- a/test/markup/ruby/prompt.expect.txt +++ b/test/markup/ruby/prompt.expect.txt @@ -7,7 +7,7 @@ jruby-1.7.16 :001 > "RVM-Format" ->> obj = OpenStruct.new :integer => 987, :symbol => :so_great +>> obj = OpenStruct.new :integer => 987, :symbol => :so_great => #<OpenStruct integer=987, symbol=:so_great> >> [obj,obj,obj] => [#<OpenStruct integer=987, symbol=:so_great>, #<OpenStruct integer=987, symbol=:so_great>, #<OpenStruct integer=987, symbol=:so_great>] @@ -22,8 +22,8 @@ irb(main):002:0> test = 1 -irb(main):001:1* class Secret -irb(main):002:2* def [](x) +irb(main):001:1* class Secret +irb(main):002:2* def [](x) irb(main):003:2* "TREASURE" if x==42 irb(main):004:1* end irb(main):005:0> end