slim.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  1. // CodeMirror, copyright (c) by Marijn Haverbeke and others
  2. // Distributed under an MIT license: https://codemirror.net/LICENSE
  3. // Slim Highlighting for CodeMirror copyright (c) HicknHack Software Gmbh
  4. ;(function (mod) {
  5. if (typeof exports == 'object' && typeof module == 'object')
  6. // CommonJS
  7. mod(require('../../lib/codemirror'), require('../htmlmixed/htmlmixed'), require('../ruby/ruby'))
  8. else if (typeof define == 'function' && define.amd)
  9. // AMD
  10. define(['../../lib/codemirror', '../htmlmixed/htmlmixed', '../ruby/ruby'], mod)
  11. // Plain browser env
  12. else mod(CodeMirror)
  13. })(function (CodeMirror) {
  14. 'use strict'
  15. CodeMirror.defineMode(
  16. 'slim',
  17. function (config) {
  18. var htmlMode = CodeMirror.getMode(config, { name: 'htmlmixed' })
  19. var rubyMode = CodeMirror.getMode(config, 'ruby')
  20. var modes = { html: htmlMode, ruby: rubyMode }
  21. var embedded = {
  22. ruby: 'ruby',
  23. javascript: 'javascript',
  24. css: 'text/css',
  25. sass: 'text/x-sass',
  26. scss: 'text/x-scss',
  27. less: 'text/x-less',
  28. styl: 'text/x-styl', // no highlighting so far
  29. coffee: 'coffeescript',
  30. asciidoc: 'text/x-asciidoc',
  31. markdown: 'text/x-markdown',
  32. textile: 'text/x-textile', // no highlighting so far
  33. creole: 'text/x-creole', // no highlighting so far
  34. wiki: 'text/x-wiki', // no highlighting so far
  35. mediawiki: 'text/x-mediawiki', // no highlighting so far
  36. rdoc: 'text/x-rdoc', // no highlighting so far
  37. builder: 'text/x-builder', // no highlighting so far
  38. nokogiri: 'text/x-nokogiri', // no highlighting so far
  39. erb: 'application/x-erb',
  40. }
  41. var embeddedRegexp = (function (map) {
  42. var arr = []
  43. for (var key in map) arr.push(key)
  44. return new RegExp('^(' + arr.join('|') + '):')
  45. })(embedded)
  46. var styleMap = {
  47. commentLine: 'comment',
  48. slimSwitch: 'operator special',
  49. slimTag: 'tag',
  50. slimId: 'attribute def',
  51. slimClass: 'attribute qualifier',
  52. slimAttribute: 'attribute',
  53. slimSubmode: 'keyword special',
  54. closeAttributeTag: null,
  55. slimDoctype: null,
  56. lineContinuation: null,
  57. }
  58. var closing = {
  59. '{': '}',
  60. '[': ']',
  61. '(': ')',
  62. }
  63. var nameStartChar = '_a-zA-Z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD'
  64. var nameChar = nameStartChar + '\\-0-9\xB7\u0300-\u036F\u203F-\u2040'
  65. var nameRegexp = new RegExp('^[:' + nameStartChar + '](?::[' + nameChar + ']|[' + nameChar + ']*)')
  66. var attributeNameRegexp = new RegExp('^[:' + nameStartChar + '][:\\.' + nameChar + ']*(?=\\s*=)')
  67. var wrappedAttributeNameRegexp = new RegExp('^[:' + nameStartChar + '][:\\.' + nameChar + ']*')
  68. var classNameRegexp = /^\.-?[_a-zA-Z]+[\w\-]*/
  69. var classIdRegexp = /^#[_a-zA-Z]+[\w\-]*/
  70. function backup(pos, tokenize, style) {
  71. var restore = function (stream, state) {
  72. state.tokenize = tokenize
  73. if (stream.pos < pos) {
  74. stream.pos = pos
  75. return style
  76. }
  77. return state.tokenize(stream, state)
  78. }
  79. return function (stream, state) {
  80. state.tokenize = restore
  81. return tokenize(stream, state)
  82. }
  83. }
  84. function maybeBackup(stream, state, pat, offset, style) {
  85. var cur = stream.current()
  86. var idx = cur.search(pat)
  87. if (idx > -1) {
  88. state.tokenize = backup(stream.pos, state.tokenize, style)
  89. stream.backUp(cur.length - idx - offset)
  90. }
  91. return style
  92. }
  93. function continueLine(state, column) {
  94. state.stack = {
  95. parent: state.stack,
  96. style: 'continuation',
  97. indented: column,
  98. tokenize: state.line,
  99. }
  100. state.line = state.tokenize
  101. }
  102. function finishContinue(state) {
  103. if (state.line == state.tokenize) {
  104. state.line = state.stack.tokenize
  105. state.stack = state.stack.parent
  106. }
  107. }
  108. function lineContinuable(column, tokenize) {
  109. return function (stream, state) {
  110. finishContinue(state)
  111. if (stream.match(/^\\$/)) {
  112. continueLine(state, column)
  113. return 'lineContinuation'
  114. }
  115. var style = tokenize(stream, state)
  116. if (stream.eol() && stream.current().match(/(?:^|[^\\])(?:\\\\)*\\$/)) {
  117. stream.backUp(1)
  118. }
  119. return style
  120. }
  121. }
  122. function commaContinuable(column, tokenize) {
  123. return function (stream, state) {
  124. finishContinue(state)
  125. var style = tokenize(stream, state)
  126. if (stream.eol() && stream.current().match(/,$/)) {
  127. continueLine(state, column)
  128. }
  129. return style
  130. }
  131. }
  132. function rubyInQuote(endQuote, tokenize) {
  133. // TODO: add multi line support
  134. return function (stream, state) {
  135. var ch = stream.peek()
  136. if (ch == endQuote && state.rubyState.tokenize.length == 1) {
  137. // step out of ruby context as it seems to complete processing all the braces
  138. stream.next()
  139. state.tokenize = tokenize
  140. return 'closeAttributeTag'
  141. } else {
  142. return ruby(stream, state)
  143. }
  144. }
  145. }
  146. function startRubySplat(tokenize) {
  147. var rubyState
  148. var runSplat = function (stream, state) {
  149. if (state.rubyState.tokenize.length == 1 && !state.rubyState.context.prev) {
  150. stream.backUp(1)
  151. if (stream.eatSpace()) {
  152. state.rubyState = rubyState
  153. state.tokenize = tokenize
  154. return tokenize(stream, state)
  155. }
  156. stream.next()
  157. }
  158. return ruby(stream, state)
  159. }
  160. return function (stream, state) {
  161. rubyState = state.rubyState
  162. state.rubyState = CodeMirror.startState(rubyMode)
  163. state.tokenize = runSplat
  164. return ruby(stream, state)
  165. }
  166. }
  167. function ruby(stream, state) {
  168. return rubyMode.token(stream, state.rubyState)
  169. }
  170. function htmlLine(stream, state) {
  171. if (stream.match(/^\\$/)) {
  172. return 'lineContinuation'
  173. }
  174. return html(stream, state)
  175. }
  176. function html(stream, state) {
  177. if (stream.match(/^#\{/)) {
  178. state.tokenize = rubyInQuote('}', state.tokenize)
  179. return null
  180. }
  181. return maybeBackup(stream, state, /[^\\]#\{/, 1, htmlMode.token(stream, state.htmlState))
  182. }
  183. function startHtmlLine(lastTokenize) {
  184. return function (stream, state) {
  185. var style = htmlLine(stream, state)
  186. if (stream.eol()) state.tokenize = lastTokenize
  187. return style
  188. }
  189. }
  190. function startHtmlMode(stream, state, offset) {
  191. state.stack = {
  192. parent: state.stack,
  193. style: 'html',
  194. indented: stream.column() + offset, // pipe + space
  195. tokenize: state.line,
  196. }
  197. state.line = state.tokenize = html
  198. return null
  199. }
  200. function comment(stream, state) {
  201. stream.skipToEnd()
  202. return state.stack.style
  203. }
  204. function commentMode(stream, state) {
  205. state.stack = {
  206. parent: state.stack,
  207. style: 'comment',
  208. indented: state.indented + 1,
  209. tokenize: state.line,
  210. }
  211. state.line = comment
  212. return comment(stream, state)
  213. }
  214. function attributeWrapper(stream, state) {
  215. if (stream.eat(state.stack.endQuote)) {
  216. state.line = state.stack.line
  217. state.tokenize = state.stack.tokenize
  218. state.stack = state.stack.parent
  219. return null
  220. }
  221. if (stream.match(wrappedAttributeNameRegexp)) {
  222. state.tokenize = attributeWrapperAssign
  223. return 'slimAttribute'
  224. }
  225. stream.next()
  226. return null
  227. }
  228. function attributeWrapperAssign(stream, state) {
  229. if (stream.match(/^==?/)) {
  230. state.tokenize = attributeWrapperValue
  231. return null
  232. }
  233. return attributeWrapper(stream, state)
  234. }
  235. function attributeWrapperValue(stream, state) {
  236. var ch = stream.peek()
  237. if (ch == '"' || ch == "'") {
  238. state.tokenize = readQuoted(ch, 'string', true, false, attributeWrapper)
  239. stream.next()
  240. return state.tokenize(stream, state)
  241. }
  242. if (ch == '[') {
  243. return startRubySplat(attributeWrapper)(stream, state)
  244. }
  245. if (stream.match(/^(true|false|nil)\b/)) {
  246. state.tokenize = attributeWrapper
  247. return 'keyword'
  248. }
  249. return startRubySplat(attributeWrapper)(stream, state)
  250. }
  251. function startAttributeWrapperMode(state, endQuote, tokenize) {
  252. state.stack = {
  253. parent: state.stack,
  254. style: 'wrapper',
  255. indented: state.indented + 1,
  256. tokenize: tokenize,
  257. line: state.line,
  258. endQuote: endQuote,
  259. }
  260. state.line = state.tokenize = attributeWrapper
  261. return null
  262. }
  263. function sub(stream, state) {
  264. if (stream.match(/^#\{/)) {
  265. state.tokenize = rubyInQuote('}', state.tokenize)
  266. return null
  267. }
  268. var subStream = new CodeMirror.StringStream(stream.string.slice(state.stack.indented), stream.tabSize)
  269. subStream.pos = stream.pos - state.stack.indented
  270. subStream.start = stream.start - state.stack.indented
  271. subStream.lastColumnPos = stream.lastColumnPos - state.stack.indented
  272. subStream.lastColumnValue = stream.lastColumnValue - state.stack.indented
  273. var style = state.subMode.token(subStream, state.subState)
  274. stream.pos = subStream.pos + state.stack.indented
  275. return style
  276. }
  277. function firstSub(stream, state) {
  278. state.stack.indented = stream.column()
  279. state.line = state.tokenize = sub
  280. return state.tokenize(stream, state)
  281. }
  282. function createMode(mode) {
  283. var query = embedded[mode]
  284. var spec = CodeMirror.mimeModes[query]
  285. if (spec) {
  286. return CodeMirror.getMode(config, spec)
  287. }
  288. var factory = CodeMirror.modes[query]
  289. if (factory) {
  290. return factory(config, { name: query })
  291. }
  292. return CodeMirror.getMode(config, 'null')
  293. }
  294. function getMode(mode) {
  295. if (!modes.hasOwnProperty(mode)) {
  296. return (modes[mode] = createMode(mode))
  297. }
  298. return modes[mode]
  299. }
  300. function startSubMode(mode, state) {
  301. var subMode = getMode(mode)
  302. var subState = CodeMirror.startState(subMode)
  303. state.subMode = subMode
  304. state.subState = subState
  305. state.stack = {
  306. parent: state.stack,
  307. style: 'sub',
  308. indented: state.indented + 1,
  309. tokenize: state.line,
  310. }
  311. state.line = state.tokenize = firstSub
  312. return 'slimSubmode'
  313. }
  314. function doctypeLine(stream, _state) {
  315. stream.skipToEnd()
  316. return 'slimDoctype'
  317. }
  318. function startLine(stream, state) {
  319. var ch = stream.peek()
  320. if (ch == '<') {
  321. return (state.tokenize = startHtmlLine(state.tokenize))(stream, state)
  322. }
  323. if (stream.match(/^[|']/)) {
  324. return startHtmlMode(stream, state, 1)
  325. }
  326. if (stream.match(/^\/(!|\[\w+])?/)) {
  327. return commentMode(stream, state)
  328. }
  329. if (stream.match(/^(-|==?[<>]?)/)) {
  330. state.tokenize = lineContinuable(stream.column(), commaContinuable(stream.column(), ruby))
  331. return 'slimSwitch'
  332. }
  333. if (stream.match(/^doctype\b/)) {
  334. state.tokenize = doctypeLine
  335. return 'keyword'
  336. }
  337. var m = stream.match(embeddedRegexp)
  338. if (m) {
  339. return startSubMode(m[1], state)
  340. }
  341. return slimTag(stream, state)
  342. }
  343. function slim(stream, state) {
  344. if (state.startOfLine) {
  345. return startLine(stream, state)
  346. }
  347. return slimTag(stream, state)
  348. }
  349. function slimTag(stream, state) {
  350. if (stream.eat('*')) {
  351. state.tokenize = startRubySplat(slimTagExtras)
  352. return null
  353. }
  354. if (stream.match(nameRegexp)) {
  355. state.tokenize = slimTagExtras
  356. return 'slimTag'
  357. }
  358. return slimClass(stream, state)
  359. }
  360. function slimTagExtras(stream, state) {
  361. if (stream.match(/^(<>?|><?)/)) {
  362. state.tokenize = slimClass
  363. return null
  364. }
  365. return slimClass(stream, state)
  366. }
  367. function slimClass(stream, state) {
  368. if (stream.match(classIdRegexp)) {
  369. state.tokenize = slimClass
  370. return 'slimId'
  371. }
  372. if (stream.match(classNameRegexp)) {
  373. state.tokenize = slimClass
  374. return 'slimClass'
  375. }
  376. return slimAttribute(stream, state)
  377. }
  378. function slimAttribute(stream, state) {
  379. if (stream.match(/^([\[\{\(])/)) {
  380. return startAttributeWrapperMode(state, closing[RegExp.$1], slimAttribute)
  381. }
  382. if (stream.match(attributeNameRegexp)) {
  383. state.tokenize = slimAttributeAssign
  384. return 'slimAttribute'
  385. }
  386. if (stream.peek() == '*') {
  387. stream.next()
  388. state.tokenize = startRubySplat(slimContent)
  389. return null
  390. }
  391. return slimContent(stream, state)
  392. }
  393. function slimAttributeAssign(stream, state) {
  394. if (stream.match(/^==?/)) {
  395. state.tokenize = slimAttributeValue
  396. return null
  397. }
  398. // should never happen, because of forward lookup
  399. return slimAttribute(stream, state)
  400. }
  401. function slimAttributeValue(stream, state) {
  402. var ch = stream.peek()
  403. if (ch == '"' || ch == "'") {
  404. state.tokenize = readQuoted(ch, 'string', true, false, slimAttribute)
  405. stream.next()
  406. return state.tokenize(stream, state)
  407. }
  408. if (ch == '[') {
  409. return startRubySplat(slimAttribute)(stream, state)
  410. }
  411. if (ch == ':') {
  412. return startRubySplat(slimAttributeSymbols)(stream, state)
  413. }
  414. if (stream.match(/^(true|false|nil)\b/)) {
  415. state.tokenize = slimAttribute
  416. return 'keyword'
  417. }
  418. return startRubySplat(slimAttribute)(stream, state)
  419. }
  420. function slimAttributeSymbols(stream, state) {
  421. stream.backUp(1)
  422. if (stream.match(/^[^\s],(?=:)/)) {
  423. state.tokenize = startRubySplat(slimAttributeSymbols)
  424. return null
  425. }
  426. stream.next()
  427. return slimAttribute(stream, state)
  428. }
  429. function readQuoted(quote, style, embed, unescaped, nextTokenize) {
  430. return function (stream, state) {
  431. finishContinue(state)
  432. var fresh = stream.current().length == 0
  433. if (stream.match(/^\\$/, fresh)) {
  434. if (!fresh) return style
  435. continueLine(state, state.indented)
  436. return 'lineContinuation'
  437. }
  438. if (stream.match(/^#\{/, fresh)) {
  439. if (!fresh) return style
  440. state.tokenize = rubyInQuote('}', state.tokenize)
  441. return null
  442. }
  443. var escaped = false,
  444. ch
  445. while ((ch = stream.next()) != null) {
  446. if (ch == quote && (unescaped || !escaped)) {
  447. state.tokenize = nextTokenize
  448. break
  449. }
  450. if (embed && ch == '#' && !escaped) {
  451. if (stream.eat('{')) {
  452. stream.backUp(2)
  453. break
  454. }
  455. }
  456. escaped = !escaped && ch == '\\'
  457. }
  458. if (stream.eol() && escaped) {
  459. stream.backUp(1)
  460. }
  461. return style
  462. }
  463. }
  464. function slimContent(stream, state) {
  465. if (stream.match(/^==?/)) {
  466. state.tokenize = ruby
  467. return 'slimSwitch'
  468. }
  469. if (stream.match(/^\/$/)) {
  470. // tag close hint
  471. state.tokenize = slim
  472. return null
  473. }
  474. if (stream.match(/^:/)) {
  475. // inline tag
  476. state.tokenize = slimTag
  477. return 'slimSwitch'
  478. }
  479. startHtmlMode(stream, state, 0)
  480. return state.tokenize(stream, state)
  481. }
  482. var mode = {
  483. // default to html mode
  484. startState: function () {
  485. var htmlState = CodeMirror.startState(htmlMode)
  486. var rubyState = CodeMirror.startState(rubyMode)
  487. return {
  488. htmlState: htmlState,
  489. rubyState: rubyState,
  490. stack: null,
  491. last: null,
  492. tokenize: slim,
  493. line: slim,
  494. indented: 0,
  495. }
  496. },
  497. copyState: function (state) {
  498. return {
  499. htmlState: CodeMirror.copyState(htmlMode, state.htmlState),
  500. rubyState: CodeMirror.copyState(rubyMode, state.rubyState),
  501. subMode: state.subMode,
  502. subState: state.subMode && CodeMirror.copyState(state.subMode, state.subState),
  503. stack: state.stack,
  504. last: state.last,
  505. tokenize: state.tokenize,
  506. line: state.line,
  507. }
  508. },
  509. token: function (stream, state) {
  510. if (stream.sol()) {
  511. state.indented = stream.indentation()
  512. state.startOfLine = true
  513. state.tokenize = state.line
  514. while (state.stack && state.stack.indented > state.indented && state.last != 'slimSubmode') {
  515. state.line = state.tokenize = state.stack.tokenize
  516. state.stack = state.stack.parent
  517. state.subMode = null
  518. state.subState = null
  519. }
  520. }
  521. if (stream.eatSpace()) return null
  522. var style = state.tokenize(stream, state)
  523. state.startOfLine = false
  524. if (style) state.last = style
  525. return styleMap.hasOwnProperty(style) ? styleMap[style] : style
  526. },
  527. blankLine: function (state) {
  528. if (state.subMode && state.subMode.blankLine) {
  529. return state.subMode.blankLine(state.subState)
  530. }
  531. },
  532. innerMode: function (state) {
  533. if (state.subMode) return { state: state.subState, mode: state.subMode }
  534. return { state: state, mode: mode }
  535. },
  536. //indent: function(state) {
  537. // return state.indented;
  538. //}
  539. }
  540. return mode
  541. },
  542. 'htmlmixed',
  543. 'ruby'
  544. )
  545. CodeMirror.defineMIME('text/x-slim', 'slim')
  546. CodeMirror.defineMIME('application/x-slim', 'slim')
  547. })