postcss-at2x
Version:
Adds at-2x keyword to background and background-image declarations to add retina support for images.
256 lines (196 loc) • 8.03 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
var _fs = require('fs');
var _fs2 = _interopRequireDefault(_fs);
var _path = require('path');
var _path2 = _interopRequireDefault(_path);
var _postcssValueParser = require('postcss-value-parser');
var _postcssValueParser2 = _interopRequireDefault(_postcssValueParser);
var _isUrl = require('is-url');
var _isUrl2 = _interopRequireDefault(_isUrl);
var _imageSize = require('image-size');
var _imageSize2 = _interopRequireDefault(_imageSize);
var _pify = require('pify');
var _pify2 = _interopRequireDefault(_pify);
require('string.prototype.includes');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var defaultResolutions = ['(min-device-pixel-ratio: 1.5)', '(min-resolution: 144dpi)', '(min-resolution: 1.5dppx)'];
function defaultResolveImagePath(value) {
return _path2.default.resolve(process.cwd(), value);
}
exports.default = at2x;
function at2x() {
var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
_ref$identifier = _ref.identifier,
identifier = _ref$identifier === undefined ? '@2x' : _ref$identifier,
_ref$detectImageSize = _ref.detectImageSize,
detectImageSize = _ref$detectImageSize === undefined ? false : _ref$detectImageSize,
_ref$resolveImagePath = _ref.resolveImagePath,
resolveImagePath = _ref$resolveImagePath === undefined ? defaultResolveImagePath : _ref$resolveImagePath,
_ref$skipMissingRetin = _ref.skipMissingRetina,
skipMissingRetina = _ref$skipMissingRetin === undefined ? false : _ref$skipMissingRetin;
return {
postcssPlugin: 'postcss-at2x',
Once(root, _ref2) {
var _this = this;
var result = _ref2.result,
api = _ref2.postcss;
// Create an empty rule so that all the new rules can be appended to this
// and then append it at the end.
var ruleContainer = api.root();
var addRulePromises = [];
root.walkRules(function (rule) {
var mediaParent = rule.parent;
rule.walkDecls(/^background/, function (decl) {
if (!backgroundWithHiResURL(decl.value)) {
return;
}
var retinaImages = createRetinaImages(decl.value, identifier);
// Remove keyword from original declaration here as createRetinaImages needs it
decl.value = removeKeyword(decl.value);
if (skipMissingRetina && !retinaImageExists(retinaImages, decl.source, resolveImagePath)) {
return;
}
var promise = getBackgroundImageSize(decl, detectImageSize, resolveImagePath, result.warn.bind(result), api).then(function (size) {
return addRetinaRule.bind(_this, ruleContainer, mediaParent, decl, retinaImages, size, api);
});
addRulePromises.push(promise);
});
});
return Promise.all(addRulePromises).then(function (addRules) {
addRules.forEach(function (addRule) {
addRule();
});
root.append(ruleContainer);
});
}
};
}
at2x.postcss = true;
function addRetinaRule(ruleContainer, mediaParent, decl, retinaImages, size, api) {
// Construct a duplicate rule but with the image urls
// replaced with retina versions
var retinaRule = api.rule({ selector: decl.parent.selector });
retinaRule.append(api.decl({
prop: 'background-image',
value: retinaImages
}));
if (size) {
retinaRule.append(api.decl(size));
}
// Create the rules and append them to the container
var params = mediaParent.name === 'media' ? combineMediaQuery(mediaParent.params.split(/,\s*/), defaultResolutions) : defaultResolutions.join(', ');
var mediaAtRule = api.atRule({ name: 'media', params });
mediaAtRule.append(retinaRule);
ruleContainer.append(mediaAtRule);
}
function retinaImageExists(retinaUrl, source, resolveImagePath) {
var urlValue = extractUrlValue(retinaUrl, source, resolveImagePath);
return _fs2.default.existsSync(urlValue);
}
function getBackgroundImageSize(decl, detectImageSize, resolveImagePath, warn, api) {
if (!detectImageSize) {
return Promise.resolve();
}
var urlValue = extractUrlValue(decl.value, decl.source, resolveImagePath);
var result = Promise.resolve();
if (urlValue !== '') {
return result.then(function () {
return (0, _pify2.default)(_imageSize2.default)(urlValue);
}).then(function (size) {
return api.decl({
prop: 'background-size',
value: `${size.width}px ${size.height}px`
});
}).catch(function (err) {
warn(err);
});
}
return result;
}
function extractUrlValue(url, source, resolveImagePath) {
var parsedValue = (0, _postcssValueParser2.default)(url);
var urlValue = '';
parsedValue.walk(function (node) {
if (node.type !== 'function' || node.type === 'function' && node.value !== 'url') {
return;
}
node.nodes.forEach(function (fp) {
if (!(0, _isUrl2.default)(fp.value)) {
urlValue = resolveImagePath(fp.value, source);
}
});
});
return urlValue;
}
/**
* Add all the resolutions to each media query to scope them
*/
function combineMediaQuery(queries, resolutions) {
return queries.reduce(function (finalQuery, query) {
resolutions.forEach(function (resolution) {
return finalQuery.push(`${query} and ${resolution}`);
});
return finalQuery;
}, []).join(', ');
}
function createRetinaImages(bgValue, identifier) {
var backgrounds = splitMultipleBackgrounds(bgValue);
var images = backgrounds.map(extractRetinaImage.bind(this, identifier));
return images.join(', ');
}
// Matches the <image> type and value within a background definition
var imageRegex = /([^\s]+)\((.+)\)/;
// Returns the <image> part of the background definition,
// and includes the identifier if it meets the criteria:
// * It's a url() image
// * It's not an svg
// * The background definition has at-2x applied
function extractRetinaImage(identifier, background) {
var match = background.match(imageRegex);
if (!match) {
return 'none';
}
var _match = _slicedToArray(match, 3),
image = _match[0],
type = _match[1],
value = _match[2];
if (background.indexOf('at-2x') === -1 || type !== 'url') {
return image;
}
var extension = _path2.default.extname(value);
if (extension === '.svg') {
return image;
}
// File name without extension
var filename = _path2.default.basename(_path2.default.basename(value), extension);
// Replace with retina filename
return image.replace(filename + extension, filename + identifier + extension);
}
function splitMultipleBackgrounds(value) {
var opened = 0;
return value.split(',').reduce(function (backgrounds, part) {
if (opened > 0) {
backgrounds[backgrounds.length - 1] += `,${part}`;
} else {
backgrounds.push(part);
}
var open = count(part, '(');
var close = count(part, ')');
opened += open - close;
return backgrounds;
}, []);
}
function count(haystack, needle) {
return haystack.split(needle).length - 1;
}
function removeKeyword(str) {
return str.replace(/\sat-2x/g, '');
}
function backgroundWithHiResURL(bgValue) {
return bgValue.includes('url(') && bgValue.includes('at-2x');
}
module.exports = exports['default'];