CodeMirror.defineMode("sass", function(config) { var tokenRegexp = function(words){ return new RegExp("^" + words.join("|")); }; var keywords = ["true", "false", "null", "auto"]; var keywordsRegexp = new RegExp("^" + keywords.join("|")); var operators = ["\\(", "\\)", "=", ">", "<", "==", ">=", "<=", "\\+", "-", "\\!=", "/", "\\*", "%", "and", "or", "not"]; var opRegexp = tokenRegexp(operators); var pseudoElementsRegexp = /^::?[\w\-]+/; var urlTokens = function(stream, state){ var ch = stream.peek(); if (ch === ")"){ stream.next(); state.tokenizer = tokenBase; return "operator"; }else if (ch === "("){ stream.next(); stream.eatSpace(); return "operator"; }else if (ch === "'" || ch === '"'){ state.tokenizer = buildStringTokenizer(stream.next()); return "string"; }else{ state.tokenizer = buildStringTokenizer(")", false); return "string"; } }; var multilineComment = function(stream, state) { if (stream.skipTo("*/")){ stream.next(); stream.next(); state.tokenizer = tokenBase; }else { stream.next(); } return "comment"; }; var buildStringTokenizer = function(quote, greedy){ if(greedy == null){ greedy = true; } function stringTokenizer(stream, state){ var nextChar = stream.next(); var peekChar = stream.peek(); var previousChar = stream.string.charAt(stream.pos-2); var endingString = ((nextChar !== "\\" && peekChar === quote) || (nextChar === quote && previousChar !== "\\")); /* console.log("previousChar: " + previousChar); console.log("nextChar: " + nextChar); console.log("peekChar: " + peekChar); console.log("ending: " + endingString); */ if (endingString){ if (nextChar !== quote && greedy) { stream.next(); } state.tokenizer = tokenBase; return "string"; }else if (nextChar === "#" && peekChar === "{"){ state.tokenizer = buildInterpolationTokenizer(stringTokenizer); stream.next(); return "operator"; }else { return "string"; } } return stringTokenizer; }; var buildInterpolationTokenizer = function(currentTokenizer){ return function(stream, state){ if (stream.peek() === "}"){ stream.next(); state.tokenizer = currentTokenizer; return "operator"; }else{ return tokenBase(stream, state); } }; }; var indent = function(state){ if (state.indentCount == 0){ state.indentCount++; var lastScopeOffset = state.scopes[0].offset; var currentOffset = lastScopeOffset + config.indentUnit; state.scopes.unshift({ offset:currentOffset }); } }; var dedent = function(state){ if (state.scopes.length == 1) { return; } state.scopes.shift(); }; var tokenBase = function(stream, state) { var ch = stream.peek(); // Single line Comment if (stream.match('//')) { stream.skipToEnd(); return "comment"; } // Multiline Comment if (stream.match('/*')){ state.tokenizer = multilineComment; return state.tokenizer(stream, state); } // Interpolation if (stream.match('#{')){ state.tokenizer = buildInterpolationTokenizer(tokenBase); return "operator"; } if (ch === "."){ stream.next(); // Match class selectors if (stream.match(/^[\w-]+/)){ indent(state); return "atom"; }else if (stream.peek() === "#"){ indent(state); return "atom"; }else{ return "operator"; } } if (ch === "#"){ stream.next(); // Hex numbers if (stream.match(/[0-9a-fA-F]{6}|[0-9a-fA-F]{3}/)){ return "number"; } // ID selectors if (stream.match(/^[\w-]+/)){ indent(state); return "atom"; } if (stream.peek() === "#"){ indent(state); return "atom"; } } // Numbers if (stream.match(/^-?[0-9\.]+/)){ return "number"; } // Units if (stream.match(/^(px|em|in)\b/)){ return "unit"; } if (stream.match(keywordsRegexp)){ return "keyword"; } if (stream.match(/^url/) && stream.peek() === "("){ state.tokenizer = urlTokens; return "atom"; } // Variables if (ch === "$"){ stream.next(); stream.eatWhile(/[\w-]/); if (stream.peek() === ":"){ stream.next(); return "variable-2"; }else{ return "variable-3"; } } if (ch === "!"){ stream.next(); if (stream.match(/^[\w]+/)){ return "keyword"; } return "operator"; } if (ch === "="){ stream.next(); // Match shortcut mixin definition if (stream.match(/^[\w-]+/)){ indent(state); return "meta"; }else { return "operator"; } } if (ch === "+"){ stream.next(); // Match shortcut mixin definition if (stream.match(/^[\w-]+/)){ return "variable-3"; }else { return "operator"; } } // Indent Directives if (stream.match(/^@(else if|if|media|else|for|each|while|mixin|function)/)){ indent(state); return "meta"; } // Other Directives if (ch === "@"){ stream.next(); stream.eatWhile(/[\w-]/); return "meta"; } // Strings if (ch === '"' || ch === "'"){ stream.next(); state.tokenizer = buildStringTokenizer(ch); return "string"; } // Pseudo element selectors if (ch == ':' && stream.match(pseudoElementsRegexp)){ return "keyword"; } // atoms if (stream.eatWhile(/[\w-&]/)){ // matches a property definition if (stream.peek() === ":" && !stream.match(pseudoElementsRegexp, false)) return "property"; else return "atom"; } if (stream.match(opRegexp)){ return "operator"; } // If we haven't returned by now, we move 1 character // and return an error stream.next(); return null; }; var tokenLexer = function(stream, state) { if (stream.sol()){ state.indentCount = 0; } var style = state.tokenizer(stream, state); var current = stream.current(); if (current === "@return"){ dedent(state); } if (style === "atom"){ indent(state); } if (style !== null){ var startOfToken = stream.pos - current.length; var withCurrentIndent = startOfToken + (config.indentUnit * state.indentCount); var newScopes = []; for (var i = 0; i < state.scopes.length; i++){ var scope = state.scopes[i]; if (scope.offset <= withCurrentIndent){ newScopes.push(scope); } } state.scopes = newScopes; } return style; }; return { startState: function() { return { tokenizer: tokenBase, scopes: [{offset: 0, type: 'sass'}], definedVars: [], definedMixins: [] }; }, token: function(stream, state) { var style = tokenLexer(stream, state); state.lastToken = { style: style, content: stream.current() }; return style; }, indent: function(state) { return state.scopes[0].offset; } }; }); CodeMirror.defineMIME("text/x-sass", "sass");