soy.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  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'))
  7. else if (typeof define == 'function' && define.amd)
  8. // AMD
  9. define(['../../lib/codemirror', '../htmlmixed/htmlmixed'], mod)
  10. // Plain browser env
  11. else mod(CodeMirror)
  12. })(function (CodeMirror) {
  13. 'use strict'
  14. var paramData = { noEndTag: true, soyState: 'param-def' }
  15. var tags = {
  16. alias: { noEndTag: true },
  17. delpackage: { noEndTag: true },
  18. namespace: { noEndTag: true, soyState: 'namespace-def' },
  19. '@attribute': paramData,
  20. '@attribute?': paramData,
  21. '@param': paramData,
  22. '@param?': paramData,
  23. '@inject': paramData,
  24. '@inject?': paramData,
  25. '@state': paramData,
  26. template: { soyState: 'templ-def', variableScope: true },
  27. extern: { soyState: 'param-def' },
  28. export: { soyState: 'export' },
  29. literal: {},
  30. msg: {},
  31. fallbackmsg: { noEndTag: true, reduceIndent: true },
  32. select: {},
  33. plural: {},
  34. let: { soyState: 'var-def' },
  35. if: {},
  36. javaimpl: {},
  37. jsimpl: {},
  38. elseif: { noEndTag: true, reduceIndent: true },
  39. else: { noEndTag: true, reduceIndent: true },
  40. switch: {},
  41. case: { noEndTag: true, reduceIndent: true },
  42. default: { noEndTag: true, reduceIndent: true },
  43. foreach: { variableScope: true, soyState: 'for-loop' },
  44. ifempty: { noEndTag: true, reduceIndent: true },
  45. for: { variableScope: true, soyState: 'for-loop' },
  46. call: { soyState: 'templ-ref' },
  47. param: { soyState: 'param-ref' },
  48. print: { noEndTag: true },
  49. deltemplate: { soyState: 'templ-def', variableScope: true },
  50. delcall: { soyState: 'templ-ref' },
  51. log: {},
  52. element: { variableScope: true },
  53. velog: {},
  54. const: { soyState: 'const-def' },
  55. }
  56. var indentingTags = Object.keys(tags).filter(function (tag) {
  57. return !tags[tag].noEndTag || tags[tag].reduceIndent
  58. })
  59. CodeMirror.defineMode(
  60. 'soy',
  61. function (config) {
  62. var textMode = CodeMirror.getMode(config, 'text/plain')
  63. var modes = {
  64. html: CodeMirror.getMode(config, { name: 'text/html', multilineTagIndentFactor: 2, multilineTagIndentPastTag: false, allowMissingTagName: true }),
  65. attributes: textMode,
  66. text: textMode,
  67. uri: textMode,
  68. trusted_resource_uri: textMode,
  69. css: CodeMirror.getMode(config, 'text/css'),
  70. js: CodeMirror.getMode(config, { name: 'text/javascript', statementIndent: 2 * config.indentUnit }),
  71. }
  72. function last(array) {
  73. return array[array.length - 1]
  74. }
  75. function tokenUntil(stream, state, untilRegExp) {
  76. if (stream.sol()) {
  77. for (var indent = 0; indent < state.indent; indent++) {
  78. if (!stream.eat(/\s/)) break
  79. }
  80. if (indent) return null
  81. }
  82. var oldString = stream.string
  83. var match = untilRegExp.exec(oldString.substr(stream.pos))
  84. if (match) {
  85. // We don't use backUp because it backs up just the position, not the state.
  86. // This uses an undocumented API.
  87. stream.string = oldString.substr(0, stream.pos + match.index)
  88. }
  89. var result = stream.hideFirstChars(state.indent, function () {
  90. var localState = last(state.localStates)
  91. return localState.mode.token(stream, localState.state)
  92. })
  93. stream.string = oldString
  94. return result
  95. }
  96. function contains(list, element) {
  97. while (list) {
  98. if (list.element === element) return true
  99. list = list.next
  100. }
  101. return false
  102. }
  103. function prepend(list, element) {
  104. return {
  105. element: element,
  106. next: list,
  107. }
  108. }
  109. function popcontext(state) {
  110. if (!state.context) return
  111. if (state.context.scope) {
  112. state.variables = state.context.scope
  113. }
  114. state.context = state.context.previousContext
  115. }
  116. // Reference a variable `name` in `list`.
  117. // Let `loose` be truthy to ignore missing identifiers.
  118. function ref(list, name, loose) {
  119. return contains(list, name) ? 'variable-2' : loose ? 'variable' : 'variable-2 error'
  120. }
  121. // Data for an open soy tag.
  122. function Context(previousContext, tag, scope) {
  123. this.previousContext = previousContext
  124. this.tag = tag
  125. this.kind = null
  126. this.scope = scope
  127. }
  128. function expression(stream, state) {
  129. var match
  130. if (stream.match(/[[]/)) {
  131. state.soyState.push('list-literal')
  132. state.context = new Context(state.context, 'list-literal', state.variables)
  133. state.lookupVariables = false
  134. return null
  135. } else if (stream.match(/\bmap(?=\()/)) {
  136. state.soyState.push('map-literal')
  137. return 'keyword'
  138. } else if (stream.match(/\brecord(?=\()/)) {
  139. state.soyState.push('record-literal')
  140. return 'keyword'
  141. } else if (stream.match(/([\w]+)(?=\()/)) {
  142. return 'variable callee'
  143. } else if ((match = stream.match(/^["']/))) {
  144. state.soyState.push('string')
  145. state.quoteKind = match[0]
  146. return 'string'
  147. } else if (stream.match(/^[(]/)) {
  148. state.soyState.push('open-parentheses')
  149. return null
  150. } else if (stream.match(/(null|true|false)(?!\w)/) || stream.match(/0x([0-9a-fA-F]{2,})/) || stream.match(/-?([0-9]*[.])?[0-9]+(e[0-9]*)?/)) {
  151. return 'atom'
  152. } else if (stream.match(/(\||[+\-*\/%]|[=!]=|\?:|[<>]=?)/)) {
  153. // Tokenize filter, binary, null propagator, and equality operators.
  154. return 'operator'
  155. } else if ((match = stream.match(/^\$([\w]+)/))) {
  156. return ref(state.variables, match[1], !state.lookupVariables)
  157. } else if ((match = stream.match(/^\w+/))) {
  158. return /^(?:as|and|or|not|in|if)$/.test(match[0]) ? 'keyword' : null
  159. }
  160. stream.next()
  161. return null
  162. }
  163. return {
  164. startState: function () {
  165. return {
  166. soyState: [],
  167. variables: prepend(null, 'ij'),
  168. scopes: null,
  169. indent: 0,
  170. quoteKind: null,
  171. context: null,
  172. lookupVariables: true, // Is unknown variables considered an error
  173. localStates: [
  174. {
  175. mode: modes.html,
  176. state: CodeMirror.startState(modes.html),
  177. },
  178. ],
  179. }
  180. },
  181. copyState: function (state) {
  182. return {
  183. tag: state.tag, // Last seen Soy tag.
  184. soyState: state.soyState.concat([]),
  185. variables: state.variables,
  186. context: state.context,
  187. indent: state.indent, // Indentation of the following line.
  188. quoteKind: state.quoteKind,
  189. lookupVariables: state.lookupVariables,
  190. localStates: state.localStates.map(function (localState) {
  191. return {
  192. mode: localState.mode,
  193. state: CodeMirror.copyState(localState.mode, localState.state),
  194. }
  195. }),
  196. }
  197. },
  198. token: function (stream, state) {
  199. var match
  200. switch (last(state.soyState)) {
  201. case 'comment':
  202. if (stream.match(/^.*?\*\//)) {
  203. state.soyState.pop()
  204. } else {
  205. stream.skipToEnd()
  206. }
  207. if (!state.context || !state.context.scope) {
  208. var paramRe = /@param\??\s+(\S+)/g
  209. var current = stream.current()
  210. for (var match; (match = paramRe.exec(current)); ) {
  211. state.variables = prepend(state.variables, match[1])
  212. }
  213. }
  214. return 'comment'
  215. case 'string':
  216. var match = stream.match(/^.*?(["']|\\[\s\S])/)
  217. if (!match) {
  218. stream.skipToEnd()
  219. } else if (match[1] == state.quoteKind) {
  220. state.quoteKind = null
  221. state.soyState.pop()
  222. }
  223. return 'string'
  224. }
  225. if (!state.soyState.length || last(state.soyState) != 'literal') {
  226. if (stream.match(/^\/\*/)) {
  227. state.soyState.push('comment')
  228. return 'comment'
  229. } else if (stream.match(stream.sol() ? /^\s*\/\/.*/ : /^\s+\/\/.*/)) {
  230. return 'comment'
  231. }
  232. }
  233. switch (last(state.soyState)) {
  234. case 'templ-def':
  235. if ((match = stream.match(/^\.?([\w]+(?!\.[\w]+)*)/))) {
  236. state.soyState.pop()
  237. return 'def'
  238. }
  239. stream.next()
  240. return null
  241. case 'templ-ref':
  242. if ((match = stream.match(/(\.?[a-zA-Z_][a-zA-Z_0-9]+)+/))) {
  243. state.soyState.pop()
  244. // If the first character is '.', it can only be a local template.
  245. if (match[0][0] == '.') {
  246. return 'variable-2'
  247. }
  248. // Otherwise
  249. return 'variable'
  250. }
  251. if ((match = stream.match(/^\$([\w]+)/))) {
  252. state.soyState.pop()
  253. return ref(state.variables, match[1], !state.lookupVariables)
  254. }
  255. stream.next()
  256. return null
  257. case 'namespace-def':
  258. if ((match = stream.match(/^\.?([\w\.]+)/))) {
  259. state.soyState.pop()
  260. return 'variable'
  261. }
  262. stream.next()
  263. return null
  264. case 'param-def':
  265. if ((match = stream.match(/^\*/))) {
  266. state.soyState.pop()
  267. state.soyState.push('param-type')
  268. return 'type'
  269. }
  270. if ((match = stream.match(/^\w+/))) {
  271. state.variables = prepend(state.variables, match[0])
  272. state.soyState.pop()
  273. state.soyState.push('param-type')
  274. return 'def'
  275. }
  276. stream.next()
  277. return null
  278. case 'param-ref':
  279. if ((match = stream.match(/^\w+/))) {
  280. state.soyState.pop()
  281. return 'property'
  282. }
  283. stream.next()
  284. return null
  285. case 'open-parentheses':
  286. if (stream.match(/[)]/)) {
  287. state.soyState.pop()
  288. return null
  289. }
  290. return expression(stream, state)
  291. case 'param-type':
  292. var peekChar = stream.peek()
  293. if ('}]=>,'.indexOf(peekChar) != -1) {
  294. state.soyState.pop()
  295. return null
  296. } else if (peekChar == '[') {
  297. state.soyState.push('param-type-record')
  298. return null
  299. } else if (peekChar == '(') {
  300. state.soyState.push('param-type-template')
  301. return null
  302. } else if (peekChar == '<') {
  303. state.soyState.push('param-type-parameter')
  304. return null
  305. } else if ((match = stream.match(/^([\w]+|[?])/))) {
  306. return 'type'
  307. }
  308. stream.next()
  309. return null
  310. case 'param-type-record':
  311. var peekChar = stream.peek()
  312. if (peekChar == ']') {
  313. state.soyState.pop()
  314. return null
  315. }
  316. if (stream.match(/^\w+/)) {
  317. state.soyState.push('param-type')
  318. return 'property'
  319. }
  320. stream.next()
  321. return null
  322. case 'param-type-parameter':
  323. if (stream.match(/^[>]/)) {
  324. state.soyState.pop()
  325. return null
  326. }
  327. if (stream.match(/^[<,]/)) {
  328. state.soyState.push('param-type')
  329. return null
  330. }
  331. stream.next()
  332. return null
  333. case 'param-type-template':
  334. if (stream.match(/[>]/)) {
  335. state.soyState.pop()
  336. state.soyState.push('param-type')
  337. return null
  338. }
  339. if (stream.match(/^\w+/)) {
  340. state.soyState.push('param-type')
  341. return 'def'
  342. }
  343. stream.next()
  344. return null
  345. case 'var-def':
  346. if ((match = stream.match(/^\$([\w]+)/))) {
  347. state.variables = prepend(state.variables, match[1])
  348. state.soyState.pop()
  349. return 'def'
  350. }
  351. stream.next()
  352. return null
  353. case 'for-loop':
  354. if (stream.match(/\bin\b/)) {
  355. state.soyState.pop()
  356. return 'keyword'
  357. }
  358. if (stream.peek() == '$') {
  359. state.soyState.push('var-def')
  360. return null
  361. }
  362. stream.next()
  363. return null
  364. case 'record-literal':
  365. if (stream.match(/^[)]/)) {
  366. state.soyState.pop()
  367. return null
  368. }
  369. if (stream.match(/[(,]/)) {
  370. state.soyState.push('map-value')
  371. state.soyState.push('record-key')
  372. return null
  373. }
  374. stream.next()
  375. return null
  376. case 'map-literal':
  377. if (stream.match(/^[)]/)) {
  378. state.soyState.pop()
  379. return null
  380. }
  381. if (stream.match(/[(,]/)) {
  382. state.soyState.push('map-value')
  383. state.soyState.push('map-value')
  384. return null
  385. }
  386. stream.next()
  387. return null
  388. case 'list-literal':
  389. if (stream.match(']')) {
  390. state.soyState.pop()
  391. state.lookupVariables = true
  392. popcontext(state)
  393. return null
  394. }
  395. if (stream.match(/\bfor\b/)) {
  396. state.lookupVariables = true
  397. state.soyState.push('for-loop')
  398. return 'keyword'
  399. }
  400. return expression(stream, state)
  401. case 'record-key':
  402. if (stream.match(/[\w]+/)) {
  403. return 'property'
  404. }
  405. if (stream.match(/^[:]/)) {
  406. state.soyState.pop()
  407. return null
  408. }
  409. stream.next()
  410. return null
  411. case 'map-value':
  412. if (stream.peek() == ')' || stream.peek() == ',' || stream.match(/^[:)]/)) {
  413. state.soyState.pop()
  414. return null
  415. }
  416. return expression(stream, state)
  417. case 'import':
  418. if (stream.eat(';')) {
  419. state.soyState.pop()
  420. state.indent -= 2 * config.indentUnit
  421. return null
  422. }
  423. if (stream.match(/\w+(?=\s+as\b)/)) {
  424. return 'variable'
  425. }
  426. if ((match = stream.match(/\w+/))) {
  427. return /\b(from|as)\b/.test(match[0]) ? 'keyword' : 'def'
  428. }
  429. if ((match = stream.match(/^["']/))) {
  430. state.soyState.push('string')
  431. state.quoteKind = match[0]
  432. return 'string'
  433. }
  434. stream.next()
  435. return null
  436. case 'tag':
  437. var endTag
  438. var tagName
  439. if (state.tag === undefined) {
  440. endTag = true
  441. tagName = ''
  442. } else {
  443. endTag = state.tag[0] == '/'
  444. tagName = endTag ? state.tag.substring(1) : state.tag
  445. }
  446. var tag = tags[tagName]
  447. if (stream.match(/^\/?}/)) {
  448. var selfClosed = stream.current() == '/}'
  449. if (selfClosed && !endTag) {
  450. popcontext(state)
  451. }
  452. if (state.tag == '/template' || state.tag == '/deltemplate') {
  453. state.variables = prepend(null, 'ij')
  454. state.indent = 0
  455. } else {
  456. state.indent -= config.indentUnit * (selfClosed || indentingTags.indexOf(state.tag) == -1 ? 2 : 1)
  457. }
  458. state.soyState.pop()
  459. return 'keyword'
  460. } else if (stream.match(/^([\w?]+)(?==)/)) {
  461. if (state.context && state.context.tag == tagName && stream.current() == 'kind' && (match = stream.match(/^="([^"]+)/, false))) {
  462. var kind = match[1]
  463. state.context.kind = kind
  464. var mode = modes[kind] || modes.html
  465. var localState = last(state.localStates)
  466. if (localState.mode.indent) {
  467. state.indent += localState.mode.indent(localState.state, '', '')
  468. }
  469. state.localStates.push({
  470. mode: mode,
  471. state: CodeMirror.startState(mode),
  472. })
  473. }
  474. return 'attribute'
  475. }
  476. return expression(stream, state)
  477. case 'template-call-expression':
  478. if (stream.match(/^([\w-?]+)(?==)/)) {
  479. return 'attribute'
  480. } else if (stream.eat('>')) {
  481. state.soyState.pop()
  482. return 'keyword'
  483. } else if (stream.eat('/>')) {
  484. state.soyState.pop()
  485. return 'keyword'
  486. }
  487. return expression(stream, state)
  488. case 'literal':
  489. if (stream.match('{/literal}', false)) {
  490. state.soyState.pop()
  491. return this.token(stream, state)
  492. }
  493. return tokenUntil(stream, state, /\{\/literal}/)
  494. case 'export':
  495. if ((match = stream.match(/\w+/))) {
  496. state.soyState.pop()
  497. if (match == 'const') {
  498. state.soyState.push('const-def')
  499. return 'keyword'
  500. } else if (match == 'extern') {
  501. state.soyState.push('param-def')
  502. return 'keyword'
  503. }
  504. } else {
  505. stream.next()
  506. }
  507. return null
  508. case 'const-def':
  509. if (stream.match(/^\w+/)) {
  510. state.soyState.pop()
  511. return 'def'
  512. }
  513. stream.next()
  514. return null
  515. }
  516. if (stream.match('{literal}')) {
  517. state.indent += config.indentUnit
  518. state.soyState.push('literal')
  519. state.context = new Context(state.context, 'literal', state.variables)
  520. return 'keyword'
  521. // A tag-keyword must be followed by whitespace, comment or a closing tag.
  522. } else if ((match = stream.match(/^\{([/@\\]?\w+\??)(?=$|[\s}]|\/[/*])/))) {
  523. var prevTag = state.tag
  524. state.tag = match[1]
  525. var endTag = state.tag[0] == '/'
  526. var indentingTag = !!tags[state.tag]
  527. var tagName = endTag ? state.tag.substring(1) : state.tag
  528. var tag = tags[tagName]
  529. if (state.tag != '/switch') state.indent += ((endTag || (tag && tag.reduceIndent)) && prevTag != 'switch' ? 1 : 2) * config.indentUnit
  530. state.soyState.push('tag')
  531. var tagError = false
  532. if (tag) {
  533. if (!endTag) {
  534. if (tag.soyState) state.soyState.push(tag.soyState)
  535. }
  536. // If a new tag, open a new context.
  537. if (!tag.noEndTag && (indentingTag || !endTag)) {
  538. state.context = new Context(state.context, state.tag, tag.variableScope ? state.variables : null)
  539. // Otherwise close the current context.
  540. } else if (endTag) {
  541. var isBalancedForExtern = tagName == 'extern' && state.context && state.context.tag == 'export'
  542. if (!state.context || (state.context.tag != tagName && !isBalancedForExtern)) {
  543. tagError = true
  544. } else if (state.context) {
  545. if (state.context.kind) {
  546. state.localStates.pop()
  547. var localState = last(state.localStates)
  548. if (localState.mode.indent) {
  549. state.indent -= localState.mode.indent(localState.state, '', '')
  550. }
  551. }
  552. popcontext(state)
  553. }
  554. }
  555. } else if (endTag) {
  556. // Assume all tags with a closing tag are defined in the config.
  557. tagError = true
  558. }
  559. return (tagError ? 'error ' : '') + 'keyword'
  560. // Not a tag-keyword; it's an implicit print tag.
  561. } else if (stream.eat('{')) {
  562. state.tag = 'print'
  563. state.indent += 2 * config.indentUnit
  564. state.soyState.push('tag')
  565. return 'keyword'
  566. } else if (!state.context && stream.sol() && stream.match(/import\b/)) {
  567. state.soyState.push('import')
  568. state.indent += 2 * config.indentUnit
  569. return 'keyword'
  570. } else if ((match = stream.match('<{'))) {
  571. state.soyState.push('template-call-expression')
  572. state.indent += 2 * config.indentUnit
  573. state.soyState.push('tag')
  574. return 'keyword'
  575. } else if ((match = stream.match('</>'))) {
  576. state.indent -= 1 * config.indentUnit
  577. return 'keyword'
  578. }
  579. return tokenUntil(stream, state, /\{|\s+\/\/|\/\*/)
  580. },
  581. indent: function (state, textAfter, line) {
  582. var indent = state.indent,
  583. top = last(state.soyState)
  584. if (top == 'comment') return CodeMirror.Pass
  585. if (top == 'literal') {
  586. if (/^\{\/literal}/.test(textAfter)) indent -= config.indentUnit
  587. } else {
  588. if (/^\s*\{\/(template|deltemplate)\b/.test(textAfter)) return 0
  589. if (/^\{(\/|(fallbackmsg|elseif|else|ifempty)\b)/.test(textAfter)) indent -= config.indentUnit
  590. if (state.tag != 'switch' && /^\{(case|default)\b/.test(textAfter)) indent -= config.indentUnit
  591. if (/^\{\/switch\b/.test(textAfter)) indent -= config.indentUnit
  592. }
  593. var localState = last(state.localStates)
  594. if (indent && localState.mode.indent) {
  595. indent += localState.mode.indent(localState.state, textAfter, line)
  596. }
  597. return indent
  598. },
  599. innerMode: function (state) {
  600. if (state.soyState.length && last(state.soyState) != 'literal') return null
  601. else return last(state.localStates)
  602. },
  603. electricInput: /^\s*\{(\/|\/template|\/deltemplate|\/switch|fallbackmsg|elseif|else|case|default|ifempty|\/literal\})$/,
  604. lineComment: '//',
  605. blockCommentStart: '/*',
  606. blockCommentEnd: '*/',
  607. blockCommentContinue: ' * ',
  608. useInnerComments: false,
  609. fold: 'indent',
  610. }
  611. },
  612. 'htmlmixed'
  613. )
  614. CodeMirror.registerHelper('wordChars', 'soy', /[\w$]/)
  615. CodeMirror.registerHelper('hintWords', 'soy', Object.keys(tags).concat(['css', 'debugger']))
  616. CodeMirror.defineMIME('text/x-soy', 'soy')
  617. })