Skip to content

Instantly share code, notes, and snippets.

@ericallam
Created December 14, 2012 21:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ericallam/4288764 to your computer and use it in GitHub Desktop.
Save ericallam/4288764 to your computer and use it in GitHub Desktop.
Sass mode for CodeMirror 3 (WIP)
// 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
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