12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301 |
- 'use strict';
- var assert = require('assert');
- var TokenStream = require('token-stream');
- var error = require('pug-error');
- var inlineTags = require('./lib/inline-tags');
- module.exports = parse;
- module.exports.Parser = Parser;
- function parse(tokens, options) {
- var parser = new Parser(tokens, options);
- var ast = parser.parse();
- return JSON.parse(JSON.stringify(ast));
- }
- /**
- * Initialize `Parser` with the given input `str` and `filename`.
- *
- * @param {String} str
- * @param {String} filename
- * @param {Object} options
- * @api public
- */
- function Parser(tokens, options) {
- options = options || {};
- if (!Array.isArray(tokens)) {
- throw new Error(
- 'Expected tokens to be an Array but got "' + typeof tokens + '"'
- );
- }
- if (typeof options !== 'object') {
- throw new Error(
- 'Expected "options" to be an object but got "' + typeof options + '"'
- );
- }
- this.tokens = new TokenStream(tokens);
- this.filename = options.filename;
- this.src = options.src;
- this.inMixin = 0;
- this.plugins = options.plugins || [];
- }
- /**
- * Parser prototype.
- */
- Parser.prototype = {
- /**
- * Save original constructor
- */
- constructor: Parser,
- error: function(code, message, token) {
- var err = error(code, message, {
- line: token.loc.start.line,
- column: token.loc.start.column,
- filename: this.filename,
- src: this.src,
- });
- throw err;
- },
- /**
- * Return the next token object.
- *
- * @return {Object}
- * @api private
- */
- advance: function() {
- return this.tokens.advance();
- },
- /**
- * Single token lookahead.
- *
- * @return {Object}
- * @api private
- */
- peek: function() {
- return this.tokens.peek();
- },
- /**
- * `n` token lookahead.
- *
- * @param {Number} n
- * @return {Object}
- * @api private
- */
- lookahead: function(n) {
- return this.tokens.lookahead(n);
- },
- /**
- * Parse input returning a string of js for evaluation.
- *
- * @return {String}
- * @api public
- */
- parse: function() {
- var block = this.emptyBlock(0);
- while ('eos' != this.peek().type) {
- if ('newline' == this.peek().type) {
- this.advance();
- } else if ('text-html' == this.peek().type) {
- block.nodes = block.nodes.concat(this.parseTextHtml());
- } else {
- var expr = this.parseExpr();
- if (expr) {
- if (expr.type === 'Block') {
- block.nodes = block.nodes.concat(expr.nodes);
- } else {
- block.nodes.push(expr);
- }
- }
- }
- }
- return block;
- },
- /**
- * Expect the given type, or throw an exception.
- *
- * @param {String} type
- * @api private
- */
- expect: function(type) {
- if (this.peek().type === type) {
- return this.advance();
- } else {
- this.error(
- 'INVALID_TOKEN',
- 'expected "' + type + '", but got "' + this.peek().type + '"',
- this.peek()
- );
- }
- },
- /**
- * Accept the given `type`.
- *
- * @param {String} type
- * @api private
- */
- accept: function(type) {
- if (this.peek().type === type) {
- return this.advance();
- }
- },
- initBlock: function(line, nodes) {
- /* istanbul ignore if */
- if ((line | 0) !== line) throw new Error('`line` is not an integer');
- /* istanbul ignore if */
- if (!Array.isArray(nodes)) throw new Error('`nodes` is not an array');
- return {
- type: 'Block',
- nodes: nodes,
- line: line,
- filename: this.filename,
- };
- },
- emptyBlock: function(line) {
- return this.initBlock(line, []);
- },
- runPlugin: function(context, tok) {
- var rest = [this];
- for (var i = 2; i < arguments.length; i++) {
- rest.push(arguments[i]);
- }
- var pluginContext;
- for (var i = 0; i < this.plugins.length; i++) {
- var plugin = this.plugins[i];
- if (plugin[context] && plugin[context][tok.type]) {
- if (pluginContext)
- throw new Error(
- 'Multiple plugin handlers found for context ' +
- JSON.stringify(context) +
- ', token type ' +
- JSON.stringify(tok.type)
- );
- pluginContext = plugin[context];
- }
- }
- if (pluginContext)
- return pluginContext[tok.type].apply(pluginContext, rest);
- },
- /**
- * tag
- * | doctype
- * | mixin
- * | include
- * | filter
- * | comment
- * | text
- * | text-html
- * | dot
- * | each
- * | code
- * | yield
- * | id
- * | class
- * | interpolation
- */
- parseExpr: function() {
- switch (this.peek().type) {
- case 'tag':
- return this.parseTag();
- case 'mixin':
- return this.parseMixin();
- case 'block':
- return this.parseBlock();
- case 'mixin-block':
- return this.parseMixinBlock();
- case 'case':
- return this.parseCase();
- case 'extends':
- return this.parseExtends();
- case 'include':
- return this.parseInclude();
- case 'doctype':
- return this.parseDoctype();
- case 'filter':
- return this.parseFilter();
- case 'comment':
- return this.parseComment();
- case 'text':
- case 'interpolated-code':
- case 'start-pug-interpolation':
- return this.parseText({block: true});
- case 'text-html':
- return this.initBlock(this.peek().loc.start.line, this.parseTextHtml());
- case 'dot':
- return this.parseDot();
- case 'each':
- return this.parseEach();
- case 'eachOf':
- return this.parseEachOf();
- case 'code':
- return this.parseCode();
- case 'blockcode':
- return this.parseBlockCode();
- case 'if':
- return this.parseConditional();
- case 'while':
- return this.parseWhile();
- case 'call':
- return this.parseCall();
- case 'interpolation':
- return this.parseInterpolation();
- case 'yield':
- return this.parseYield();
- case 'id':
- case 'class':
- if (!this.peek().loc.start) debugger;
- this.tokens.defer({
- type: 'tag',
- val: 'div',
- loc: this.peek().loc,
- filename: this.filename,
- });
- return this.parseExpr();
- default:
- var pluginResult = this.runPlugin('expressionTokens', this.peek());
- if (pluginResult) return pluginResult;
- this.error(
- 'INVALID_TOKEN',
- 'unexpected token "' + this.peek().type + '"',
- this.peek()
- );
- }
- },
- parseDot: function() {
- this.advance();
- return this.parseTextBlock();
- },
- /**
- * Text
- */
- parseText: function(options) {
- var tags = [];
- var lineno = this.peek().loc.start.line;
- var nextTok = this.peek();
- loop: while (true) {
- switch (nextTok.type) {
- case 'text':
- var tok = this.advance();
- tags.push({
- type: 'Text',
- val: tok.val,
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- });
- break;
- case 'interpolated-code':
- var tok = this.advance();
- tags.push({
- type: 'Code',
- val: tok.val,
- buffer: tok.buffer,
- mustEscape: tok.mustEscape !== false,
- isInline: true,
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- });
- break;
- case 'newline':
- if (!options || !options.block) break loop;
- var tok = this.advance();
- var nextType = this.peek().type;
- if (nextType === 'text' || nextType === 'interpolated-code') {
- tags.push({
- type: 'Text',
- val: '\n',
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- });
- }
- break;
- case 'start-pug-interpolation':
- this.advance();
- tags.push(this.parseExpr());
- this.expect('end-pug-interpolation');
- break;
- default:
- var pluginResult = this.runPlugin('textTokens', nextTok, tags);
- if (pluginResult) break;
- break loop;
- }
- nextTok = this.peek();
- }
- if (tags.length === 1) return tags[0];
- else return this.initBlock(lineno, tags);
- },
- parseTextHtml: function() {
- var nodes = [];
- var currentNode = null;
- loop: while (true) {
- switch (this.peek().type) {
- case 'text-html':
- var text = this.advance();
- if (!currentNode) {
- currentNode = {
- type: 'Text',
- val: text.val,
- filename: this.filename,
- line: text.loc.start.line,
- column: text.loc.start.column,
- isHtml: true,
- };
- nodes.push(currentNode);
- } else {
- currentNode.val += '\n' + text.val;
- }
- break;
- case 'indent':
- var block = this.block();
- block.nodes.forEach(function(node) {
- if (node.isHtml) {
- if (!currentNode) {
- currentNode = node;
- nodes.push(currentNode);
- } else {
- currentNode.val += '\n' + node.val;
- }
- } else {
- currentNode = null;
- nodes.push(node);
- }
- });
- break;
- case 'code':
- currentNode = null;
- nodes.push(this.parseCode(true));
- break;
- case 'newline':
- this.advance();
- break;
- default:
- break loop;
- }
- }
- return nodes;
- },
- /**
- * ':' expr
- * | block
- */
- parseBlockExpansion: function() {
- var tok = this.accept(':');
- if (tok) {
- var expr = this.parseExpr();
- return expr.type === 'Block'
- ? expr
- : this.initBlock(tok.loc.start.line, [expr]);
- } else {
- return this.block();
- }
- },
- /**
- * case
- */
- parseCase: function() {
- var tok = this.expect('case');
- var node = {
- type: 'Case',
- expr: tok.val,
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- };
- var block = this.emptyBlock(tok.loc.start.line + 1);
- this.expect('indent');
- while ('outdent' != this.peek().type) {
- switch (this.peek().type) {
- case 'comment':
- case 'newline':
- this.advance();
- break;
- case 'when':
- block.nodes.push(this.parseWhen());
- break;
- case 'default':
- block.nodes.push(this.parseDefault());
- break;
- default:
- var pluginResult = this.runPlugin('caseTokens', this.peek(), block);
- if (pluginResult) break;
- this.error(
- 'INVALID_TOKEN',
- 'Unexpected token "' +
- this.peek().type +
- '", expected "when", "default" or "newline"',
- this.peek()
- );
- }
- }
- this.expect('outdent');
- node.block = block;
- return node;
- },
- /**
- * when
- */
- parseWhen: function() {
- var tok = this.expect('when');
- if (this.peek().type !== 'newline') {
- return {
- type: 'When',
- expr: tok.val,
- block: this.parseBlockExpansion(),
- debug: false,
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- };
- } else {
- return {
- type: 'When',
- expr: tok.val,
- debug: false,
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- };
- }
- },
- /**
- * default
- */
- parseDefault: function() {
- var tok = this.expect('default');
- return {
- type: 'When',
- expr: 'default',
- block: this.parseBlockExpansion(),
- debug: false,
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- };
- },
- /**
- * code
- */
- parseCode: function(noBlock) {
- var tok = this.expect('code');
- assert(
- typeof tok.mustEscape === 'boolean',
- 'Please update to the newest version of pug-lexer.'
- );
- var node = {
- type: 'Code',
- val: tok.val,
- buffer: tok.buffer,
- mustEscape: tok.mustEscape !== false,
- isInline: !!noBlock,
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- };
- // todo: why is this here? It seems like a hacky workaround
- if (node.val.match(/^ *else/)) node.debug = false;
- if (noBlock) return node;
- var block;
- // handle block
- block = 'indent' == this.peek().type;
- if (block) {
- if (tok.buffer) {
- this.error(
- 'BLOCK_IN_BUFFERED_CODE',
- 'Buffered code cannot have a block attached to it',
- this.peek()
- );
- }
- node.block = this.block();
- }
- return node;
- },
- parseConditional: function() {
- var tok = this.expect('if');
- var node = {
- type: 'Conditional',
- test: tok.val,
- consequent: this.emptyBlock(tok.loc.start.line),
- alternate: null,
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- };
- // handle block
- if ('indent' == this.peek().type) {
- node.consequent = this.block();
- }
- var currentNode = node;
- while (true) {
- if (this.peek().type === 'newline') {
- this.expect('newline');
- } else if (this.peek().type === 'else-if') {
- tok = this.expect('else-if');
- currentNode = currentNode.alternate = {
- type: 'Conditional',
- test: tok.val,
- consequent: this.emptyBlock(tok.loc.start.line),
- alternate: null,
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- };
- if ('indent' == this.peek().type) {
- currentNode.consequent = this.block();
- }
- } else if (this.peek().type === 'else') {
- this.expect('else');
- if (this.peek().type === 'indent') {
- currentNode.alternate = this.block();
- }
- break;
- } else {
- break;
- }
- }
- return node;
- },
- parseWhile: function() {
- var tok = this.expect('while');
- var node = {
- type: 'While',
- test: tok.val,
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- };
- // handle block
- if ('indent' == this.peek().type) {
- node.block = this.block();
- } else {
- node.block = this.emptyBlock(tok.loc.start.line);
- }
- return node;
- },
- /**
- * block code
- */
- parseBlockCode: function() {
- var tok = this.expect('blockcode');
- var line = tok.loc.start.line;
- var column = tok.loc.start.column;
- var body = this.peek();
- var text = '';
- if (body.type === 'start-pipeless-text') {
- this.advance();
- while (this.peek().type !== 'end-pipeless-text') {
- tok = this.advance();
- switch (tok.type) {
- case 'text':
- text += tok.val;
- break;
- case 'newline':
- text += '\n';
- break;
- default:
- var pluginResult = this.runPlugin('blockCodeTokens', tok, tok);
- if (pluginResult) {
- text += pluginResult;
- break;
- }
- this.error(
- 'INVALID_TOKEN',
- 'Unexpected token type: ' + tok.type,
- tok
- );
- }
- }
- this.advance();
- }
- return {
- type: 'Code',
- val: text,
- buffer: false,
- mustEscape: false,
- isInline: false,
- line: line,
- column: column,
- filename: this.filename,
- };
- },
- /**
- * comment
- */
- parseComment: function() {
- var tok = this.expect('comment');
- var block;
- if ((block = this.parseTextBlock())) {
- return {
- type: 'BlockComment',
- val: tok.val,
- block: block,
- buffer: tok.buffer,
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- };
- } else {
- return {
- type: 'Comment',
- val: tok.val,
- buffer: tok.buffer,
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- };
- }
- },
- /**
- * doctype
- */
- parseDoctype: function() {
- var tok = this.expect('doctype');
- return {
- type: 'Doctype',
- val: tok.val,
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- };
- },
- parseIncludeFilter: function() {
- var tok = this.expect('filter');
- var attrs = [];
- if (this.peek().type === 'start-attributes') {
- attrs = this.attrs();
- }
- return {
- type: 'IncludeFilter',
- name: tok.val,
- attrs: attrs,
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- };
- },
- /**
- * filter attrs? text-block
- */
- parseFilter: function() {
- var tok = this.expect('filter');
- var block,
- attrs = [];
- if (this.peek().type === 'start-attributes') {
- attrs = this.attrs();
- }
- if (this.peek().type === 'text') {
- var textToken = this.advance();
- block = this.initBlock(textToken.loc.start.line, [
- {
- type: 'Text',
- val: textToken.val,
- line: textToken.loc.start.line,
- column: textToken.loc.start.column,
- filename: this.filename,
- },
- ]);
- } else if (this.peek().type === 'filter') {
- block = this.initBlock(tok.loc.start.line, [this.parseFilter()]);
- } else {
- block = this.parseTextBlock() || this.emptyBlock(tok.loc.start.line);
- }
- return {
- type: 'Filter',
- name: tok.val,
- block: block,
- attrs: attrs,
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- };
- },
- /**
- * each block
- */
- parseEach: function() {
- var tok = this.expect('each');
- var node = {
- type: 'Each',
- obj: tok.code,
- val: tok.val,
- key: tok.key,
- block: this.block(),
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- };
- if (this.peek().type == 'else') {
- this.advance();
- node.alternate = this.block();
- }
- return node;
- },
- parseEachOf: function() {
- var tok = this.expect('eachOf');
- var node = {
- type: 'EachOf',
- obj: tok.code,
- val: tok.val,
- block: this.block(),
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- };
- return node;
- },
- /**
- * 'extends' name
- */
- parseExtends: function() {
- var tok = this.expect('extends');
- var path = this.expect('path');
- return {
- type: 'Extends',
- file: {
- type: 'FileReference',
- path: path.val.trim(),
- line: path.loc.start.line,
- column: path.loc.start.column,
- filename: this.filename,
- },
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- };
- },
- /**
- * 'block' name block
- */
- parseBlock: function() {
- var tok = this.expect('block');
- var node =
- 'indent' == this.peek().type
- ? this.block()
- : this.emptyBlock(tok.loc.start.line);
- node.type = 'NamedBlock';
- node.name = tok.val.trim();
- node.mode = tok.mode;
- node.line = tok.loc.start.line;
- node.column = tok.loc.start.column;
- return node;
- },
- parseMixinBlock: function() {
- var tok = this.expect('mixin-block');
- if (!this.inMixin) {
- this.error(
- 'BLOCK_OUTISDE_MIXIN',
- 'Anonymous blocks are not allowed unless they are part of a mixin.',
- tok
- );
- }
- return {
- type: 'MixinBlock',
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- };
- },
- parseYield: function() {
- var tok = this.expect('yield');
- return {
- type: 'YieldBlock',
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- };
- },
- /**
- * include block?
- */
- parseInclude: function() {
- var tok = this.expect('include');
- var node = {
- type: 'Include',
- file: {
- type: 'FileReference',
- filename: this.filename,
- },
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- };
- var filters = [];
- while (this.peek().type === 'filter') {
- filters.push(this.parseIncludeFilter());
- }
- var path = this.expect('path');
- node.file.path = path.val.trim();
- node.file.line = path.loc.start.line;
- node.file.column = path.loc.start.column;
- if (
- (/\.jade$/.test(node.file.path) || /\.pug$/.test(node.file.path)) &&
- !filters.length
- ) {
- node.block =
- 'indent' == this.peek().type
- ? this.block()
- : this.emptyBlock(tok.loc.start.line);
- if (/\.jade$/.test(node.file.path)) {
- console.warn(
- this.filename +
- ', line ' +
- tok.loc.start.line +
- ':\nThe .jade extension is deprecated, use .pug for "' +
- node.file.path +
- '".'
- );
- }
- } else {
- node.type = 'RawInclude';
- node.filters = filters;
- if (this.peek().type === 'indent') {
- this.error(
- 'RAW_INCLUDE_BLOCK',
- 'Raw inclusion cannot contain a block',
- this.peek()
- );
- }
- }
- return node;
- },
- /**
- * call ident block
- */
- parseCall: function() {
- var tok = this.expect('call');
- var name = tok.val;
- var args = tok.args;
- var mixin = {
- type: 'Mixin',
- name: name,
- args: args,
- block: this.emptyBlock(tok.loc.start.line),
- call: true,
- attrs: [],
- attributeBlocks: [],
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- };
- this.tag(mixin);
- if (mixin.code) {
- mixin.block.nodes.push(mixin.code);
- delete mixin.code;
- }
- if (mixin.block.nodes.length === 0) mixin.block = null;
- return mixin;
- },
- /**
- * mixin block
- */
- parseMixin: function() {
- var tok = this.expect('mixin');
- var name = tok.val;
- var args = tok.args;
- if ('indent' == this.peek().type) {
- this.inMixin++;
- var mixin = {
- type: 'Mixin',
- name: name,
- args: args,
- block: this.block(),
- call: false,
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- };
- this.inMixin--;
- return mixin;
- } else {
- this.error(
- 'MIXIN_WITHOUT_BODY',
- 'Mixin ' + name + ' declared without body',
- tok
- );
- }
- },
- /**
- * indent (text | newline)* outdent
- */
- parseTextBlock: function() {
- var tok = this.accept('start-pipeless-text');
- if (!tok) return;
- var block = this.emptyBlock(tok.loc.start.line);
- while (this.peek().type !== 'end-pipeless-text') {
- var tok = this.advance();
- switch (tok.type) {
- case 'text':
- block.nodes.push({
- type: 'Text',
- val: tok.val,
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- });
- break;
- case 'newline':
- block.nodes.push({
- type: 'Text',
- val: '\n',
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- });
- break;
- case 'start-pug-interpolation':
- block.nodes.push(this.parseExpr());
- this.expect('end-pug-interpolation');
- break;
- case 'interpolated-code':
- block.nodes.push({
- type: 'Code',
- val: tok.val,
- buffer: tok.buffer,
- mustEscape: tok.mustEscape !== false,
- isInline: true,
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- });
- break;
- default:
- var pluginResult = this.runPlugin('textBlockTokens', tok, block, tok);
- if (pluginResult) break;
- this.error(
- 'INVALID_TOKEN',
- 'Unexpected token type: ' + tok.type,
- tok
- );
- }
- }
- this.advance();
- return block;
- },
- /**
- * indent expr* outdent
- */
- block: function() {
- var tok = this.expect('indent');
- var block = this.emptyBlock(tok.loc.start.line);
- while ('outdent' != this.peek().type) {
- if ('newline' == this.peek().type) {
- this.advance();
- } else if ('text-html' == this.peek().type) {
- block.nodes = block.nodes.concat(this.parseTextHtml());
- } else {
- var expr = this.parseExpr();
- if (expr.type === 'Block') {
- block.nodes = block.nodes.concat(expr.nodes);
- } else {
- block.nodes.push(expr);
- }
- }
- }
- this.expect('outdent');
- return block;
- },
- /**
- * interpolation (attrs | class | id)* (text | code | ':')? newline* block?
- */
- parseInterpolation: function() {
- var tok = this.advance();
- var tag = {
- type: 'InterpolatedTag',
- expr: tok.val,
- selfClosing: false,
- block: this.emptyBlock(tok.loc.start.line),
- attrs: [],
- attributeBlocks: [],
- isInline: false,
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- };
- return this.tag(tag, {selfClosingAllowed: true});
- },
- /**
- * tag (attrs | class | id)* (text | code | ':')? newline* block?
- */
- parseTag: function() {
- var tok = this.advance();
- var tag = {
- type: 'Tag',
- name: tok.val,
- selfClosing: false,
- block: this.emptyBlock(tok.loc.start.line),
- attrs: [],
- attributeBlocks: [],
- isInline: inlineTags.indexOf(tok.val) !== -1,
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- };
- return this.tag(tag, {selfClosingAllowed: true});
- },
- /**
- * Parse tag.
- */
- tag: function(tag, options) {
- var seenAttrs = false;
- var attributeNames = [];
- var selfClosingAllowed = options && options.selfClosingAllowed;
- // (attrs | class | id)*
- out: while (true) {
- switch (this.peek().type) {
- case 'id':
- case 'class':
- var tok = this.advance();
- if (tok.type === 'id') {
- if (attributeNames.indexOf('id') !== -1) {
- this.error(
- 'DUPLICATE_ID',
- 'Duplicate attribute "id" is not allowed.',
- tok
- );
- }
- attributeNames.push('id');
- }
- tag.attrs.push({
- name: tok.type,
- val: "'" + tok.val + "'",
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- mustEscape: false,
- });
- continue;
- case 'start-attributes':
- if (seenAttrs) {
- console.warn(
- this.filename +
- ', line ' +
- this.peek().loc.start.line +
- ':\nYou should not have pug tags with multiple attributes.'
- );
- }
- seenAttrs = true;
- tag.attrs = tag.attrs.concat(this.attrs(attributeNames));
- continue;
- case '&attributes':
- var tok = this.advance();
- tag.attributeBlocks.push({
- type: 'AttributeBlock',
- val: tok.val,
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- });
- break;
- default:
- var pluginResult = this.runPlugin(
- 'tagAttributeTokens',
- this.peek(),
- tag,
- attributeNames
- );
- if (pluginResult) break;
- break out;
- }
- }
- // check immediate '.'
- if ('dot' == this.peek().type) {
- tag.textOnly = true;
- this.advance();
- }
- // (text | code | ':')?
- switch (this.peek().type) {
- case 'text':
- case 'interpolated-code':
- var text = this.parseText();
- if (text.type === 'Block') {
- tag.block.nodes.push.apply(tag.block.nodes, text.nodes);
- } else {
- tag.block.nodes.push(text);
- }
- break;
- case 'code':
- tag.block.nodes.push(this.parseCode(true));
- break;
- case ':':
- this.advance();
- var expr = this.parseExpr();
- tag.block =
- expr.type === 'Block' ? expr : this.initBlock(tag.line, [expr]);
- break;
- case 'newline':
- case 'indent':
- case 'outdent':
- case 'eos':
- case 'start-pipeless-text':
- case 'end-pug-interpolation':
- break;
- case 'slash':
- if (selfClosingAllowed) {
- this.advance();
- tag.selfClosing = true;
- break;
- }
- default:
- var pluginResult = this.runPlugin(
- 'tagTokens',
- this.peek(),
- tag,
- options
- );
- if (pluginResult) break;
- this.error(
- 'INVALID_TOKEN',
- 'Unexpected token `' +
- this.peek().type +
- '` expected `text`, `interpolated-code`, `code`, `:`' +
- (selfClosingAllowed ? ', `slash`' : '') +
- ', `newline` or `eos`',
- this.peek()
- );
- }
- // newline*
- while ('newline' == this.peek().type) this.advance();
- // block?
- if (tag.textOnly) {
- tag.block = this.parseTextBlock() || this.emptyBlock(tag.line);
- } else if ('indent' == this.peek().type) {
- var block = this.block();
- for (var i = 0, len = block.nodes.length; i < len; ++i) {
- tag.block.nodes.push(block.nodes[i]);
- }
- }
- return tag;
- },
- attrs: function(attributeNames) {
- this.expect('start-attributes');
- var attrs = [];
- var tok = this.advance();
- while (tok.type === 'attribute') {
- if (tok.name !== 'class' && attributeNames) {
- if (attributeNames.indexOf(tok.name) !== -1) {
- this.error(
- 'DUPLICATE_ATTRIBUTE',
- 'Duplicate attribute "' + tok.name + '" is not allowed.',
- tok
- );
- }
- attributeNames.push(tok.name);
- }
- attrs.push({
- name: tok.name,
- val: tok.val,
- line: tok.loc.start.line,
- column: tok.loc.start.column,
- filename: this.filename,
- mustEscape: tok.mustEscape !== false,
- });
- tok = this.advance();
- }
- this.tokens.defer(tok);
- this.expect('end-attributes');
- return attrs;
- },
- };
|