// CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: https://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) // Plain browser env else 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 = CodeMirror.startState(rubyMode) 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 = CodeMirror.startState(subMode) 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.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') })