Mercurial
comparison .cms/lib/codemirror/mode/slim/slim.js @ 0:78edf6b517a0 draft
24.10
author | Coffee CMS <info@coffee-cms.ru> |
---|---|
date | Fri, 11 Oct 2024 22:40:23 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:78edf6b517a0 |
---|---|
1 // CodeMirror, copyright (c) by Marijn Haverbeke and others | |
2 // Distributed under an MIT license: https://codemirror.net/5/LICENSE | |
3 | |
4 // Slim Highlighting for CodeMirror copyright (c) HicknHack Software Gmbh | |
5 | |
6 (function(mod) { | |
7 if (typeof exports == "object" && typeof module == "object") // CommonJS | |
8 mod(require("../../lib/codemirror"), require("../htmlmixed/htmlmixed"), require("../ruby/ruby")); | |
9 else if (typeof define == "function" && define.amd) // AMD | |
10 define(["../../lib/codemirror", "../htmlmixed/htmlmixed", "../ruby/ruby"], mod); | |
11 else // Plain browser env | |
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", // no highlighting so far | |
28 coffee: "coffeescript", | |
29 asciidoc: "text/x-asciidoc", | |
30 markdown: "text/x-markdown", | |
31 textile: "text/x-textile", // no highlighting so far | |
32 creole: "text/x-creole", // no highlighting so far | |
33 wiki: "text/x-wiki", // no highlighting so far | |
34 mediawiki: "text/x-mediawiki", // no highlighting so far | |
35 rdoc: "text/x-rdoc", // no highlighting so far | |
36 builder: "text/x-builder", // no highlighting so far | |
37 nokogiri: "text/x-nokogiri", // no highlighting so far | |
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 // TODO: add multi line support | |
140 return function(stream, state) { | |
141 var ch = stream.peek(); | |
142 if (ch == endQuote && state.rubyState.tokenize.length == 1) { | |
143 // step out of ruby context as it seems to complete processing all the braces | |
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, // pipe + space | |
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 // should never happen, because of forward lookup | |
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(/^\/$/)) { // tag close hint | |
496 state.tokenize = slim; | |
497 return null; | |
498 } | |
499 if (stream.match(/^:/)) { // inline tag | |
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 // default to html mode | |
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 //indent: function(state) { | |
567 // return state.indented; | |
568 //} | |
569 }; | |
570 return mode; | |
571 }, "htmlmixed", "ruby"); | |
572 | |
573 CodeMirror.defineMIME("text/x-slim", "slim"); | |
574 CodeMirror.defineMIME("application/x-slim", "slim"); | |
575 }); |