Skip to content

Commit 6a28fd6

Browse files
Eric Allammarijnh
authored andcommitted
Add a Sass mode
1 parent dbb72fe commit 6a28fd6

File tree

1 file changed

+360
-0
lines changed

1 file changed

+360
-0
lines changed

mode/sass/sass.js

Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
CodeMirror.defineMode("sass", function(config) {
2+
indentUnit = config.indentUnit;
3+
4+
var tokenRegexp = function(words){
5+
return new RegExp("^" + words.join("|"));
6+
}
7+
8+
var controlDirectives = ["@for", "@while", "@if",
9+
"@each", "@mixin", "@function",
10+
"@else", "@else if"];
11+
12+
var controlRegexp = new RegExp(controlDirectives.join("|"));
13+
14+
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"];
15+
var keywords = ["true", "false", "null", "auto"];
16+
var keywordsRegexp = new RegExp("^" + keywords.join("|"))
17+
18+
var operators = ["\\(", "\\)", "=", ">", "<", "==", ">=", "<=", "\\+", "-", "\\!=", "/", "\\*", "%", "and", "or", "not"]
19+
var opRegexp = tokenRegexp(operators);
20+
21+
function htmlTag(val){
22+
for(var i=0; i<tags.length; i++){
23+
if(val === tags[i]){
24+
return true;
25+
}
26+
}
27+
}
28+
29+
30+
var pseudoElements = [':first-line', ':hover', ':first-letter', ':active', ':visited', ':before', ':after', ':link', ':focus', ':first-child', ':lang'];
31+
var pseudoElementsRegexp = new RegExp("^(" + pseudoElements.join("\\b|") + ")");
32+
33+
var urlTokens = function(stream, state){
34+
var ch = stream.peek();
35+
36+
if (ch === ")"){
37+
stream.next();
38+
state.tokenizer = tokenBase;
39+
return "operator";
40+
}else if (ch === "("){
41+
stream.next();
42+
stream.eatSpace();
43+
44+
return "operator";
45+
}else if (ch === "'" || ch === '"'){
46+
state.tokenizer = buildStringTokenizer(stream.next());
47+
return "string"
48+
}else{
49+
state.tokenizer = buildStringTokenizer(")", false);
50+
return "string";
51+
}
52+
}
53+
var multilineComment = function(stream, state) {
54+
var ch;
55+
56+
if (stream.skipTo("*/")){
57+
stream.next();
58+
stream.next();
59+
state.tokenizer = tokenBase;
60+
}else {
61+
stream.next();
62+
}
63+
64+
return "comment";
65+
}
66+
67+
var buildStringTokenizer = function(quote, greedy){
68+
if(greedy == null){ greedy = true }
69+
70+
function stringTokenizer(stream, state){
71+
var escaped = false, ch;
72+
73+
var nextChar = stream.next();
74+
var peekChar = stream.peek();
75+
var previousChar = stream.string.charAt(stream.pos-2);
76+
77+
var endingString = ((nextChar !== "\\" && peekChar === quote) || (nextChar === quote && previousChar !== "\\"));
78+
79+
/*
80+
console.log("previousChar: " + previousChar);
81+
console.log("nextChar: " + nextChar);
82+
console.log("peekChar: " + peekChar);
83+
console.log("ending: " + endingString);
84+
*/
85+
86+
if (endingString){
87+
if (nextChar !== quote && greedy) { stream.next(); }
88+
state.tokenizer = tokenBase;
89+
return "string"
90+
}else if (nextChar === "#" && peekChar === "{"){
91+
state.tokenizer = buildInterpolationTokenizer(stringTokenizer);
92+
stream.next();
93+
return "operator";
94+
}else {
95+
return "string";
96+
}
97+
}
98+
99+
return stringTokenizer;
100+
}
101+
102+
var buildInterpolationTokenizer = function(currentTokenizer){
103+
return function(stream, state){
104+
if (stream.peek() === "}"){
105+
stream.next();
106+
state.tokenizer = currentTokenizer;
107+
return "operator";
108+
}else{
109+
return tokenBase(stream, state);
110+
}
111+
}
112+
}
113+
114+
var indent = function(stream, state){
115+
if (state.indentCount == 0){
116+
state.indentCount++;
117+
var lastScopeOffset = state.scopes[0].offset;
118+
var currentOffset = lastScopeOffset + indentUnit;
119+
state.scopes.unshift({ offset:currentOffset });
120+
}
121+
}
122+
123+
var dedent = function(stream, state){
124+
if (state.scopes.length == 1) { return; }
125+
126+
state.scopes.shift();
127+
}
128+
129+
var tokenBase = function(stream, state) {
130+
var ch = stream.peek();
131+
132+
// Single line Comment
133+
if (stream.match('//')) {
134+
stream.skipToEnd();
135+
return "comment";
136+
}
137+
138+
// Multiline Comment
139+
if (stream.match('/*')){
140+
state.tokenizer = multilineComment;
141+
return state.tokenizer(stream, state);
142+
}
143+
144+
// Interpolation
145+
if (stream.match('#{')){
146+
state.tokenizer = buildInterpolationTokenizer(tokenBase);
147+
return "operator";
148+
}
149+
150+
if (ch === "."){
151+
stream.next();
152+
153+
// Match class selectors
154+
if (stream.match(/^[\w-]+/)){
155+
indent(stream, state);
156+
return "atom";
157+
}else if (stream.peek() === "#"){
158+
indent(stream, state);
159+
return "atom";
160+
}else{
161+
return "operator";
162+
}
163+
}
164+
165+
if (ch === "#"){
166+
stream.next();
167+
168+
// Hex numbers
169+
if (stream.match(/[0-9a-fA-F]{6}|[0-9a-fA-F]{3}/)){
170+
return "number";
171+
}
172+
173+
// ID selectors
174+
if (stream.match(/^[\w-]+/)){
175+
indent(stream, state);
176+
return "atom";
177+
}
178+
179+
if (stream.peek() === "#"){
180+
indent(stream, state);
181+
return "atom";
182+
}
183+
}
184+
185+
// Numbers
186+
if (stream.match(/^-?[0-9\.]+/)){
187+
return "number";
188+
}
189+
190+
// Units
191+
if (stream.match(/^(px|em|in)\b/)){
192+
return "unit";
193+
}
194+
195+
if (stream.match(keywordsRegexp)){
196+
return "keyword";
197+
}
198+
199+
if (stream.match(/^url/) && stream.peek() === "("){
200+
state.tokenizer = urlTokens;
201+
return "atom";
202+
}
203+
204+
// Variables
205+
if (ch === "$"){
206+
stream.next();
207+
stream.eatWhile(/[\w-]/);
208+
209+
if (stream.peek() === ":"){
210+
stream.next();
211+
return "variable-2";
212+
}else{
213+
return "variable-3";
214+
}
215+
}
216+
217+
if (ch === "!"){
218+
stream.next();
219+
220+
if (stream.match(/^[\w]+/)){
221+
return "keyword";
222+
}
223+
224+
return "operator";
225+
}
226+
227+
if (ch === "="){
228+
stream.next();
229+
230+
// Match shortcut mixin definition
231+
if (stream.match(/^[\w-]+/)){
232+
indent(stream, state);
233+
return "meta";
234+
}else {
235+
return "operator";
236+
}
237+
}
238+
239+
if (ch === "+"){
240+
stream.next();
241+
242+
// Match shortcut mixin definition
243+
if (stream.match(/^[\w-]+/)){
244+
return "variable-3";
245+
}else {
246+
return "operator";
247+
}
248+
}
249+
250+
// Indent Directives
251+
if (stream.match(/^@(else if|if|media|else|for|each|while|mixin|function)/)){
252+
indent(stream, state);
253+
return "meta";
254+
}
255+
256+
// Other Directives
257+
if (ch === "@"){
258+
stream.next();
259+
stream.eatWhile(/[\w-]/);
260+
return "meta";
261+
}
262+
263+
// Strings
264+
if (ch === '"' || ch === "'"){
265+
stream.next();
266+
state.tokenizer = buildStringTokenizer(ch);
267+
return "string";
268+
}
269+
270+
// Pseudo element selectors
271+
if (stream.match(pseudoElementsRegexp)){
272+
return "keyword";
273+
}
274+
275+
// atoms
276+
if (stream.eatWhile(/[\w-&]/)){
277+
278+
var current = stream.current();
279+
// matches a property definition
280+
if (stream.peek() === ":"){
281+
// if this is an html tag and it has a pseudo selector, then it's an atom
282+
if (htmlTag(current) && stream.match(pseudoElementsRegexp, false)){
283+
return "atom";
284+
}else{
285+
stream.next();
286+
return "property";
287+
}
288+
}
289+
return "atom";
290+
}
291+
292+
if (stream.match(opRegexp)){
293+
return "operator";
294+
}
295+
296+
// If we haven't returned by now, we move 1 character
297+
// and return an error
298+
stream.next();
299+
return 'error';
300+
}
301+
302+
var tokenLexer = function(stream, state) {
303+
if (stream.sol()){
304+
state.indentCount = 0;
305+
}
306+
var style = state.tokenizer(stream, state);
307+
var current = stream.current();
308+
309+
if (current === "@return"){
310+
dedent(stream, state);
311+
}
312+
313+
if (style === "atom" && htmlTag(current)){
314+
indent(stream, state);
315+
}
316+
317+
if (style !== "error"){
318+
var startOfToken = stream.pos - current.length;
319+
var withCurrentIndent = startOfToken + (config.indentUnit * state.indentCount);
320+
321+
var newScopes = [];
322+
323+
for (var i = 0; i < state.scopes.length; i++){
324+
var scope = state.scopes[i];
325+
326+
if (scope.offset <= withCurrentIndent){
327+
newScopes.push(scope);
328+
}
329+
}
330+
331+
state.scopes = newScopes;
332+
}
333+
334+
335+
return style;
336+
}
337+
338+
return {
339+
startState: function() {
340+
return {
341+
tokenizer: tokenBase,
342+
scopes: [{offset: 0, type: 'sass'}],
343+
definedVars: [],
344+
definedMixins: [],
345+
};
346+
},
347+
token: function(stream, state) {
348+
var style = tokenLexer(stream, state);
349+
350+
state.lastToken = { style: style, content: stream.current() }
351+
352+
return style;
353+
},
354+
355+
indent: function(state) {
356+
var indent = state.scopes[0].offset;
357+
return state.scopes[0].offset;
358+
}
359+
};
360+
});

0 commit comments

Comments
 (0)