2 LESS mode - http://www.lesscss.org/
3 Ported to CodeMirror by Peter Kroon <plakroon@gmail.com>
4 Report bugs/issues here: https://github.com/marijnh/CodeMirror/issues GitHub: @peterkroon
7 CodeMirror
.defineMode("less", function(config
) {
8 var indentUnit
= config
.indentUnit
, type
;
9 function ret(style
, tp
) {type
= tp
; return style
;}
11 var selectors
= /(^\:root$|^\:nth\-child$|^\:nth\-last\-child$|^\:nth\-of\-type$|^\:nth\-last\-of\-type$|^\:first\-child$|^\:last\-child$|^\:first\-of\-type$|^\:last\-of\-type$|^\:only\-child$|^\:only\-of\-type$|^\:empty$|^\:link|^\:visited$|^\:active$|^\:hover$|^\:focus$|^\:target$|^\:lang$|^\:enabled^\:disabled$|^\:checked$|^\:first\-line$|^\:first\-letter$|^\:before$|^\:after$|^\:not$|^\:required$|^\:invalid$)/;
13 function tokenBase(stream
, state
) {
14 var ch
= stream
.next();
16 if (ch
== "@") {stream
.eatWhile(/[\w\-]/); return ret("meta", stream
.current());}
17 else if (ch
== "/" && stream
.eat("*")) {
18 state
.tokenize
= tokenCComment
;
19 return tokenCComment(stream
, state
);
21 else if (ch
== "<" && stream
.eat("!")) {
22 state
.tokenize
= tokenSGMLComment
;
23 return tokenSGMLComment(stream
, state
);
25 else if (ch
== "=") ret(null, "compare");
26 else if (ch
== "|" && stream
.eat("=")) return ret(null, "compare");
27 else if (ch
== "\"" || ch
== "'") {
28 state
.tokenize
= tokenString(ch
);
29 return state
.tokenize(stream
, state
);
31 else if (ch
== "/") { // e.g.: .png will not be parsed as a class
33 state
.tokenize
= tokenSComment
;
34 return tokenSComment(stream
, state
);
36 if(type
== "string" || type
== "(")return ret("string", "string");
37 if(state
.stack
[state
.stack
.length
-1] != undefined)return ret(null, ch
);
38 stream
.eatWhile(/[\a-zA-Z0-9\-_.\s]/);
39 if( /\/|\)|#/.test(stream
.peek() || (stream
.eatSpace() && stream
.peek() == ")")) || stream
.eol() )return ret("string", "string"); // let url(/images/logo.png) without quotes return as string
43 stream
.match(/^\s*\w*/);
44 return ret("keyword", "important");
46 else if (/\d/.test(ch
)) {
47 stream
.eatWhile(/[\w.%]/);
48 return ret("number", "unit");
50 else if (/[,+<>*\/]/.test(ch
)) {
51 if(stream
.peek() == "=" || type
== "a")return ret("string", "string");
52 return ret(null, "select-op");
54 else if (/[;{}:\[\]()~\|]/.test(ch
)) {
56 stream
.eatWhile(/[a-z\\\-]/);
57 if( selectors
.test(stream
.current()) ){
58 return ret("tag", "tag");
59 }else if(stream
.peek() == ":"){//::-webkit-search-decoration
61 stream
.eatWhile(/[a-z\\\-]/);
62 if(stream
.current().match(/\:\:\-(o|ms|moz|webkit)\-/))return ret("string", "string");
63 if( selectors
.test(stream
.current().substring(1)) )return ret("tag", "tag");
69 if(type
== "r")return ret("string", "string");
75 if(type
== "(" || type
== "string")return ret("string", "string"); // allow url(../image.png)
76 stream
.eatWhile(/[\a-zA-Z0-9\-_]/);
77 if(stream
.peek() == " ")stream
.eatSpace();
78 if(stream
.peek() == ")")return ret("number", "unit");//rgba(0,0,0,.25);
79 return ret("tag", "tag");
82 //we don't eat white-space, we want the hex color and or id only
83 stream
.eatWhile(/[A-Za-z0-9]/);
84 //check if there is a proper hex color length e.g. #eee || #eeeEEE
85 if(stream
.current().length
== 4 || stream
.current().length
== 7){
86 if(stream
.current().match(/[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}/,false) != null){//is there a valid hex color value present in the current stream
87 //when not a valid hex value, parse as id
88 if(stream
.current().substring(1) != stream
.current().match(/[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}/,false))return ret("atom", "tag");
91 //when hex value declaration doesn't end with [;,] but is does with a slash/cc comment treat it as an id, just like the other hex values that don't end with[;,]
92 if( /[\/<>.(){!$%^&*_\-\\?=+\|#'~`]/.test(stream
.peek()) )return ret("atom", "tag");
93 //#time { color: #aaa }
94 else if(stream
.peek() == "}" )return ret("number", "unit");
95 //we have a valid hex color value, parse as id whenever an element/class is defined after the hex(id) value e.g. #eee aaa || #eee .aaa
96 else if( /[a-zA-Z\\]/.test(stream
.peek()) )return ret("atom", "tag");
97 //when a hex value is on the end of a line, parse as id
98 else if(stream
.eol())return ret("atom", "tag");
100 else return ret("number", "unit");
101 }else{//when not a valid hexvalue in the current stream e.g. #footer
102 stream
.eatWhile(/[\w\\\-]/);
103 return ret("atom", "tag");
105 }else{//when not a valid hexvalue length
106 stream
.eatWhile(/[\w\\\-]/);
107 return ret("atom", "tag");
110 else if (ch
== "&") {
111 stream
.eatWhile(/[\w\-]/);
112 return ret(null, ch
);
115 stream
.eatWhile(/[\w\\\-_%.{]/);
116 if(type
== "string"){
117 return ret("string", "string");
118 }else if(stream
.current().match(/(^http$|^https$)/) != null){
119 stream
.eatWhile(/[\w\\\-_%.{:\/]/);
120 return ret("string", "string");
121 }else if(stream
.peek() == "<" || stream
.peek() == ">"){
122 return ret("tag", "tag");
123 }else if( /\(/.test(stream
.peek()) ){
124 return ret(null, ch
);
125 }else if (stream
.peek() == "/" && state
.stack
[state
.stack
.length
-1] != undefined){ // url(dir/center/image.png)
126 return ret("string", "string");
127 }else if( stream
.current().match(/\-\d|\-.\d/) ){ // match e.g.: -5px -0.4 etc... only colorize the minus sign
128 //commment out these 2 comment if you want the minus sign to be parsed as null -500px
129 //stream.backUp(stream.current().length-1);
130 //return ret(null, ch); //console.log( stream.current() );
131 return ret("number", "unit");
132 }else if( /\/|[\s\)]/.test(stream
.peek() || stream
.eol() || (stream
.eatSpace() && stream
.peek() == "/")) && stream
.current().indexOf(".") !== -1){
133 if(stream
.current().substring(stream
.current().length
-1,stream
.current().length
) == "{"){
135 return ret("tag", "tag");
138 if( /[{<>.a-zA-Z\/]/.test(stream
.peek()) || stream
.eol() )return ret("tag", "tag"); // e.g. button.icon-plus
139 return ret("string", "string"); // let url(/images/logo.png) without quotes return as string
140 }else if( stream
.eol() || stream
.peek() == "[" || stream
.peek() == "#" || type
== "tag" ){
141 if(stream
.current().substring(stream
.current().length
-1,stream
.current().length
) == "{")stream
.backUp(1);
142 return ret("tag", "tag");
143 }else if(type
== "compare" || type
== "a" || type
== "("){
144 return ret("string", "string");
145 }else if(type
== "|" || stream
.current() == "-" || type
== "["){
146 return ret(null, ch
);
147 }else if(stream
.peek() == ":") {
149 var t_v
= stream
.peek() == ":" ? true : false;
151 var old_pos
= stream
.pos
;
152 var sc
= stream
.current().length
;
153 stream
.eatWhile(/[a-z\\\-]/);
154 var new_pos
= stream
.pos
;
155 if(stream
.current().substring(sc
-1).match(selectors
) != null){
156 stream
.backUp(new_pos
-(old_pos
-1));
157 return ret("tag", "tag");
158 } else stream
.backUp(new_pos
-(old_pos
-1));
162 if(t_v
)return ret("tag", "tag"); else return ret("variable", "variable");
164 return ret("variable", "variable");
169 function tokenSComment(stream
, state
) { // SComment = Slash comment
171 state
.tokenize
= tokenBase
;
172 return ret("comment", "comment");
175 function tokenCComment(stream
, state
) {
176 var maybeEnd
= false, ch
;
177 while ((ch
= stream
.next()) != null) {
178 if (maybeEnd
&& ch
== "/") {
179 state
.tokenize
= tokenBase
;
182 maybeEnd
= (ch
== "*");
184 return ret("comment", "comment");
187 function tokenSGMLComment(stream
, state
) {
189 while ((ch
= stream
.next()) != null) {
190 if (dashes
>= 2 && ch
== ">") {
191 state
.tokenize
= tokenBase
;
194 dashes
= (ch
== "-") ? dashes
+ 1 : 0;
196 return ret("comment", "comment");
199 function tokenString(quote
) {
200 return function(stream
, state
) {
201 var escaped
= false, ch
;
202 while ((ch
= stream
.next()) != null) {
203 if (ch
== quote
&& !escaped
)
205 escaped
= !escaped
&& ch
== "\\";
207 if (!escaped
) state
.tokenize
= tokenBase
;
208 return ret("string", "string");
213 startState: function(base
) {
214 return {tokenize
: tokenBase
,
215 baseIndent
: base
|| 0,
219 token: function(stream
, state
) {
220 if (stream
.eatSpace()) return null;
221 var style
= state
.tokenize(stream
, state
);
223 var context
= state
.stack
[state
.stack
.length
-1];
224 if (type
== "hash" && context
== "rule") style
= "atom";
225 else if (style
== "variable") {
226 if (context
== "rule") style
= null; //"tag"
227 else if (!context
|| context
== "@media{") {
228 style
= stream
.current() == "when" ? "variable" :
229 /[\s,|\s\)|\s]/.test(stream
.peek()) ? "tag" : type
;
233 if (context
== "rule" && /^[\{\};]$/.test(type
))
236 if (context
== "@media") state
.stack
[state
.stack
.length
-1] = "@media{";
237 else state
.stack
.push("{");
239 else if (type
== "}") state
.stack
.pop();
240 else if (type
== "@media") state
.stack
.push("@media");
241 else if (context
== "{" && type
!= "comment") state
.stack
.push("rule");
245 indent: function(state
, textAfter
) {
246 var n
= state
.stack
.length
;
247 if (/^\}/.test(textAfter
))
248 n
-= state
.stack
[state
.stack
.length
-1] == "rule" ? 2 : 1;
249 return state
.baseIndent
+ n
* indentUnit
;
256 CodeMirror
.defineMIME("text/x-less", "less");
257 if (!CodeMirror
.mimeModes
.hasOwnProperty("text/css"))
258 CodeMirror
.defineMIME("text/css", "less");