<commit_before>/*
    Copyright (c) 2013-2015, Fionn Kelleher All rights reserved.

    Redistribution and use in source and binary forms, with or without modification,
    are permitted provided that the following conditions are met:

        Redistributions of source code must retain the above copyright notice,
        this list of conditions and the following disclaimer.

        Redistributions in binary form must reproduce the above copyright notice,
        this list of conditions and the following disclaimer in the documentation and/or other materials
        provided with the distribution.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
    ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
    IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
    INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
    (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
    OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
    WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
    OF SUCH DAMAGE.
*/
var _ = require("./utils");

module.exports = {
    // Parse Twitch badges..
    badges: function badges(tags) {
        if (_.isString(tags["badges"])) {
            var badges = {};
            var explode = tags["badges"].split(",");

            for (var i = 0; i < explode.length; i++) {
                var parts = explode[i].split("/");
                if (!parts[1]) return;
                badges[parts[0]] = parts[1];
            }

            tags["badges-raw"] = tags["badges"];
            tags["badges"] = badges;
        }
        if (_.isBoolean(tags["badges"])) { tags["badges-raw"] = null; }

        return tags;
    },

    // Parse Twitch emotes..
    emotes: function emotes(tags) {
        if (_.isString(tags["emotes"])) {
            var emoticons = tags["emotes"].split("/");
            var emotes = {};

            for (var i = 0; i < emoticons.length; i++) {
                var parts = emoticons[i].split(":");
                if (!parts[1]) return;
                emotes[parts[0]] = parts[1].split(",");
            }

            tags["emotes-raw"] = tags["emotes"];
            tags["emotes"] = emotes;
        }
        if (_.isBoolean(tags["emotes"])) { tags["emotes-raw"] = null; }

        return tags;
    },

    // Parse regex emotes..
    emoteRegex: function emoteRegex(msg, code, id, obj) {
        var space = /\S+/g;
        var regex = new RegExp(_.unescapeHtml(code));
        var match;

        // Check if emote code matches using RegExp and push it to the object..
        while ((match = space.exec(msg)) !== null) {
            if (regex.test(match[0])) {
                obj[id] = obj[id] || [];
                obj[id].push([match.index, space.lastIndex - 1])
            }
        }
    },

    // Parse string emotes..
    emoteString: function emoteString(msg, code, id, obj) {
        var space = /\S+/g;
        var match;

        // Check if emote code matches and push it to the object..
        while ((match = space.exec(msg)) !== null) {
            if (match[0] === _.unescapeHtml(code)) {
                obj[id] = obj[id] || [];
                obj[id].push([match.index, space.lastIndex - 1]);
            }
        }
    },

    // Transform the emotes object to a string with the following format..
    // emote_id:first_index-last_index,another_first-another_last/another_emote_id:first_index-last_index
    transformEmotes: function transformEmotes(emotes) {
        var transformed = "";

        Object.keys(emotes).forEach((id) => {
            transformed = `${transformed+id}:`;
            emotes[id].forEach((index) => {
                transformed = `${transformed+index.join("-")},`;
            });
            transformed = `${transformed.slice(0,-1)}/`;
        });

        return transformed.slice(0,-1);
    },

    // Parse Twitch messages..
    msg: function msg(data) {
        var message = {
            raw: data,
            tags: {},
            prefix: null,
            command: null,
            params: []
        }

        // Position and nextspace are used by the parser as a reference..
        var position = 0;
        var nextspace = 0;

        // The first thing we check for is IRCv3.2 message tags.
        // http://ircv3.atheme.org/specification/message-tags-3.2
        if (data.charCodeAt(0) === 64) {
            var nextspace = data.indexOf(" ");

            // Malformed IRC message..
            if (nextspace === -1) {
                return null;
            }

            // Tags are split by a semi colon..
            var rawTags = data.slice(1, nextspace).split(";");

            for (var i = 0; i < rawTags.length; i++) {
                // Tags delimited by an equals sign are key=value tags.
                // If there's no equals, we assign the tag a value of true.
                var tag = rawTags[i];
                var pair = tag.split("=");
                message.tags[pair[0]] = tag.substring(tag.indexOf("=") + 1) || true;
            }

            position = nextspace + 1;
        }

        // Skip any trailing whitespace..
        while (data.charCodeAt(position) === 32) {
            position++;
        }

        // Extract the message's prefix if present. Prefixes are prepended with a colon..
        if (data.charCodeAt(position) === 58) {
            nextspace = data.indexOf(" ", position);

            // If there's nothing after the prefix, deem this message to be malformed.
            if (nextspace === -1) {
                return null;
            }

            message.prefix = data.slice(position + 1, nextspace);
            position = nextspace + 1;

            // Skip any trailing whitespace..
            while (data.charCodeAt(position) === 32) {
                position++;
            }
        }

        nextspace = data.indexOf(" ", position);

        // If there's no more whitespace left, extract everything from the
        // current position to the end of the string as the command..
        if (nextspace === -1) {
            if (data.length > position) {
                message.command = data.slice(position);
                return message;
            }

            return null;
        }

        // Else, the command is the current position up to the next space. After
        // that, we expect some parameters.
        message.command = data.slice(position, nextspace);

        position = nextspace + 1;

        // Skip any trailing whitespace..
        while (data.charCodeAt(position) === 32) {
            position++;
        }

        while (position < data.length) {
            nextspace = data.indexOf(" ", position);

            // If the character is a colon, we've got a trailing parameter.
            // At this point, there are no extra params, so we push everything
            // from after the colon to the end of the string, to the params array
            // and break out of the loop.
            if (data.charCodeAt(position) === 58) {
                message.params.push(data.slice(position + 1));
                break;
            }

            // If we still have some whitespace...
            if (nextspace !== -1) {
                // Push whatever's between the current position and the next
                // space to the params array.
                message.params.push(data.slice(position, nextspace));
                position = nextspace + 1;

                // Skip any trailing whitespace and continue looping.
                while (data.charCodeAt(position) === 32) {
                    position++;
                }

                continue;
            }

            // If we don't have any more whitespace and the param isn't trailing,
            // push everything remaining to the params array.
            if (nextspace === -1) {
                message.params.push(data.slice(position));
                break;
            }
        }

        return message;
    }
}
<commit_msg>parser: add word boundary so smileys like :/ do not match URLs<commit_after>/*
    Copyright (c) 2013-2015, Fionn Kelleher All rights reserved.

    Redistribution and use in source and binary forms, with or without modification,
    are permitted provided that the following conditions are met:

        Redistributions of source code must retain the above copyright notice,
        this list of conditions and the following disclaimer.

        Redistributions in binary form must reproduce the above copyright notice,
        this list of conditions and the following disclaimer in the documentation and/or other materials
        provided with the distribution.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
    ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
    IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
    INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
    (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
    OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
    WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
    OF SUCH DAMAGE.
*/
var _ = require("./utils");

module.exports = {
    // Parse Twitch badges..
    badges: function badges(tags) {
        if (_.isString(tags["badges"])) {
            var badges = {};
            var explode = tags["badges"].split(",");

            for (var i = 0; i < explode.length; i++) {
                var parts = explode[i].split("/");
                if (!parts[1]) return;
                badges[parts[0]] = parts[1];
            }

            tags["badges-raw"] = tags["badges"];
            tags["badges"] = badges;
        }
        if (_.isBoolean(tags["badges"])) { tags["badges-raw"] = null; }

        return tags;
    },

    // Parse Twitch emotes..
    emotes: function emotes(tags) {
        if (_.isString(tags["emotes"])) {
            var emoticons = tags["emotes"].split("/");
            var emotes = {};

            for (var i = 0; i < emoticons.length; i++) {
                var parts = emoticons[i].split(":");
                if (!parts[1]) return;
                emotes[parts[0]] = parts[1].split(",");
            }

            tags["emotes-raw"] = tags["emotes"];
            tags["emotes"] = emotes;
        }
        if (_.isBoolean(tags["emotes"])) { tags["emotes-raw"] = null; }

        return tags;
    },

    // Parse regex emotes..
    emoteRegex: function emoteRegex(msg, code, id, obj) {
        var space = /\S+/g;
        var regex = new RegExp("(\\b|^|\s)"+_.unescapeHtml(code)+"(\\b|$|\s)");
        var match;

        // Check if emote code matches using RegExp and push it to the object..
        while ((match = space.exec(msg)) !== null) {
            if (regex.test(match[0])) {
                obj[id] = obj[id] || [];
                obj[id].push([match.index, space.lastIndex - 1])
            }
        }
    },

    // Parse string emotes..
    emoteString: function emoteString(msg, code, id, obj) {
        var space = /\S+/g;
        var match;

        // Check if emote code matches and push it to the object..
        while ((match = space.exec(msg)) !== null) {
            if (match[0] === _.unescapeHtml(code)) {
                obj[id] = obj[id] || [];
                obj[id].push([match.index, space.lastIndex - 1]);
            }
        }
    },

    // Transform the emotes object to a string with the following format..
    // emote_id:first_index-last_index,another_first-another_last/another_emote_id:first_index-last_index
    transformEmotes: function transformEmotes(emotes) {
        var transformed = "";

        Object.keys(emotes).forEach((id) => {
            transformed = `${transformed+id}:`;
            emotes[id].forEach((index) => {
                transformed = `${transformed+index.join("-")},`;
            });
            transformed = `${transformed.slice(0,-1)}/`;
        });

        return transformed.slice(0,-1);
    },

    // Parse Twitch messages..
    msg: function msg(data) {
        var message = {
            raw: data,
            tags: {},
            prefix: null,
            command: null,
            params: []
        }

        // Position and nextspace are used by the parser as a reference..
        var position = 0;
        var nextspace = 0;

        // The first thing we check for is IRCv3.2 message tags.
        // http://ircv3.atheme.org/specification/message-tags-3.2
        if (data.charCodeAt(0) === 64) {
            var nextspace = data.indexOf(" ");

            // Malformed IRC message..
            if (nextspace === -1) {
                return null;
            }

            // Tags are split by a semi colon..
            var rawTags = data.slice(1, nextspace).split(";");

            for (var i = 0; i < rawTags.length; i++) {
                // Tags delimited by an equals sign are key=value tags.
                // If there's no equals, we assign the tag a value of true.
                var tag = rawTags[i];
                var pair = tag.split("=");
                message.tags[pair[0]] = tag.substring(tag.indexOf("=") + 1) || true;
            }

            position = nextspace + 1;
        }

        // Skip any trailing whitespace..
        while (data.charCodeAt(position) === 32) {
            position++;
        }

        // Extract the message's prefix if present. Prefixes are prepended with a colon..
        if (data.charCodeAt(position) === 58) {
            nextspace = data.indexOf(" ", position);

            // If there's nothing after the prefix, deem this message to be malformed.
            if (nextspace === -1) {
                return null;
            }

            message.prefix = data.slice(position + 1, nextspace);
            position = nextspace + 1;

            // Skip any trailing whitespace..
            while (data.charCodeAt(position) === 32) {
                position++;
            }
        }

        nextspace = data.indexOf(" ", position);

        // If there's no more whitespace left, extract everything from the
        // current position to the end of the string as the command..
        if (nextspace === -1) {
            if (data.length > position) {
                message.command = data.slice(position);
                return message;
            }

            return null;
        }

        // Else, the command is the current position up to the next space. After
        // that, we expect some parameters.
        message.command = data.slice(position, nextspace);

        position = nextspace + 1;

        // Skip any trailing whitespace..
        while (data.charCodeAt(position) === 32) {
            position++;
        }

        while (position < data.length) {
            nextspace = data.indexOf(" ", position);

            // If the character is a colon, we've got a trailing parameter.
            // At this point, there are no extra params, so we push everything
            // from after the colon to the end of the string, to the params array
            // and break out of the loop.
            if (data.charCodeAt(position) === 58) {
                message.params.push(data.slice(position + 1));
                break;
            }

            // If we still have some whitespace...
            if (nextspace !== -1) {
                // Push whatever's between the current position and the next
                // space to the params array.
                message.params.push(data.slice(position, nextspace));
                position = nextspace + 1;

                // Skip any trailing whitespace and continue looping.
                while (data.charCodeAt(position) === 32) {
                    position++;
                }

                continue;
            }

            // If we don't have any more whitespace and the param isn't trailing,
            // push everything remaining to the params array.
            if (nextspace === -1) {
                message.params.push(data.slice(position));
                break;
            }
        }

        return message;
    }
}
<|endoftext|>


