UNPKG

ssi

Version:

Server Side Includes for NodeJS

301 lines (239 loc) 8.82 kB
var path = require("path"); var Conditional = require("./Conditional"); var ATTRIBUTE_MATCHER = /([a-z]+)="(.+?)"/g; var INTERPOLATION_MATCHER = /\$\{(.+?)\}/g; module.exports = (function() { "use strict"; var mergeSimpleObject = function() { var output = {}; for (var i = 0; i < arguments.length; i++) { var argument = arguments[i]; for (var key in argument) { if (argument.hasOwnProperty(key)) { output[key] = argument[key]; } } } return output; }; var DirectiveHandler = function(ioUtils, directiveRegex) { this.parser = undefined; this.ioUtils = ioUtils; this.conditionals = []; this.currentConditional = undefined; this.directiveRegex = directiveRegex; }; DirectiveHandler.prototype = { /* Public Methods */ handleDirective: function(directive, directiveName, currentFile, variables) { if (this._inConditional()) { if (!this._isConditional(directiveName)) { this.currentConditional.addDirective(directive); return {output: ""}; } } var attributes = this._parseAttributes(directive); function interpolate() { for (var i = 0; i < attributes.length; i++) { var attribute = attributes[i]; attribute.name = this._interpolate(attribute.name, variables, false); attribute.value = this._interpolate(attribute.value, variables, false); } } switch (directiveName) { case "if": interpolate.apply(this); return this._handleIf(attributes, variables); case "elif": interpolate.apply(this); return this._handleElseIf(attributes, variables); case "else": interpolate.apply(this); return this._handleElse(); case "endif": interpolate.apply(this); return this._handleEndIf(currentFile, variables); case "set": interpolate.apply(this); return this._handleSet(attributes); case "echo": interpolate.apply(this); return this._handleEcho(attributes, variables); case "include": interpolate.apply(this); return this._handleInclude(attributes, currentFile, variables); } return {error: "Could not find parse directive #" + directiveName}; }, /* Private Methods */ _interpolate: function(string, variables, shouldWrap) { var instance = this; return string.replace(INTERPOLATION_MATCHER, function(variable, variableName) { var value; // Either return the variable value or the original expression if it doesn't exist if (variables[variableName] !== undefined) { value = variables[variableName]; } else if (process.env[variableName] !== undefined) { value = process.env[variableName]; } if (value !== undefined) { if (shouldWrap) { // Escape all double quotes and wrap the value in double quotes return instance._wrap(variables[variableName]); } return value; } return variable; }); }, _parseAttributes: function(directive) { var attributes = []; directive.replace(ATTRIBUTE_MATCHER, function(attribute, name, value) { attributes.push({name: name, value: value}); }); return attributes; }, _parseExpression: function(expression) { if (expression.match(INTERPOLATION_MATCHER)) { return {error: "Could not resolve all variables"} } // Return a boolean for the truthiness of the expression return {truthy: !!eval(expression)}; }, _wrap: function(value) { if (this._shouldWrap(value)) { return "\"" + value.toString().replace(/"/g, "\\\"") + "\""; } return value; }, _shouldWrap: function(value) { var type = typeof value; return (type !== "boolean" && type !== "number"); }, _handleSet: function(attributes) { if (attributes.length === 2 && attributes[0].name === "var" && attributes[1].name === "value") { return {variables: [{ name: attributes[0].value, value: attributes[1].value }]}; } return {error: "Directive #set did not contain a 'var' and 'value' attribute"}; }, _handleEcho: function(attributes, variables) { if (attributes.length == 1 && attributes[0].name === "var") { return {output: variables[attributes[0].value]}; } return {error: "Directive #echo did not contain a 'var' attribute"}; }, _handleInclude: function(attributes, currentFile, variables) { if (attributes.length !== 1) { return {error: "Directive #include did not contain the correct number of attributes"}; } else if (attributes[0].name !== "virtual" && attributes[0].name !== "file") { return {error: "Directive #include did not contain a 'file' or 'virtual' attribute"}; } var attribute = attributes[0]; var attributeName = attribute.name; var filename = attribute.value; var results = {output: ""}; if (attributeName === "file") { results = {output: this.ioUtils.readFileSync(currentFile, filename)}; } else if (attributeName === "virtual") { results = {output: this.ioUtils.readVirtualSync(currentFile, filename)}; } // Parse the contents of the file to handle SSI directives var parsed = this.parser.parse(this.ioUtils.resolveFullPath(currentFile, filename), results.output, variables); results.output = parsed.contents; results.variables = []; for (var key in parsed.variables) { if (parsed.variables.hasOwnProperty(key)) { results.variables.push({ name: key, value: parsed.variables[key] }); } } return results; }, _handleIf: function(attributes, variables) { this.conditionals = []; if (attributes.length === 1 && attributes[0].name === "expr") { return this._handleExpr(attributes[0].value, variables); } return {error: "If does not have a single 'expr' attribute"}; }, _handleElseIf: function(attributes, variables) { if (attributes.length === 1 && attributes[0].name === "expr") { if (!this._inConditional()) { return {error: "Elif while not inside of If block"}; } return this._handleExpr(attributes[0].value, variables); } return {error: "Elif does not have a single 'expr' attribute"}; }, _handleElse: function() { if (!this._inConditional()) { return {error: "Else while not inside of If block"}; } // As a hack, just provide an always true expression return this._handleExpr("true"); }, _handleEndIf: function(currentFile, pageVariables) { if (!this._inConditional()) { return {error: "Endif while not inside of If block"}; } for (var i = 0; i < this.conditionals.length; i++) { var conditional = this.conditionals[i]; var variables = {}; // Find the first conditional that is true if (this._parseExpression(conditional.getExpression()).truthy) { var directiveHandler = new DirectiveHandler(this.ioUtils, this.directiveRegex); var output = {output: "", variables: {}}; // Iterate over the directives contained by the conditional, and parse them for (var j = 0; j < conditional.getDirectives().length; j++) { var directive = conditional.getDirectives()[j]; // We can assume this matches the directive format //noinspection JSValidateTypes var directiveName = new RegExp(this.directiveRegex).exec(directive)[1]; var results = directiveHandler.handleDirective(directive, directiveName, currentFile, mergeSimpleObject(variables, pageVariables)); output.output += results.output || ""; output.variables = mergeSimpleObject(output.variables, results.variables || {}); } this.conditionals = []; this.currentConditional = undefined; return output; } } this.conditionals = []; this.currentConditional = undefined; return {output: ""}; }, _handleExpr: function(expression, variables) { // HACK for Regex variable support if (expression.split(' ').length == 3) var statement = expression.split(' '); if (statement) { var variable = variables[statement[0].substring(1)]; var regex = new RegExp(/^\/(.*)\/$/.test(statement[2]) ? statement[2].slice(1, -1).replace(/\\\\/g, "\\") : statement[2]); var result = regex.exec(variable); expression = (statement[1] == '!=' ? !!!result : !!result).toString(); result && result.forEach(function(val, index){ variables[index.toString()] = val; }); } // Create a new conditional, put it on the stack and assign as current conditional var conditional = new Conditional(expression); this.conditionals.push(conditional); this.currentConditional = conditional; return {output: ""}; }, _isConditional: function(directive) { return (directive === "if" || directive === "elif" || directive === "else" || directive === "endif"); }, _inConditional: function() { return this.conditionals.length > 0; } }; // Export the DirectiveHandler for use return DirectiveHandler; })();