sass.js 16 KB

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