You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

459 lines
11 KiB

7 months ago
  1. // CodeMirror, copyright (c) by Marijn Haverbeke and others
  2. // Distributed under an MIT license: https://codemirror.net/5/LICENSE
  3. (function(mod) {
  4. if (typeof exports == "object" && typeof module == "object") // CommonJS
  5. mod(require("../../lib/codemirror"), require("../css/css"));
  6. else if (typeof define == "function" && define.amd) // AMD
  7. define(["../../lib/codemirror", "../css/css"], mod);
  8. else // Plain browser env
  9. mod(CodeMirror);
  10. })(function(CodeMirror) {
  11. "use strict";
  12. CodeMirror.defineMode("sass", function(config) {
  13. var cssMode = CodeMirror.mimeModes["text/css"];
  14. var propertyKeywords = cssMode.propertyKeywords || {},
  15. colorKeywords = cssMode.colorKeywords || {},
  16. valueKeywords = cssMode.valueKeywords || {},
  17. fontProperties = cssMode.fontProperties || {};
  18. function tokenRegexp(words) {
  19. return new RegExp("^" + words.join("|"));
  20. }
  21. var keywords = ["true", "false", "null", "auto"];
  22. var keywordsRegexp = new RegExp("^" + keywords.join("|"));
  23. var operators = ["\\(", "\\)", "=", ">", "<", "==", ">=", "<=", "\\+", "-",
  24. "\\!=", "/", "\\*", "%", "and", "or", "not", ";","\\{","\\}",":"];
  25. var opRegexp = tokenRegexp(operators);
  26. var pseudoElementsRegexp = /^::?[a-zA-Z_][\w\-]*/;
  27. var word;
  28. function isEndLine(stream) {
  29. return !stream.peek() || stream.match(/\s+$/, false);
  30. }
  31. function urlTokens(stream, state) {
  32. var ch = stream.peek();
  33. if (ch === ")") {
  34. stream.next();
  35. state.tokenizer = tokenBase;
  36. return "operator";
  37. } else if (ch === "(") {
  38. stream.next();
  39. stream.eatSpace();
  40. return "operator";
  41. } else if (ch === "'" || ch === '"') {
  42. state.tokenizer = buildStringTokenizer(stream.next());
  43. return "string";
  44. } else {
  45. state.tokenizer = buildStringTokenizer(")", false);
  46. return "string";
  47. }
  48. }
  49. function comment(indentation, multiLine) {
  50. return function(stream, state) {
  51. if (stream.sol() && stream.indentation() <= indentation) {
  52. state.tokenizer = tokenBase;
  53. return tokenBase(stream, state);
  54. }
  55. if (multiLine && stream.skipTo("*/")) {
  56. stream.next();
  57. stream.next();
  58. state.tokenizer = tokenBase;
  59. } else {
  60. stream.skipToEnd();
  61. }
  62. return "comment";
  63. };
  64. }
  65. function buildStringTokenizer(quote, greedy) {
  66. if (greedy == null) { greedy = true; }
  67. function stringTokenizer(stream, state) {
  68. var nextChar = stream.next();
  69. var peekChar = stream.peek();
  70. var previousChar = stream.string.charAt(stream.pos-2);
  71. var endingString = ((nextChar !== "\\" && peekChar === quote) || (nextChar === quote && previousChar !== "\\"));
  72. if (endingString) {
  73. if (nextChar !== quote && greedy) { stream.next(); }
  74. if (isEndLine(stream)) {
  75. state.cursorHalf = 0;
  76. }
  77. state.tokenizer = tokenBase;
  78. return "string";
  79. } else if (nextChar === "#" && peekChar === "{") {
  80. state.tokenizer = buildInterpolationTokenizer(stringTokenizer);
  81. stream.next();
  82. return "operator";
  83. } else {
  84. return "string";
  85. }
  86. }
  87. return stringTokenizer;
  88. }
  89. function buildInterpolationTokenizer(currentTokenizer) {
  90. return function(stream, state) {
  91. if (stream.peek() === "}") {
  92. stream.next();
  93. state.tokenizer = currentTokenizer;
  94. return "operator";
  95. } else {
  96. return tokenBase(stream, state);
  97. }
  98. };
  99. }
  100. function indent(state) {
  101. if (state.indentCount == 0) {
  102. state.indentCount++;
  103. var lastScopeOffset = state.scopes[0].offset;
  104. var currentOffset = lastScopeOffset + config.indentUnit;
  105. state.scopes.unshift({ offset:currentOffset });
  106. }
  107. }
  108. function dedent(state) {
  109. if (state.scopes.length == 1) return;
  110. state.scopes.shift();
  111. }
  112. function tokenBase(stream, state) {
  113. var ch = stream.peek();
  114. // Comment
  115. if (stream.match("/*")) {
  116. state.tokenizer = comment(stream.indentation(), true);
  117. return state.tokenizer(stream, state);
  118. }
  119. if (stream.match("//")) {
  120. state.tokenizer = comment(stream.indentation(), false);
  121. return state.tokenizer(stream, state);
  122. }
  123. // Interpolation
  124. if (stream.match("#{")) {
  125. state.tokenizer = buildInterpolationTokenizer(tokenBase);
  126. return "operator";
  127. }
  128. // Strings
  129. if (ch === '"' || ch === "'") {
  130. stream.next();
  131. state.tokenizer = buildStringTokenizer(ch);
  132. return "string";
  133. }
  134. if(!state.cursorHalf){// state.cursorHalf === 0
  135. // first half i.e. before : for key-value pairs
  136. // including selectors
  137. if (ch === "-") {
  138. if (stream.match(/^-\w+-/)) {
  139. return "meta";
  140. }
  141. }
  142. if (ch === ".") {
  143. stream.next();
  144. if (stream.match(/^[\w-]+/)) {
  145. indent(state);
  146. return "qualifier";
  147. } else if (stream.peek() === "#") {
  148. indent(state);
  149. return "tag";
  150. }
  151. }
  152. if (ch === "#") {
  153. stream.next();
  154. // ID selectors
  155. if (stream.match(/^[\w-]+/)) {
  156. indent(state);
  157. return "builtin";
  158. }
  159. if (stream.peek() === "#") {
  160. indent(state);
  161. return "tag";
  162. }
  163. }
  164. // Variables
  165. if (ch === "$") {
  166. stream.next();
  167. stream.eatWhile(/[\w-]/);
  168. return "variable-2";
  169. }
  170. // Numbers
  171. if (stream.match(/^-?[0-9\.]+/))
  172. return "number";
  173. // Units
  174. if (stream.match(/^(px|em|in)\b/))
  175. return "unit";
  176. if (stream.match(keywordsRegexp))
  177. return "keyword";
  178. if (stream.match(/^url/) && stream.peek() === "(") {
  179. state.tokenizer = urlTokens;
  180. return "atom";
  181. }
  182. if (ch === "=") {
  183. // Match shortcut mixin definition
  184. if (stream.match(/^=[\w-]+/)) {
  185. indent(state);
  186. return "meta";
  187. }
  188. }
  189. if (ch === "+") {
  190. // Match shortcut mixin definition
  191. if (stream.match(/^\+[\w-]+/)){
  192. return "variable-3";
  193. }
  194. }
  195. if(ch === "@"){
  196. if(stream.match('@extend')){
  197. if(!stream.match(/\s*[\w]/))
  198. dedent(state);
  199. }
  200. }
  201. // Indent Directives
  202. if (stream.match(/^@(else if|if|media|else|for|each|while|mixin|function)/)) {
  203. indent(state);
  204. return "def";
  205. }
  206. // Other Directives
  207. if (ch === "@") {
  208. stream.next();
  209. stream.eatWhile(/[\w-]/);
  210. return "def";
  211. }
  212. if (stream.eatWhile(/[\w-]/)){
  213. if(stream.match(/ *: *[\w-\+\$#!\("']/,false)){
  214. word = stream.current().toLowerCase();
  215. var prop = state.prevProp + "-" + word;
  216. if (propertyKeywords.hasOwnProperty(prop)) {
  217. return "property";
  218. } else if (propertyKeywords.hasOwnProperty(word)) {
  219. state.prevProp = word;
  220. return "property";
  221. } else if (fontProperties.hasOwnProperty(word)) {
  222. return "property";
  223. }
  224. return "tag";
  225. }
  226. else if(stream.match(/ *:/,false)){
  227. indent(state);
  228. state.cursorHalf = 1;
  229. state.prevProp = stream.current().toLowerCase();
  230. return "property";
  231. }
  232. else if(stream.match(/ *,/,false)){
  233. return "tag";
  234. }
  235. else{
  236. indent(state);
  237. return "tag";
  238. }
  239. }
  240. if(ch === ":"){
  241. if (stream.match(pseudoElementsRegexp)){ // could be a pseudo-element
  242. return "variable-3";
  243. }
  244. stream.next();
  245. state.cursorHalf=1;
  246. return "operator";
  247. }
  248. } // cursorHalf===0 ends here
  249. else{
  250. if (ch === "#") {
  251. stream.next();
  252. // Hex numbers
  253. if (stream.match(/[0-9a-fA-F]{6}|[0-9a-fA-F]{3}/)){
  254. if (isEndLine(stream)) {
  255. state.cursorHalf = 0;
  256. }
  257. return "number";
  258. }
  259. }
  260. // Numbers
  261. if (stream.match(/^-?[0-9\.]+/)){
  262. if (isEndLine(stream)) {
  263. state.cursorHalf = 0;
  264. }
  265. return "number";
  266. }
  267. // Units
  268. if (stream.match(/^(px|em|in)\b/)){
  269. if (isEndLine(stream)) {
  270. state.cursorHalf = 0;
  271. }
  272. return "unit";
  273. }
  274. if (stream.match(keywordsRegexp)){
  275. if (isEndLine(stream)) {
  276. state.cursorHalf = 0;
  277. }
  278. return "keyword";
  279. }
  280. if (stream.match(/^url/) && stream.peek() === "(") {
  281. state.tokenizer = urlTokens;
  282. if (isEndLine(stream)) {
  283. state.cursorHalf = 0;
  284. }
  285. return "atom";
  286. }
  287. // Variables
  288. if (ch === "$") {
  289. stream.next();
  290. stream.eatWhile(/[\w-]/);
  291. if (isEndLine(stream)) {
  292. state.cursorHalf = 0;
  293. }
  294. return "variable-2";
  295. }
  296. // bang character for !important, !default, etc.
  297. if (ch === "!") {
  298. stream.next();
  299. state.cursorHalf = 0;
  300. return stream.match(/^[\w]+/) ? "keyword": "operator";
  301. }
  302. if (stream.match(opRegexp)){
  303. if (isEndLine(stream)) {
  304. state.cursorHalf = 0;
  305. }
  306. return "operator";
  307. }
  308. // attributes
  309. if (stream.eatWhile(/[\w-]/)) {
  310. if (isEndLine(stream)) {
  311. state.cursorHalf = 0;
  312. }
  313. word = stream.current().toLowerCase();
  314. if (valueKeywords.hasOwnProperty(word)) {
  315. return "atom";
  316. } else if (colorKeywords.hasOwnProperty(word)) {
  317. return "keyword";
  318. } else if (propertyKeywords.hasOwnProperty(word)) {
  319. state.prevProp = stream.current().toLowerCase();
  320. return "property";
  321. } else {
  322. return "tag";
  323. }
  324. }
  325. //stream.eatSpace();
  326. if (isEndLine(stream)) {
  327. state.cursorHalf = 0;
  328. return null;
  329. }
  330. } // else ends here
  331. if (stream.match(opRegexp))
  332. return "operator";
  333. // If we haven't returned by now, we move 1 character
  334. // and return an error
  335. stream.next();
  336. return null;
  337. }
  338. function tokenLexer(stream, state) {
  339. if (stream.sol()) state.indentCount = 0;
  340. var style = state.tokenizer(stream, state);
  341. var current = stream.current();
  342. if (current === "@return" || current === "}"){
  343. dedent(state);
  344. }
  345. if (style !== null) {
  346. var startOfToken = stream.pos - current.length;
  347. var withCurrentIndent = startOfToken + (config.indentUnit * state.indentCount);
  348. var newScopes = [];
  349. for (var i = 0; i < state.scopes.length; i++) {
  350. var scope = state.scopes[i];
  351. if (scope.offset <= withCurrentIndent)
  352. newScopes.push(scope);
  353. }
  354. state.scopes = newScopes;
  355. }
  356. return style;
  357. }
  358. return {
  359. startState: function() {
  360. return {
  361. tokenizer: tokenBase,
  362. scopes: [{offset: 0, type: "sass"}],
  363. indentCount: 0,
  364. cursorHalf: 0, // cursor half tells us if cursor lies after (1)
  365. // or before (0) colon (well... more or less)
  366. definedVars: [],
  367. definedMixins: []
  368. };
  369. },
  370. token: function(stream, state) {
  371. var style = tokenLexer(stream, state);
  372. state.lastToken = { style: style, content: stream.current() };
  373. return style;
  374. },
  375. indent: function(state) {
  376. return state.scopes[0].offset;
  377. },
  378. blockCommentStart: "/*",
  379. blockCommentEnd: "*/",
  380. lineComment: "//",
  381. fold: "indent"
  382. };
  383. }, "css");
  384. CodeMirror.defineMIME("text/x-sass", "sass");
  385. });