// CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: http://codemirror.net/LICENSE // Slim Highlighting for CodeMirror copyright (c) HicknHack Software Gmbh (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS mod(require("../../lib/codemirror"), require("../htmlmixed/htmlmixed"), require("../ruby/ruby")); else if (typeof define == "function" && define.amd) // AMD define(["../../lib/codemirror", "../htmlmixed/htmlmixed", "../ruby/ruby"], mod); else // Plain browser env mod(CodeMirror); })(function(CodeMirror) { "use strict"; CodeMirror.defineMode("slim", function(config) { var htmlMode = CodeMirror.getMode(config, {name: "htmlmixed"}); var rubyMode = CodeMirror.getMode(config, "ruby"); var modes = { html: htmlMode, ruby: rubyMode }; var embedded = { ruby: "ruby", javascript: "javascript", css: "text/css", sass: "text/x-sass", scss: "text/x-scss", less: "text/x-less", styl: "text/x-styl", // no highlighting so far coffee: "coffeescript", asciidoc: "text/x-asciidoc", markdown: "text/x-markdown", textile: "text/x-textile", // no highlighting so far creole: "text/x-creole", // no highlighting so far wiki: "text/x-wiki", // no highlighting so far mediawiki: "text/x-mediawiki", // no highlighting so far rdoc: "text/x-rdoc", // no highlighting so far builder: "text/x-builder", // no highlighting so far nokogiri: "text/x-nokogiri", // no highlighting so far erb: "application/x-erb" }; var embeddedRegexp = function(map){ var arr = []; for(var key in map) arr.push(key); return new RegExp("^("+arr.join('|')+"):"); }(embedded); var styleMap = { "commentLine": "comment", "slimSwitch": "operator special", "slimTag": "tag", "slimId": "attribute def", "slimClass": "attribute qualifier", "slimAttribute": "attribute", "slimSubmode": "keyword special", "closeAttributeTag": null, "slimDoctype": null, "lineContinuation": null }; var closing = { "{": "}", "[": "]", "(": ")" }; 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"; var nameChar = nameStartChar + "\\-0-9\xB7\u0300-\u036F\u203F-\u2040"; var nameRegexp = new RegExp("^[:"+nameStartChar+"](?::["+nameChar+"]|["+nameChar+"]*)"); var attributeNameRegexp = new RegExp("^[:"+nameStartChar+"][:\\."+nameChar+"]*(?=\\s*=)"); var wrappedAttributeNameRegexp = new RegExp("^[:"+nameStartChar+"][:\\."+nameChar+"]*"); var classNameRegexp = /^\.-?[_a-zA-Z]+[\w\-]*/; var classIdRegexp = /^#[_a-zA-Z]+[\w\-]*/; function backup(pos, tokenize, style) { var restore = function(stream, state) { state.tokenize = tokenize; if (stream.pos < pos) { stream.pos = pos; return style; } return state.tokenize(stream, state); }; return function(stream, state) { state.tokenize = restore; return tokenize(stream, state); }; } function maybeBackup(stream, state, pat, offset, style) { var cur = stream.current(); var idx = cur.search(pat); if (idx > -1) { state.tokenize = backup(stream.pos, state.tokenize, style); stream.backUp(cur.length - idx - offset); } return style; } function continueLine(state, column) { state.stack = { parent: state.stack, style: "continuation", indented: column, tokenize: state.line }; state.line = state.tokenize; } function finishContinue(state) { if (state.line == state.tokenize) { state.line = state.stack.tokenize; state.stack = state.stack.parent; } } function lineContinuable(column, tokenize) { return function(stream, state) { finishContinue(state); if (stream.match(/^\\$/)) { continueLine(state, column); return "lineContinuation"; } var style = tokenize(stream, state); if (stream.eol() && stream.current().match(/(?:^|[^\\])(?:\\\\)*\\$/)) { stream.backUp(1); } return style; }; } function commaContinuable(column, tokenize) { return function(stream, state) { finishContinue(state); var style = tokenize(stream, state); if (stream.eol() && stream.current().match(/,$/)) { continueLine(state, column); } return style; }; } function rubyInQuote(endQuote, tokenize) { // TODO: add multi line support return function(stream, state) { var ch = stream.peek(); if (ch == endQuote && state.rubyState.tokenize.length == 1) { // step out of ruby context as it seems to complete processing all the braces stream.next(); state.tokenize = tokenize; return "closeAttributeTag"; } else { return ruby(stream, state); } }; } function startRubySplat(tokenize) { var rubyState; var runSplat = function(stream, state) { if (state.rubyState.tokenize.length == 1 && !state.rubyState.context.prev) { stream.backUp(1); if (stream.eatSpace()) { state.rubyState = rubyState; state.tokenize = tokenize; return tokenize(stream, state); } stream.next(); } return ruby(stream, state); }; return function(stream, state) { rubyState = state.rubyState; state.rubyState = rubyMode.startState(); state.tokenize = runSplat; return ruby(stream, state); }; } function ruby(stream, state) { return rubyMode.token(stream, state.rubyState); } function htmlLine(stream, state) { if (stream.match(/^\\$/)) { return "lineContinuation"; } return html(stream, state); } function html(stream, state) { if (stream.match(/^#\{/)) { state.tokenize = rubyInQuote("}", state.tokenize); return null; } return maybeBackup(stream, state, /[^\\]#\{/, 1, htmlMode.token(stream, state.htmlState)); } function startHtmlLine(lastTokenize) { return function(stream, state) { var style = htmlLine(stream, state); if (stream.eol()) state.tokenize = lastTokenize; return style; }; } function startHtmlMode(stream, state, offset) { state.stack = { parent: state.stack, style: "html", indented: stream.column() + offset, // pipe + space tokenize: state.line }; state.line = state.tokenize = html; return null; } function comment(stream, state) { stream.skipToEnd(); return state.stack.style; } function commentMode(stream, state) { state.stack = { parent: state.stack, style: "comment", indented: state.indented + 1, tokenize: state.line }; state.line = comment; return comment(stream, state); } function attributeWrapper(stream, state) { if (stream.eat(state.stack.endQuote)) { state.line = state.stack.line; state.tokenize = state.stack.tokenize; state.stack = state.stack.parent; return null; } if (stream.match(wrappedAttributeNameRegexp)) { state.tokenize = attributeWrapperAssign; return "slimAttribute"; } stream.next(); return null; } function attributeWrapperAssign(stream, state) { if (stream.match(/^==?/)) { state.tokenize = attributeWrapperValue; return null; } return attributeWrapper(stream, state); } function attributeWrapperValue(stream, state) { var ch = stream.peek(); if (ch == '"' || ch == "\'") { state.tokenize = readQuoted(ch, "string", true, false, attributeWrapper); stream.next(); return state.tokenize(stream, state); } if (ch == '[') { return startRubySplat(attributeWrapper)(stream, state); } if (stream.match(/^(true|false|nil)\b/)) { state.tokenize = attributeWrapper; return "keyword"; } return startRubySplat(attributeWrapper)(stream, state); } function startAttributeWrapperMode(state, endQuote, tokenize) { state.stack = { parent: state.stack, style: "wrapper", indented: state.indented + 1, tokenize: tokenize, line: state.line, endQuote: endQuote }; state.line = state.tokenize = attributeWrapper; return null; } function sub(stream, state) { if (stream.match(/^#\{/)) { state.tokenize = rubyInQuote("}", state.tokenize); return null; } var subStream = new CodeMirror.StringStream(stream.string.slice(state.stack.indented), stream.tabSize); subStream.pos = stream.pos - state.stack.indented; subStream.start = stream.start - state.stack.indented; subStream.lastColumnPos = stream.lastColumnPos - state.stack.indented; subStream.lastColumnValue = stream.lastColumnValue - state.stack.indented; var style = state.subMode.token(subStream, state.subState); stream.pos = subStream.pos + state.stack.indented; return style; } function firstSub(stream, state) { state.stack.indented = stream.column(); state.line = state.tokenize = sub; return state.tokenize(stream, state); } function createMode(mode) { var query = embedded[mode]; var spec = CodeMirror.mimeModes[query]; if (spec) { return CodeMirror.getMode(config, spec); } var factory = CodeMirror.modes[query]; if (factory) { return factory(config, {name: query}); } return CodeMirror.getMode(config, "null"); } function getMode(mode) { if (!modes.hasOwnProperty(mode)) { return modes[mode] = createMode(mode); } return modes[mode]; } function startSubMode(mode, state) { var subMode = getMode(mode); var subState = subMode.startState && subMode.startState(); state.subMode = subMode; state.subState = subState; state.stack = { parent: state.stack, style: "sub", indented: state.indented + 1, tokenize: state.line }; state.line = state.tokenize = firstSub; return "slimSubmode"; } function doctypeLine(stream, _state) { stream.skipToEnd(); return "slimDoctype"; } function startLine(stream, state) { var ch = stream.peek(); if (ch == '<') { return (state.tokenize = startHtmlLine(state.tokenize))(stream, state); } if (stream.match(/^[|']/)) { return startHtmlMode(stream, state, 1); } if (stream.match(/^\/(!|\[\w+])?/)) { return commentMode(stream, state); } if (stream.match(/^(-|==?[<>]?)/)) { state.tokenize = lineContinuable(stream.column(), commaContinuable(stream.column(), ruby)); return "slimSwitch"; } if (stream.match(/^doctype\b/)) { state.tokenize = doctypeLine; return "keyword"; } var m = stream.match(embeddedRegexp); if (m) { return startSubMode(m[1], state); } return slimTag(stream, state); } function slim(stream, state) { if (state.startOfLine) { return startLine(stream, state); } return slimTag(stream, state); } function slimTag(stream, state) { if (stream.eat('*')) { state.tokenize = startRubySplat(slimTagExtras); return null; } if (stream.match(nameRegexp)) { state.tokenize = slimTagExtras; return "slimTag"; } return slimClass(stream, state); } function slimTagExtras(stream, state) { if (stream.match(/^(<>?|><?)/)) { state.tokenize = slimClass; return null; } return slimClass(stream, state); } function slimClass(stream, state) { if (stream.match(classIdRegexp)) { state.tokenize = slimClass; return "slimId"; } if (stream.match(classNameRegexp)) { state.tokenize = slimClass; return "slimClass"; } return slimAttribute(stream, state); } function slimAttribute(stream, state) { if (stream.match(/^([\[\{\(])/)) { return startAttributeWrapperMode(state, closing[RegExp.$1], slimAttribute); } if (stream.match(attributeNameRegexp)) { state.tokenize = slimAttributeAssign; return "slimAttribute"; } if (stream.peek() == '*') { stream.next(); state.tokenize = startRubySplat(slimContent); return null; } return slimContent(stream, state); } function slimAttributeAssign(stream, state) { if (stream.match(/^==?/)) { state.tokenize = slimAttributeValue; return null; } // should never happen, because of forward lookup return slimAttribute(stream, state); } function slimAttributeValue(stream, state) { var ch = stream.peek(); if (ch == '"' || ch == "\'") { state.tokenize = readQuoted(ch, "string", true, false, slimAttribute); stream.next(); return state.tokenize(stream, state); } if (ch == '[') { return startRubySplat(slimAttribute)(stream, state); } if (ch == ':') { return startRubySplat(slimAttributeSymbols)(stream, state); } if (stream.match(/^(true|false|nil)\b/)) { state.tokenize = slimAttribute; return "keyword"; } return startRubySplat(slimAttribute)(stream, state); } function slimAttributeSymbols(stream, state) { stream.backUp(1); if (stream.match(/^[^\s],(?=:)/)) { state.tokenize = startRubySplat(slimAttributeSymbols); return null; } stream.next(); return slimAttribute(stream, state); } function readQuoted(quote, style, embed, unescaped, nextTokenize) { return function(stream, state) { finishContinue(state); var fresh = stream.current().length == 0; if (stream.match(/^\\$/, fresh)) { if (!fresh) return style; continueLine(state, state.indented); return "lineContinuation"; } if (stream.match(/^#\{/, fresh)) { if (!fresh) return style; state.tokenize = rubyInQuote("}", state.tokenize); return null; } var escaped = false, ch; while ((ch = stream.next()) != null) { if (ch == quote && (unescaped || !escaped)) { state.tokenize = nextTokenize; break; } if (embed && ch == "#" && !escaped) { if (stream.eat("{")) { stream.backUp(2); break; } } escaped = !escaped && ch == "\\"; } if (stream.eol() && escaped) { stream.backUp(1); } return style; }; } function slimContent(stream, state) { if (stream.match(/^==?/)) { state.tokenize = ruby; return "slimSwitch"; } if (stream.match(/^\/$/)) { // tag close hint state.tokenize = slim; return null; } if (stream.match(/^:/)) { // inline tag state.tokenize = slimTag; return "slimSwitch"; } startHtmlMode(stream, state, 0); return state.tokenize(stream, state); } var mode = { // default to html mode startState: function() { var htmlState = htmlMode.startState(); var rubyState = rubyMode.startState(); return { htmlState: htmlState, rubyState: rubyState, stack: null, last: null, tokenize: slim, line: slim, indented: 0 }; }, copyState: function(state) { return { htmlState : CodeMirror.copyState(htmlMode, state.htmlState), rubyState: CodeMirror.copyState(rubyMode, state.rubyState), subMode: state.subMode, subState: state.subMode && CodeMirror.copyState(state.subMode, state.subState), stack: state.stack, last: state.last, tokenize: state.tokenize, line: state.line }; }, token: function(stream, state) { if (stream.sol()) { state.indented = stream.indentation(); state.startOfLine = true; state.tokenize = state.line; while (state.stack && state.stack.indented > state.indented && state.last != "slimSubmode") { state.line = state.tokenize = state.stack.tokenize; state.stack = state.stack.parent; state.subMode = null; state.subState = null; } } if (stream.eatSpace()) return null; var style = state.tokenize(stream, state); state.startOfLine = false; if (style) state.last = style; return styleMap.hasOwnProperty(style) ? styleMap[style] : style; }, blankLine: function(state) { if (state.subMode && state.subMode.blankLine) { return state.subMode.blankLine(state.subState); } }, innerMode: function(state) { if (state.subMode) return {state: state.subState, mode: state.subMode}; return {state: state, mode: mode}; } //indent: function(state) { // return state.indented; //} }; return mode; }, "htmlmixed", "ruby"); CodeMirror.defineMIME("text/x-slim", "slim"); CodeMirror.defineMIME("application/x-slim", "slim"); });