Commit | Line | Data |
---|---|---|
77b7b761 TD |
1 | /* |
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 | |
5 | */ | |
6 | ||
7 | CodeMirror.defineMode("less", function(config) { | |
8 | var indentUnit = config.indentUnit, type; | |
9 | function ret(style, tp) {type = tp; return style;} | |
10 | ||
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$)/; | |
12 | ||
13 | function tokenBase(stream, state) { | |
14 | var ch = stream.next(); | |
15 | ||
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); | |
20 | } | |
21 | else if (ch == "<" && stream.eat("!")) { | |
22 | state.tokenize = tokenSGMLComment; | |
23 | return tokenSGMLComment(stream, state); | |
24 | } | |
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); | |
30 | } | |
31 | else if (ch == "/") { // e.g.: .png will not be parsed as a class | |
32 | if(stream.eat("/")){ | |
33 | state.tokenize = tokenSComment; | |
34 | return tokenSComment(stream, state); | |
35 | }else{ | |
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 | |
40 | } | |
41 | } | |
42 | else if (ch == "!") { | |
43 | stream.match(/^\s*\w*/); | |
44 | return ret("keyword", "important"); | |
45 | } | |
46 | else if (/\d/.test(ch)) { | |
47 | stream.eatWhile(/[\w.%]/); | |
48 | return ret("number", "unit"); | |
49 | } | |
50 | else if (/[,+<>*\/]/.test(ch)) { | |
51 | if(stream.peek() == "=" || type == "a")return ret("string", "string"); | |
52 | return ret(null, "select-op"); | |
53 | } | |
54 | else if (/[;{}:\[\]()~\|]/.test(ch)) { | |
55 | if(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 | |
60 | stream.next(); | |
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"); | |
64 | return ret(null, ch); | |
65 | }else{ | |
66 | return ret(null, ch); | |
67 | } | |
68 | }else if(ch == "~"){ | |
69 | if(type == "r")return ret("string", "string"); | |
70 | }else{ | |
71 | return ret(null, ch); | |
72 | } | |
73 | } | |
74 | else if (ch == ".") { | |
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"); | |
80 | } | |
81 | else if (ch == "#") { | |
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"); | |
89 | //eat white-space | |
90 | stream.eatSpace(); | |
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"); | |
99 | //default | |
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"); | |
104 | } | |
105 | }else{//when not a valid hexvalue length | |
106 | stream.eatWhile(/[\w\\\-]/); | |
107 | return ret("atom", "tag"); | |
108 | } | |
109 | } | |
110 | else if (ch == "&") { | |
111 | stream.eatWhile(/[\w\-]/); | |
112 | return ret(null, ch); | |
113 | } | |
114 | else { | |
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) == "{"){ | |
134 | stream.backUp(1); | |
135 | return ret("tag", "tag"); | |
136 | }//end if | |
137 | stream.eatSpace(); | |
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() == ":") { | |
148 | stream.next(); | |
149 | var t_v = stream.peek() == ":" ? true : false; | |
150 | if(!t_v){ | |
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)); | |
159 | }else{ | |
160 | stream.backUp(1); | |
161 | } | |
162 | if(t_v)return ret("tag", "tag"); else return ret("variable", "variable"); | |
163 | }else{ | |
164 | return ret("variable", "variable"); | |
165 | } | |
166 | } | |
167 | } | |
168 | ||
169 | function tokenSComment(stream, state) { // SComment = Slash comment | |
170 | stream.skipToEnd(); | |
171 | state.tokenize = tokenBase; | |
172 | return ret("comment", "comment"); | |
173 | } | |
174 | ||
175 | function tokenCComment(stream, state) { | |
176 | var maybeEnd = false, ch; | |
177 | while ((ch = stream.next()) != null) { | |
178 | if (maybeEnd && ch == "/") { | |
179 | state.tokenize = tokenBase; | |
180 | break; | |
181 | } | |
182 | maybeEnd = (ch == "*"); | |
183 | } | |
184 | return ret("comment", "comment"); | |
185 | } | |
186 | ||
187 | function tokenSGMLComment(stream, state) { | |
188 | var dashes = 0, ch; | |
189 | while ((ch = stream.next()) != null) { | |
190 | if (dashes >= 2 && ch == ">") { | |
191 | state.tokenize = tokenBase; | |
192 | break; | |
193 | } | |
194 | dashes = (ch == "-") ? dashes + 1 : 0; | |
195 | } | |
196 | return ret("comment", "comment"); | |
197 | } | |
198 | ||
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) | |
204 | break; | |
205 | escaped = !escaped && ch == "\\"; | |
206 | } | |
207 | if (!escaped) state.tokenize = tokenBase; | |
208 | return ret("string", "string"); | |
209 | }; | |
210 | } | |
211 | ||
212 | return { | |
213 | startState: function(base) { | |
214 | return {tokenize: tokenBase, | |
215 | baseIndent: base || 0, | |
216 | stack: []}; | |
217 | }, | |
218 | ||
219 | token: function(stream, state) { | |
220 | if (stream.eatSpace()) return null; | |
221 | var style = state.tokenize(stream, state); | |
222 | ||
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; | |
230 | } | |
231 | } | |
232 | ||
233 | if (context == "rule" && /^[\{\};]$/.test(type)) | |
234 | state.stack.pop(); | |
235 | if (type == "{") { | |
236 | if (context == "@media") state.stack[state.stack.length-1] = "@media{"; | |
237 | else state.stack.push("{"); | |
238 | } | |
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"); | |
242 | return style; | |
243 | }, | |
244 | ||
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; | |
250 | }, | |
251 | ||
252 | electricChars: "}" | |
253 | }; | |
254 | }); | |
255 | ||
256 | CodeMirror.defineMIME("text/x-less", "less"); | |
257 | if (!CodeMirror.mimeModes.hasOwnProperty("text/css")) | |
258 | CodeMirror.defineMIME("text/css", "less"); |