<commit_before>import path from 'path';
import glob from 'glob';
import fs from 'fs';
import async from 'async';

import { MonicError } from './error';
import { FileStructure } from './file';
import { SourceMapConsumer } from 'source-map';

/**
 * Parser class
 */
export default class Parser {
	/**
	 * @param {{
	 *   lineSeparator: string,
	 *   replacers: !Array
	 * }} params - additional parameters:
	 *   *) params.lineSeparator - EOL symbol;
	 *   *) params.replacers - an array of transform functions.
	 */
	constructor(params) {
		this.nl = params.lineSeparator;
		this.replacers = params.replacers || [];
		this.realpathCache = {};
		this.cache = {};
	}

	/**
	 * Checks a file for existence
	 * and returns an absolute path to it
	 *
	 * @param {string} file - the file path
	 * @param {function(Error, string=)} callback - a callback function
	 */
	normalizePath(file, callback) {
		file = path.normalize(path.resolve(file));

		if (this.realpathCache[file]) {
			callback(null, file);

		} else {
			async.waterfall([
				(next) => fs.stat(file, next),
				(stat, next) => {
					if (!stat.isFile()) {
						return next(new Error(`"${file}" is not a file`));
					}

					this.realpathCache[file] = true;
					next(null, file);
				}

			], callback);
		}
	}

	/**
	 * Parses a file and returns it structure
	 *
	 * @param {string} file - the file path
	 * @param {function(Error, FileStructure=, string=)} callback - a callback function
	 */
	parseFile(file, callback) {
		async.waterfall([
			(next) => this.normalizePath(file, next),

			(src, next) => {
				if (this.cache[src]) {
					return next(null, src, this.cache[src]);
				}

				fs.readFile(src, (err, content) => {
					next(err, src, String(content));
				});
			},

			(src, content, next) => {
				if (typeof content !== 'string') {
					return next(null, content, src);
				}

				this.parse(src, content, next);
			}

		], callback);
	}

	/**
	 * Parses a text and returns it structure
	 *
	 * @param {string} file - a path to a file
	 * @param {string} content - the source text
	 * @param {function(Error, FileStructure=, string=)} callback - a callback function
	 */
	parse(file, content, callback) {
		if (this.cache[file]) {
			return callback(null, this.cache[file], file);
		}

		const
			actions = [],
			dirname = path.dirname(file);

		content = $C(this.replacers).reduce((content, el) => {
			content = el(content, file);
			return content;

		}, content);

		// URL transformers
		content = content.replace(/^(\s*\/\/(?:#include|without)\s+)(.*)/gm, (sstr, decl, src) => {
			actions.push((cb) => {
				const parts = src.split('::');
				glob(path.join(dirname, parts[0]), null, (err, files) => {
					if (files) {
						content = content.replace(sstr, $C(files).reduce((res, el) => {
							parts[0] = path.relative(dirname, el);
							res += decl + parts.join('::') + (files.length ? this.nl : '');
							return res;
						}, ''));
					}

					cb(err, files);
				});
			});

			return sstr;
		});

		let sourceMap;
		content = content.replace(/\/\/# sourceMappingURL=(.*?)\s*$/, (sstr, url) => {
			actions.push((cb) => {
				fs.readFile(path.normalize(path.resolve(path.dirname(file), url)), (err, str) => {
					try {
						sourceMap = new SourceMapConsumer(JSON.parse(str));

					} catch (ignore) {

					} finally {
						cb(null);
					}

					content = content.replace(sstr, this.nl);
				});
			});

			return sstr;
		});

		async.parallel(actions, (err) => {
			if (err) {
				return callback(err);
			}

			const
				fileStructure = new FileStructure(file, this.nl),
				lines = content.split(/\r?\n|\r/);

			this.cache[file] = fileStructure;
			const parseLines = (start) => {
				const
					errors = [];

				let
					info,
					i;

				function appendError(err) {
					const
						msg = err.message;

					errors.push(new MonicError(msg, file, i + 1));
					fileStructure.error(msg, info);
				}

				function asyncParseCallback(err) {
					if (err) {
						appendError(err);
					}

					parseLines(i + 1);
				}

				let original;
				if (sourceMap) {
					let
						originalMap = [];

					sourceMap.eachMapping((el) => {
						originalMap.push({
							generated: {
								line: el.generatedLine,
								column: el.generatedColumn
							},

							original: {
								line: el.originalLine,
								column: el.originalColumn
							},

							source: el.source,
							name: el.name
						});
					});

					if (sourceMap.sourcesContent) {
						$C(sourceMap.sourcesContent).forEach((content, i) => {
							const
								src = sourceMap.sources[i];

							$C(originalMap).forEach((el) => {
								if (el.source === src) {
									el.sourcesContent = content;
								}
							});
						});
					}

					original = $C(originalMap).group('generated > line');
				}

				for (i = start; i < lines.length; i++) {
					const
						line = lines[i],
						val = line + (i < lines.length - 1 ? this.nl : '');

					if (original) {
						info = original[i] || {ignore: true};

					} else {
						info = {
							generated: {
								column: 0
							},

							original: {
								line: i + 1,
								column: 0
							},

							source: file,
							sourcesContent: content,
							line
						};
					}

					if (line.match(/^\s*\/\/#(.*)/)) {
						if (RegExp.$1) {
							const
								command = RegExp.$1.split(' '),
								dir = String(command.shift());

							const
								key = '_' + dir,
								params = command.join(' ');

							if (/^(?:include|without)$/.test(dir)) {
								return this[key](fileStructure, params, asyncParseCallback);

							} else if (/^(?:label|endlabel|if|endif|set|unset)$/.test(dir)) {
								try {
									this[key](fileStructure, params);

								} catch (err) {
									appendError(err);
								}

							} else {
								fileStructure.addCode(val, info);
							}
						}

					} else {
						fileStructure.addCode(val, info);
					}
				}

				callback(null, fileStructure, file);
			};

			parseLines(0);
		});
	}

	/**
	 * Directive #include
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 * @param {string} value - a value of the directive
	 * @param {function(Error=)} callback - a callback function
	 */
	_include(file, value, callback) {
		let
			paramsParts = value.split('::'),
			includeFileName = paramsParts.shift();

		paramsParts = $C(paramsParts).reduce((res, el) => {
			res[el] = true;
			return res;
		}, {});

		if (includeFileName) {
			this.parseFile(file.getRelativePathOf(includeFileName), (err, includeFile) => {
				if (err) {
					return callback(err);
				}

				file.addInclude(includeFile, paramsParts);
				callback();
			});

		} else {
			$C(paramsParts).forEach((el, key) => {
				file.root.labels[key] = true;
			});

			callback();
		}
	}

	/**
	 * Directive #without
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 * @param {string} value - a value of the directive
	 * @param {function(Error=)} callback - a callback function
	 */
	_without(file, value, callback) {
		let
			paramsParts = value.split('::'),
			includeFname = file.getRelativePathOf(paramsParts.shift());

		paramsParts = $C(paramsParts).reduce((res, el) => {
			res[el] = true;
			return res;
		}, {});

		this.parseFile(includeFname, (err, includeFile) => {
			if (err) {
				return callback(err);
			}

			file.addWithout(includeFile, paramsParts);
			callback();
		});
	}

	/**
	 * Directive #label
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 * @param {string} value - a value of the directive
	 */
	_label(file, value) {
		file.beginLabel(value);
	}

	/**
	 * Directive #endlabel
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 */
	_endlabel(file) {
		file.endLabel();
	}

	/**
	 * Directive #if
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 * @param {string} value - a value of the directive
	 */
	_if(file, value) {
		value = value.trim();

		if (!value) {
			throw new Error('Bad "if" directive');
		}

		const
			args = value.split(/\s+/);

		let res = true;
		if (args.length > 1 && args[0] === 'not') {
			res = false;
			args.shift();
		}

		file.beginIf(args[0], res);
	}

	/**
	 * Directive #endif
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 */
	_endif(file) {
		file.endIf();
	}

	/**
	 * Directive #set
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 * @param {string} value - a value of the directive
	 */
	_set(file, value) {
		value = value.trim();

		if (!value) {
			throw new Error('Bad "set" directive');
		}

		file.addSet(value);
	}

	/**
	 * Directive #unset
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 * @param {string} value - a value of the directive
	 */
	_unset(file, value) {
		value = value.trim();

		if (!value) {
			throw new Error('Bad "unset" directive');
		}

		file.addUnset(value);
	}
}
<commit_msg>Bugfix<commit_after>import path from 'path';
import glob from 'glob';
import fs from 'fs';
import async from 'async';

import { MonicError } from './error';
import { FileStructure } from './file';
import { SourceMapConsumer } from 'source-map';

/**
 * Parser class
 */
export default class Parser {
	/**
	 * @param {{
	 *   lineSeparator: string,
	 *   replacers: !Array
	 * }} params - additional parameters:
	 *   *) params.lineSeparator - EOL symbol;
	 *   *) params.replacers - an array of transform functions.
	 */
	constructor(params) {
		this.nl = params.lineSeparator;
		this.replacers = params.replacers || [];
		this.realpathCache = {};
		this.cache = {};
	}

	/**
	 * Checks a file for existence
	 * and returns an absolute path to it
	 *
	 * @param {string} file - the file path
	 * @param {function(Error, string=)} callback - a callback function
	 */
	normalizePath(file, callback) {
		file = path.normalize(path.resolve(file));

		if (this.realpathCache[file]) {
			callback(null, file);

		} else {
			async.waterfall([
				(next) => fs.stat(file, next),
				(stat, next) => {
					if (!stat.isFile()) {
						return next(new Error(`"${file}" is not a file`));
					}

					this.realpathCache[file] = true;
					next(null, file);
				}

			], callback);
		}
	}

	/**
	 * Parses a file and returns it structure
	 *
	 * @param {string} file - the file path
	 * @param {function(Error, FileStructure=, string=)} callback - a callback function
	 */
	parseFile(file, callback) {
		async.waterfall([
			(next) => this.normalizePath(file, next),

			(src, next) => {
				if (this.cache[src]) {
					return next(null, src, this.cache[src]);
				}

				fs.readFile(src, (err, content) => {
					next(err, src, String(content));
				});
			},

			(src, content, next) => {
				if (typeof content !== 'string') {
					return next(null, content, src);
				}

				this.parse(src, content, next);
			}

		], callback);
	}

	/**
	 * Parses a text and returns it structure
	 *
	 * @param {string} file - a path to a file
	 * @param {string} content - the source text
	 * @param {function(Error, FileStructure=, string=)} callback - a callback function
	 */
	parse(file, content, callback) {
		if (this.cache[file]) {
			return callback(null, this.cache[file], file);
		}

		const
			actions = [],
			dirname = path.dirname(file);

		content = $C(this.replacers).reduce((content, el) => {
			content = el(content, file);
			return content;

		}, content);

		// URL transformers
		content = content.replace(/^(\s*\/\/(?:#include|without)\s+)(.*)/gm, (sstr, decl, src) => {
			actions.push((cb) => {
				const parts = src.split('::');
				glob(path.join(dirname, parts[0]), null, (err, files) => {
					if (files) {
						content = content.replace(sstr, $C(files).reduce((res, el, i) => {
							parts[0] = path.relative(dirname, el);
							res += decl + parts.join('::') + (files.length > 1 && i !== files.length - 1 ? this.nl : '');
							return res;
						}, ''));
					}

					cb(err, files);
				});
			});

			return sstr;
		});

		let sourceMap;
		content = content.replace(/\/\/# sourceMappingURL=(.*?)\s*$/, (sstr, url) => {
			actions.push((cb) => {
				fs.readFile(path.normalize(path.resolve(path.dirname(file), url)), (err, str) => {
					try {
						sourceMap = new SourceMapConsumer(JSON.parse(str));

					} catch (ignore) {

					} finally {
						cb(null);
					}

					content = content.replace(sstr, this.nl);
				});
			});

			return sstr;
		});

		async.parallel(actions, (err) => {
			if (err) {
				return callback(err);
			}

			const
				fileStructure = new FileStructure(file, this.nl),
				lines = content.split(/\r?\n|\r/);

			this.cache[file] = fileStructure;
			const parseLines = (start) => {
				const
					errors = [];

				let
					info,
					i;

				function appendError(err) {
					const
						msg = err.message;

					errors.push(new MonicError(msg, file, i + 1));
					fileStructure.error(msg, info);
				}

				function asyncParseCallback(err) {
					if (err) {
						appendError(err);
					}

					parseLines(i + 1);
				}

				let original;
				if (sourceMap) {
					let
						originalMap = [];

					sourceMap.eachMapping((el) => {
						originalMap.push({
							generated: {
								line: el.generatedLine,
								column: el.generatedColumn
							},

							original: {
								line: el.originalLine,
								column: el.originalColumn
							},

							source: el.source,
							name: el.name
						});
					});

					if (sourceMap.sourcesContent) {
						$C(sourceMap.sourcesContent).forEach((content, i) => {
							const
								src = sourceMap.sources[i];

							$C(originalMap).forEach((el) => {
								if (el.source === src) {
									el.sourcesContent = content;
								}
							});
						});
					}

					original = $C(originalMap).group('generated > line');
				}

				for (i = start; i < lines.length; i++) {
					const
						line = lines[i],
						val = line + (i < lines.length - 1 ? this.nl : '');

					if (original) {
						info = original[i] || {ignore: true};

					} else {
						info = {
							generated: {
								column: 0
							},

							original: {
								line: i + 1,
								column: 0
							},

							source: file,
							sourcesContent: content,
							line
						};
					}

					if (line.match(/^\s*\/\/#(.*)/)) {
						if (RegExp.$1) {
							const
								command = RegExp.$1.split(' '),
								dir = String(command.shift());

							const
								key = '_' + dir,
								params = command.join(' ');

							if (/^(?:include|without)$/.test(dir)) {
								return this[key](fileStructure, params, asyncParseCallback);

							} else if (/^(?:label|endlabel|if|endif|set|unset)$/.test(dir)) {
								try {
									this[key](fileStructure, params);

								} catch (err) {
									appendError(err);
								}

							} else {
								fileStructure.addCode(val, info);
							}
						}

					} else {
						fileStructure.addCode(val, info);
					}
				}

				callback(null, fileStructure, file);
			};

			parseLines(0);
		});
	}

	/**
	 * Directive #include
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 * @param {string} value - a value of the directive
	 * @param {function(Error=)} callback - a callback function
	 */
	_include(file, value, callback) {
		let
			paramsParts = value.split('::'),
			includeFileName = paramsParts.shift();

		paramsParts = $C(paramsParts).reduce((res, el) => {
			res[el] = true;
			return res;
		}, {});

		if (includeFileName) {
			this.parseFile(file.getRelativePathOf(includeFileName), (err, includeFile) => {
				if (err) {
					return callback(err);
				}

				file.addInclude(includeFile, paramsParts);
				callback();
			});

		} else {
			$C(paramsParts).forEach((el, key) => {
				file.root.labels[key] = true;
			});

			callback();
		}
	}

	/**
	 * Directive #without
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 * @param {string} value - a value of the directive
	 * @param {function(Error=)} callback - a callback function
	 */
	_without(file, value, callback) {
		let
			paramsParts = value.split('::'),
			includeFname = file.getRelativePathOf(paramsParts.shift());

		paramsParts = $C(paramsParts).reduce((res, el) => {
			res[el] = true;
			return res;
		}, {});

		this.parseFile(includeFname, (err, includeFile) => {
			if (err) {
				return callback(err);
			}

			file.addWithout(includeFile, paramsParts);
			callback();
		});
	}

	/**
	 * Directive #label
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 * @param {string} value - a value of the directive
	 */
	_label(file, value) {
		file.beginLabel(value);
	}

	/**
	 * Directive #endlabel
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 */
	_endlabel(file) {
		file.endLabel();
	}

	/**
	 * Directive #if
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 * @param {string} value - a value of the directive
	 */
	_if(file, value) {
		value = value.trim();

		if (!value) {
			throw new Error('Bad "if" directive');
		}

		const
			args = value.split(/\s+/);

		let res = true;
		if (args.length > 1 && args[0] === 'not') {
			res = false;
			args.shift();
		}

		file.beginIf(args[0], res);
	}

	/**
	 * Directive #endif
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 */
	_endif(file) {
		file.endIf();
	}

	/**
	 * Directive #set
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 * @param {string} value - a value of the directive
	 */
	_set(file, value) {
		value = value.trim();

		if (!value) {
			throw new Error('Bad "set" directive');
		}

		file.addSet(value);
	}

	/**
	 * Directive #unset
	 *
	 * @private
	 * @param {!FileStructure} file - a file structure
	 * @param {string} value - a value of the directive
	 */
	_unset(file, value) {
		value = value.trim();

		if (!value) {
			throw new Error('Bad "unset" directive');
		}

		file.addUnset(value);
	}
}
<|endoftext|>


<commit_before>var stream = require("stream"),
    util = require("util");

var Parser = module.exports = function Parser(options) {
  stream.Transform.call(this);

  options = options || {};

  this._maximumHeaderBytes = options.maximumHeaderBytes || 65536;

  this._buffer = Buffer(0);
  this._headerBytes = 0;
  this._moreHeaders = true;
  this._headerLines = [];
};
util.inherits(Parser, stream.Transform);

Parser.prototype._transform = function _transform(input, encoding, done) {
  if (this._moreHeaders) {
    this._buffer = Buffer.concat([this._buffer, input]);

    this._headerBytes += input.length;

    if (this._headerBytes > this._maximumHeaderBytes) {
      this.emit("error", Error("too much header data received, aborting parsing"));
      this._moreHeaders = false;
      return;
    }

    var offset = 0, i = 0;
    while (i < this._buffer.length - 1) {
      for (i = offset; i < this._buffer.length - 1; ++i) {
        if (this._buffer[i] === 0x0d && this._buffer[i+1] === 0x0a) {
          this._moreHeaders = this.pushHeaderLine(this._buffer.toString("ascii", offset, i));

          if (!this._moreHeaders) {
            break;
          }

          offset = i + 2;
        }
      }

      if (!this._moreHeaders) {
        this._buffer = null;
        break;
      }

      if (offset > 0) {
        this._buffer = this._buffer.slice(offset);
      }
    }
  }

  this.push(input);

  done();
};

Parser.prototype.pushHeaderLine = function pushHeaderLine(line) {
  if (line.length === 0) {
    setImmediate(this.parseHeaders.bind(this));

    return false;
  } else {
    this._headerLines.push(line);

    return true;
  }
};

Parser.prototype.parseHeaders = function parseHeaders() {
  var firstLine = this._headerLines.shift();

  var matches = firstLine.match(/^([^ ]+)\s+([^ ]+?)\s+HTTP\/(\d+)\.(\d+)$/);
  if (!matches) {
    var err = Error("couldn't parse initial header line");
    err.data = firstLine;
    this.emit("error", err);
    return;
  }

  var method = matches[1],
      path = matches[2],
      versionMajor = parseInt(matches[3], 10),
      versionMinor = parseInt(matches[4], 10);

  var headers = Object.create(null);

  this._headerLines.forEach(function(line) {
    var matches = line.match(/^([^:]+)(?::(.+))?$/);

    var name = matches[1].toLowerCase(),
        value = matches[2].trim();

    if (typeof headers[name] === "undefined") {
      headers[name] = [];
    }

    headers[name].push(value);
  });

  this._headerLines = null;

  this.emit("headers", [method, path, headers]);
};
<commit_msg>Use read-stream/process.nextTick for Node 0.8.x<commit_after>var stream = require("read-stream"),
    util = require("util");

var Parser = module.exports = function Parser(options) {
  stream.Transform.call(this);

  options = options || {};

  this._maximumHeaderBytes = options.maximumHeaderBytes || 65536;

  this._buffer = Buffer(0);
  this._headerBytes = 0;
  this._moreHeaders = true;
  this._headerLines = [];
};
util.inherits(Parser, stream.Transform);

Parser.prototype._transform = function _transform(input, encoding, done) {
  if (this._moreHeaders) {
    this._buffer = Buffer.concat([this._buffer, input]);

    this._headerBytes += input.length;

    if (this._headerBytes > this._maximumHeaderBytes) {
      this.emit("error", Error("too much header data received, aborting parsing"));
      this._moreHeaders = false;
      return;
    }

    var offset = 0, i = 0;
    while (i < this._buffer.length - 1) {
      for (i = offset; i < this._buffer.length - 1; ++i) {
        if (this._buffer[i] === 0x0d && this._buffer[i+1] === 0x0a) {
          this._moreHeaders = this.pushHeaderLine(this._buffer.toString("ascii", offset, i));

          if (!this._moreHeaders) {
            break;
          }

          offset = i + 2;
        }
      }

      if (!this._moreHeaders) {
        this._buffer = null;
        break;
      }

      if (offset > 0) {
        this._buffer = this._buffer.slice(offset);
      }
    }
  }

  this.push(input);

  done();
};

Parser.prototype.pushHeaderLine = function pushHeaderLine(line) {
  if (line.length === 0) {
    process.nextTick(this.parseHeaders.bind(this));

    return false;
  } else {
    this._headerLines.push(line);

    return true;
  }
};

Parser.prototype.parseHeaders = function parseHeaders() {
  var firstLine = this._headerLines.shift();

  var matches = firstLine.match(/^([^ ]+)\s+([^ ]+?)\s+HTTP\/(\d+)\.(\d+)$/);
  if (!matches) {
    var err = Error("couldn't parse initial header line");
    err.data = firstLine;
    this.emit("error", err);
    return;
  }

  var method = matches[1],
      path = matches[2],
      versionMajor = parseInt(matches[3], 10),
      versionMinor = parseInt(matches[4], 10);

  var headers = Object.create(null);

  this._headerLines.forEach(function(line) {
    var matches = line.match(/^([^:]+)(?::(.+))?$/);

    var name = matches[1].toLowerCase(),
        value = matches[2].trim();

    if (typeof headers[name] === "undefined") {
      headers[name] = [];
    }

    headers[name].push(value);
  });

  this._headerLines = null;

  this.emit("headers", [method, path, headers]);
};
<|endoftext|>


<commit_before>
/**
 * Module dependencies.
 */

var assert = require('assert');
var debug = require('debug')('stream-parser');

/**
 * Module exports.
 */

module.exports = Parser;

/**
 * The `Parser` stream mixin works with either `Writable` or `Transform` stream
 * instances/subclasses. Provides a convenient generic "parsing" API:
 *
 *   _bytes(n, cb) - buffers "n" bytes and then calls "cb" with the "chunk"
 *
 * If you extend a `Transform` stream, then the `_passthrough()` function is also
 * added:
 *
 *   _passthrough(n, cb) - passes through "n" bytes untouched and then calls "cb"
 *
 * @param {Stream} stream Transform or Writable stream instance to extend
 * @api public
 */

function Parser (stream) {
  var isTransform = stream && 'function' == typeof stream._transform;
  var isWritable = stream && 'function' == typeof stream._write;
  if (!isTransform && !isWritable) throw new Error('must pass a Writable or Transform stream in');
  debug('extending Parser into stream');

  // number of bytes left to parser for the next "chunk"
  stream._parserBytesLeft = 0;

  // array of Buffer instances that make up the next "chunk"
  stream._parserBuffers = [];

  // number of bytes parsed so far for the next "chunk"
  stream._parserBuffered = 0;

  // flag the keeps track of if we're buffering or passing-through
  stream._parserBuffering = false;

  // the callback for the next "chunk"
  stream._parserCallback = null;

  // both Transform streams and Writable streams get `_bytes()`
  stream._bytes = bytes;

  // only Transform streams get the `_passthrough()` function
  if (isTransform) stream._passthrough = passthrough;

  // take control of the streams2 callback functions for this stream
  if (isTransform) {
    stream._transform = transform;
  } else {
    stream._write = write;
  }
}

/**
 * Buffers `n` bytes and then invokes `fn` once that amount has been collected.
 *
 * @param {Number} n the number of bytes to buffer
 * @param {Function} fn callback function to invoke when `n` bytes are buffered
 * @api public
 */

function bytes (n, fn) {
  assert(!this._parserCallback, 'there is already a "callback" set!');
  assert(isFinite(n), 'can only buffer a finite number of bytes, got "' + n + '"');
  debug('buffering "%d" bytes', n);
  this._parserBytesLeft = n;
  this._parserCallback = fn;
  this._parserBuffering = true;
}

/**
 * Passes through `n` bytes to the readable side of this stream untouched,
 * then invokes `fn` once that amount has been passed through.
 *
 * @param {Number} n the number of bytes to pass through
 * @param {Function} fn callback function to invoke when `n` bytes have passed through
 * @api public
 */

function passthrough (n, fn) {
  assert(!this._parserCallback, 'There is already a "callback" set!');
  assert(isFinite(n), 'can only pass through a finite number of bytes, got "' + n + '"');
  debug('passing through "%d" bytes', n);
  this._parserBytesLeft = n;
  this._parserCallback = fn;
  this._parserBuffering = false;
}

/**
 * The internal buffering/passthrough logic...
 */

function write (chunk, fn) {
  debug('write(%d bytes)', chunk.length);
  data.call(this, chunk, null, fn);
}

function transform (chunk, output, fn) {
  debug('transform(%d bytes)', chunk.length);
  data.call(this, chunk, output, fn);
}

function data (chunk, output, fn) {
  /*if (this.done) {
    debug('_transform called, but stream is "done"!');
    return done();
  }*/
  assert(this._parserBytesLeft > 0, 'got data but not currently parsing anything');
  if (chunk.length <= this._parserBytesLeft) {
    // small buffer fits within the "_parserBytesLeft" window
    process.call(this, chunk, output);
    fn();
  } else {
    // large buffer needs to be sliced on "_parserBytesLeft" and processed
    var b = chunk.slice(0, this._parserBytesLeft);
    process.call(this, b, output);
    if (chunk.length > b.length) {
      data.call(this, chunk.slice(b.length), output, fn);
    }
  }
}

function process (chunk, output) {
  this._parserBytesLeft -= chunk.length;
  debug('%d bytes left for this piece', this._parserBytesLeft);

  if (this._parserBuffering) {
    // buffer
    this._parserBuffers.push(chunk);
    this._parserBuffered += chunk.length;
  } else {
    // passthrough
    output(chunk);
  }

  if (0 === this._parserBytesLeft) {
    // done with this "piece", invoke the callback
    var cb = this._parserCallback;
    if (cb && this._parserBuffering && this._parserBuffers.length > 1) {
      chunk = Buffer.concat(this._parserBuffers, this._parserBuffered);
    }
    if (!this._parserBuffering) {
      chunk = null;
    }
    this._parserCallback = null;
    this._parserBuffered = 0;
    this._parserBuffering = true;
    this._parserBuffers.splice(0); // empty
    if (cb) {
      if (chunk) {
        cb.call(this, chunk, output); // buffered
      } else {
        cb.call(this, output); // passthrough
      }
    }
    //assert(/*this.done || */this._parserBytesLeft > 0, 'buffer() or passthrough() were not called in the callback');
  }
}
<commit_msg>don't use Function#call() when not necessary<commit_after>
/**
 * Module dependencies.
 */

var assert = require('assert');
var debug = require('debug')('stream-parser');

/**
 * Module exports.
 */

module.exports = Parser;

/**
 * The `Parser` stream mixin works with either `Writable` or `Transform` stream
 * instances/subclasses. Provides a convenient generic "parsing" API:
 *
 *   _bytes(n, cb) - buffers "n" bytes and then calls "cb" with the "chunk"
 *
 * If you extend a `Transform` stream, then the `_passthrough()` function is also
 * added:
 *
 *   _passthrough(n, cb) - passes through "n" bytes untouched and then calls "cb"
 *
 * @param {Stream} stream Transform or Writable stream instance to extend
 * @api public
 */

function Parser (stream) {
  var isTransform = stream && 'function' == typeof stream._transform;
  var isWritable = stream && 'function' == typeof stream._write;
  if (!isTransform && !isWritable) throw new Error('must pass a Writable or Transform stream in');
  debug('extending Parser into stream');

  // number of bytes left to parser for the next "chunk"
  stream._parserBytesLeft = 0;

  // array of Buffer instances that make up the next "chunk"
  stream._parserBuffers = [];

  // number of bytes parsed so far for the next "chunk"
  stream._parserBuffered = 0;

  // flag the keeps track of if we're buffering or passing-through
  stream._parserBuffering = false;

  // the callback for the next "chunk"
  stream._parserCallback = null;

  // both Transform streams and Writable streams get `_bytes()`
  stream._bytes = bytes;

  // only Transform streams get the `_passthrough()` function
  if (isTransform) stream._passthrough = passthrough;

  // take control of the streams2 callback functions for this stream
  if (isTransform) {
    stream._transform = transform;
  } else {
    stream._write = write;
  }
}

/**
 * Buffers `n` bytes and then invokes `fn` once that amount has been collected.
 *
 * @param {Number} n the number of bytes to buffer
 * @param {Function} fn callback function to invoke when `n` bytes are buffered
 * @api public
 */

function bytes (n, fn) {
  assert(!this._parserCallback, 'there is already a "callback" set!');
  assert(isFinite(n), 'can only buffer a finite number of bytes, got "' + n + '"');
  debug('buffering "%d" bytes', n);
  this._parserBytesLeft = n;
  this._parserCallback = fn;
  this._parserBuffering = true;
}

/**
 * Passes through `n` bytes to the readable side of this stream untouched,
 * then invokes `fn` once that amount has been passed through.
 *
 * @param {Number} n the number of bytes to pass through
 * @param {Function} fn callback function to invoke when `n` bytes have passed through
 * @api public
 */

function passthrough (n, fn) {
  assert(!this._parserCallback, 'There is already a "callback" set!');
  assert(isFinite(n), 'can only pass through a finite number of bytes, got "' + n + '"');
  debug('passing through "%d" bytes', n);
  this._parserBytesLeft = n;
  this._parserCallback = fn;
  this._parserBuffering = false;
}

/**
 * The internal buffering/passthrough logic...
 */

function write (chunk, fn) {
  debug('write(%d bytes)', chunk.length);
  data(this, chunk, null, fn);
}

function transform (chunk, output, fn) {
  debug('transform(%d bytes)', chunk.length);
  data(this, chunk, output, fn);
}

function data (stream, chunk, output, fn) {
  /*if (stream.done) {
    debug('_transform called, but stream is "done"!');
    return done();
  }*/
  assert(stream._parserBytesLeft > 0, 'got data but not currently parsing anything');
  if (chunk.length <= stream._parserBytesLeft) {
    // small buffer fits within the "_parserBytesLeft" window
    process(stream, chunk, output);
    fn();
  } else {
    // large buffer needs to be sliced on "_parserBytesLeft" and processed
    var b = chunk.slice(0, stream._parserBytesLeft);
    process(stream, b, output);
    if (chunk.length > b.length) {
      data(stream, chunk.slice(b.length), output, fn);
    }
  }
}

function process (stream, chunk, output) {
  stream._parserBytesLeft -= chunk.length;
  debug('%d bytes left for stream piece', stream._parserBytesLeft);

  if (stream._parserBuffering) {
    // buffer
    stream._parserBuffers.push(chunk);
    stream._parserBuffered += chunk.length;
  } else {
    // passthrough
    output(chunk);
  }

  if (0 === stream._parserBytesLeft) {
    // done with stream "piece", invoke the callback
    var cb = stream._parserCallback;
    if (cb && stream._parserBuffering && stream._parserBuffers.length > 1) {
      chunk = Buffer.concat(stream._parserBuffers, stream._parserBuffered);
    }
    if (!stream._parserBuffering) {
      chunk = null;
    }
    stream._parserCallback = null;
    stream._parserBuffered = 0;
    stream._parserBuffering = true;
    stream._parserBuffers.splice(0); // empty
    if (cb) {
      if (chunk) {
        cb.call(stream, chunk, output); // buffered
      } else {
        cb.call(stream, output); // passthrough
      }
    }
    //assert(/*stream.done || */stream._parserBytesLeft > 0, 'buffer() or passthrough() were not called in the callback');
  }
}
<|endoftext|>


<commit_before>var helpers = require('./helpers'),
    filters = require('./filters'),
    _ = require('underscore'),

