UNPKG

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