haml.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  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('../htmlmixed/htmlmixed'), require('../ruby/ruby'))
  7. else if (typeof define == 'function' && define.amd)
  8. // AMD
  9. define(['../../lib/codemirror', '../htmlmixed/htmlmixed', '../ruby/ruby'], mod)
  10. // Plain browser env
  11. else mod(CodeMirror)
  12. })(function (CodeMirror) {
  13. 'use strict'
  14. // full haml mode. This handled embedded ruby and html fragments too
  15. CodeMirror.defineMode(
  16. 'haml',
  17. function (config) {
  18. var htmlMode = CodeMirror.getMode(config, { name: 'htmlmixed' })
  19. var rubyMode = CodeMirror.getMode(config, 'ruby')
  20. function rubyInQuote(endQuote) {
  21. return function (stream, state) {
  22. var ch = stream.peek()
  23. if (ch == endQuote && state.rubyState.tokenize.length == 1) {
  24. // step out of ruby context as it seems to complete processing all the braces
  25. stream.next()
  26. state.tokenize = html
  27. return 'closeAttributeTag'
  28. } else {
  29. return ruby(stream, state)
  30. }
  31. }
  32. }
  33. function ruby(stream, state) {
  34. if (stream.match('-#')) {
  35. stream.skipToEnd()
  36. return 'comment'
  37. }
  38. return rubyMode.token(stream, state.rubyState)
  39. }
  40. function html(stream, state) {
  41. var ch = stream.peek()
  42. // handle haml declarations. All declarations that cant be handled here
  43. // will be passed to html mode
  44. if (state.previousToken.style == 'comment') {
  45. if (state.indented > state.previousToken.indented) {
  46. stream.skipToEnd()
  47. return 'commentLine'
  48. }
  49. }
  50. if (state.startOfLine) {
  51. if (ch == '!' && stream.match('!!')) {
  52. stream.skipToEnd()
  53. return 'tag'
  54. } else if (stream.match(/^%[\w:#\.]+=/)) {
  55. state.tokenize = ruby
  56. return 'hamlTag'
  57. } else if (stream.match(/^%[\w:]+/)) {
  58. return 'hamlTag'
  59. } else if (ch == '/') {
  60. stream.skipToEnd()
  61. return 'comment'
  62. }
  63. }
  64. if (state.startOfLine || state.previousToken.style == 'hamlTag') {
  65. if (ch == '#' || ch == '.') {
  66. stream.match(/[\w-#\.]*/)
  67. return 'hamlAttribute'
  68. }
  69. }
  70. // do not handle --> as valid ruby, make it HTML close comment instead
  71. if (state.startOfLine && !stream.match('-->', false) && (ch == '=' || ch == '-')) {
  72. state.tokenize = ruby
  73. return state.tokenize(stream, state)
  74. }
  75. if (state.previousToken.style == 'hamlTag' || state.previousToken.style == 'closeAttributeTag' || state.previousToken.style == 'hamlAttribute') {
  76. if (ch == '(') {
  77. state.tokenize = rubyInQuote(')')
  78. return state.tokenize(stream, state)
  79. } else if (ch == '{') {
  80. if (!stream.match(/^\{%.*/)) {
  81. state.tokenize = rubyInQuote('}')
  82. return state.tokenize(stream, state)
  83. }
  84. }
  85. }
  86. return htmlMode.token(stream, state.htmlState)
  87. }
  88. return {
  89. // default to html mode
  90. startState: function () {
  91. var htmlState = CodeMirror.startState(htmlMode)
  92. var rubyState = CodeMirror.startState(rubyMode)
  93. return {
  94. htmlState: htmlState,
  95. rubyState: rubyState,
  96. indented: 0,
  97. previousToken: { style: null, indented: 0 },
  98. tokenize: html,
  99. }
  100. },
  101. copyState: function (state) {
  102. return {
  103. htmlState: CodeMirror.copyState(htmlMode, state.htmlState),
  104. rubyState: CodeMirror.copyState(rubyMode, state.rubyState),
  105. indented: state.indented,
  106. previousToken: state.previousToken,
  107. tokenize: state.tokenize,
  108. }
  109. },
  110. token: function (stream, state) {
  111. if (stream.sol()) {
  112. state.indented = stream.indentation()
  113. state.startOfLine = true
  114. }
  115. if (stream.eatSpace()) return null
  116. var style = state.tokenize(stream, state)
  117. state.startOfLine = false
  118. // dont record comment line as we only want to measure comment line with
  119. // the opening comment block
  120. if (style && style != 'commentLine') {
  121. state.previousToken = { style: style, indented: state.indented }
  122. }
  123. // if current state is ruby and the previous token is not `,` reset the
  124. // tokenize to html
  125. if (stream.eol() && state.tokenize == ruby) {
  126. stream.backUp(1)
  127. var ch = stream.peek()
  128. stream.next()
  129. if (ch && ch != ',') {
  130. state.tokenize = html
  131. }
  132. }
  133. // reprocess some of the specific style tag when finish setting previousToken
  134. if (style == 'hamlTag') {
  135. style = 'tag'
  136. } else if (style == 'commentLine') {
  137. style = 'comment'
  138. } else if (style == 'hamlAttribute') {
  139. style = 'attribute'
  140. } else if (style == 'closeAttributeTag') {
  141. style = null
  142. }
  143. return style
  144. },
  145. }
  146. },
  147. 'htmlmixed',
  148. 'ruby'
  149. )
  150. CodeMirror.defineMIME('text/x-haml', 'haml')
  151. })