You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

575 lines
18 KiB

7 months ago
  1. // CodeMirror, copyright (c) by Marijn Haverbeke and others
  2. // Distributed under an MIT license: https://codemirror.net/5/LICENSE
  3. // Slim Highlighting for CodeMirror copyright (c) HicknHack Software Gmbh
  4. (function(mod) {
  5. if (typeof exports == "object" && typeof module == "object") // CommonJS
  6. mod(require("../../lib/codemirror"), require("../htmlmixed/htmlmixed"), require("../ruby/ruby"));
  7. else if (typeof define == "function" && define.amd) // AMD
  8. define(["../../lib/codemirror", "../htmlmixed/htmlmixed", "../ruby/ruby"], mod);
  9. else // Plain browser env
  10. mod(CodeMirror);
  11. })(function(CodeMirror) {
  12. "use strict";
  13. CodeMirror.defineMode("slim", function(config) {
  14. var htmlMode = CodeMirror.getMode(config, {name: "htmlmixed"});
  15. var rubyMode = CodeMirror.getMode(config, "ruby");
  16. var modes = { html: htmlMode, ruby: rubyMode };
  17. var embedded = {
  18. ruby: "ruby",
  19. javascript: "javascript",
  20. css: "text/css",
  21. sass: "text/x-sass",
  22. scss: "text/x-scss",
  23. less: "text/x-less",
  24. styl: "text/x-styl", // no highlighting so far
  25. coffee: "coffeescript",
  26. asciidoc: "text/x-asciidoc",
  27. markdown: "text/x-markdown",
  28. textile: "text/x-textile", // no highlighting so far
  29. creole: "text/x-creole", // no highlighting so far
  30. wiki: "text/x-wiki", // no highlighting so far
  31. mediawiki: "text/x-mediawiki", // no highlighting so far
  32. rdoc: "text/x-rdoc", // no highlighting so far
  33. builder: "text/x-builder", // no highlighting so far
  34. nokogiri: "text/x-nokogiri", // no highlighting so far
  35. erb: "application/x-erb"
  36. };
  37. var embeddedRegexp = function(map){
  38. var arr = [];
  39. for(var key in map) arr.push(key);
  40. return new RegExp("^("+arr.join('|')+"):");
  41. }(embedded);
  42. var styleMap = {
  43. "commentLine": "comment",
  44. "slimSwitch": "operator special",
  45. "slimTag": "tag",
  46. "slimId": "attribute def",
  47. "slimClass": "attribute qualifier",
  48. "slimAttribute": "attribute",
  49. "slimSubmode": "keyword special",
  50. "closeAttributeTag": null,
  51. "slimDoctype": null,
  52. "lineContinuation": null
  53. };
  54. var closing = {
  55. "{": "}",
  56. "[": "]",
  57. "(": ")"
  58. };
  59. var nameStartChar = "_a-zA-Z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD";
  60. var nameChar = nameStartChar + "\\-0-9\xB7\u0300-\u036F\u203F-\u2040";
  61. var nameRegexp = new RegExp("^[:"+nameStartChar+"](?::["+nameChar+"]|["+nameChar+"]*)");
  62. var attributeNameRegexp = new RegExp("^[:"+nameStartChar+"][:\\."+nameChar+"]*(?=\\s*=)");
  63. var wrappedAttributeNameRegexp = new RegExp("^[:"+nameStartChar+"][:\\."+nameChar+"]*");
  64. var classNameRegexp = /^\.-?[_a-zA-Z]+[\w\-]*/;
  65. var classIdRegexp = /^#[_a-zA-Z]+[\w\-]*/;
  66. function backup(pos, tokenize, style) {
  67. var restore = function(stream, state) {
  68. state.tokenize = tokenize;
  69. if (stream.pos < pos) {
  70. stream.pos = pos;
  71. return style;
  72. }
  73. return state.tokenize(stream, state);
  74. };
  75. return function(stream, state) {
  76. state.tokenize = restore;
  77. return tokenize(stream, state);
  78. };
  79. }
  80. function maybeBackup(stream, state, pat, offset, style) {
  81. var cur = stream.current();
  82. var idx = cur.search(pat);
  83. if (idx > -1) {
  84. state.tokenize = backup(stream.pos, state.tokenize, style);
  85. stream.backUp(cur.length - idx - offset);
  86. }
  87. return style;
  88. }
  89. function continueLine(state, column) {
  90. state.stack = {
  91. parent: state.stack,
  92. style: "continuation",
  93. indented: column,
  94. tokenize: state.line
  95. };
  96. state.line = state.tokenize;
  97. }
  98. function finishContinue(state) {
  99. if (state.line == state.tokenize) {
  100. state.line = state.stack.tokenize;
  101. state.stack = state.stack.parent;
  102. }
  103. }
  104. function lineContinuable(column, tokenize) {
  105. return function(stream, state) {
  106. finishContinue(state);
  107. if (stream.match(/^\\$/)) {
  108. continueLine(state, column);
  109. return "lineContinuation";
  110. }
  111. var style = tokenize(stream, state);
  112. if (stream.eol() && stream.current().match(/(?:^|[^\\])(?:\\\\)*\\$/)) {
  113. stream.backUp(1);
  114. }
  115. return style;
  116. };
  117. }
  118. function commaContinuable(column, tokenize) {
  119. return function(stream, state) {
  120. finishContinue(state);
  121. var style = tokenize(stream, state);
  122. if (stream.eol() && stream.current().match(/,$/)) {
  123. continueLine(state, column);
  124. }
  125. return style;
  126. };
  127. }
  128. function rubyInQuote(endQuote, tokenize) {
  129. // TODO: add multi line support
  130. return function(stream, state) {
  131. var ch = stream.peek();
  132. if (ch == endQuote && state.rubyState.tokenize.length == 1) {
  133. // step out of ruby context as it seems to complete processing all the braces
  134. stream.next();
  135. state.tokenize = tokenize;
  136. return "closeAttributeTag";
  137. } else {
  138. return ruby(stream, state);
  139. }
  140. };
  141. }
  142. function startRubySplat(tokenize) {
  143. var rubyState;
  144. var runSplat = function(stream, state) {
  145. if (state.rubyState.tokenize.length == 1 && !state.rubyState.context.prev) {
  146. stream.backUp(1);
  147. if (stream.eatSpace()) {
  148. state.rubyState = rubyState;
  149. state.tokenize = tokenize;
  150. return tokenize(stream, state);
  151. }
  152. stream.next();
  153. }
  154. return ruby(stream, state);
  155. };
  156. return function(stream, state) {
  157. rubyState = state.rubyState;
  158. state.rubyState = CodeMirror.startState(rubyMode);
  159. state.tokenize = runSplat;
  160. return ruby(stream, state);
  161. };
  162. }
  163. function ruby(stream, state) {
  164. return rubyMode.token(stream, state.rubyState);
  165. }
  166. function htmlLine(stream, state) {
  167. if (stream.match(/^\\$/)) {
  168. return "lineContinuation";
  169. }
  170. return html(stream, state);
  171. }
  172. function html(stream, state) {
  173. if (stream.match(/^#\{/)) {
  174. state.tokenize = rubyInQuote("}", state.tokenize);
  175. return null;
  176. }
  177. return maybeBackup(stream, state, /[^\\]#\{/, 1, htmlMode.token(stream, state.htmlState));
  178. }
  179. function startHtmlLine(lastTokenize) {
  180. return function(stream, state) {
  181. var style = htmlLine(stream, state);
  182. if (stream.eol()) state.tokenize = lastTokenize;
  183. return style;
  184. };
  185. }
  186. function startHtmlMode(stream, state, offset) {
  187. state.stack = {
  188. parent: state.stack,
  189. style: "html",
  190. indented: stream.column() + offset, // pipe + space
  191. tokenize: state.line
  192. };
  193. state.line = state.tokenize = html;
  194. return null;
  195. }
  196. function comment(stream, state) {
  197. stream.skipToEnd();
  198. return state.stack.style;
  199. }
  200. function commentMode(stream, state) {
  201. state.stack = {
  202. parent: state.stack,
  203. style: "comment",
  204. indented: state.indented + 1,
  205. tokenize: state.line
  206. };
  207. state.line = comment;
  208. return comment(stream, state);
  209. }
  210. function attributeWrapper(stream, state) {
  211. if (stream.eat(state.stack.endQuote)) {
  212. state.line = state.stack.line;
  213. state.tokenize = state.stack.tokenize;
  214. state.stack = state.stack.parent;
  215. return null;
  216. }
  217. if (stream.match(wrappedAttributeNameRegexp)) {
  218. state.tokenize = attributeWrapperAssign;
  219. return "slimAttribute";
  220. }
  221. stream.next();
  222. return null;
  223. }
  224. function attributeWrapperAssign(stream, state) {
  225. if (stream.match(/^==?/)) {
  226. state.tokenize = attributeWrapperValue;
  227. return null;
  228. }
  229. return attributeWrapper(stream, state);
  230. }
  231. function attributeWrapperValue(stream, state) {
  232. var ch = stream.peek();
  233. if (ch == '"' || ch == "\'") {
  234. state.tokenize = readQuoted(ch, "string", true, false, attributeWrapper);
  235. stream.next();
  236. return state.tokenize(stream, state);
  237. }
  238. if (ch == '[') {
  239. return startRubySplat(attributeWrapper)(stream, state);
  240. }
  241. if (stream.match(/^(true|false|nil)\b/)) {
  242. state.tokenize = attributeWrapper;
  243. return "keyword";
  244. }
  245. return startRubySplat(attributeWrapper)(stream, state);
  246. }
  247. function startAttributeWrapperMode(state, endQuote, tokenize) {
  248. state.stack = {
  249. parent: state.stack,
  250. style: "wrapper",
  251. indented: state.indented + 1,
  252. tokenize: tokenize,
  253. line: state.line,
  254. endQuote: endQuote
  255. };
  256. state.line = state.tokenize = attributeWrapper;
  257. return null;
  258. }
  259. function sub(stream, state) {
  260. if (stream.match(/^#\{/)) {
  261. state.tokenize = rubyInQuote("}", state.tokenize);
  262. return null;
  263. }
  264. var subStream = new CodeMirror.StringStream(stream.string.slice(state.stack.indented), stream.tabSize);
  265. subStream.pos = stream.pos - state.stack.indented;
  266. subStream.start = stream.start - state.stack.indented;
  267. subStream.lastColumnPos = stream.lastColumnPos - state.stack.indented;
  268. subStream.lastColumnValue = stream.lastColumnValue - state.stack.indented;
  269. var style = state.subMode.token(subStream, state.subState);
  270. stream.pos = subStream.pos + state.stack.indented;
  271. return style;
  272. }
  273. function firstSub(stream, state) {
  274. state.stack.indented = stream.column();
  275. state.line = state.tokenize = sub;
  276. return state.tokenize(stream, state);
  277. }
  278. function createMode(mode) {
  279. var query = embedded[mode];
  280. var spec = CodeMirror.mimeModes[query];
  281. if (spec) {
  282. return CodeMirror.getMode(config, spec);
  283. }
  284. var factory = CodeMirror.modes[query];
  285. if (factory) {
  286. return factory(config, {name: query});
  287. }
  288. return CodeMirror.getMode(config, "null");
  289. }
  290. function getMode(mode) {
  291. if (!modes.hasOwnProperty(mode)) {
  292. return modes[mode] = createMode(mode);
  293. }
  294. return modes[mode];
  295. }
  296. function startSubMode(mode, state) {
  297. var subMode = getMode(mode);
  298. var subState = CodeMirror.startState(subMode);
  299. state.subMode = subMode;
  300. state.subState = subState;
  301. state.stack = {
  302. parent: state.stack,
  303. style: "sub",
  304. indented: state.indented + 1,
  305. tokenize: state.line
  306. };
  307. state.line = state.tokenize = firstSub;
  308. return "slimSubmode";
  309. }
  310. function doctypeLine(stream, _state) {
  311. stream.skipToEnd();
  312. return "slimDoctype";
  313. }
  314. function startLine(stream, state) {
  315. var ch = stream.peek();
  316. if (ch == '<') {
  317. return (state.tokenize = startHtmlLine(state.tokenize))(stream, state);
  318. }
  319. if (stream.match(/^[|']/)) {
  320. return startHtmlMode(stream, state, 1);
  321. }
  322. if (stream.match(/^\/(!|\[\w+])?/)) {
  323. return commentMode(stream, state);
  324. }
  325. if (stream.match(/^(-|==?[<>]?)/)) {
  326. state.tokenize = lineContinuable(stream.column(), commaContinuable(stream.column(), ruby));
  327. return "slimSwitch";
  328. }
  329. if (stream.match(/^doctype\b/)) {
  330. state.tokenize = doctypeLine;
  331. return "keyword";
  332. }
  333. var m = stream.match(embeddedRegexp);
  334. if (m) {
  335. return startSubMode(m[1], state);
  336. }
  337. return slimTag(stream, state);
  338. }
  339. function slim(stream, state) {
  340. if (state.startOfLine) {
  341. return startLine(stream, state);
  342. }
  343. return slimTag(stream, state);
  344. }
  345. function slimTag(stream, state) {
  346. if (stream.eat('*')) {
  347. state.tokenize = startRubySplat(slimTagExtras);
  348. return null;
  349. }
  350. if (stream.match(nameRegexp)) {
  351. state.tokenize = slimTagExtras;
  352. return "slimTag";
  353. }
  354. return slimClass(stream, state);
  355. }
  356. function slimTagExtras(stream, state) {
  357. if (stream.match(/^(<>?|><?)/)) {
  358. state.tokenize = slimClass;
  359. return null;
  360. }
  361. return slimClass(stream, state);
  362. }
  363. function slimClass(stream, state) {
  364. if (stream.match(classIdRegexp)) {
  365. state.tokenize = slimClass;
  366. return "slimId";
  367. }
  368. if (stream.match(classNameRegexp)) {
  369. state.tokenize = slimClass;
  370. return "slimClass";
  371. }
  372. return slimAttribute(stream, state);
  373. }
  374. function slimAttribute(stream, state) {
  375. if (stream.match(/^([\[\{\(])/)) {
  376. return startAttributeWrapperMode(state, closing[RegExp.$1], slimAttribute);
  377. }
  378. if (stream.match(attributeNameRegexp)) {
  379. state.tokenize = slimAttributeAssign;
  380. return "slimAttribute";
  381. }
  382. if (stream.peek() == '*') {
  383. stream.next();
  384. state.tokenize = startRubySplat(slimContent);
  385. return null;
  386. }
  387. return slimContent(stream, state);
  388. }
  389. function slimAttributeAssign(stream, state) {
  390. if (stream.match(/^==?/)) {
  391. state.tokenize = slimAttributeValue;
  392. return null;
  393. }
  394. // should never happen, because of forward lookup
  395. return slimAttribute(stream, state);
  396. }
  397. function slimAttributeValue(stream, state) {
  398. var ch = stream.peek();
  399. if (ch == '"' || ch == "\'") {
  400. state.tokenize = readQuoted(ch, "string", true, false, slimAttribute);
  401. stream.next();
  402. return state.tokenize(stream, state);
  403. }
  404. if (ch == '[') {
  405. return startRubySplat(slimAttribute)(stream, state);
  406. }
  407. if (ch == ':') {
  408. return startRubySplat(slimAttributeSymbols)(stream, state);
  409. }
  410. if (stream.match(/^(true|false|nil)\b/)) {
  411. state.tokenize = slimAttribute;
  412. return "keyword";
  413. }
  414. return startRubySplat(slimAttribute)(stream, state);
  415. }
  416. function slimAttributeSymbols(stream, state) {
  417. stream.backUp(1);
  418. if (stream.match(/^[^\s],(?=:)/)) {
  419. state.tokenize = startRubySplat(slimAttributeSymbols);
  420. return null;
  421. }
  422. stream.next();
  423. return slimAttribute(stream, state);
  424. }
  425. function readQuoted(quote, style, embed, unescaped, nextTokenize) {
  426. return function(stream, state) {
  427. finishContinue(state);
  428. var fresh = stream.current().length == 0;
  429. if (stream.match(/^\\$/, fresh)) {
  430. if (!fresh) return style;
  431. continueLine(state, state.indented);
  432. return "lineContinuation";
  433. }
  434. if (stream.match(/^#\{/, fresh)) {
  435. if (!fresh) return style;
  436. state.tokenize = rubyInQuote("}", state.tokenize);
  437. return null;
  438. }
  439. var escaped = false, ch;
  440. while ((ch = stream.next()) != null) {
  441. if (ch == quote && (unescaped || !escaped)) {
  442. state.tokenize = nextTokenize;
  443. break;
  444. }
  445. if (embed && ch == "#" && !escaped) {
  446. if (stream.eat("{")) {
  447. stream.backUp(2);
  448. break;
  449. }
  450. }
  451. escaped = !escaped && ch == "\\";
  452. }
  453. if (stream.eol() && escaped) {
  454. stream.backUp(1);
  455. }
  456. return style;
  457. };
  458. }
  459. function slimContent(stream, state) {
  460. if (stream.match(/^==?/)) {
  461. state.tokenize = ruby;
  462. return "slimSwitch";
  463. }
  464. if (stream.match(/^\/$/)) { // tag close hint
  465. state.tokenize = slim;
  466. return null;
  467. }
  468. if (stream.match(/^:/)) { // inline tag
  469. state.tokenize = slimTag;
  470. return "slimSwitch";
  471. }
  472. startHtmlMode(stream, state, 0);
  473. return state.tokenize(stream, state);
  474. }
  475. var mode = {
  476. // default to html mode
  477. startState: function() {
  478. var htmlState = CodeMirror.startState(htmlMode);
  479. var rubyState = CodeMirror.startState(rubyMode);
  480. return {
  481. htmlState: htmlState,
  482. rubyState: rubyState,
  483. stack: null,
  484. last: null,
  485. tokenize: slim,
  486. line: slim,
  487. indented: 0
  488. };
  489. },
  490. copyState: function(state) {
  491. return {
  492. htmlState : CodeMirror.copyState(htmlMode, state.htmlState),
  493. rubyState: CodeMirror.copyState(rubyMode, state.rubyState),
  494. subMode: state.subMode,
  495. subState: state.subMode && CodeMirror.copyState(state.subMode, state.subState),
  496. stack: state.stack,
  497. last: state.last,
  498. tokenize: state.tokenize,
  499. line: state.line
  500. };
  501. },
  502. token: function(stream, state) {
  503. if (stream.sol()) {
  504. state.indented = stream.indentation();
  505. state.startOfLine = true;
  506. state.tokenize = state.line;
  507. while (state.stack && state.stack.indented > state.indented && state.last != "slimSubmode") {
  508. state.line = state.tokenize = state.stack.tokenize;
  509. state.stack = state.stack.parent;
  510. state.subMode = null;
  511. state.subState = null;
  512. }
  513. }
  514. if (stream.eatSpace()) return null;
  515. var style = state.tokenize(stream, state);
  516. state.startOfLine = false;
  517. if (style) state.last = style;
  518. return styleMap.hasOwnProperty(style) ? styleMap[style] : style;
  519. },
  520. blankLine: function(state) {
  521. if (state.subMode && state.subMode.blankLine) {
  522. return state.subMode.blankLine(state.subState);
  523. }
  524. },
  525. innerMode: function(state) {
  526. if (state.subMode) return {state: state.subState, mode: state.subMode};
  527. return {state: state, mode: mode};
  528. }
  529. //indent: function(state) {
  530. // return state.indented;
  531. //}
  532. };
  533. return mode;
  534. }, "htmlmixed", "ruby");
  535. CodeMirror.defineMIME("text/x-slim", "slim");
  536. CodeMirror.defineMIME("application/x-slim", "slim");
  537. });