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