Created
December 14, 2012 21:22
-
-
Save ericallam/4288764 to your computer and use it in GitHub Desktop.
Sass mode for CodeMirror 3 (WIP)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This is a comment | |
/* This comment is | |
* several lines long. | |
* since it uses the CSS comment syntax, | |
* it will appear in the CSS output. */ | |
@import "foo.scss" | |
@import "foo.css" | |
@import "foo" screen | |
@import "http://foo.com/bar" | |
// TODO: should just treat these like ruless | |
@import url(foo) | |
@import "rounded-corners", "text-shadow" | |
$family: unquote("Droid+Sans") | |
@import url("http://fonts.googleapis.com/css?family=\#{$family}") | |
$grid-width: 40px | |
$gutter-width: 10px | |
@function grid-width($n) | |
@return $n * $grid-width + ($n - 1) * $gutter-width | |
=apply-to-ie6-only | |
* html | |
@content | |
+apply-to-ie6-only | |
#logo | |
background-image: url(/logo.gif) | |
#sidebar | |
width: grid-width(5) | |
@mixin box-shadow($shadows...) | |
-moz-box-shadow: $shadows | |
-webkit-box-shadow: $shadows | |
box-shadow: $shadows | |
.shadows | |
@include box-shadow(0px 4px 5px #666, 2px 6px 10px #999) | |
@for $i from 1 through 3 | |
.item-#{$i} | |
width: 2em * $i | |
@each $animal in puma, sea-slug, egret, salamander | |
.#{$animal}-icon | |
background-image: url('/images/#{$animal}.png') | |
@while $i > 0 | |
.item-#{$i} | |
width: 2em * $i | |
$i: $i - 2 | |
$type: monster | |
@mixin compound | |
@include highlighted-background | |
@include header-text | |
@mixin highlighted-background | |
background-color: #fc0 | |
@mixin header-text | |
font-size: 20px | |
@mixin sexy-border($color, $width) | |
border: | |
color: $color | |
width: $width | |
style: dashed | |
h1 | |
@include sexy-border($color: blue, $width: 2in) | |
p | |
@include sexy-border(blue, 1in) | |
p | |
@if $type == ocean | |
color: blue | |
@else if $type == matador | |
color: red | |
@else if $type == monster | |
color: green | |
@else | |
color: black | |
$blue: #3bbfce | |
$margin: 16px | |
$red: #999 | |
$name: foo | |
$attr: border | |
$content: "Second content?" !default | |
// TODO operator | |
@debug 10em + 12em | |
@media screen | |
.sidebar | |
@media (orientation: landscape) | |
width: 500px | |
.content-navigation | |
border-color: $blue | |
color: darken($blue, 9%) | |
@import "example" | |
background: url(http://google.com/image.png) | |
.border | |
padding: $margin / 2 | |
border-color: $blue | |
#thanks | |
color: darken($red, 10%) !optional | |
font 'Hello "World"' | |
background-image: url("/image/hacked.png") | |
// TODO make % signs work | |
#context a%extreme | |
color: "hello" | |
// Fix this incorrect indention | |
// Can have more than one selector | |
.main p.whatever | |
.another-class | |
border: 10px | |
color: hsl($hue: 0, $saturation: 100%, $lightness: 50%) | |
@extend .error | |
.hoverlink | |
@extend a:hover | |
a:hover | |
text-decoration: underline | |
p.#{$name} | |
#{$attr}-color: blue |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
CodeMirror.defineMode("sass", function(config) { | |
indentUnit = config.indentUnit; | |
var tokenRegexp = function(words){ | |
return new RegExp("^" + words.join("|")); | |
} | |
var controlDirectives = ["@for", "@while", "@if", | |
"@each", "@mixin", "@function", | |
"@else", "@else if"]; | |
var controlRegexp = new RegExp(controlDirectives.join("|")); | |
var tags = ["&", "a","abbr","acronym","address","applet","area","article","aside","audio","b","base","basefont","bdi","bdo","big","blockquote","body","br","button","canvas","caption","cite","code","col","colgroup","command","datalist","dd","del","details","dfn","dir","div","dl","dt","em","embed","fieldset","figcaption","figure","font","footer","form","frame","frameset","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","iframe","img","input","ins","keygen","kbd","label","legend","li","link","map","mark","menu","meta","meter","nav","noframes","noscript","object","ol","optgroup","option","output","p","param","pre","progress","q","rp","rt","ruby","s","samp","script","section","select","small","source","span","strike","strong","style","sub","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","title","tr","track","tt","u","ul","var","video","wbr"]; | |
var keywords = ["true", "false", "null", "auto"]; | |
var keywordsRegexp = new RegExp("^" + keywords.join("|")) | |
var operators = ["\\(", "\\)", "=", ">", "<", "==", ">=", "<=", "\\+", "-", "\\!=", "/", "\\*", "%", "and", "or", "not"] | |
var opRegexp = tokenRegexp(operators); | |
function htmlTag(val){ | |
for(var i=0; i<tags.length; i++){ | |
if(val === tags[i]){ | |
return true; | |
} | |
} | |
} | |
var pseudoElements = [':first-line', ':hover', ':first-letter', ':active', ':visited', ':before', ':after', ':link', ':focus', ':first-child', ':lang']; | |
var pseudoElementsRegexp = new RegExp("^(" + pseudoElements.join("\\b|") + ")"); | |
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) { | |
var ch; | |
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 escaped = false, ch; | |
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(stream, state){ | |
if (state.indentCount == 0){ | |
state.indentCount++; | |
var lastScopeOffset = state.scopes[0].offset; | |
var currentOffset = lastScopeOffset + indentUnit; | |
state.scopes.unshift({ offset:currentOffset }); | |
} | |
} | |
var dedent = function(stream, 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(stream, state); | |
return "atom"; | |
}else if (stream.peek() === "#"){ | |
indent(stream, 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(stream, state); | |
return "atom"; | |
} | |
if (stream.peek() === "#"){ | |
indent(stream, state); | |
return "atom"; | |
} | |
} | |
// Numbers | |
if (stream.match(/^-?[0-9\.]+/)){ | |
return "number"; | |
} | |
// Units | |
if (stream.match(/^(px|em|in)/)){ | |
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(stream, 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(stream, 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 (stream.match(pseudoElementsRegexp)){ | |
return "keyword"; | |
} | |
// atoms | |
if (stream.eatWhile(/[\w-&]/)){ | |
var current = stream.current(); | |
// matches a property definition | |
if (stream.peek() === ":"){ | |
// if this is an html tag and it has a pseudo selector, then it's an atom | |
if (htmlTag(current) && stream.match(pseudoElementsRegexp, false)){ | |
return "atom"; | |
}else{ | |
stream.next(); | |
return "property"; | |
} | |
} | |
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 'error'; | |
} | |
var tokenLexer = function(stream, state) { | |
if (stream.sol()){ | |
state.indentCount = 0; | |
} | |
var style = state.tokenizer(stream, state); | |
var current = stream.current(); | |
// console.log(style + "[" + stream.pos + "]" + current); | |
if (current === "@return"){ | |
dedent(stream, state); | |
} | |
if (style === "atom" && htmlTag(current)){ | |
indent(stream, state); | |
} | |
if (style !== "error"){ | |
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() } | |
// console.log("token[" + stream.current() + "][" + style + "]"); | |
return style; | |
}, | |
indent: function(state) { | |
var indent = state.scopes[0].offset; | |
// console.log("indent[" + indent + "]"); | |
return state.scopes[0].offset; | |
} | |
}; | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment