UNPKG

css-sprite-loader

Version:

A webpack loader to convert png into sprite image

260 lines (230 loc) 9.25 kB
const postcss = require('postcss'); const { utils } = require('base-css-image-loader'); const meta = require('./meta'); const { default: CSSFruit, Background, URL } = require('css-fruit'); CSSFruit.config({ forceParsing: { url: true, 'image-set': true, length: true, percentage: true, }, }); function genMediaQuery(resolution, defaultResolution, selector, content) { const dppx = resolution.slice(0, -1); if (resolution > defaultResolution) { return ` @media (-webkit-min-device-pixel-ratio: ${dppx}), (min-resolution: ${dppx}dppx) { ${selector} { ${content} } } `; } else if (resolution < defaultResolution) { return ` @media (-webkit-max-device-pixel-ratio: ${dppx}), (max-resolution: ${dppx}dppx) { ${selector} { ${content} } } `; } } module.exports = postcss.plugin('css-sprite-parser', ({ loaderContext }) => (styles, result) => { const promises = []; const plugin = loaderContext[meta.PLUGIN_NAME]; const options = plugin.options; const data = plugin.data; let imageSetFallback = options.imageSetFallback; if (imageSetFallback === true) imageSetFallback = { preserve: true }; styles.walkRules((rule) => { const decls = rule.nodes.filter((node) => node.type === 'decl' && node.prop.startsWith('background')); if (!decls.length) return; /** * Core variable 0 */ const oldBackground = CSSFruit.absorb(decls); if (!oldBackground.valid) { rule.warn(result, 'Invalid background'); return; } if (!oldBackground.image) return; // For browsers if (oldBackground.image._type === 'image-set') oldBackground.image.prefix = '-webkit-'; const oldBackgroundString = oldBackground.toString(); /** * Core variable 1 */ const ruleItem = { id: 'ID' + utils.genMD5(oldBackgroundString), defaultResolution: '1x', }; /** * Core variable 2 */ const imageSet = []; let oldResolutions; if (oldBackground.image._type === 'url') { imageSet.push({ url: oldBackground.image, src: undefined, resolution: undefined, needSprite: false, groupName: undefined, }); } else if (oldBackground.image._type === 'image-set') { oldResolutions = Object.keys(oldBackground.image.values); oldResolutions.forEach((resolution) => { imageSet.push({ url: oldBackground.image.values[resolution], src: undefined, resolution, needSprite: false, groupName: undefined, }); }); } else return; // Other type like linear-gradient // Check whether need sprite const checkWhetherNeedSprite = (url) => { if (!url.path.endsWith('.png')) return false; if (options.filter === 'query') return !!(url.query && url.query[options.queryParam]); else if (options.filter instanceof RegExp) return options.filter.test(url.path); else if (options.filter === 'all') return true; else throw new TypeError(`Unknow filter value '${options.filter}'`); }; let someNeedSprite = false; imageSet.forEach((image) => { image.needSprite = checkWhetherNeedSprite(image.url); if (image.needSprite) someNeedSprite = image.needSprite; }); if (!someNeedSprite && !(oldBackground.image._type === 'image-set' && imageSetFallback)) return; // Fill image object, add retina image in imageSet if (oldBackground.image._type === 'url') { const image = imageSet[0]; const query = image.url.query; const baseGroupName = query && typeof query[options.queryParam] === 'string' ? query[options.queryParam] : options.defaultName; image.src = image.url.path; // According to query retina, collect image set const pathRE = /(^.*?)(?:@(\d+x))?\.png$/; const paramRE = /^retina@?(\d+x)$/; const found = image.url.path.match(pathRE); if (!found) throw new Error('Error format of filePath'); let [, basePath, defaultResolution] = found; if (!defaultResolution) defaultResolution = '1x'; // 路径本身指示默认分辨率 image.groupName = baseGroupName; image.resolution = ruleItem.defaultResolution = defaultResolution; Object.keys(query).forEach((param) => { // @compat: old version if (param === 'retina') param = 'retina@2x'; const found = param.match(paramRE); if (!found) return; const resolution = found[1]; const url = new URL(image.url.toString()); url.path = `${basePath}@${resolution}.png`; imageSet.push({ url, src: url.path, resolution, needSprite: image.needSprite, groupName: `${baseGroupName}@${resolution}`, }); }); } else if (oldBackground.image._type === 'image-set') { imageSet.forEach((image, index) => { const query = image.url.query; const baseGroupName = query && typeof query[options.queryParam] === 'string' ? query[options.queryParam] : options.defaultName; image.src = image.url.path; image.groupName = `${baseGroupName}@${image.resolution}`; if (index === 0) { // 第一项指示默认分辨率 image.groupName = baseGroupName; ruleItem.defaultResolution = image.resolution; } }); } /** * Core variable 3 */ const blockSize = { width: undefined, height: undefined, }; // Check width & height rule.walkDecls((decl) => { if (decl.prop === 'width') blockSize.width = decl.value; else if (decl.prop === 'height') blockSize.height = decl.value; }); promises.push(Promise.all(imageSet.map((image) => new Promise((resolve, reject) => { loaderContext.resolve(loaderContext.context, image.src, (err, result) => err ? reject(err) : resolve(result)); }))).then((filePaths) => { // Clean decls in source decls.forEach((decl) => decl.remove()); const outputs = []; filePaths.forEach((filePath, index) => { if (!filePath) throw new Error(`Cannot resolve file path '${imageSet[index].src}'`); loaderContext.addDependency(filePath); const image = imageSet[index]; const groupItem = { id: ruleItem.id, groupName: image.groupName, filePath, oldBackground, blockSize, resolution: image.resolution, content: undefined, // new background cached }; let content = `${meta.REPLACER_NAME}(${image.groupName}, ${groupItem.id})`; if (image.needSprite) { if (!data[image.groupName]) data[image.groupName] = {}; // background 的各种内容没变,id 一定不会变 if (!data[image.groupName][groupItem.id]) data[image.groupName][groupItem.id] = groupItem; } else { const background = new Background(oldBackgroundString); background.image = image.url; content = background.toString(); } if (image.resolution === ruleItem.defaultResolution) rule.append({ prop: 'background', value: content }); else { // No problem in async function outputs.push(genMediaQuery(image.resolution, ruleItem.defaultResolution, rule.selector, `background: ${content};`)); } }); if (oldBackground.image._type === 'image-set' && !someNeedSprite && imageSetFallback.preserve) outputs.push(` ${rule.selector} { background: ${oldBackgroundString}; } `); if (outputs.length) rule.after(outputs.join('')); })); }); if (promises.length) { plugin.shouldGenerate = true; loaderContext._module[meta.MODULE_MARK] = true; } return Promise.all(promises); });