less.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  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
  5. GitHub: @peterkroon
  6. */
  7. CodeMirror.defineMode("less", function(config) {
  8. var indentUnit = config.indentUnit, type;
  9. function ret(style, tp) {type = tp; return style;}
  10. 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$)/;
  11. function tokenBase(stream, state) {
  12. var ch = stream.next();
  13. if (ch == "@") {stream.eatWhile(/[\w\-]/); return ret("meta", stream.current());}
  14. else if (ch == "/" && stream.eat("*")) {
  15. state.tokenize = tokenCComment;
  16. return tokenCComment(stream, state);
  17. } else if (ch == "<" && stream.eat("!")) {
  18. state.tokenize = tokenSGMLComment;
  19. return tokenSGMLComment(stream, state);
  20. } else if (ch == "=") ret(null, "compare");
  21. else if (ch == "|" && stream.eat("=")) return ret(null, "compare");
  22. else if (ch == "\"" || ch == "'") {
  23. state.tokenize = tokenString(ch);
  24. return state.tokenize(stream, state);
  25. } else if (ch == "/") { // e.g.: .png will not be parsed as a class
  26. if(stream.eat("/")){
  27. state.tokenize = tokenSComment;
  28. return tokenSComment(stream, state);
  29. } else {
  30. if(type == "string" || type == "(") return ret("string", "string");
  31. if(state.stack[state.stack.length-1] != undefined) return ret(null, ch);
  32. stream.eatWhile(/[\a-zA-Z0-9\-_.\s]/);
  33. if( /\/|\)|#/.test(stream.peek() || (stream.eatSpace() && stream.peek() == ")")) || stream.eol() )return ret("string", "string"); // let url(/images/logo.png) without quotes return as string
  34. }
  35. } else if (ch == "!") {
  36. stream.match(/^\s*\w*/);
  37. return ret("keyword", "important");
  38. } else if (/\d/.test(ch)) {
  39. stream.eatWhile(/[\w.%]/);
  40. return ret("number", "unit");
  41. } else if (/[,+<>*\/]/.test(ch)) {
  42. if(stream.peek() == "=" || type == "a")return ret("string", "string");
  43. if(ch === ",")return ret(null, ch);
  44. return ret(null, "select-op");
  45. } else if (/[;{}:\[\]()~\|]/.test(ch)) {
  46. if(ch == ":"){
  47. stream.eatWhile(/[a-z\\\-]/);
  48. if( selectors.test(stream.current()) ){
  49. return ret("tag", "tag");
  50. } else if(stream.peek() == ":"){//::-webkit-search-decoration
  51. stream.next();
  52. stream.eatWhile(/[a-z\\\-]/);
  53. if(stream.current().match(/\:\:\-(o|ms|moz|webkit)\-/))return ret("string", "string");
  54. if( selectors.test(stream.current().substring(1)) )return ret("tag", "tag");
  55. return ret(null, ch);
  56. } else {
  57. return ret(null, ch);
  58. }
  59. } else if(ch == "~"){
  60. if(type == "r")return ret("string", "string");
  61. } else {
  62. return ret(null, ch);
  63. }
  64. } else if (ch == ".") {
  65. if(type == "(" || type == "string")return ret("string", "string"); // allow url(../image.png)
  66. stream.eatWhile(/[\a-zA-Z0-9\-_]/);
  67. if(stream.peek() == " ")stream.eatSpace();
  68. if(stream.peek() == ")")return ret("number", "unit");//rgba(0,0,0,.25);
  69. return ret("tag", "tag");
  70. } else if (ch == "#") {
  71. //we don't eat white-space, we want the hex color and or id only
  72. stream.eatWhile(/[A-Za-z0-9]/);
  73. //check if there is a proper hex color length e.g. #eee || #eeeEEE
  74. if(stream.current().length == 4 || stream.current().length == 7){
  75. 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
  76. //when not a valid hex value, parse as id
  77. if(stream.current().substring(1) != stream.current().match(/[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}/,false))return ret("atom", "tag");
  78. //eat white-space
  79. stream.eatSpace();
  80. //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[;,]
  81. if( /[\/<>.(){!$%^&*_\-\\?=+\|#'~`]/.test(stream.peek()) )return ret("atom", "tag");
  82. //#time { color: #aaa }
  83. else if(stream.peek() == "}" )return ret("number", "unit");
  84. //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
  85. else if( /[a-zA-Z\\]/.test(stream.peek()) )return ret("atom", "tag");
  86. //when a hex value is on the end of a line, parse as id
  87. else if(stream.eol())return ret("atom", "tag");
  88. //default
  89. else return ret("number", "unit");
  90. } else {//when not a valid hexvalue in the current stream e.g. #footer
  91. stream.eatWhile(/[\w\\\-]/);
  92. return ret("atom", "tag");
  93. }
  94. } else {//when not a valid hexvalue length
  95. stream.eatWhile(/[\w\\\-]/);
  96. return ret("atom", "tag");
  97. }
  98. } else if (ch == "&") {
  99. stream.eatWhile(/[\w\-]/);
  100. return ret(null, ch);
  101. } else {
  102. stream.eatWhile(/[\w\\\-_%.{]/);
  103. if(type == "string"){
  104. return ret("string", "string");
  105. } else if(stream.current().match(/(^http$|^https$)/) != null){
  106. stream.eatWhile(/[\w\\\-_%.{:\/]/);
  107. return ret("string", "string");
  108. } else if(stream.peek() == "<" || stream.peek() == ">" || stream.peek() == "+"){
  109. return ret("tag", "tag");
  110. } else if( /\(/.test(stream.peek()) ){
  111. return ret(null, ch);
  112. } else if (stream.peek() == "/" && state.stack[state.stack.length-1] != undefined){ // url(dir/center/image.png)
  113. return ret("string", "string");
  114. } else if( stream.current().match(/\-\d|\-.\d/) ){ // match e.g.: -5px -0.4 etc... only colorize the minus sign
  115. //commment out these 2 comment if you want the minus sign to be parsed as null -500px
  116. //stream.backUp(stream.current().length-1);
  117. //return ret(null, ch);
  118. return ret("number", "unit");
  119. } else if( /\/|[\s\)]/.test(stream.peek() || stream.eol() || (stream.eatSpace() && stream.peek() == "/")) && stream.current().indexOf(".") !== -1){
  120. if(stream.current().substring(stream.current().length-1,stream.current().length) == "{"){
  121. stream.backUp(1);
  122. return ret("tag", "tag");
  123. }//end if
  124. stream.eatSpace();
  125. if( /[{<>.a-zA-Z\/]/.test(stream.peek()) || stream.eol() )return ret("tag", "tag"); // e.g. button.icon-plus
  126. return ret("string", "string"); // let url(/images/logo.png) without quotes return as string
  127. } else if( stream.eol() || stream.peek() == "[" || stream.peek() == "#" || type == "tag" ){
  128. if(stream.current().substring(stream.current().length-1,stream.current().length) == "{")stream.backUp(1);
  129. return ret("tag", "tag");
  130. } else if(type == "compare" || type == "a" || type == "("){
  131. return ret("string", "string");
  132. } else if(type == "|" || stream.current() == "-" || type == "["){
  133. if(type == "|" )return ret("tag", "tag");
  134. return ret(null, ch);
  135. } else if(stream.peek() == ":") {
  136. stream.next();
  137. var t_v = stream.peek() == ":" ? true : false;
  138. if(!t_v){
  139. var old_pos = stream.pos;
  140. var sc = stream.current().length;
  141. stream.eatWhile(/[a-z\\\-]/);
  142. var new_pos = stream.pos;
  143. if(stream.current().substring(sc-1).match(selectors) != null){
  144. stream.backUp(new_pos-(old_pos-1));
  145. return ret("tag", "tag");
  146. } else stream.backUp(new_pos-(old_pos-1));
  147. } else {
  148. stream.backUp(1);
  149. }
  150. if(t_v)return ret("tag", "tag"); else return ret("variable", "variable");
  151. } else if(state.stack[state.stack.length-1] === "font-family"){
  152. return ret(null, null);
  153. } else {
  154. if(state.stack[state.stack.length-1] === "{" || type === "select-op" || (state.stack[state.stack.length-1] === "rule" && type === ",") )return ret("tag", "tag");
  155. return ret("variable", "variable");
  156. }
  157. }
  158. }
  159. function tokenSComment(stream, state) { // SComment = Slash comment
  160. stream.skipToEnd();
  161. state.tokenize = tokenBase;
  162. return ret("comment", "comment");
  163. }
  164. function tokenCComment(stream, state) {
  165. var maybeEnd = false, ch;
  166. while ((ch = stream.next()) != null) {
  167. if (maybeEnd && ch == "/") {
  168. state.tokenize = tokenBase;
  169. break;
  170. }
  171. maybeEnd = (ch == "*");
  172. }
  173. return ret("comment", "comment");
  174. }
  175. function tokenSGMLComment(stream, state) {
  176. var dashes = 0, ch;
  177. while ((ch = stream.next()) != null) {
  178. if (dashes >= 2 && ch == ">") {
  179. state.tokenize = tokenBase;
  180. break;
  181. }
  182. dashes = (ch == "-") ? dashes + 1 : 0;
  183. }
  184. return ret("comment", "comment");
  185. }
  186. function tokenString(quote) {
  187. return function(stream, state) {
  188. var escaped = false, ch;
  189. while ((ch = stream.next()) != null) {
  190. if (ch == quote && !escaped)
  191. break;
  192. escaped = !escaped && ch == "\\";
  193. }
  194. if (!escaped) state.tokenize = tokenBase;
  195. return ret("string", "string");
  196. };
  197. }
  198. return {
  199. startState: function(base) {
  200. return {tokenize: tokenBase,
  201. baseIndent: base || 0,
  202. stack: []};
  203. },
  204. token: function(stream, state) {
  205. if (stream.eatSpace()) return null;
  206. var style = state.tokenize(stream, state);
  207. var context = state.stack[state.stack.length-1];
  208. if (type == "hash" && context == "rule") style = "atom";
  209. else if (style == "variable") {
  210. if (context == "rule") style = null; //"tag"
  211. else if (!context || context == "@media{") {
  212. style = stream.current() == "when" ? "variable" :
  213. /[\s,|\s\)|\s]/.test(stream.peek()) ? "tag" : type;
  214. }
  215. }
  216. if (context == "rule" && /^[\{\};]$/.test(type))
  217. state.stack.pop();
  218. if (type == "{") {
  219. if (context == "@media") state.stack[state.stack.length-1] = "@media{";
  220. else state.stack.push("{");
  221. }
  222. else if (type == "}") state.stack.pop();
  223. else if (type == "@media") state.stack.push("@media");
  224. else if (stream.current() === "font-family") state.stack[state.stack.length-1] = "font-family";
  225. else if (context == "{" && type != "comment" && type !== "tag") state.stack.push("rule");
  226. else if (stream.peek() === ":" && stream.current().match(/@|#/) === null) style = type;
  227. return style;
  228. },
  229. indent: function(state, textAfter) {
  230. var n = state.stack.length;
  231. if (/^\}/.test(textAfter))
  232. n -= state.stack[state.stack.length-1] == "rule" ? 2 : 1;
  233. return state.baseIndent + n * indentUnit;
  234. },
  235. electricChars: "}"
  236. };
  237. });
  238. CodeMirror.defineMIME("text/x-less", "less");
  239. if (!CodeMirror.mimeModes.hasOwnProperty("text/css"))
  240. CodeMirror.defineMIME("text/css", "less");