/* Language: Ruby Description: Ruby is a dynamic, open source programming language with a focus on simplicity and productivity. Website: https://www.ruby-lang.org/ Author: Anton Kovalyov Contributors: Peter Leonov , Vasily Polovnyov , Loren Segal , Pascal Hurni , Cedric Sohrauer Category: common */ function ruby(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+)*/); // very popular ruby built-ins that one might even assume // are actual keywords (despite that not being the case) const PSEUDO_KWS = [ "include", "extend", "prepend", "public", "private", "protected", "raise", "throw" ]; const RUBY_KEYWORDS = { "variable.constant": [ "__FILE__", "__LINE__", "__ENCODING__" ], "variable.language": [ "self", "super", ], keyword: [ "alias", "and", "begin", "BEGIN", "break", "case", "class", "defined", "do", "else", "elsif", "end", "END", "ensure", "for", "if", "in", "module", "next", "not", "or", "redo", "require", "rescue", "retry", "return", "then", "undef", "unless", "until", "when", "while", "yield", ...PSEUDO_KWS ], built_in: [ "proc", "lambda", "attr_accessor", "attr_reader", "attr_writer", "define_method", "private_constant", "module_function" ], literal: [ "true", "false", "nil" ] }; const YARDOCTAG = { className: 'doctag', begin: '@[A-Za-z]+' }; const IRB_OBJECT = { begin: '#<', end: '>' }; const COMMENT_MODES = [ hljs.COMMENT( '#', '$', { contains: [ YARDOCTAG ] } ), hljs.COMMENT( '^=begin', '^=end', { contains: [ YARDOCTAG ], relevance: 10 } ), hljs.COMMENT('^__END__', hljs.MATCH_NOTHING_RE) ]; const SUBST = { className: 'subst', begin: /#\{/, end: /\}/, keywords: RUBY_KEYWORDS }; const STRING = { className: 'string', contains: [ hljs.BACKSLASH_ESCAPE, SUBST ], variants: [ { begin: /'/, end: /'/ }, { begin: /"/, end: /"/ }, { begin: /`/, end: /`/ }, { begin: /%[qQwWx]?\(/, end: /\)/ }, { begin: /%[qQwWx]?\[/, end: /\]/ }, { begin: /%[qQwWx]?\{/, end: /\}/ }, { begin: /%[qQwWx]?/ }, { begin: /%[qQwWx]?\//, end: /\// }, { begin: /%[qQwWx]?%/, end: /%/ }, { begin: /%[qQwWx]?-/, end: /-/ }, { begin: /%[qQwWx]?\|/, 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 // positive (auto-detect, etc.) begin: regex.concat( /<<[-~]?'?/, regex.lookahead(/(\w+)(?=\W)[^\n]*\n(?:[^\n]*\n)*?\s*\1\b/) ), contains: [ hljs.END_SAME_AS_BEGIN({ begin: /(\w+)/, end: /(\w+)/, contains: [ hljs.BACKSLASH_ESCAPE, SUBST ] }) ] } ] }; // Ruby syntax is underdocumented, but this grammar seems to be accurate // as of version 2.7.2 (confirmed with (irb and `Ripper.sexp(...)`) // https://docs.ruby-lang.org/en/2.7.0/doc/syntax/literals_rdoc.html#label-Numbers const decimal = '[1-9](_?[0-9])*|0'; const digits = '[0-9](_?[0-9])*'; const NUMBER = { className: 'number', relevance: 0, variants: [ // decimal integer/float, optionally exponential or rational, optionally imaginary { begin: `\\b(${decimal})(\\.(${digits}))?([eE][+-]?(${digits})|r)?i?\\b` }, // explicit decimal/binary/octal/hexadecimal integer, // optionally rational and/or imaginary { begin: "\\b0[dD][0-9](_?[0-9])*r?i?\\b" }, { begin: "\\b0[bB][0-1](_?[0-1])*r?i?\\b" }, { begin: "\\b0[oO][0-7](_?[0-7])*r?i?\\b" }, { begin: "\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*r?i?\\b" }, // 0-prefixed implicit octal integer, optionally rational and/or imaginary { begin: "\\b0(_?[0-7])+r?i?\\b" } ] }; const PARAMS = { variants: [ { match: /\(\)/, }, { className: 'params', begin: /\(/, end: /(?=\))/, excludeBegin: true, endsParent: true, keywords: RUBY_KEYWORDS, } ] }; 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/, 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" } }; // CamelCase const CLASS_REFERENCE = { relevance: 0, match: CLASS_NAME_RE, scope: "title.class" }; const RUBY_DEFAULT_CONTAINS = [ STRING, CLASS_DEFINITION, INCLUDE_EXTEND, OBJECT_CREATION, UPPER_CASE_CONSTANT, CLASS_REFERENCE, METHOD_DEFINITION, { // swallow namespace qualifiers before symbols begin: hljs.IDENT_RE + '::' }, { className: 'symbol', begin: hljs.UNDERSCORE_IDENT_RE + '(!|\\?)?:', relevance: 0 }, { className: 'symbol', begin: ':(?!\\s)', contains: [ STRING, { begin: RUBY_METHOD_RE } ], relevance: 0 }, NUMBER, { // negative-look forward attempts to prevent false matches like: // @ident@ or $ident$ that might indicate this is not ruby at all className: "variable", begin: '(\\$\\W)|((\\$|@@?)(\\w+))(?=[^@$?])' + `(?![A-Za-z])(?![@$?'])` }, { 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 }, { // regexp container begin: '(' + hljs.RE_STARTERS_RE + '|unless)\\s*', keywords: 'unless', contains: [ { className: 'regexp', contains: [ hljs.BACKSLASH_ESCAPE, SUBST ], illegal: /\n/, variants: [ { begin: '/', end: '/[a-z]*' }, { begin: /%r\{/, end: /\}[a-z]*/ }, { begin: '%r\\(', end: '\\)[a-z]*' }, { begin: '%r!', end: '![a-z]*' }, { begin: '%r\\[', end: '\\][a-z]*' } ] } ].concat(IRB_OBJECT, COMMENT_MODES), relevance: 0 } ].concat(IRB_OBJECT, COMMENT_MODES); SUBST.contains = RUBY_DEFAULT_CONTAINS; PARAMS.contains = RUBY_DEFAULT_CONTAINS; // >> // ?> const SIMPLE_PROMPT = "[>?]>"; // irb(main):001:0> const DEFAULT_PROMPT = "[\\w#]+\\(\\w+\\):\\d+:\\d+[>*]"; const RVM_PROMPT = "(\\w+-)?\\d+\\.\\d+\\.\\d+(p\\d+)?[^\\d][^>]+>"; const IRB_DEFAULT = [ { begin: /^\s*=>/, starts: { end: '$', contains: RUBY_DEFAULT_CONTAINS } }, { className: 'meta.prompt', begin: '^(' + SIMPLE_PROMPT + "|" + DEFAULT_PROMPT + '|' + RVM_PROMPT + ')(?=[ ])', starts: { end: '$', keywords: RUBY_KEYWORDS, contains: RUBY_DEFAULT_CONTAINS } } ]; COMMENT_MODES.unshift(IRB_OBJECT); return { name: 'Ruby', aliases: [ 'rb', 'gemspec', 'podspec', 'thor', 'irb' ], keywords: RUBY_KEYWORDS, illegal: /\/\*/, contains: [ hljs.SHEBANG({ binary: "ruby" }) ] .concat(IRB_DEFAULT) .concat(COMMENT_MODES) .concat(RUBY_DEFAULT_CONTAINS) }; } export { ruby as default };