node-sass-self-contained-importer
Version:
Used to execute sass/scss import and meanwhile resolve relative asset paths declared in the imported file as relative to the importing file. This plugin works as an importer for node-sass, so it supports gulp.js.
304 lines (255 loc) • 8.98 kB
JavaScript
var fs = require("fs"),
path = require("path"),
XXLogger = require("xxlogger");
/** 插件名称 */
var PLUGIN_NAME = "node-sass-self-contained-importer";
var logger = XXLogger.ofName(PLUGIN_NAME, PLUGIN_NAME + ".log");
logger.setLevel("warn");
/**
* 设定参数默认值
*/
var setDftValue = function(ops, dftOps){
ops = ops || {};
dftOps = dftOps || {};
/* 参数不存在时,从默认参数中读取并赋值 */
for(var p in dftOps)
if(!(p in ops))
ops[p] = dftOps[p];
return ops;
};
/**
* 判断给定的路径是否是绝对路径
* @param {String} str 要判断的字符串
*/
var isAbsolute = function(str){
if(null == str || "" === str.trim())
return false;
return /^\s*(http:|https:|ftp:|data:|\/)/i.test(String(str));
};
/**
* 使用给定的属性为给定的对象创建getter
* @param {Object} target 要创建getter的目标对象
* @param {Object} fields 属性集合
*/
var generateGetters = function(target, fields){
for(var p in fields){
target["get" + (p.substring(0, 1).toUpperCase() + p.substring(1))] = (function(p){
return function(){
return fields[p];
};
})(p);
}
};
/**
* 使用给定的属性为给定的对象创建setter
* @param {Object} target 要创建setter的目标对象
* @param {Object} fields 属性集合
*/
var generateSetters = function(target, fields){
for(var p in fields){
target["set" + (p.substring(0, 1).toUpperCase() + p.substring(1))] = (function(p){
return function(v){
fields[p] = v;
return this;
};
})(p);
}
};
/**
* 引入元数据描述
* @constructor
* @param {String} _href 要引入的文件名
* @param {Number} _beginIndex 引入语句在上下文中的起始索引
* @param {Number} _endIndex 引入语句在上下文中的中止索引(最后一个字符的下一个字符所在的索引)
*/
var LinkMeta = function(_href, _beginIndex, _endIndex){
var fields = {
href: "",
beginIndex: -1,
endIndex: -1,
replacement: null/* 替换内容 */
};
generateGetters(this, fields);
generateSetters(this, fields);
fields.href = _href;
fields.beginIndex = _beginIndex;
fields.endIndex = _endIndex;
};
/**
* 依据特定模式,从给定的css字符串中解析要引入的文件
* @param {String} str 要解析的字符串
* @return {LinkMeta[]} 解析出的,代表“要引入的文件”的引入元数据
*/
var parseCssLinkMetas = function(str){
if(null == str || "" === str.trim())
return [];
var linkRegeExp = /\burl\b\s*\(\s*([^)]*)\s*\)/gim;
var arr = [], tmp;
while((tmp = linkRegeExp.exec(str)) != null){
logger.debug("Matched css: {}", tmp[0]);
var href = tmp[1].trim(),
beginIndex = tmp.index,
endIndex = beginIndex + tmp[0].length;
/* 去除路径前后可能存在的双引号 */
var beginingChar = href.charAt(0),
endingChar = href.charAt(href.length - 1);
if(/['"]/.test(String(beginingChar)))
href = href.substring(1).trim();
if(/['"]/.test(String(endingChar)))
href = href.substring(0, href.length - 1).trim();
/* 忽略绝对路径的引用 */
if(isAbsolute(href)){
logger.debug("Ignore absolute asset: {}", href);
continue;
}
var meta = new LinkMeta(href, beginIndex, endIndex);
meta.src = tmp;
arr.push(meta);
}
return arr;
};
/**
* 根据提供的CSS正文和引用元数据执行引用替换操作
* @param {String} str 样式正文
* @param {LinkMeta[]} linkMetaArray 元数据列表
* @returns {String} 替换后的新内容
*/
var replaceCssLinks = function(str, linkMetaArray){
if(null == str || "" === str.trim())
return str;
if(null == linkMetaArray || 0 === linkMetaArray.length)
return str;
var dstString = "", lastEndIndex = 0;
linkMetaArray.forEach(function(meta){
var beginIndex = meta.getBeginIndex(),
endIndex = meta.getEndIndex(),
replacement = meta.getReplacement();
if(null === replacement || undefined === replacement){
replacement = meta.getHref();
}
replacement = replacement.replace(/\\/gm, "/");
replacement = "url(\"" + replacement + "\")";
dstString += str.substring(lastEndIndex, beginIndex) + replacement;
lastEndIndex = endIndex;
});
dstString += str.substring(lastEndIndex);
return dstString;
};
/**
* 调整给定资源引用中的路径,使其成为相对于提供的基准路径的相对路径
* @param {LinkMeta[]} linkMetaArray 元数据列表
* @param {String} fileAbsolutePath 声明资源引用的样式文件的绝对路径
* @param {String} baseAbsolutePath 调整资源引用的路径时,用于决定最终相对路径的基准路径的绝对路径。如:最终引用资源的css所在的目录
*/
var updateReferenceAsRelativeTo = function(linkMetaArray, fileAbsolutePath, baseAbsolutePath){
if(!Array.isArray(linkMetaArray) || 0 === linkMetaArray.length)
return;
var fileDirAbsolutePath = path.dirname(fileAbsolutePath);
linkMetaArray.forEach(function(meta){
var href = meta.getHref();
if(null == href || "" === (href = String(href).trim()))
return;
var assetAbsolutePath = path.resolve(fileDirAbsolutePath, href);
var newRelativePath = path.relative(baseAbsolutePath, assetAbsolutePath);
logger.debug("Update {}({}) -> {}({})", href, fileAbsolutePath, newRelativePath, baseAbsolutePath);
meta.setReplacement(newRelativePath);
});
};
/**
* 根据给定的SASS文件猜测可能的物理文件名
* @param {String} importedSassFileAbsolutePath 通过import语法指定的要引入的资源的绝对路径
* @returns {String[]} 对应import路径可能存在的物理文件名列表
*/
var guessPotentialFiles = function(importedSassFileAbsolutePath){
var dir = path.dirname(importedSassFileAbsolutePath);
var name = path.basename(importedSassFileAbsolutePath);
var prefix = ["_", ""],
appendix = [".scss", ".sass", ".SCSS", ".SASS", ""];
var arr = [];
for(var i = 0; i < prefix.length; i++)
for(var j = 0; j < appendix.length; j++)
arr.push(path.resolve(dir, prefix[i] + name + appendix[j]));
return arr;
};
/**
* 根据给定的SASS文件查找对应的物理文件名。如果有多个物理文件名存在,则返回匹配的第一个
* @param {String} importedSassFileAbsolutePath 通过import语法指定的要引入的资源的绝对路径
* @returns {String|null} 匹配的物理文件名列表
*/
var findMatchingFilePath = function(importedSassFileAbsolutePath){
var arr = guessPotentialFiles(importedSassFileAbsolutePath);
for(var i = 0; i < arr.length; i++){
if(fs.existsSync(arr[i]) && fs.statSync(arr[i]).isFile())
return arr[i];
}
return null;
};
/**
* 查找并更新正文中资源的引用路径
* @param {String} fileContent css文件正文
* @param {String} fileAbsolutePath css文件的绝度路径
* @param {String} baseAbsolutePath 调整资源引用的路径时,用于决定最终相对路径的基准路径的绝对路径。如:最终引用资源的css所在的目录
* @returns {String}
*/
var parseAndUpdateAssetReferenceAsRelativeTo = function(fileContent, fileAbsolutePath, baseAbsolutePath){
var linkMetas = parseCssLinkMetas(fileContent);
updateReferenceAsRelativeTo(linkMetas, fileAbsolutePath, baseAbsolutePath);
return replaceCssLinks(fileContent, linkMetas);
};
/**
* 读取文件正文
* @type {Function}
*/
var readFile = (function(){
var fileContentCache = {};
var cacheMaxTime = 1000;
var fileCacheTime = {};
return function(filePath, async, callback){
var now = Date.now();
if(filePath in fileContentCache && (now - fileCacheTime[filePath] <= cacheMaxTime))
logger.debug("Using cached content for file: {}", filePath);
if(async){
if(filePath in fileContentCache)
callback && callback(null, fileContentCache[filePath]);
else{
//fs.readFile()'s callback will not be triggered. Why?
fs.readFile(filePath, "utf8", function(err, content){
if(!err){
fileContentCache[filePath] = content;
fileCacheTime[filePath] = now;
}else{
logger.error("Fail to read file content: {}. {}", filePath, e);
console.error(err);
}
callback && callback.apply(null, arguments);
});
}
}else{
if(filePath in fileContentCache && (now - fileCacheTime[filePath] <= cacheMaxTime))
return fileContentCache[filePath];
try{
var content = fs.readFileSync(filePath, "utf8");
fileContentCache[filePath] = content;
fileCacheTime[filePath] = now;
return content;
}catch(e){
logger.error("Fail to read file content: {}. {}", filePath, e);
console.error(e);
return null;
}
}
};
})();
module.exports = {
PLUGIN_NAME: PLUGIN_NAME,
logger: logger,
setDftValue: setDftValue,
LinkMeta: LinkMeta,
parseCssLinkMetas: parseCssLinkMetas,
updateReferenceAsRelativeTo: updateReferenceAsRelativeTo,
replaceCssLinks: replaceCssLinks,
parseAndUpdateAssetReferenceAsRelativeTo: parseAndUpdateAssetReferenceAsRelativeTo,
guessPotentialFiles: guessPotentialFiles,
findMatchingFilePath: findMatchingFilePath,
readFile: readFile
};