UNPKG

gulp-build-html

Version:

Used to process html files to automatically concat css and js files, and meanwhile update html references.

300 lines (253 loc) 11.2 kB
var path = require("path"), fs = require("fs"), utils = require("wzh.node-utils"), Vinyl = require('vinyl'), BuildBlock = require("./BuildBlock"), lib = require("./lib"), constants = require("./constants"); var logger = lib.logger; var slotFor = "merge-reference-content"; var rBuildBegin = /<\s*slot\s+([^>]*)>/gi, rBuildEnd = /<\s*\/\s*slot(?:[^>]*)>/gi; /** * 支持的构建产出的目标位置 * @type {string[]} */ var supportedOutputLocations = [ "inline",/* 以内联方式构建至插槽声明的地方 */ "file"/* 构建至单独的文件中 */ ]; /** * 默认的构建产出的目标位置 * @type {string} */ var defaultOutputLocation = "file"; /** * 从给定的字符串中解析构建块。构建块格式: * <slot for="merge-content" * fileType="css" * outputLocation="inline|file" * outputFilePath="../css/index.html.allinone.css" * ></slot> * * @param {String} str 声明有构建块的字符串 * @param {String} [containingHtmlFilePath] 声明了该构建块的html文件路径 * @returns {BuildBlock[]} */ var parseBuildBlocks = function(str, containingHtmlFilePath){ rBuildBegin.lastIndex = 0; rBuildEnd.lastIndex = 0; var beginList = [], endIndexList = []; if(utils.string.isEmptyString(str, true)) return []; /* 确定构建块开始头 */ var tmp; while((tmp = rBuildBegin.exec(str)) != null){ logger.debug("Matched build block beginning: {} in {}", tmp[0], containingHtmlFilePath); var buildBlock = new BuildBlock().setBeginIndex(tmp.index); if(null != containingHtmlFilePath) buildBlock.setContainingHtmlFilePath(containingHtmlFilePath); var _slotFor = utils.html.getAttribute(tmp[1], "for"); if(null == _slotFor || _slotFor.value.trim().toLowerCase() !== slotFor) continue; var fileType = utils.html.getAttribute(tmp[1], "fileType"); if(null == fileType){ logger.error("No file type(attr: 'fileType') found in slot: {} in {}", tmp[0], containingHtmlFilePath); continue; } fileType = fileType.value.trim().toLowerCase(); buildBlock.setType(fileType).setBuildOption("fileType", fileType); var outputLocation = utils.html.getAttribute(tmp[1], "outputLocation"); if(null == outputLocation) outputLocation = defaultOutputLocation; else{ outputLocation = outputLocation.value.trim().toLowerCase(); if(supportedOutputLocations.indexOf(outputLocation) === -1){ logger.warn("Unknown output location: {} in slot: {} in {}, using '{}' instead.", outputLocation, tmp[0], containingHtmlFilePath, defaultOutputLocation); outputLocation = defaultOutputLocation; } } buildBlock.setBuildOption("outputLocation", outputLocation); if("file" === outputLocation){ var outputFilePath = utils.html.getAttribute(tmp[1], "outputFilePath"); if(null == outputFilePath){ if(null != containingHtmlFilePath) outputFilePath = path.basename(containingHtmlFilePath) + "." + fileType; else outputFilePath = "${fileName}.allinone." + fileType; logger.warn("No output file path(attr: 'outputFilePath') found in slot: {} in {}, using '{}' instead", tmp[0], containingHtmlFilePath, outputFilePath); }else outputFilePath = utils.string.fillParamValue(outputFilePath.value, { fileName: path.basename(containingHtmlFilePath), fileBaseName: path.basename(containingHtmlFilePath, path.extname(containingHtmlFilePath)), }); buildBlock.setTargetFileName(outputFilePath).setBuildOption("outputFilePath", outputFilePath); } beginList.push(buildBlock); } /* 确定构建块结束尾 */ while((tmp = rBuildEnd.exec(str)) != null){ logger.debug("Matched build block ending: {} in {}", tmp[0], containingHtmlFilePath); endIndexList.push({index: tmp.index, str: tmp[0]}); } /* 确定构建块的开始和结束位置 */ var list = []; for(var i = 0; i < endIndexList.length; i++){ var endIndex = endIndexList[i].index, endStr = endIndexList[i].str; var matchedBeginListIndex = -1; for(var j = 0; j < beginList.length; j++){ var beginIndex = beginList[j].getBeginIndex(); if(beginIndex < endIndex && beginIndex > matchedBeginListIndex) matchedBeginListIndex = j; } if(-1 !== matchedBeginListIndex){ beginList[matchedBeginListIndex].setEndIndex(endIndex + endStr.length - 1); list.push(beginList[matchedBeginListIndex]); beginList.splice(matchedBeginListIndex, 1); } } /* 确定各个构建块之间的引用 */ list.forEach(function(buildBlock){ var type = String(buildBlock.getType()).toLowerCase(), targetFileName = buildBlock.getTargetFileName(); var referenceStr = str.substring(buildBlock.getBeginIndex(), buildBlock.getEndIndex() + 1); switch(type){ case "js": while((tmp = constants.scriptRegexp.exec(referenceStr)) != null){ var src = utils.html.getAttribute(tmp[0], "src"); if(null != src && !utils.string.isEmptyString(src.value, true))/* 外联脚本 */ buildBlock.addReference(src.value); else if(!utils.string.isEmptyString(tmp[1], true))/* 内联脚本 */ buildBlock.addReference(tmp[1], true); } break; case "css": while((tmp = constants.styleRegexp.exec(referenceStr)) != null){ var rel = utils.html.getAttribute(tmp[0], "rel"); if(null != rel && "stylesheet" === rel.value.trim().toLowerCase()) buildBlock.addReference(tmp[1]); } break; default: logger.error("Unknown build type: " + type + " to generate file: " + targetFileName); } }); return list; }; /** * 执行构建动作 * @param {String} fileContent 文件正文 * @param {String} fileAbsolutePath 文件的绝对路径 * @param {Object} [ops] 控制选项 * @param {GeneratedFileInstaller} [ops.generatedCssFileInstaller] 生成的css文件的安装器 * @param {GeneratedContentInstaller} [ops.generatedCssContentInstaller] 生成的css文件的安装器 * @param {GeneratedFileInstaller} [ops.generatedJsFileInstaller] 生成的js文件的安装器 * @param {GeneratedContentInstaller} [ops.generatedJsContentInstaller] 生成的js文件的安装器 * @param {BuildCompleteListener} [ops.oncomplete] 构建完成后要执行的方法 * @returns {{newFileContent: String, generatedVinylFiles: Vinyl[]}} 生成的vinyl文件实例集合 */ var build = function(fileContent, fileAbsolutePath, ops){ var rst = { newFileContent: "", generatedVinylFiles: [] }; if(utils.string.isEmptyString(fileContent, true)) return rst; logger.debug("Processing file: {}", fileAbsolutePath); var blocks = parseBuildBlocks(fileContent, fileAbsolutePath); var lastIndex = 0; blocks.forEach(function(block){ var outputLocation = block.getBuildOption("outputLocation") || defaultOutputLocation; var targetFileName = block.getTargetFileName(true); /* 过滤不存在的文件引用 */ var filePathsOrContents = block.getReferencedFilePathsOrContents(true); filePathsOrContents = filePathsOrContents.filter(function(filePathOrContent){ if(BuildBlock.isReferenceContent(filePathOrContent)) return true; if(!fs.existsSync(filePathOrContent) || !fs.statSync(filePathOrContent).isFile()){ if(outputLocation === "file") logger.error("File: {} not found while generating file: {} for {}", filePathOrContent, targetFileName, fileAbsolutePath); else if(outputLocation === "inline") logger.error("File: {} not found while concatting file content for {}", filePathOrContent, fileAbsolutePath); return false; } return true; }); var ifBuiltSuccessfully = false; var generatedFileContent = ""; if(outputLocation === "file"){ generatedFileContent = block.concatReferenceContent(filePathsOrContents, path.dirname(targetFileName)); logger.info("Generating file: {}", targetFileName); /* 写入合并后的文件 */ utils.fs.mkdirSync(path.dirname(targetFileName)); fs.appendFileSync(targetFileName, generatedFileContent); rst.generatedVinylFiles.push(new Vinyl({ base: path.dirname(fileAbsolutePath), path: targetFileName, contents: new Buffer(generatedFileContent) })); /* 调整 html 中的引用 */ var installStr = block.getTargetFileReferringHtml(); switch(block.getType()){ case "css": if(null != ops && typeof ops.generatedCssFileInstaller === "function") installStr = ops.generatedCssFileInstaller(targetFileName, generatedFileContent, fileAbsolutePath); break; case "js": if(null != ops && typeof ops.generatedJsFileInstaller === "function") installStr = ops.generatedJsFileInstaller(targetFileName, generatedFileContent, fileAbsolutePath); break; } rst.newFileContent += fileContent.substring(lastIndex, block.getBeginIndex()); rst.newFileContent += "\r\n" + installStr + "\r\n"; ifBuiltSuccessfully = true; }else if(outputLocation === "inline"){ generatedFileContent = block.concatReferenceContent(filePathsOrContents, path.dirname(fileAbsolutePath)); /** * 暂不支持针对正文内容的进一步操作,如压缩、丑化等。 * 如果开发者需要对内容进一步处理,则可以结合插件 gulp-resolve-import 使用,例如: * * 1. 在html中声明构建 * <slot for = "merge-reference-content" * fileType = "js" * outputLocation = "file" * outputFilePath = "${fileBaseName}.allinone.js" * > * <script src = "1.js"></script> * <script src = "2.js"></script> * </slot> * 2. 在构建脚本中指明文件的安装方式 * var buildHtml = require("gulp-build-html"); * buildHtml.setGeneratedFileReferenceSetupMethod("js", function(generatedFilePath, generatedFileContent, processingHtmlFilePath){ * return "<script type = 'text/javascript'><link rel = 'import' href = '" + generatedFilePath + "'/></script>"; * }); */ var installStr = block.getTargetContentReferringHtml(generatedFileContent); switch(block.getType()){ case "css": if(null != ops && typeof ops.generatedCssContentInstaller === "function") installStr = ops.generatedCssContentInstaller(generatedFileContent, fileAbsolutePath); break; case "js": if(null != ops && typeof ops.generatedJsContentInstaller === "function") installStr = ops.generatedJsContentInstaller (generatedFileContent, fileAbsolutePath); break; } rst.newFileContent += fileContent.substring(lastIndex, block.getBeginIndex()); rst.newFileContent += "\r\n" + installStr + "\r\n"; ifBuiltSuccessfully = true; }else{ logger.error("Unknown output location: {}", outputLocation); } if(ifBuiltSuccessfully && null != ops) utils.function.try2Call(ops.oncomplete, null, targetFileName, block.getReferencedFilePathsOrContents(true)); lastIndex = block.getEndIndex() + 1;/* 结束索引为“构建块最后一个字符所在的索引” */ }); rst.newFileContent += fileContent.substring(lastIndex); return rst; }; module.exports = { parseBuildBlocks: parseBuildBlocks, build: build };