ruby.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. /*
  2. Language: Ruby
  3. Description: Ruby is a dynamic, open source programming language with a focus on simplicity and productivity.
  4. Website: https://www.ruby-lang.org/
  5. Author: Anton Kovalyov <anton@kovalyov.net>
  6. Contributors: Peter Leonov <gojpeg@yandex.ru>, Vasily Polovnyov <vast@whiteants.net>, Loren Segal <lsegal@soen.ca>, Pascal Hurni <phi@ruby-reactive.org>, Cedric Sohrauer <sohrauer@googlemail.com>
  7. Category: common, scripting
  8. */
  9. function ruby(hljs) {
  10. const regex = hljs.regex;
  11. const RUBY_METHOD_RE = '([a-zA-Z_]\\w*[!?=]?|[-+~]@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?)';
  12. // TODO: move concepts like CAMEL_CASE into `modes.js`
  13. const CLASS_NAME_RE = regex.either(
  14. /\b([A-Z]+[a-z0-9]+)+/,
  15. // ends in caps
  16. /\b([A-Z]+[a-z0-9]+)+[A-Z]+/,
  17. )
  18. ;
  19. const CLASS_NAME_WITH_NAMESPACE_RE = regex.concat(CLASS_NAME_RE, /(::\w+)*/);
  20. // very popular ruby built-ins that one might even assume
  21. // are actual keywords (despite that not being the case)
  22. const PSEUDO_KWS = [
  23. "include",
  24. "extend",
  25. "prepend",
  26. "public",
  27. "private",
  28. "protected",
  29. "raise",
  30. "throw"
  31. ];
  32. const RUBY_KEYWORDS = {
  33. "variable.constant": [
  34. "__FILE__",
  35. "__LINE__",
  36. "__ENCODING__"
  37. ],
  38. "variable.language": [
  39. "self",
  40. "super",
  41. ],
  42. keyword: [
  43. "alias",
  44. "and",
  45. "begin",
  46. "BEGIN",
  47. "break",
  48. "case",
  49. "class",
  50. "defined",
  51. "do",
  52. "else",
  53. "elsif",
  54. "end",
  55. "END",
  56. "ensure",
  57. "for",
  58. "if",
  59. "in",
  60. "module",
  61. "next",
  62. "not",
  63. "or",
  64. "redo",
  65. "require",
  66. "rescue",
  67. "retry",
  68. "return",
  69. "then",
  70. "undef",
  71. "unless",
  72. "until",
  73. "when",
  74. "while",
  75. "yield",
  76. ...PSEUDO_KWS
  77. ],
  78. built_in: [
  79. "proc",
  80. "lambda",
  81. "attr_accessor",
  82. "attr_reader",
  83. "attr_writer",
  84. "define_method",
  85. "private_constant",
  86. "module_function"
  87. ],
  88. literal: [
  89. "true",
  90. "false",
  91. "nil"
  92. ]
  93. };
  94. const YARDOCTAG = {
  95. className: 'doctag',
  96. begin: '@[A-Za-z]+'
  97. };
  98. const IRB_OBJECT = {
  99. begin: '#<',
  100. end: '>'
  101. };
  102. const COMMENT_MODES = [
  103. hljs.COMMENT(
  104. '#',
  105. '$',
  106. { contains: [ YARDOCTAG ] }
  107. ),
  108. hljs.COMMENT(
  109. '^=begin',
  110. '^=end',
  111. {
  112. contains: [ YARDOCTAG ],
  113. relevance: 10
  114. }
  115. ),
  116. hljs.COMMENT('^__END__', hljs.MATCH_NOTHING_RE)
  117. ];
  118. const SUBST = {
  119. className: 'subst',
  120. begin: /#\{/,
  121. end: /\}/,
  122. keywords: RUBY_KEYWORDS
  123. };
  124. const STRING = {
  125. className: 'string',
  126. contains: [
  127. hljs.BACKSLASH_ESCAPE,
  128. SUBST
  129. ],
  130. variants: [
  131. {
  132. begin: /'/,
  133. end: /'/
  134. },
  135. {
  136. begin: /"/,
  137. end: /"/
  138. },
  139. {
  140. begin: /`/,
  141. end: /`/
  142. },
  143. {
  144. begin: /%[qQwWx]?\(/,
  145. end: /\)/
  146. },
  147. {
  148. begin: /%[qQwWx]?\[/,
  149. end: /\]/
  150. },
  151. {
  152. begin: /%[qQwWx]?\{/,
  153. end: /\}/
  154. },
  155. {
  156. begin: /%[qQwWx]?</,
  157. end: />/
  158. },
  159. {
  160. begin: /%[qQwWx]?\//,
  161. end: /\//
  162. },
  163. {
  164. begin: /%[qQwWx]?%/,
  165. end: /%/
  166. },
  167. {
  168. begin: /%[qQwWx]?-/,
  169. end: /-/
  170. },
  171. {
  172. begin: /%[qQwWx]?\|/,
  173. end: /\|/
  174. },
  175. // in the following expressions, \B in the beginning suppresses recognition of ?-sequences
  176. // where ? is the last character of a preceding identifier, as in: `func?4`
  177. { begin: /\B\?(\\\d{1,3})/ },
  178. { begin: /\B\?(\\x[A-Fa-f0-9]{1,2})/ },
  179. { begin: /\B\?(\\u\{?[A-Fa-f0-9]{1,6}\}?)/ },
  180. { begin: /\B\?(\\M-\\C-|\\M-\\c|\\c\\M-|\\M-|\\C-\\M-)[\x20-\x7e]/ },
  181. { begin: /\B\?\\(c|C-)[\x20-\x7e]/ },
  182. { begin: /\B\?\\?\S/ },
  183. // heredocs
  184. {
  185. // this guard makes sure that we have an entire heredoc and not a false
  186. // positive (auto-detect, etc.)
  187. begin: regex.concat(
  188. /<<[-~]?'?/,
  189. regex.lookahead(/(\w+)(?=\W)[^\n]*\n(?:[^\n]*\n)*?\s*\1\b/)
  190. ),
  191. contains: [
  192. hljs.END_SAME_AS_BEGIN({
  193. begin: /(\w+)/,
  194. end: /(\w+)/,
  195. contains: [
  196. hljs.BACKSLASH_ESCAPE,
  197. SUBST
  198. ]
  199. })
  200. ]
  201. }
  202. ]
  203. };
  204. // Ruby syntax is underdocumented, but this grammar seems to be accurate
  205. // as of version 2.7.2 (confirmed with (irb and `Ripper.sexp(...)`)
  206. // https://docs.ruby-lang.org/en/2.7.0/doc/syntax/literals_rdoc.html#label-Numbers
  207. const decimal = '[1-9](_?[0-9])*|0';
  208. const digits = '[0-9](_?[0-9])*';
  209. const NUMBER = {
  210. className: 'number',
  211. relevance: 0,
  212. variants: [
  213. // decimal integer/float, optionally exponential or rational, optionally imaginary
  214. { begin: `\\b(${decimal})(\\.(${digits}))?([eE][+-]?(${digits})|r)?i?\\b` },
  215. // explicit decimal/binary/octal/hexadecimal integer,
  216. // optionally rational and/or imaginary
  217. { begin: "\\b0[dD][0-9](_?[0-9])*r?i?\\b" },
  218. { begin: "\\b0[bB][0-1](_?[0-1])*r?i?\\b" },
  219. { begin: "\\b0[oO][0-7](_?[0-7])*r?i?\\b" },
  220. { begin: "\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*r?i?\\b" },
  221. // 0-prefixed implicit octal integer, optionally rational and/or imaginary
  222. { begin: "\\b0(_?[0-7])+r?i?\\b" }
  223. ]
  224. };
  225. const PARAMS = {
  226. variants: [
  227. {
  228. match: /\(\)/,
  229. },
  230. {
  231. className: 'params',
  232. begin: /\(/,
  233. end: /(?=\))/,
  234. excludeBegin: true,
  235. endsParent: true,
  236. keywords: RUBY_KEYWORDS,
  237. }
  238. ]
  239. };
  240. const INCLUDE_EXTEND = {
  241. match: [
  242. /(include|extend)\s+/,
  243. CLASS_NAME_WITH_NAMESPACE_RE
  244. ],
  245. scope: {
  246. 2: "title.class"
  247. },
  248. keywords: RUBY_KEYWORDS
  249. };
  250. const CLASS_DEFINITION = {
  251. variants: [
  252. {
  253. match: [
  254. /class\s+/,
  255. CLASS_NAME_WITH_NAMESPACE_RE,
  256. /\s+<\s+/,
  257. CLASS_NAME_WITH_NAMESPACE_RE
  258. ]
  259. },
  260. {
  261. match: [
  262. /\b(class|module)\s+/,
  263. CLASS_NAME_WITH_NAMESPACE_RE
  264. ]
  265. }
  266. ],
  267. scope: {
  268. 2: "title.class",
  269. 4: "title.class.inherited"
  270. },
  271. keywords: RUBY_KEYWORDS
  272. };
  273. const UPPER_CASE_CONSTANT = {
  274. relevance: 0,
  275. match: /\b[A-Z][A-Z_0-9]+\b/,
  276. className: "variable.constant"
  277. };
  278. const METHOD_DEFINITION = {
  279. match: [
  280. /def/, /\s+/,
  281. RUBY_METHOD_RE
  282. ],
  283. scope: {
  284. 1: "keyword",
  285. 3: "title.function"
  286. },
  287. contains: [
  288. PARAMS
  289. ]
  290. };
  291. const OBJECT_CREATION = {
  292. relevance: 0,
  293. match: [
  294. CLASS_NAME_WITH_NAMESPACE_RE,
  295. /\.new[. (]/
  296. ],
  297. scope: {
  298. 1: "title.class"
  299. }
  300. };
  301. // CamelCase
  302. const CLASS_REFERENCE = {
  303. relevance: 0,
  304. match: CLASS_NAME_RE,
  305. scope: "title.class"
  306. };
  307. const RUBY_DEFAULT_CONTAINS = [
  308. STRING,
  309. CLASS_DEFINITION,
  310. INCLUDE_EXTEND,
  311. OBJECT_CREATION,
  312. UPPER_CASE_CONSTANT,
  313. CLASS_REFERENCE,
  314. METHOD_DEFINITION,
  315. {
  316. // swallow namespace qualifiers before symbols
  317. begin: hljs.IDENT_RE + '::' },
  318. {
  319. className: 'symbol',
  320. begin: hljs.UNDERSCORE_IDENT_RE + '(!|\\?)?:',
  321. relevance: 0
  322. },
  323. {
  324. className: 'symbol',
  325. begin: ':(?!\\s)',
  326. contains: [
  327. STRING,
  328. { begin: RUBY_METHOD_RE }
  329. ],
  330. relevance: 0
  331. },
  332. NUMBER,
  333. {
  334. // negative-look forward attempts to prevent false matches like:
  335. // @ident@ or $ident$ that might indicate this is not ruby at all
  336. className: "variable",
  337. begin: '(\\$\\W)|((\\$|@@?)(\\w+))(?=[^@$?])' + `(?![A-Za-z])(?![@$?'])`
  338. },
  339. {
  340. className: 'params',
  341. begin: /\|/,
  342. end: /\|/,
  343. excludeBegin: true,
  344. excludeEnd: true,
  345. relevance: 0, // this could be a lot of things (in other languages) other than params
  346. keywords: RUBY_KEYWORDS
  347. },
  348. { // regexp container
  349. begin: '(' + hljs.RE_STARTERS_RE + '|unless)\\s*',
  350. keywords: 'unless',
  351. contains: [
  352. {
  353. className: 'regexp',
  354. contains: [
  355. hljs.BACKSLASH_ESCAPE,
  356. SUBST
  357. ],
  358. illegal: /\n/,
  359. variants: [
  360. {
  361. begin: '/',
  362. end: '/[a-z]*'
  363. },
  364. {
  365. begin: /%r\{/,
  366. end: /\}[a-z]*/
  367. },
  368. {
  369. begin: '%r\\(',
  370. end: '\\)[a-z]*'
  371. },
  372. {
  373. begin: '%r!',
  374. end: '![a-z]*'
  375. },
  376. {
  377. begin: '%r\\[',
  378. end: '\\][a-z]*'
  379. }
  380. ]
  381. }
  382. ].concat(IRB_OBJECT, COMMENT_MODES),
  383. relevance: 0
  384. }
  385. ].concat(IRB_OBJECT, COMMENT_MODES);
  386. SUBST.contains = RUBY_DEFAULT_CONTAINS;
  387. PARAMS.contains = RUBY_DEFAULT_CONTAINS;
  388. // >>
  389. // ?>
  390. const SIMPLE_PROMPT = "[>?]>";
  391. // irb(main):001:0>
  392. const DEFAULT_PROMPT = "[\\w#]+\\(\\w+\\):\\d+:\\d+[>*]";
  393. const RVM_PROMPT = "(\\w+-)?\\d+\\.\\d+\\.\\d+(p\\d+)?[^\\d][^>]+>";
  394. const IRB_DEFAULT = [
  395. {
  396. begin: /^\s*=>/,
  397. starts: {
  398. end: '$',
  399. contains: RUBY_DEFAULT_CONTAINS
  400. }
  401. },
  402. {
  403. className: 'meta.prompt',
  404. begin: '^(' + SIMPLE_PROMPT + "|" + DEFAULT_PROMPT + '|' + RVM_PROMPT + ')(?=[ ])',
  405. starts: {
  406. end: '$',
  407. keywords: RUBY_KEYWORDS,
  408. contains: RUBY_DEFAULT_CONTAINS
  409. }
  410. }
  411. ];
  412. COMMENT_MODES.unshift(IRB_OBJECT);
  413. return {
  414. name: 'Ruby',
  415. aliases: [
  416. 'rb',
  417. 'gemspec',
  418. 'podspec',
  419. 'thor',
  420. 'irb'
  421. ],
  422. keywords: RUBY_KEYWORDS,
  423. illegal: /\/\*/,
  424. contains: [ hljs.SHEBANG({ binary: "ruby" }) ]
  425. .concat(IRB_DEFAULT)
  426. .concat(COMMENT_MODES)
  427. .concat(RUBY_DEFAULT_CONTAINS)
  428. };
  429. }
  430. export { ruby as default };