1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192 |
- '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 '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;
- },
- /**
- * '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;
- }
- };
|