    variableRegexp  = /^\{\{.*?\}\}$/,
    logicRegexp     = /^\{%[^\r]*?%\}$/,
    commentRegexp   = /^\{#[^\r]*?#\}$/,

    TEMPLATE = exports.TEMPLATE = 0,
    LOGIC_TOKEN = 1,
    VAR_TOKEN   = 2;

exports.TOKEN_TYPES = {
    TEMPLATE: TEMPLATE,
    LOGIC: LOGIC_TOKEN,
    VAR: VAR_TOKEN
};

function getMethod(input) {
    return input.match(/^[\w\.]+/)[0];
}

function doubleEscape(input) {
    return input.replace(/\\/g, '\\\\');
}

function getArgs(input) {
    return doubleEscape(input.replace(/^[\w\.]+\(|\)$/g, ''));
}

function getContextVar(varName, context) {
    var a = varName.split(".");
    while (a.length) {
        context = context[a.splice(0, 1)[0]];
    }
    return context;
}

function getTokenArgs(token, parts) {
    parts = _.map(parts, doubleEscape);

    var i = 0,
        l = parts.length,
        arg,
        ender,
        out = [];

    function concat(from, ending) {
        var end = new RegExp('\\' + ending + '$'),
            i = from,
            out = '';

        while (!(end).test(out) && i < parts.length) {
            out += ' ' + parts[i];
            parts[i] = null;
            i += 1;
        }

        if (!end.test(out)) {
            throw new Error('Malformed arguments ' + out + ' sent to tag.');
        }

        return out.replace(/^ /, '');
    }

    for (i; i < l; i += 1) {
        arg = parts[i];
        if (arg === null || (/^\s+$/).test(arg)) {
            continue;
        }

        if (
            ((/^\"/).test(arg) && !(/\"[\]\}]?$/).test(arg))
                || ((/^\'/).test(arg) && !(/\'[\]\}]?$/).test(arg))
                || ((/^\{/).test(arg) && !(/\}$/).test(arg))
                || ((/^\[/).test(arg) && !(/\]$/).test(arg))
        ) {
            switch (arg.substr(0, 1)) {
            case "'":
                ender = "'";
                break;
            case '"':
                ender = '"';
                break;
            case '[':
                ender = ']';
                break;
            case '{':
                ender = '}';
                break;
            }
            out.push(concat(i, ender));
            continue;
        }

        out.push(arg);
    }

    return out;
}

function findSubBlocks(topToken, blocks) {
    _.each(topToken.tokens, function (token, index) {
            if (token.name === 'block') {
                blocks[token.args[0]] = token;
                findSubBlocks(token, blocks);
            }
        });
}

function getParentBlock(token) {
    var block;

    if ( token.parentBlock ) {
        block = token.parentBlock;
    } else if ( token.parent ) {
        _.any( token.parent, function (parent, index) {
                block = getParentBlock( parent );

                // return first encountered block in parent tree
                if ( block !== undefined ) {
                    return true;
                }
            }, this);
    }

    return block;
}

exports.parseVariable = function (token, escape) {
    if (!token) {
        return {
            type: null,
            name: '',
            filters: [],
            escape: escape
        };
    }

    var filters = [],
        parts = token.replace(/^\{\{ *| *\}\}$/g, '').split('|'),
        varname = parts.shift(),
        i = 0,
        l = parts.length,
        args = null,
        filter_name,
        part;

    if ((/\(/).test(varname)) {
        args = getArgs(varname.replace(/^\w+\./, ''));
        varname = getMethod(varname);
    }

    for (i; i < l; i += 1) {
        part = parts[i];
        if (part && ((/^[\w\.]+\(/).test(part) || (/\)$/).test(part)) && !(/^[\w\.]+\([^\)]*\)$/).test(part)) {
            parts[i] += '|' + parts[i + 1];
            parts[i + 1] = false;
        }
    }
    parts = _.without(parts, false);

    i = 0;
    l = parts.length;
    for (i; i < l; i += 1) {
        part = parts[i];
        filter_name = getMethod(part);
        if ((/\(/).test(part)) {
            filters.push({
                name: filter_name,
                args: getArgs(part)
            });
        } else {
            filters.push({ name: filter_name, args: '' });
        }
    }

    return {
        type: VAR_TOKEN,
        name: varname,
        args: args,
        filters: filters,
        escape: escape
    };
};

exports.parse = function (data, tags, autoescape) {
    var rawtokens = data.replace(/(^\s+)|(\s+$)/g, '').split(/(\{%[^\r]*?%\}|\{\{.*?\}\}|\{#[^\r]*?#\})/),
        escape = !!autoescape,
        last_escape = escape,
        stack = [[]],
        index = 0,
        i = 0,
        j = rawtokens.length,
        filters = [],
        filter_name,
        varname,
        token,
        parts,
        part,
        names,
        matches,
        tagname,
        lines = 1,
        curline = 1,
        newlines = null,
        lastToken,
        rawStart = /^\{\% *raw *\%\}/,
        rawEnd = /\{\% *endraw *\%\}$/,
        inRaw = false,
        stripAfter = false,
        stripBefore = false,
        stripStart = false,
        stripEnd = false;

    for (i; i < j; i += 1) {
        token = rawtokens[i];
        curline = lines;
        newlines = token.match(/\n/g);
        stripAfter = false;
        stripBefore = false;
        stripStart = false;
        stripEnd = false;

        if (newlines) {
            lines += newlines.length;
        }

        if (inRaw !== false && !rawEnd.test(token)) {
            inRaw += token;
            continue;
        }

        // Ignore empty strings and comments
        if (token.length === 0 || commentRegexp.test(token)) {
            continue;
        } else if (/^(\s|\n)+$/.test(token)) {
            token = token.replace(/ +/, ' ').replace(/\n+/, '\n');
        } else if (variableRegexp.test(token)) {
            token = exports.parseVariable(token, escape);
        } else if (logicRegexp.test(token)) {
            if (rawEnd.test(token)) {
                // Don't care about the content in a raw tag, so end tag may not start correctly
                token = inRaw + token.replace(rawEnd, '');
                inRaw = false;
                stack[index].push(token);
                continue;
            }

            if (rawStart.test(token)) {
                // Have to check the whole token directly, not just parts, as the tag may not end correctly while in raw
                inRaw = token.replace(rawStart, '');
                continue;
            }

            parts = token.replace(/^\{%\s*|\s*%\}$/g, '').split(' ');
            if (parts[0] === '-') {
                stripBefore = true;
                parts.shift();
            }
            tagname = parts.shift();
            if (_.last(parts) === '-') {
                stripAfter = true;
                parts.pop();
            }

            if (index > 0 && (/^end/).test(tagname)) {
                lastToken = _.last(stack[stack.length - 2]);
                if ('end' + lastToken.name === tagname) {
                    if (lastToken.name === 'autoescape') {
                        escape = last_escape;
                    }
                    lastToken.strip.end = stripBefore;
                    lastToken.strip.after = stripAfter;
                    stack.pop();
                    index -= 1;
                    continue;
                }

                throw new Error('Expected end tag for "' + lastToken.name + '", but found "' + tagname + '" at line ' + lines + '.');
            }

            if (!tags.hasOwnProperty(tagname)) {
                throw new Error('Unknown logic tag at line ' + lines + ': "' + tagname + '".');
            }

            if (tagname === 'autoescape') {
                last_escape = escape;
                escape = (!parts.length || parts[0] === 'true') ? ((parts.length >= 2) ? parts[1] : true) : false;
            }

            token = {
                type: LOGIC_TOKEN,
                line: curline,
                name: tagname,
                compile: tags[tagname],
                parent: _.uniq(stack[stack.length - 2] || []),
                strip: {
                    before: stripBefore,
                    after: stripAfter,
                    start: false,
                    end: false
                }
            };
            token.args = getTokenArgs(token, parts);

            if (tags[tagname].ends) {
                token.strip.after = false;
                token.strip.start = stripAfter;
                stack[index].push(token);
                stack.push(token.tokens = []);
                index += 1;
                continue;
            }
        }

        // Everything else is treated as a string
        stack[index].push(token);
    }

    if (inRaw !== false) {
        throw new Error('Missing expected end tag for "raw" on line ' + curline + '.');
    }

    if (index !== 0) {
        lastToken = _.last(stack[stack.length - 2]);
        throw new Error('Missing end tag for "' + lastToken.name + '" that was opened on line ' + lastToken.line + '.');
    }

    return stack[index];
};

function precompile(indent, context) {
    var filepath,
        extendsHasVar,
        parentTokens,
        preservedTokens = [];

    // Precompile - extract blocks and create hierarchy based on 'extends' tags
    // TODO: make block and extends tags accept context variables

    // Only precompile at the template level
    if (this.type === TEMPLATE) {

        _.each(this.tokens, function (token, index) {

            if (!extendsHasVar) {
                // Load the parent template
                if (token.name === 'extends') {
                    filepath = token.args[0];

                    if (!helpers.isStringLiteral(filepath)) {

                        if (!context) {
                            extendsHasVar = true;
                            return;
                        }
                        filepath = "\"" + getContextVar(filepath, context) + "\"";
                    }

                    if (!helpers.isStringLiteral(filepath) || token.args.length > 1) {
                        throw new Error('Extends tag on line ' + token.line + ' accepts exactly one string literal as an argument.');
                    }
                    if (index > 0) {
                        throw new Error('Extends tag must be the first tag in the template, but "extends" found on line ' + token.line + '.');
                    }
                    token.template = this.compileFile(filepath.replace(/['"]/g, ''));
                    this.parent = token.template;

                    // inherit tokens/blocks from parent.
                    this.blocks = _.extend({}, this.parent.blocks, this.blocks);

                } else if (token.name === 'block') { // Make a list of blocks
                    var blockname = token.args[0];

                    if (!helpers.isValidBlockName(blockname) || token.args.length !== 1) {
                        throw new Error('Invalid block tag name "' + blockname + '" on line ' + token.line + '.');
                    }

                    // store blocks as flat reference list on top-level
                    // template object
                    this.blocks[blockname] = token;

                    // child tokens may contain more blocks at this template
                    // level - apply to flat this.blocks object
                    findSubBlocks(token, this.blocks);

                    // search parent list for a matching block, replacing the
                    // parent template block tokens with the derived token.
                    if (this.parent) {

                        // Store parent token object on a derived block
                        token.parentBlock = this.parent.blocks[blockname];

                        // this will return -1 for a nested block
                        var parentBlockIndex = _.indexOf(this.parent.tokens,
                                this.parent.blocks[blockname]);
                        if ( parentBlockIndex >= 0 ) {
                            this.parent.tokens[parentBlockIndex] = token;
                        }

                    }
                } else if (token.name === 'set') {
                    preservedTokens.push(token);
                }
            }
        }, this);

        // If extendsHasVar == true, then we know {% extends %} is not using a string literal, thus we can't
        // compile until render is called, so we return false.
        if (extendsHasVar) {
            return false;
        }

        if (this.parent && this.parent.tokens) {
            this.tokens = preservedTokens.concat(this.parent.tokens);
        }
    }
}

exports.compile = function compile(indent, /* option no longer needed */ deprecated, context, template) {
    var code = '',
        wrappedInMethod;

    indent = indent || '';

    // Template parameter is optional (not used at the top-level), initialize
    if (this.type === TEMPLATE) {
        template = this;
    }

    // Initialize blocks
    if (!this.blocks) {
        this.blocks = {};
    }

    // Precompile step - process block inheritence into true token hierarchy
    if (precompile.call(this, indent, context) === false) {
        return false;
    }

    // If this is not a template then just iterate through its tokens
    _.each(this.tokens, function (token, index) {
        var name, key, args, prev, next;
        if (typeof token === 'string') {
            prev = this.tokens[index - 1];
            next = this.tokens[index + 1];
            if (prev && prev.strip && prev.strip.after) {
                token = token.replace(/^\s+/, '');
            }
            if (next && next.strip && next.strip.before) {
                token = token.replace(/\s+$/, '');
            }
            code += '_output += "' + doubleEscape(token).replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/"/g, '\\"') + '";\n';
            return code;
        }

        if (typeof token !== 'object') {
            return; // Tokens can be either strings or objects
        }

        if (token.type === VAR_TOKEN) {
            name = token.name.replace(/\W/g, '_');
            key = (helpers.isLiteral(name)) ? '["' + name + '"]' : '.' + name;
            args = (token.args && token.args.length) ? token.args : '';

            code += 'if (typeof _context !== "undefined" && typeof _context' + key + ' === "function") {\n';
            wrappedInMethod = helpers.wrapMethod('', { name: name, args: args }, '_context');
            code += '    _output = (typeof _output === "undefined") ? ' + wrappedInMethod + ': _output + ' + wrappedInMethod + ';\n';
            if (helpers.isValidName(name)) {
                code += '} else if (typeof ' + name + ' === "function") {\n';
                wrappedInMethod = helpers.wrapMethod('', { name: name, args: args });
                code += '    _output = (typeof _output === "undefined") ? ' + wrappedInMethod + ': _output + ' + wrappedInMethod + ';\n';
            }
            code += '} else {\n';
            code += helpers.setVar('__' + name, token);
            code += '    _output = (typeof _output === "undefined") ? __' + name + ': _output + __' + name + ';\n';
            code += '}\n';
        }

        if (token.type !== LOGIC_TOKEN) {
            return; // Tokens can be either VAR_TOKEN or LOGIC_TOKEN
        }

        if (token.name === 'block') {
            var blockname = token.args[0];

            // Sanity check - the template should be in the flat block list.
            if (!template.blocks.hasOwnProperty(blockname)) {
                throw new Error('Unrecognized nested block.  Block \"' + blockname +
                                '\" at line ' + token.line + ' of \"' + template.id +
                                '\" is not in template block list.');
            }

            code += exports.compile.call(template.blocks[token.args[0]], indent + '  ', null, context, template );
        } else if (token.name === 'parent') {
            var parentBlock = getParentBlock(token);
            if (!parentBlock) {
                throw new Error('No parent block found for parent tag at line ' +
                                token.line + '.');
            }

            code += exports.compile.call(parentBlock, indent + '  ', null, context);
        } else if (token.hasOwnProperty("compile")) {
            if (token.strip.start && token.tokens.length && typeof token.tokens[0] === 'string') {
                token.tokens[0] = token.tokens[0].replace(/^\s+/, '');
            }
            if (token.strip.end && token.tokens.length && typeof _.last(token.tokens) === 'string') {
                token.tokens[token.tokens.length - 1] = _.last(token.tokens).replace(/\s+$/, '');
            }
            code += token.compile(indent + '  ', null, exports);
        }
        else {
            code += exports.compile.call(token, indent + '  ', null, context);
        }

    }, this);

    return code;
};

<commit_msg>Fixed lint errors in parser.js.<commit_after>var helpers = require('./helpers'),
    filters = require('./filters'),
    _ = require('underscore'),

    variableRegexp  = /^\{\{.*?\}\}$/,
    logicRegexp     = /^\{%[^\r]*?%\}$/,
    commentRegexp   = /^\{#[^\r]*?#\}$/,

    TEMPLATE = exports.TEMPLATE = 0,
    LOGIC_TOKEN = 1,
    VAR_TOKEN   = 2;

exports.TOKEN_TYPES = {
    TEMPLATE: TEMPLATE,
    LOGIC: LOGIC_TOKEN,
    VAR: VAR_TOKEN
};

function getMethod(input) {
    return input.match(/^[\w\.]+/)[0];
}

function doubleEscape(input) {
    return input.replace(/\\/g, '\\\\');
}

function getArgs(input) {
    return doubleEscape(input.replace(/^[\w\.]+\(|\)$/g, ''));
}

function getContextVar(varName, context) {
    var a = varName.split(".");
    while (a.length) {
        context = context[a.splice(0, 1)[0]];
    }
    return context;
}

function getTokenArgs(token, parts) {
    parts = _.map(parts, doubleEscape);

    var i = 0,
        l = parts.length,
        arg,
        ender,
        out = [];

    function concat(from, ending) {
        var end = new RegExp('\\' + ending + '$'),
            i = from,
            out = '';

        while (!(end).test(out) && i < parts.length) {
            out += ' ' + parts[i];
            parts[i] = null;
            i += 1;
        }

        if (!end.test(out)) {
            throw new Error('Malformed arguments ' + out + ' sent to tag.');
        }

        return out.replace(/^ /, '');
    }

    for (i; i < l; i += 1) {
        arg = parts[i];
        if (arg === null || (/^\s+$/).test(arg)) {
            continue;
        }

        if (
            ((/^\"/).test(arg) && !(/\"[\]\}]?$/).test(arg))
                || ((/^\'/).test(arg) && !(/\'[\]\}]?$/).test(arg))
                || ((/^\{/).test(arg) && !(/\}$/).test(arg))
                || ((/^\[/).test(arg) && !(/\]$/).test(arg))
        ) {
            switch (arg.substr(0, 1)) {
            case "'":
                ender = "'";
                break;
            case '"':
                ender = '"';
                break;
            case '[':
                ender = ']';
                break;
            case '{':
                ender = '}';
                break;
            }
            out.push(concat(i, ender));
            continue;
        }

        out.push(arg);
    }

    return out;
}

function findSubBlocks(topToken, blocks) {
    _.each(topToken.tokens, function (token, index) {
        if (token.name === 'block') {
            blocks[token.args[0]] = token;
            findSubBlocks(token, blocks);
        }
    });
}

function getParentBlock(token) {
    var block;

    if (token.parentBlock) {
        block = token.parentBlock;
    } else if (token.parent) {
        _.any(token.parent, function (parent, index) {
            block = getParentBlock(parent);

            // return first encountered block in parent tree
            if (block !== undefined) {
                return true;
            }
        }, this);
    }

    return block;
}

exports.parseVariable = function (token, escape) {
    if (!token) {
        return {
            type: null,
            name: '',
            filters: [],
            escape: escape
        };
    }

    var filters = [],
        parts = token.replace(/^\{\{ *| *\}\}$/g, '').split('|'),
        varname = parts.shift(),
        i = 0,
        l = parts.length,
        args = null,
        filter_name,
        part;

    if ((/\(/).test(varname)) {
        args = getArgs(varname.replace(/^\w+\./, ''));
        varname = getMethod(varname);
    }

    for (i; i < l; i += 1) {
        part = parts[i];
        if (part && ((/^[\w\.]+\(/).test(part) || (/\)$/).test(part)) && !(/^[\w\.]+\([^\)]*\)$/).test(part)) {
            parts[i] += '|' + parts[i + 1];
            parts[i + 1] = false;
        }
    }
    parts = _.without(parts, false);

    i = 0;
    l = parts.length;
    for (i; i < l; i += 1) {
        part = parts[i];
        filter_name = getMethod(part);
        if ((/\(/).test(part)) {
            filters.push({
                name: filter_name,
                args: getArgs(part)
            });
        } else {
            filters.push({ name: filter_name, args: '' });
        }
    }

    return {
        type: VAR_TOKEN,
        name: varname,
        args: args,
        filters: filters,
        escape: escape
    };
};

exports.parse = function (data, tags, autoescape) {
    var rawtokens = data.replace(/(^\s+)|(\s+$)/g, '').split(/(\{%[^\r]*?%\}|\{\{.*?\}\}|\{#[^\r]*?#\})/),
        escape = !!autoescape,
        last_escape = escape,
        stack = [[]],
        index = 0,
        i = 0,
        j = rawtokens.length,
        filters = [],
        filter_name,
        varname,
        token,
        parts,
        part,
        names,
        matches,
        tagname,
        lines = 1,
        curline = 1,
        newlines = null,
        lastToken,
        rawStart = /^\{\% *raw *\%\}/,
        rawEnd = /\{\% *endraw *\%\}$/,
        inRaw = false,
        stripAfter = false,
        stripBefore = false,
        stripStart = false,
        stripEnd = false;

    for (i; i < j; i += 1) {
        token = rawtokens[i];
        curline = lines;
        newlines = token.match(/\n/g);
        stripAfter = false;
        stripBefore = false;
        stripStart = false;
        stripEnd = false;

        if (newlines) {
            lines += newlines.length;
        }

        if (inRaw !== false && !rawEnd.test(token)) {
            inRaw += token;
            continue;
        }

        // Ignore empty strings and comments
        if (token.length === 0 || commentRegexp.test(token)) {
            continue;
        } else if (/^(\s|\n)+$/.test(token)) {
            token = token.replace(/ +/, ' ').replace(/\n+/, '\n');
        } else if (variableRegexp.test(token)) {
            token = exports.parseVariable(token, escape);
        } else if (logicRegexp.test(token)) {
            if (rawEnd.test(token)) {
                // Don't care about the content in a raw tag, so end tag may not start correctly
                token = inRaw + token.replace(rawEnd, '');
                inRaw = false;
                stack[index].push(token);
                continue;
            }

            if (rawStart.test(token)) {
                // Have to check the whole token directly, not just parts, as the tag may not end correctly while in raw
                inRaw = token.replace(rawStart, '');
                continue;
            }

            parts = token.replace(/^\{%\s*|\s*%\}$/g, '').split(' ');
            if (parts[0] === '-') {
                stripBefore = true;
                parts.shift();
            }
            tagname = parts.shift();
            if (_.last(parts) === '-') {
                stripAfter = true;
                parts.pop();
            }

            if (index > 0 && (/^end/).test(tagname)) {
                lastToken = _.last(stack[stack.length - 2]);
                if ('end' + lastToken.name === tagname) {
                    if (lastToken.name === 'autoescape') {
                        escape = last_escape;
                    }
                    lastToken.strip.end = stripBefore;
                    lastToken.strip.after = stripAfter;
                    stack.pop();
                    index -= 1;
                    continue;
                }

                throw new Error('Expected end tag for "' + lastToken.name + '", but found "' + tagname + '" at line ' + lines + '.');
            }

            if (!tags.hasOwnProperty(tagname)) {
                throw new Error('Unknown logic tag at line ' + lines + ': "' + tagname + '".');
            }

            if (tagname === 'autoescape') {
                last_escape = escape;
                escape = (!parts.length || parts[0] === 'true') ? ((parts.length >= 2) ? parts[1] : true) : false;
            }

            token = {
                type: LOGIC_TOKEN,
                line: curline,
                name: tagname,
                compile: tags[tagname],
                parent: _.uniq(stack[stack.length - 2] || []),
                strip: {
                    before: stripBefore,
                    after: stripAfter,
                    start: false,
                    end: false
                }
            };
            token.args = getTokenArgs(token, parts);

            if (tags[tagname].ends) {
                token.strip.after = false;
                token.strip.start = stripAfter;
                stack[index].push(token);
                stack.push(token.tokens = []);
                index += 1;
                continue;
            }
        }

        // Everything else is treated as a string
        stack[index].push(token);
    }

    if (inRaw !== false) {
        throw new Error('Missing expected end tag for "raw" on line ' + curline + '.');
    }

    if (index !== 0) {
        lastToken = _.last(stack[stack.length - 2]);
        throw new Error('Missing end tag for "' + lastToken.name + '" that was opened on line ' + lastToken.line + '.');
    }

    return stack[index];
};

function precompile(indent, context) {
    var filepath,
        extendsHasVar,
        parentTokens,
        preservedTokens = [];

    // Precompile - extract blocks and create hierarchy based on 'extends' tags
    // TODO: make block and extends tags accept context variables

    // Only precompile at the template level
    if (this.type === TEMPLATE) {

        _.each(this.tokens, function (token, index) {

            if (!extendsHasVar) {
                // Load the parent template
                if (token.name === 'extends') {
                    filepath = token.args[0];

                    if (!helpers.isStringLiteral(filepath)) {

                        if (!context) {
                            extendsHasVar = true;
                            return;
                        }
                        filepath = "\"" + getContextVar(filepath, context) + "\"";
                    }

                    if (!helpers.isStringLiteral(filepath) || token.args.length > 1) {
                        throw new Error('Extends tag on line ' + token.line + ' accepts exactly one string literal as an argument.');
                    }
                    if (index > 0) {
                        throw new Error('Extends tag must be the first tag in the template, but "extends" found on line ' + token.line + '.');
                    }
                    token.template = this.compileFile(filepath.replace(/['"]/g, ''));
                    this.parent = token.template;

                    // inherit tokens/blocks from parent.
                    this.blocks = _.extend({}, this.parent.blocks, this.blocks);

                } else if (token.name === 'block') { // Make a list of blocks
                    var blockname = token.args[0],
                        parentBlockIndex;

                    if (!helpers.isValidBlockName(blockname) || token.args.length !== 1) {
                        throw new Error('Invalid block tag name "' + blockname + '" on line ' + token.line + '.');
                    }

                    // store blocks as flat reference list on top-level
                    // template object
                    this.blocks[blockname] = token;

                    // child tokens may contain more blocks at this template
                    // level - apply to flat this.blocks object
                    findSubBlocks(token, this.blocks);

                    // search parent list for a matching block, replacing the
                    // parent template block tokens with the derived token.
                    if (this.parent) {

                        // Store parent token object on a derived block
                        token.parentBlock = this.parent.blocks[blockname];

                        // this will return -1 for a nested block
                        parentBlockIndex = _.indexOf(this.parent.tokens,
                                this.parent.blocks[blockname]);
                        if (parentBlockIndex >= 0) {
                            this.parent.tokens[parentBlockIndex] = token;
                        }

                    }
                } else if (token.name === 'set') {
                    preservedTokens.push(token);
                }
            }
        }, this);

        // If extendsHasVar == true, then we know {% extends %} is not using a string literal, thus we can't
        // compile until render is called, so we return false.
        if (extendsHasVar) {
            return false;
        }

        if (this.parent && this.parent.tokens) {
            this.tokens = preservedTokens.concat(this.parent.tokens);
        }
    }
}

exports.compile = function compile(indent, /* option no longer needed */ deprecated, context, template) {
    var code = '',
        wrappedInMethod,
        blockname,
        parentBlock;

    indent = indent || '';

    // Template parameter is optional (not used at the top-level), initialize
    if (this.type === TEMPLATE) {
        template = this;
    }

    // Initialize blocks
    if (!this.blocks) {
        this.blocks = {};
    }

    // Precompile step - process block inheritence into true token hierarchy
    if (precompile.call(this, indent, context) === false) {
        return false;
    }

    // If this is not a template then just iterate through its tokens
    _.each(this.tokens, function (token, index) {
        var name, key, args, prev, next;
        if (typeof token === 'string') {
            prev = this.tokens[index - 1];
            next = this.tokens[index + 1];
            if (prev && prev.strip && prev.strip.after) {
                token = token.replace(/^\s+/, '');
            }
            if (next && next.strip && next.strip.before) {
                token = token.replace(/\s+$/, '');
            }
            code += '_output += "' + doubleEscape(token).replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/"/g, '\\"') + '";\n';
            return code;
        }

        if (typeof token !== 'object') {
            return; // Tokens can be either strings or objects
        }

        if (token.type === VAR_TOKEN) {
            name = token.name.replace(/\W/g, '_');
            key = (helpers.isLiteral(name)) ? '["' + name + '"]' : '.' + name;
            args = (token.args && token.args.length) ? token.args : '';

            code += 'if (typeof _context !== "undefined" && typeof _context' + key + ' === "function") {\n';
            wrappedInMethod = helpers.wrapMethod('', { name: name, args: args }, '_context');
            code += '    _output = (typeof _output === "undefined") ? ' + wrappedInMethod + ': _output + ' + wrappedInMethod + ';\n';
            if (helpers.isValidName(name)) {
                code += '} else if (typeof ' + name + ' === "function") {\n';
                wrappedInMethod = helpers.wrapMethod('', { name: name, args: args });
                code += '    _output = (typeof _output === "undefined") ? ' + wrappedInMethod + ': _output + ' + wrappedInMethod + ';\n';
            }
            code += '} else {\n';
            code += helpers.setVar('__' + name, token);
            code += '    _output = (typeof _output === "undefined") ? __' + name + ': _output + __' + name + ';\n';
            code += '}\n';
        }

        if (token.type !== LOGIC_TOKEN) {
            return; // Tokens can be either VAR_TOKEN or LOGIC_TOKEN
        }

        if (token.name === 'block') {
            blockname = token.args[0];

            // Sanity check - the template should be in the flat block list.
            if (!template.blocks.hasOwnProperty(blockname)) {
                throw new Error('Unrecognized nested block.  Block \"' + blockname +
                                '\" at line ' + token.line + ' of \"' + template.id +
                                '\" is not in template block list.');
            }

            code += exports.compile.call(template.blocks[token.args[0]], indent + '  ', null, context, template);
        } else if (token.name === 'parent') {
            parentBlock = getParentBlock(token);
            if (!parentBlock) {
                throw new Error('No parent block found for parent tag at line ' +
                                token.line + '.');
            }

            code += exports.compile.call(parentBlock, indent + '  ', null, context);
        } else if (token.hasOwnProperty("compile")) {
            if (token.strip.start && token.tokens.length && typeof token.tokens[0] === 'string') {
                token.tokens[0] = token.tokens[0].replace(/^\s+/, '');
            }
            if (token.strip.end && token.tokens.length && typeof _.last(token.tokens) === 'string') {
                token.tokens[token.tokens.length - 1] = _.last(token.tokens).replace(/\s+$/, '');
            }
            code += token.compile(indent + '  ', null, exports);
        } else {
            code += exports.compile.call(token, indent + '  ', null, context);
        }

    }, this);

    return code;
};

<|endoftext|>


<commit_before>'use strict';

const esprima = require('esprima');
const esquery = require('esquery');
const parseutils = {};
// const fs = require('fs');

parseutils.cache = {};
parseutils.tokens = [];

let ast;

/**
 * function to parse through ast and return desired information based off search parameter
 * @param {string} string - search term, i.e. '[attr="foo"]'
 * @param {object} AST - AST that will be searched. Did not call param 'ast' as
 *              that variable has been used already
 * @returns {array} - array of matching AST nodes, each of which is an Object
 **/
parseutils.query = function (string, AST) {
  const parseInformation = esquery.parse(string);
  return esquery.match(AST, parseInformation);
};

/**
* function parses ast and finds all function declarations, adding parameters
* @param {object} node - an AST node, representing a function, whose parameters we want to grab
* @returns {array} - function parameters
**/
parseutils.functionParameterParse = function (node) {
  return node.params.map(paramsObj => paramsObj.name);
};

/**
* function parses ast and finds all function declarations, adding names
* @param {object} node - an AST node, representing a function, whose name we want to grab
* @returns {string} - function name
**/
parseutils.functionNameParse = function (node) {
  return node.id.name;
};

/**
* function parses ast and finds all variable declarations, determining how they were declared (let, var, const)
* @param {object} node - an AST node, representing a varable, whose declaration we want to grab
* @returns {string} - variable declaration of let, var, or const
**/
parseutils.variableKindParse = function (node) {
  return node.kind;
};

/**
* function parses ast and finds all variable names
* @param {object} node - an AST node, representing a variable, whose name we want to grab
* @returns {string} - variable name
**/
parseutils.variableNameParse = function (node) {
  return node.declarations.map(dec => dec.id.name);
};
const types = [
  {
    type: 'VariableDeclaration',
    callbacks: [[parseutils.variableKindParse, 'kind'],
      [parseutils.variableNameParse, 'variables']],
  },
  { type: 'ForStatement', callbacks: null },
  { type: 'ForInStatement', callbacks: null },
  { type: 'ForOfStatement', callbacks: null },
  { type: 'WhileStatement', callbacks: null },
  { type: 'DoWhileStatement', callbacks: null },
  { type: 'SwitchStatement', callbacks: null },
  { type: 'SwitchCase', callbacks: null },
  { type: 'IfStatement', callbacks: null },
  { type: 'BreakStatement', callbacks: null },
  { type: 'YieldExpression', callbacks: null },
  { type: 'CallExpression', callbacks: null },
  { type: 'ReturnStatement', callbacks: null },
  {
    type: 'FunctionDeclaration',
    callbacks: [[parseutils.functionParameterParse, 'parameters'],
      [parseutils.functionNameParse, 'name']],
  },
  {
    type: 'FunctionExpression',
    callbacks: [[parseutils.functionParameterParse, 'parameters']],
  },
  {
    type: 'ArrowFunctionExpression',
    callbacks: [[parseutils.functionParameterParse, 'parameters']],
  },
];

/**
* function parses ast and finds all global variables not declared with let, var, or const
* @param {object} node - an AST node, representing a program, whose globals we want to grab
* @deprecated since 0.1.5, force 'use strict';
* @returns {string} - variable name
**/
// parseutils.assignmentExpParse = function (node) {
//   return node.left.name;
// };

/**
* Object contains all of the AST node types we parse. To add a new node type:
* @param {string} type - selector for the node type. See https://www.npmjs.com/package/esquery.
* @param {array} callbacks - an array of callbacks, containing the following:
* @param {array} - A single array containing a single callback, with
* @param (function} - the callback function to run, in the 0th index of the array
* @param {string} - the key in which you want to store the callback's return, in the 1st index of the array)
*/
/**
 * function parses ast and finds all searchString nodes, runs callbacks after
 * @param searchString - finds all nodes with this string via esquery
 * @param callbacks - array with nested arrays [[function, logName], ...], logs extra parameters
 **/
parseutils.parseFunction = function (searchString, callbacks) {
  if (!parseutils.cache[searchString]) {
    parseutils.cache[searchString] = [];
  }

  const output = parseutils.query(`[type=${searchString}]`, ast);

  // Tempting to use map and concat but seems like push is faster
  //  http://jsperf.com/multi-array-concat/7
  output.forEach(node => {
    parseutils.cache[searchString].push({
      startLine: node.loc.start.line,
      endLine: node.loc.end.line,
    });
    if (callbacks) {
      callbacks.forEach(cbArray => {
        const name = cbArray[1];
        const func = cbArray[0];
        const extraOutput = func(node);
        parseutils.cache[searchString][parseutils.cache[searchString].length - 1][name]
          = extraOutput;
      });
    }
  });
};

/**
* function loops through the types object and returns an array of callbacks to be run, when the AST is parsed
**/
parseutils.asyncTasks = types.map(function (el) {
  return parseutils.parseFunction.bind(this, el.type, (el.callbacks || null));
});

/**
* function parses a Javascript file and returns an object containing interesting nodes and their details,
* by looping through it with the {@link #parseutils.asyncTasks} array
* @param {string} text - the text of the file to analyze
* @returns {object} - cache object
**/
function parser(text) {
  parseutils.cache = {};

  ast = esprima.parse(text, {
    loc: true,
  });

  parseutils.asyncTasks.forEach(func => {
    func();
  });
  return parseutils.cache;
}

module.exports = {
  parser,
  parseutils,
  types,
};
<commit_msg>restructured type object in file<commit_after>'use strict';

const esprima = require('esprima');
const esquery = require('esquery');
const parseutils = {};
// const fs = require('fs');

parseutils.cache = {};
parseutils.tokens = [];

let ast;

/**
 * function to parse through ast and return desired information based off search parameter
 * @param {string} string - search term, i.e. '[attr="foo"]'
 * @param {object} AST - AST that will be searched. Did not call param 'ast' as
 *              that variable has been used already
 * @returns {array} - array of matching AST nodes, each of which is an Object
 **/
parseutils.query = function (string, AST) {
  const parseInformation = esquery.parse(string);
  return esquery.match(AST, parseInformation);
};

/**
* function parses ast and finds all function declarations, adding parameters
* @param {object} node - an AST node, representing a function, whose parameters we want to grab
* @returns {array} - function parameters
**/
parseutils.functionParameterParse = function (node) {
  return node.params.map(paramsObj => paramsObj.name);
};

/**
* function parses ast and finds all function declarations, adding names
* @param {object} node - an AST node, representing a function, whose name we want to grab
* @returns {string} - function name
**/
parseutils.functionNameParse = function (node) {
  return node.id.name;
};

/**
* function parses ast and finds all variable declarations, determining how they were declared (let, var, const)
* @param {object} node - an AST node, representing a varable, whose declaration we want to grab
* @returns {string} - variable declaration of let, var, or const
**/
parseutils.variableKindParse = function (node) {
  return node.kind;
};

/**
* function parses ast and finds all variable names
* @param {object} node - an AST node, representing a variable, whose name we want to grab
* @returns {string} - variable name
**/
parseutils.variableNameParse = function (node) {
  return node.declarations.map(dec => dec.id.name);
};
const types = [
  { type: 'BreakStatement', callbacks: null },
  { type: 'ReturnStatement', callbacks: null },
  { type: 'YieldExpression', callbacks: null },
  {
    type: 'FunctionDeclaration',
    callbacks: [[parseutils.functionParameterParse, 'parameters'],
      [parseutils.functionNameParse, 'name']],
  },
  {
    type: 'FunctionExpression',
    callbacks: [[parseutils.functionParameterParse, 'parameters']],
  },
  {
    type: 'ArrowFunctionExpression',
    callbacks: [[parseutils.functionParameterParse, 'parameters']],
  },
  { type: 'SwitchCase', callbacks: null },
  {
    type: 'VariableDeclaration',
    callbacks: [[parseutils.variableKindParse, 'kind'],
      [parseutils.variableNameParse, 'variables']],
  },
  { type: 'CallExpression', callbacks: null },
  { type: 'SwitchStatement', callbacks: null },
  { type: 'ForStatement', callbacks: null },
  { type: 'ForInStatement', callbacks: null },
  { type: 'ForOfStatement', callbacks: null },
  { type: 'WhileStatement', callbacks: null },
  { type: 'DoWhileStatement', callbacks: null },
  { type: 'IfStatement', callbacks: null },
];

/**
* function parses ast and finds all global variables not declared with let, var, or const
* @param {object} node - an AST node, representing a program, whose globals we want to grab
* @deprecated since 0.1.5, force 'use strict';
* @returns {string} - variable name
**/
// parseutils.assignmentExpParse = function (node) {
//   return node.left.name;
// };

/**
* Object contains all of the AST node types we parse. To add a new node type:
* @param {string} type - selector for the node type. See https://www.npmjs.com/package/esquery.
* @param {array} callbacks - an array of callbacks, containing the following:
* @param {array} - A single array containing a single callback, with
* @param (function} - the callback function to run, in the 0th index of the array
* @param {string} - the key in which you want to store the callback's return, in the 1st index of the array)
*/
/**
 * function parses ast and finds all searchString nodes, runs callbacks after
 * @param searchString - finds all nodes with this string via esquery
 * @param callbacks - array with nested arrays [[function, logName], ...], logs extra parameters
 **/
parseutils.parseFunction = function (searchString, callbacks) {
  if (!parseutils.cache[searchString]) {
    parseutils.cache[searchString] = [];
  }

  const output = parseutils.query(`[type=${searchString}]`, ast);

  // Tempting to use map and concat but seems like push is faster
  //  http://jsperf.com/multi-array-concat/7
  output.forEach(node => {
    parseutils.cache[searchString].push({
      startLine: node.loc.start.line,
      endLine: node.loc.end.line,
    });
    if (callbacks) {
      callbacks.forEach(cbArray => {
        const name = cbArray[1];
        const func = cbArray[0];
        const extraOutput = func(node);
        parseutils.cache[searchString][parseutils.cache[searchString].length - 1][name]
          = extraOutput;
      });
    }
  });
};

/**
* function loops through the types object and returns an array of callbacks to be run, when the AST is parsed
**/
parseutils.asyncTasks = types.map(function (el) {
  return parseutils.parseFunction.bind(this, el.type, (el.callbacks || null));
});

/**
* function parses a Javascript file and returns an object containing interesting nodes and their details,
* by looping through it with the {@link #parseutils.asyncTasks} array
* @param {string} text - the text of the file to analyze
* @returns {object} - cache object
**/
function parser(text) {
  parseutils.cache = {};

  ast = esprima.parse(text, {
    loc: true,
  });

  parseutils.asyncTasks.forEach(func => {
    func();
  });
  return parseutils.cache;
}

module.exports = {
  parser,
  parseutils,
  types,
};
<|endoftext|>