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
JavaScript
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
};