UNPKG

grunt-html-build

Version:

Grunt HTML Builder - Appends scripts and styles, Removes debug parts, append html partials, Template options

441 lines (374 loc) 16 kB
/* * grunt-html-build * https://github.com/spatools/grunt-html-build * Copyright (c) 2013 SPA Tools * Code below is licensed under MIT License * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without restriction, * including without limitation the rights to use, copy, modify, merge, * publish, distribute, sublicense, and/or sell copies of the Software, * and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ module.exports = function (grunt) { //#region Global Properties var // Init _ = require("lodash"), URL = require("url"), path = require("path"), beautifier = require("js-beautify"), beautify = { js: beautifier.js, css: beautifier.css, html: beautifier.html }, // Tags Regular Expressions regexTagStartTemplate = "<!--\\s*%parseTag%:(\\w+)\\s*(inline)?\\s*(optional)?\\s*(recursive)?\\s*(noprocess)?\\s*([^\\s]*)\\s*(?:\\[(.*)\\])?\\s*-->", // <!-- build:{type} (inline) (optional) (recursive) {name} [attributes...] --> {} required () optional regexTagEndTemplate = "<!--\\s*\\/%parseTag%\\s*-->", // <!-- /build --> regexTagStart = "", regexTagEnd = "", isFileRegex = /\.(\w+){2,4}$/, processFileRegex = /\$\(([^\)]*)\)/; //#endregion //#region Private Methods function getBuildTags(content) { var lines = content.replace(/\r?\n/g, "\n").split(/\n/), tag = false, tags = [], last; lines.forEach(function (l) { var tagStart = l.match(new RegExp(regexTagStart)), tagEnd = new RegExp(regexTagEnd).test(l); if (tagStart) { tag = true; last = { type: tagStart[1], inline: !!tagStart[2], optional: !!tagStart[3], recursive: !!tagStart[4], noprocess: !!tagStart[5], name: tagStart[6], attributes: tagStart[7], lines: [] }; tags.push(last); } // switch back tag flag when endbuild if (tag && tagEnd) { last.lines.push(l); tag = false; } if (tag && last) { last.lines.push(l); } }); return tags; } function defaultProcessPath(pathes, params, opt) { //takes an array of paths and validates them var local = grunt.file.expand(opt, pathes), flattenPaths = _.isArray(pathes) ? _.flattenDeep(pathes) : pathes, remote = flattenPaths.filter(function (path) { //for loading from cdn return /^((http|https):)?(\\|\/\/)/.test(path); //is http, https, or // }); if (params.relative && opt.cwd) { local = local.map(function (src) { return path.join(opt.cwd, src); }); } return _.uniq(local.concat(remote)); } function validateBlockWithName(tag, params) { var src = params[tag.type + "s"], keys = tag.name.split("."), ln = keys.length; for (var i = 0; i < ln; i++) { src = src[keys[i]]; // Search target } if (src) { // check if we want to use current file name, if so update src where necessary if( params.useFileName && src.toString().match(/_CURRENT_FILE_NAME_/g) ) { src = src.toString().replace("_CURRENT_FILE_NAME_", processedFile); } var opt = {}, files = src; if (_.isObject(src)) { if (src.files) { opt = _.omit(src, "files"); files = src.files; } else { // if paths are named, just take values files = _.values(src); } } if (!Array.isArray(files)) { files = [files]; } if (params.processFiles) { var filesContext = params.filesContext; files = files.map(function(f) { return f.replace(processFileRegex, function(val, name) { return filesContext[name] || val; }); }); } return params.processPath(files, params, opt); } } function validateBlockAlways(tag) { return true; } function setTagRegexes(parseTag) { regexTagStart = regexTagStartTemplate.replace(/%parseTag%/, function () { return parseTag }); regexTagEnd = regexTagEndTemplate.replace(/%parseTag%/, function () { return parseTag }); } function createFilesContext(src) { return { path: src, dir: path.dirname(src), file: path.basename(src), filename: path.basename(src, path.extname(src)), dirname: path.basename(path.dirname(src)), platform: process.platform }; } //#endregion //#region Processors Methods function createTemplateData(options, src, attrs) { var extend = { src: src || "", attributes: attrs || "" }; return { data: _.extend({}, options.data, extend) }; } function processTemplate(template, options, src, attrs) { return grunt.template.process(template, createTemplateData(options, src, attrs)); } function createAttributes(options, src) { var attrs = options.attributes || ""; if (options.type === "script") { attrs = 'type="text/javascript" ' + attrs; } else if (options.type === "style" && !options.inline) { if (path.extname(src) === ".less") { attrs = 'type="text/css" rel="stylesheet/less" ' + attrs; } else { attrs = 'type="text/css" rel="stylesheet" ' + attrs; } } return attrs.trim(); } function processHtmlTagTemplate(options, src) { var template = templates[options.type + (options.inline ? "-inline" : "")], attrs = createAttributes(options, src); if (!options.inline || options.noprocess) { return template .replace(/\<\%\= (src|attributes) \%\>/g, function (match, p1) { if (p1 === "src") { return src; } else if (p1 === "attributes") { return attrs; } }); } else { return processTemplate(template, options, src, attrs); } } function processHtmlTag(options) { if (options.inline) { var content = options.files.map(grunt.file.read).join(options.EOL); return processHtmlTagTemplate(options, content); } else { var destDir = options.relative && isFileRegex.test(options.dest) ? path.dirname(options.dest) : options.dest; return options.files.map(function (f) { var url = (options.relative && !/^((http|https):)?(\\|\/\/)/.test(f)) ? path.relative(destDir, f) : f; url = url.replace(/\\/g, "/"); if (options.prefix) { url = URL.resolve(options.prefix.replace(/\\/g, "/"), url); } if (options.suffix) { var suffix = typeof options.suffix === "function" ? options.suffix(f, url) : options.suffix; if (suffix) { url += "?" + suffix; } } return processHtmlTagTemplate(options, url); }).join(options.EOL); } } //#endregion //#region Processors / Validators / Templates var templates = { 'script': '<script <%= attributes %> src="<%= src %>"></script>', 'script-inline': '<script <%= attributes %>><%= src %></script>', 'style': '<link <%= attributes %> href="<%= src %>" />', 'style-inline': '<style <%= attributes %>><%= src %></style>' }, validators = { script: validateBlockWithName, style: validateBlockWithName, section: validateBlockWithName, process: validateBlockAlways, remove: validateBlockAlways, //base method validate: function (tag, params) { if (!validators[tag.type]) { return false; } return validators[tag.type](tag, params); } }, processors = { script: processHtmlTag, style: processHtmlTag, section: function (options) { return options.files.map(function (f) { var content = grunt.file.read(f).toString(); return options.recursive ? transformContent(content, options.params, options.dest) : content; }).join(options.EOL); }, process: function (options) { return options.lines .map(function (l) { return processTemplate(l, options); }) .join(options.EOL) .replace(new RegExp(regexTagStart), "") .replace(new RegExp(regexTagEnd), ""); }, remove: function (options) { if (!options.name) return ""; var targets = options.name.split(","); if (targets.indexOf(grunt.task.current.target) < 0) { return options.lines.join(options.EOL).replace(new RegExp(regexTagStart), "").replace(new RegExp(regexTagEnd), ""); } return ""; }, //base method transform: function (options) { return processors[options.type](options); } }; //#endregion function ensureContent(content, params) { if (!params.EOL) { var match = content.match(/\r?\n/); params.EOL = match ? match[0] : "\n"; } return content.replace(/\r?\n/g, params.EOL); } function transformContent(content, params, dest) { var tags = getBuildTags(content), config = grunt.config(); tags.forEach(function (tag) { var raw = tag.lines.join(params.EOL), result = "", prefix = "", suffix = "", tagFiles = validators.validate(tag, params); if (tagFiles) { var options = _.extend({}, tag, { data: _.extend({}, config, params.data), files: tagFiles, dest: dest, prefix: params.prefix, suffix: params.suffix, relative: params.relative, EOL: params.EOL, params: params }); result = processors.transform(options); } else if (tagFiles === false) { grunt.log.warn("Unknown tag detected: '" + tag.type + "'"); if (!params.allowUnknownTags) { grunt.fail.warn("Use 'parseTag' or 'allowUnknownTags' options to avoid this issue"); } } else if (tag.optional) { if (params.logOptionals) { grunt.log.warn("Tag with type: '" + tag.type + "' and name: '" + tag.name + "' is not configured in your Gruntfile.js but is set optional, deleting block !"); } } else { grunt.fail.warn("Tag with type '" + tag.type + "' and name: '" + tag.name + "' is not configured in your Gruntfile.js !"); } if (params.keepTags) { prefix = raw.match(new RegExp(regexTagStart + "\\s*"))[0]; suffix = raw.match(new RegExp(regexTagEnd + "\\s*"))[0]; } content = content.replace(raw, function () { return prefix + result + suffix; }); }); if (params.beautify) { content = beautify.html(content, _.isObject(params.beautify) ? params.beautify : {}); } return content; } grunt.registerMultiTask('htmlbuild', "Grunt HTML Builder - Replace scripts and styles, Removes debug parts, append html partials, Template options", function () { var params = this.options({ beautify: false, logOptionals: false, relative: true, basePath: false, keepTags: false, scripts: {}, styles: {}, sections: {}, data: {}, parseTag: 'build', processFiles: false, processPath: defaultProcessPath }); setTagRegexes(params.parseTag); this.files.forEach(function (file) { var dest = file.dest || "", destPath, content; file.src.forEach(function (src) { if (params.processFiles) { params.filesContext = createFilesContext(src); } // replace files in the same folder if (params.replace) { destPath = src; } // copy original folder structure into dest folder and compile templates else if (params.basePath || params.basePath === "") { // new path = dest + (src path without basePath at the beginning) destPath = dest + src.substring(params.basePath.length, src.length); } // Regex for path else if (isFileRegex.test(dest)) { destPath = dest; } // default: copy all files into destination else { destPath = path.join(dest, path.basename(src)); } content = grunt.file.read(src); content = ensureContent(content, params); content = transformContent(content, params, dest); // write the contents to destination grunt.file.write(destPath, content); grunt.log.ok("File " + destPath + " created !"); }); }); }); };