parser.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903
  1. var utils = require('./utils')
  2. var lexer = require('./lexer')
  3. var _t = lexer.types
  4. var _reserved = [
  5. 'break',
  6. 'case',
  7. 'catch',
  8. 'continue',
  9. 'debugger',
  10. 'default',
  11. 'delete',
  12. 'do',
  13. 'else',
  14. 'finally',
  15. 'for',
  16. 'function',
  17. 'if',
  18. 'in',
  19. 'instanceof',
  20. 'new',
  21. 'return',
  22. 'switch',
  23. 'this',
  24. 'throw',
  25. 'try',
  26. 'typeof',
  27. 'var',
  28. 'void',
  29. 'while',
  30. 'with'
  31. ]
  32. /**
  33. * Filters are simply functions that perform transformations on their first input argument.
  34. * Filters are run at render time, so they may not directly modify the compiled template structure in any way.
  35. * All of Swig's built-in filters are written in this same way. For more examples, reference the `filters.js` file in Swig's source.
  36. *
  37. * To disable auto-escaping on a custom filter, simply add a property to the filter method `safe = true;` and the output from this will not be escaped, no matter what the global settings are for Swig.
  38. *
  39. * @typedef {function} Filter
  40. *
  41. * @example
  42. * // This filter will return 'bazbop' if the idx on the input is not 'foobar'
  43. * swig.setFilter('foobar', function (input, idx) {
  44. * return input[idx] === 'foobar' ? input[idx] : 'bazbop';
  45. * });
  46. * // myvar = ['foo', 'bar', 'baz', 'bop'];
  47. * // => {{ myvar|foobar(3) }}
  48. * // Since myvar[3] !== 'foobar', we render:
  49. * // => bazbop
  50. *
  51. * @example
  52. * // This filter will disable auto-escaping on its output:
  53. * function bazbop (input) { return input; }
  54. * bazbop.safe = true;
  55. * swig.setFilter('bazbop', bazbop);
  56. * // => {{ "<p>"|bazbop }}
  57. * // => <p>
  58. *
  59. * @param {*} input Input argument, automatically sent from Swig's built-in parser.
  60. * @param {...*} [args] All other arguments are defined by the Filter author.
  61. * @return {*}
  62. */
  63. /*!
  64. * Makes a string safe for a regular expression.
  65. * @param {string} str
  66. * @return {string}
  67. * @private
  68. */
  69. function escapeRegExp (str) {
  70. return str.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
  71. }
  72. /**
  73. * Parse strings of variables and tags into tokens for future compilation.
  74. * @class
  75. * @param {array} tokens Pre-split tokens read by the Lexer.
  76. * @param {object} filters Keyed object of filters that may be applied to variables.
  77. * @param {boolean} autoescape Whether or not this should be autoescaped.
  78. * @param {number} line Beginning line number for the first token.
  79. * @param {string} [filename] Name of the file being parsed.
  80. * @private
  81. */
  82. function TokenParser (tokens, filters, autoescape, line, filename) {
  83. this.out = []
  84. this.state = []
  85. this.filterApplyIdx = []
  86. this._parsers = {}
  87. this.line = line
  88. this.filename = filename
  89. this.filters = filters
  90. this.escape = autoescape
  91. this.parse = function () {
  92. var self = this
  93. if (self._parsers.start) {
  94. self._parsers.start.call(self)
  95. }
  96. utils.each(tokens, function (token, i) {
  97. var prevToken = tokens[i - 1]
  98. self.isLast = i === tokens.length - 1
  99. if (prevToken) {
  100. while (prevToken.type === _t.WHITESPACE) {
  101. i -= 1
  102. prevToken = tokens[i - 1]
  103. }
  104. }
  105. self.prevToken = prevToken
  106. self.parseToken(token)
  107. })
  108. if (self._parsers.end) {
  109. self._parsers.end.call(self)
  110. }
  111. if (self.escape) {
  112. self.filterApplyIdx = [0]
  113. if (typeof self.escape === 'string') {
  114. self.parseToken({ type: _t.FILTER, match: 'e' })
  115. self.parseToken({ type: _t.COMMA, match: ',' })
  116. self.parseToken({ type: _t.STRING, match: String(autoescape) })
  117. self.parseToken({ type: _t.PARENCLOSE, match: ')' })
  118. } else {
  119. self.parseToken({ type: _t.FILTEREMPTY, match: 'e' })
  120. }
  121. }
  122. return self.out
  123. }
  124. }
  125. TokenParser.prototype = {
  126. /**
  127. * Set a custom method to be called when a token type is found.
  128. *
  129. * @example
  130. * parser.on(types.STRING, function (token) {
  131. * this.out.push(token.match);
  132. * });
  133. * @example
  134. * parser.on('start', function () {
  135. * this.out.push('something at the beginning of your args')
  136. * });
  137. * parser.on('end', function () {
  138. * this.out.push('something at the end of your args');
  139. * });
  140. *
  141. * @param {number} type Token type ID. Found in the Lexer.
  142. * @param {Function} fn Callback function. Return true to continue executing the default parsing function.
  143. * @return {undefined}
  144. */
  145. on: function (type, fn) {
  146. this._parsers[type] = fn
  147. },
  148. /**
  149. * Parse a single token.
  150. * @param {{match: string, type: number, line: number}} token Lexer token object.
  151. * @return {undefined}
  152. * @private
  153. */
  154. parseToken: function (token) {
  155. var self = this
  156. var fn = self._parsers[token.type] || self._parsers['*']
  157. var match = token.match
  158. var prevToken = self.prevToken
  159. var prevTokenType = prevToken ? prevToken.type : null
  160. var lastState = self.state.length ? self.state[self.state.length - 1] : null
  161. var temp
  162. if (fn && typeof fn === 'function') {
  163. if (!fn.call(this, token)) {
  164. return
  165. }
  166. }
  167. if (
  168. lastState &&
  169. prevToken &&
  170. lastState === _t.FILTER &&
  171. prevTokenType === _t.FILTER &&
  172. token.type !== _t.PARENCLOSE &&
  173. token.type !== _t.COMMA &&
  174. token.type !== _t.OPERATOR &&
  175. token.type !== _t.FILTER &&
  176. token.type !== _t.FILTEREMPTY
  177. ) {
  178. self.out.push(', ')
  179. }
  180. if (lastState && lastState === _t.METHODOPEN) {
  181. self.state.pop()
  182. if (token.type !== _t.PARENCLOSE) {
  183. self.out.push(', ')
  184. }
  185. }
  186. switch (token.type) {
  187. case _t.WHITESPACE:
  188. break
  189. case _t.STRING:
  190. self.filterApplyIdx.push(self.out.length)
  191. self.out.push(match.replace(/\\/g, '\\\\'))
  192. break
  193. case _t.NUMBER:
  194. case _t.BOOL:
  195. self.filterApplyIdx.push(self.out.length)
  196. self.out.push(match)
  197. break
  198. case _t.FILTER:
  199. if (
  200. !self.filters.hasOwnProperty(match) ||
  201. typeof self.filters[match] !== 'function'
  202. ) {
  203. utils.throwError(
  204. 'Invalid filter "' + match + '"',
  205. self.line,
  206. self.filename
  207. )
  208. }
  209. self.escape = self.filters[match].safe ? false : self.escape
  210. self.out.splice(
  211. self.filterApplyIdx[self.filterApplyIdx.length - 1],
  212. 0,
  213. '_filters["' + match + '"]('
  214. )
  215. self.state.push(token.type)
  216. break
  217. case _t.FILTEREMPTY:
  218. if (
  219. !self.filters.hasOwnProperty(match) ||
  220. typeof self.filters[match] !== 'function'
  221. ) {
  222. utils.throwError(
  223. 'Invalid filter "' + match + '"',
  224. self.line,
  225. self.filename
  226. )
  227. }
  228. self.escape = self.filters[match].safe ? false : self.escape
  229. self.out.splice(
  230. self.filterApplyIdx[self.filterApplyIdx.length - 1],
  231. 0,
  232. '_filters["' + match + '"]('
  233. )
  234. self.out.push(')')
  235. break
  236. case _t.FUNCTION:
  237. case _t.FUNCTIONEMPTY:
  238. self.out.push(
  239. '((typeof _ctx.' +
  240. match +
  241. ' !== "undefined") ? _ctx.' +
  242. match +
  243. ' : ((typeof ' +
  244. match +
  245. ' !== "undefined") ? ' +
  246. match +
  247. ' : _fn))('
  248. )
  249. self.escape = false
  250. if (token.type === _t.FUNCTIONEMPTY) {
  251. self.out[self.out.length - 1] = self.out[self.out.length - 1] + ')'
  252. } else {
  253. self.state.push(token.type)
  254. }
  255. self.filterApplyIdx.push(self.out.length - 1)
  256. break
  257. case _t.PARENOPEN:
  258. self.state.push(token.type)
  259. if (self.filterApplyIdx.length) {
  260. self.out.splice(
  261. self.filterApplyIdx[self.filterApplyIdx.length - 1],
  262. 0,
  263. '('
  264. )
  265. if (prevToken && prevTokenType === _t.VAR) {
  266. temp = prevToken.match.split('.').slice(0, -1)
  267. self.out.push(' || _fn).call(' + self.checkMatch(temp))
  268. self.state.push(_t.METHODOPEN)
  269. self.escape = false
  270. } else {
  271. self.out.push(' || _fn)(')
  272. }
  273. self.filterApplyIdx.push(self.out.length - 3)
  274. } else {
  275. self.out.push('(')
  276. self.filterApplyIdx.push(self.out.length - 1)
  277. }
  278. break
  279. case _t.PARENCLOSE:
  280. temp = self.state.pop()
  281. if (
  282. temp !== _t.PARENOPEN &&
  283. temp !== _t.FUNCTION &&
  284. temp !== _t.FILTER
  285. ) {
  286. utils.throwError('Mismatched nesting state', self.line, self.filename)
  287. }
  288. self.out.push(')')
  289. // Once off the previous entry
  290. self.filterApplyIdx.pop()
  291. if (temp !== _t.FILTER) {
  292. // Once for the open paren
  293. self.filterApplyIdx.pop()
  294. }
  295. break
  296. case _t.COMMA:
  297. if (
  298. lastState !== _t.FUNCTION &&
  299. lastState !== _t.FILTER &&
  300. lastState !== _t.ARRAYOPEN &&
  301. lastState !== _t.CURLYOPEN &&
  302. lastState !== _t.PARENOPEN &&
  303. lastState !== _t.COLON
  304. ) {
  305. utils.throwError('Unexpected comma', self.line, self.filename)
  306. }
  307. if (lastState === _t.COLON) {
  308. self.state.pop()
  309. }
  310. self.out.push(', ')
  311. self.filterApplyIdx.pop()
  312. break
  313. case _t.LOGIC:
  314. case _t.COMPARATOR:
  315. if (
  316. !prevToken ||
  317. prevTokenType === _t.COMMA ||
  318. prevTokenType === token.type ||
  319. prevTokenType === _t.BRACKETOPEN ||
  320. prevTokenType === _t.CURLYOPEN ||
  321. prevTokenType === _t.PARENOPEN ||
  322. prevTokenType === _t.FUNCTION
  323. ) {
  324. utils.throwError('Unexpected logic', self.line, self.filename)
  325. }
  326. self.out.push(token.match)
  327. break
  328. case _t.NOT:
  329. self.out.push(token.match)
  330. break
  331. case _t.VAR:
  332. self.parseVar(token, match, lastState)
  333. break
  334. case _t.BRACKETOPEN:
  335. if (
  336. !prevToken ||
  337. (prevTokenType !== _t.VAR &&
  338. prevTokenType !== _t.BRACKETCLOSE &&
  339. prevTokenType !== _t.PARENCLOSE)
  340. ) {
  341. self.state.push(_t.ARRAYOPEN)
  342. self.filterApplyIdx.push(self.out.length)
  343. } else {
  344. self.state.push(token.type)
  345. }
  346. self.out.push('[')
  347. break
  348. case _t.BRACKETCLOSE:
  349. temp = self.state.pop()
  350. if (temp !== _t.BRACKETOPEN && temp !== _t.ARRAYOPEN) {
  351. utils.throwError(
  352. 'Unexpected closing square bracket',
  353. self.line,
  354. self.filename
  355. )
  356. }
  357. self.out.push(']')
  358. self.filterApplyIdx.pop()
  359. break
  360. case _t.CURLYOPEN:
  361. self.state.push(token.type)
  362. self.out.push('{')
  363. self.filterApplyIdx.push(self.out.length - 1)
  364. break
  365. case _t.COLON:
  366. if (lastState !== _t.CURLYOPEN) {
  367. utils.throwError('Unexpected colon', self.line, self.filename)
  368. }
  369. self.state.push(token.type)
  370. self.out.push(':')
  371. self.filterApplyIdx.pop()
  372. break
  373. case _t.CURLYCLOSE:
  374. if (lastState === _t.COLON) {
  375. self.state.pop()
  376. }
  377. if (self.state.pop() !== _t.CURLYOPEN) {
  378. utils.throwError(
  379. 'Unexpected closing curly brace',
  380. self.line,
  381. self.filename
  382. )
  383. }
  384. self.out.push('}')
  385. self.filterApplyIdx.pop()
  386. break
  387. case _t.DOTKEY:
  388. if (
  389. !prevToken ||
  390. (prevTokenType !== _t.VAR &&
  391. prevTokenType !== _t.BRACKETCLOSE &&
  392. prevTokenType !== _t.DOTKEY &&
  393. prevTokenType !== _t.PARENCLOSE &&
  394. prevTokenType !== _t.FUNCTIONEMPTY &&
  395. prevTokenType !== _t.FILTEREMPTY &&
  396. prevTokenType !== _t.CURLYCLOSE)
  397. ) {
  398. utils.throwError(
  399. 'Unexpected key "' + match + '"',
  400. self.line,
  401. self.filename
  402. )
  403. }
  404. self.out.push('.' + match)
  405. break
  406. case _t.OPERATOR:
  407. self.out.push(' ' + match + ' ')
  408. self.filterApplyIdx.pop()
  409. break
  410. }
  411. },
  412. /**
  413. * Parse variable token
  414. * @param {{match: string, type: number, line: number}} token Lexer token object.
  415. * @param {string} match Shortcut for token.match
  416. * @param {number} lastState Lexer token type state.
  417. * @return {undefined}
  418. * @private
  419. */
  420. parseVar: function (token, match, lastState) {
  421. var self = this
  422. match = match.split('.')
  423. if (_reserved.indexOf(match[0]) !== -1) {
  424. utils.throwError(
  425. 'Reserved keyword "' +
  426. match[0] +
  427. '" attempted to be used as a variable',
  428. self.line,
  429. self.filename
  430. )
  431. }
  432. self.filterApplyIdx.push(self.out.length)
  433. if (lastState === _t.CURLYOPEN) {
  434. if (match.length > 1) {
  435. utils.throwError('Unexpected dot', self.line, self.filename)
  436. }
  437. self.out.push(match[0])
  438. return
  439. }
  440. self.out.push(self.checkMatch(match))
  441. },
  442. /**
  443. * Return contextual dot-check string for a match
  444. * @param {string} match Shortcut for token.match
  445. * @private
  446. */
  447. checkMatch: function (match) {
  448. var temp = match[0]
  449. var result
  450. function checkDot (ctx) {
  451. var c = ctx + temp
  452. var m = match
  453. var build = ''
  454. build = '(typeof ' + c + ' !== "undefined" && ' + c + ' !== null'
  455. utils.each(m, function (v, i) {
  456. if (i === 0) {
  457. return
  458. }
  459. build +=
  460. ' && ' +
  461. c +
  462. '.' +
  463. v +
  464. ' !== undefined && ' +
  465. c +
  466. '.' +
  467. v +
  468. ' !== null'
  469. c += '.' + v
  470. })
  471. build += ')'
  472. return build
  473. }
  474. function buildDot (ctx) {
  475. return '(' + checkDot(ctx) + ' ? ' + ctx + match.join('.') + ' : "")'
  476. }
  477. result =
  478. '(' +
  479. checkDot('_ctx.') +
  480. ' ? ' +
  481. buildDot('_ctx.') +
  482. ' : ' +
  483. buildDot('') +
  484. ')'
  485. return '(' + result + ' !== null ? ' + result + ' : ' + '"" )'
  486. }
  487. }
  488. /**
  489. * Parse a source string into tokens that are ready for compilation.
  490. *
  491. * @example
  492. * exports.parse('{{ tacos }}', {}, tags, filters);
  493. * // => [{ compile: [Function], ... }]
  494. *
  495. * @params {object} swig The current Swig instance
  496. * @param {string} source Swig template source.
  497. * @param {object} opts Swig options object.
  498. * @param {object} tags Keyed object of tags that can be parsed and compiled.
  499. * @param {object} filters Keyed object of filters that may be applied to variables.
  500. * @return {array} List of tokens ready for compilation.
  501. */
  502. exports.parse = function (swig, source, opts, tags, filters) {
  503. source = source.replace(/\r\n/g, '\n')
  504. var escape = opts.autoescape
  505. var tagOpen = opts.tagControls[0]
  506. var tagClose = opts.tagControls[1]
  507. var varOpen = opts.varControls[0]
  508. var varClose = opts.varControls[1]
  509. var escapedTagOpen = escapeRegExp(tagOpen)
  510. var escapedTagClose = escapeRegExp(tagClose)
  511. var escapedVarOpen = escapeRegExp(varOpen)
  512. var escapedVarClose = escapeRegExp(varClose)
  513. var tagStrip = new RegExp(
  514. '^' + escapedTagOpen + '-?\\s*-?|-?\\s*-?' + escapedTagClose + '$',
  515. 'g'
  516. )
  517. var tagStripBefore = new RegExp('^' + escapedTagOpen + '-')
  518. var tagStripAfter = new RegExp('-' + escapedTagClose + '$')
  519. var varStrip = new RegExp(
  520. '^' + escapedVarOpen + '-?\\s*-?|-?\\s*-?' + escapedVarClose + '$',
  521. 'g'
  522. )
  523. var varStripBefore = new RegExp('^' + escapedVarOpen + '-')
  524. var varStripAfter = new RegExp('-' + escapedVarClose + '$')
  525. var cmtOpen = opts.cmtControls[0]
  526. var cmtClose = opts.cmtControls[1]
  527. var anyChar = '[\\s\\S]*?'
  528. // Split the template source based on variable, tag, and comment blocks
  529. // /(\{%[\s\S]*?%\}|\{\{[\s\S]*?\}\}|\{#[\s\S]*?#\})/
  530. var splitter = new RegExp(
  531. '(' +
  532. escapedTagOpen +
  533. anyChar +
  534. escapedTagClose +
  535. '|' +
  536. escapedVarOpen +
  537. anyChar +
  538. escapedVarClose +
  539. '|' +
  540. escapeRegExp(cmtOpen) +
  541. anyChar +
  542. escapeRegExp(cmtClose) +
  543. ')'
  544. )
  545. var line = 1
  546. var stack = []
  547. var parent = null
  548. var tokens = []
  549. var blocks = {}
  550. var inRaw = false
  551. var stripNext
  552. /**
  553. * Parse a variable.
  554. * @param {string} str String contents of the variable, between <i>{{</i> and <i>}}</i>
  555. * @param {number} line The line number that this variable starts on.
  556. * @return {VarToken} Parsed variable token object.
  557. * @private
  558. */
  559. function parseVariable (str, line) {
  560. var lexedTokens = lexer.read(utils.strip(str))
  561. var parser
  562. var out
  563. parser = new TokenParser(lexedTokens, filters, escape, line, opts.filename)
  564. out = parser.parse().join('')
  565. if (parser.state.length) {
  566. utils.throwError('Unable to parse "' + str + '"', line, opts.filename)
  567. }
  568. /**
  569. * A parsed variable token.
  570. * @typedef {object} VarToken
  571. * @property {function} compile Method for compiling this token.
  572. */
  573. return {
  574. compile: function () {
  575. return '_output += ' + out + ';\n'
  576. }
  577. }
  578. }
  579. exports.parseVariable = parseVariable
  580. /**
  581. * Parse a tag.
  582. * @param {string} str String contents of the tag, between <i>{%</i> and <i>%}</i>
  583. * @param {number} line The line number that this tag starts on.
  584. * @return {TagToken} Parsed token object.
  585. * @private
  586. */
  587. function parseTag (str, line) {
  588. var lexedTokens, parser, chunks, tagName, tag, args, last
  589. if (utils.startsWith(str, 'end')) {
  590. last = stack[stack.length - 1]
  591. if (
  592. last &&
  593. last.name === str.split(/\s+/)[0].replace(/^end/, '') &&
  594. last.ends
  595. ) {
  596. switch (last.name) {
  597. case 'autoescape':
  598. escape = opts.autoescape
  599. break
  600. case 'raw':
  601. inRaw = false
  602. break
  603. }
  604. stack.pop()
  605. return
  606. }
  607. if (!inRaw) {
  608. utils.throwError(
  609. 'Unexpected end of tag "' + str.replace(/^end/, '') + '"',
  610. line,
  611. opts.filename
  612. )
  613. }
  614. }
  615. if (inRaw) {
  616. return
  617. }
  618. chunks = str.split(/\s+(.+)?/)
  619. tagName = chunks.shift()
  620. if (!tags.hasOwnProperty(tagName)) {
  621. utils.throwError('Unexpected tag "' + str + '"', line, opts.filename)
  622. }
  623. lexedTokens = lexer.read(utils.strip(chunks.join(' ')))
  624. parser = new TokenParser(lexedTokens, filters, false, line, opts.filename)
  625. tag = tags[tagName]
  626. /**
  627. * Define custom parsing methods for your tag.
  628. * @callback parse
  629. *
  630. * @example
  631. * exports.parse = function (str, line, parser, types, options, swig) {
  632. * parser.on('start', function () {
  633. * // ...
  634. * });
  635. * parser.on(types.STRING, function (token) {
  636. * // ...
  637. * });
  638. * };
  639. *
  640. * @param {string} str The full token string of the tag.
  641. * @param {number} line The line number that this tag appears on.
  642. * @param {TokenParser} parser A TokenParser instance.
  643. * @param {TYPES} types Lexer token type enum.
  644. * @param {TagToken[]} stack The current stack of open tags.
  645. * @param {SwigOpts} options Swig Options Object.
  646. * @param {object} swig The Swig instance (gives acces to loaders, parsers, etc)
  647. */
  648. if (!tag.parse(chunks[1], line, parser, _t, stack, opts, swig)) {
  649. utils.throwError('Unexpected tag "' + tagName + '"', line, opts.filename)
  650. }
  651. parser.parse()
  652. args = parser.out
  653. switch (tagName) {
  654. case 'autoescape':
  655. escape = args[0] !== 'false' ? args[0] : false
  656. break
  657. case 'raw':
  658. inRaw = true
  659. break
  660. }
  661. /**
  662. * A parsed tag token.
  663. * @typedef {Object} TagToken
  664. * @property {compile} [compile] Method for compiling this token.
  665. * @property {array} [args] Array of arguments for the tag.
  666. * @property {Token[]} [content=[]] An array of tokens that are children of this Token.
  667. * @property {boolean} [ends] Whether or not this tag requires an end tag.
  668. * @property {string} name The name of this tag.
  669. */
  670. return {
  671. block: !!tags[tagName].block,
  672. compile: tag.compile,
  673. args: args,
  674. content: [],
  675. ends: tag.ends,
  676. name: tagName
  677. }
  678. }
  679. /**
  680. * Strip the whitespace from the previous token, if it is a string.
  681. * @param {object} token Parsed token.
  682. * @return {object} If the token was a string, trailing whitespace will be stripped.
  683. */
  684. function stripPrevToken (token) {
  685. if (typeof token === 'string') {
  686. token = token.replace(/\s*$/, '')
  687. }
  688. return token
  689. }
  690. /*!
  691. * Loop over the source, split via the tag/var/comment regular expression splitter.
  692. * Send each chunk to the appropriate parser.
  693. */
  694. utils.each(source.split(splitter), function (chunk) {
  695. var token, lines, stripPrev, prevToken, prevChildToken
  696. if (!chunk) {
  697. return
  698. }
  699. // Is a variable?
  700. if (
  701. !inRaw &&
  702. utils.startsWith(chunk, varOpen) &&
  703. utils.endsWith(chunk, varClose)
  704. ) {
  705. stripPrev = varStripBefore.test(chunk)
  706. stripNext = varStripAfter.test(chunk)
  707. token = parseVariable(chunk.replace(varStrip, ''), line)
  708. // Is a tag?
  709. } else if (
  710. utils.startsWith(chunk, tagOpen) &&
  711. utils.endsWith(chunk, tagClose)
  712. ) {
  713. stripPrev = tagStripBefore.test(chunk)
  714. stripNext = tagStripAfter.test(chunk)
  715. token = parseTag(chunk.replace(tagStrip, ''), line)
  716. if (token) {
  717. if (token.name === 'extends') {
  718. parent = token.args
  719. .join('')
  720. .replace(/^'|'$/g, '')
  721. .replace(/^"|"$/g, '')
  722. } else if (token.block && !stack.length) {
  723. blocks[token.args.join('')] = token
  724. }
  725. }
  726. if (inRaw && !token) {
  727. token = chunk
  728. }
  729. // Is a content string?
  730. } else if (
  731. inRaw ||
  732. (!utils.startsWith(chunk, cmtOpen) && !utils.endsWith(chunk, cmtClose))
  733. ) {
  734. token = stripNext ? chunk.replace(/^\s*/, '') : chunk
  735. stripNext = false
  736. } else if (
  737. utils.startsWith(chunk, cmtOpen) &&
  738. utils.endsWith(chunk, cmtClose)
  739. ) {
  740. return
  741. }
  742. // Did this tag ask to strip previous whitespace? <code>{%- ... %}</code> or <code>{{- ... }}</code>
  743. if (stripPrev && tokens.length) {
  744. prevToken = tokens.pop()
  745. if (typeof prevToken === 'string') {
  746. prevToken = stripPrevToken(prevToken)
  747. } else if (prevToken.content && prevToken.content.length) {
  748. prevChildToken = stripPrevToken(prevToken.content.pop())
  749. prevToken.content.push(prevChildToken)
  750. }
  751. tokens.push(prevToken)
  752. }
  753. // This was a comment, so let's just keep going.
  754. if (!token) {
  755. return
  756. }
  757. // If there's an open item in the stack, add this to its content.
  758. if (stack.length) {
  759. stack[stack.length - 1].content.push(token)
  760. } else {
  761. tokens.push(token)
  762. }
  763. // If the token is a tag that requires an end tag, open it on the stack.
  764. if (token.name && token.ends) {
  765. stack.push(token)
  766. }
  767. lines = chunk.match(/\n/g)
  768. line += lines ? lines.length : 0
  769. })
  770. return {
  771. name: opts.filename,
  772. parent: parent,
  773. tokens: tokens,
  774. blocks: blocks
  775. }
  776. }
  777. /**
  778. * Compile an array of tokens.
  779. * @param {Token[]} template An array of template tokens.
  780. * @param {Templates[]} parents Array of parent templates.
  781. * @param {SwigOpts} [options] Swig options object.
  782. * @param {string} [blockName] Name of the current block context.
  783. * @return {string} Partial for a compiled JavaScript method that will output a rendered template.
  784. */
  785. exports.compile = function (template, parents, options, blockName) {
  786. var out = ''
  787. var tokens = utils.isArray(template) ? template : template.tokens
  788. utils.each(tokens, function (token) {
  789. var o
  790. if (typeof token === 'string') {
  791. out +=
  792. '_output += "' +
  793. token
  794. .replace(/\\/g, '\\\\')
  795. .replace(/\n|\r/g, '\\n')
  796. .replace(/"/g, '\\"') +
  797. '";\n'
  798. return
  799. }
  800. /**
  801. * Compile callback for VarToken and TagToken objects.
  802. * @callback compile
  803. *
  804. * @example
  805. * exports.compile = function (compiler, args, content, parents, options, blockName) {
  806. * if (args[0] === 'foo') {
  807. * return compiler(content, parents, options, blockName) + '\n';
  808. * }
  809. * return '_output += "fallback";\n';
  810. * };
  811. *
  812. * @param {parserCompiler} compiler
  813. * @param {array} [args] Array of parsed arguments on the for the token.
  814. * @param {array} [content] Array of content within the token.
  815. * @param {array} [parents] Array of parent templates for the current template context.
  816. * @param {SwigOpts} [options] Swig Options Object
  817. * @param {string} [blockName] Name of the direct block parent, if any.
  818. */
  819. o = token.compile(
  820. exports.compile,
  821. token.args ? token.args.slice(0) : [],
  822. token.content ? token.content.slice(0) : [],
  823. parents,
  824. options,
  825. blockName
  826. )
  827. out += o || ''
  828. })
  829. return out
  830. }