mirror of https://github.com/gogits/gogs.git
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
575 lines
18 KiB
// 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 = 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.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 = CodeMirror.startState(htmlMode); |
|
var rubyState = CodeMirror.startState(rubyMode); |
|
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"); |
|
});
|
|
|