UNPKG

@markosamuli/postcss-at2x

Version:

Adds at-2x keyword to background and background-image declarations to add retina support for images.

250 lines (189 loc) 7.93 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _fs = _interopRequireDefault(require("fs")); var _path = _interopRequireDefault(require("path")); var _postcss = _interopRequireDefault(require("postcss")); var _postcssValueParser = _interopRequireDefault(require("postcss-value-parser")); var _isUrl = _interopRequireDefault(require("is-url")); var _imageSize = _interopRequireDefault(require("image-size")); var _pify = _interopRequireDefault(require("pify")); require("string.prototype.includes"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } function _iterableToArrayLimit(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"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } var defaultResolutions = ['(min-device-pixel-ratio: 1.5)', '(min-resolution: 144dpi)', '(min-resolution: 1.5dppx)']; function defaultResolveImagePath(value) { return _path.default.resolve(process.cwd(), value); } var _default = _postcss.default.plugin('postcss-at2x', at2x); exports.default = _default; function at2x() { var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, _ref$identifier = _ref.identifier, identifier = _ref$identifier === void 0 ? '@2x' : _ref$identifier, _ref$detectImageSize = _ref.detectImageSize, detectImageSize = _ref$detectImageSize === void 0 ? false : _ref$detectImageSize, _ref$resolveImagePath = _ref.resolveImagePath, resolveImagePath = _ref$resolveImagePath === void 0 ? defaultResolveImagePath : _ref$resolveImagePath, _ref$skipMissingRetin = _ref.skipMissingRetina, skipMissingRetina = _ref$skipMissingRetin === void 0 ? false : _ref$skipMissingRetin; return function (root, result) { var _this = this; // Create an empty rule so that all the new rules can be appended to this // and then append it at the end. var ruleContainer = _postcss.default.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)).then(function (size) { return addRetinaRule.bind(_this, ruleContainer, mediaParent, decl, retinaImages, size); }); addRulePromises.push(promise); }); }); return Promise.all(addRulePromises).then(function (addRules) { addRules.forEach(function (addRule) { addRule(); }); root.append(ruleContainer); }); }; } function addRetinaRule(ruleContainer, mediaParent, decl, retinaImages, size) { // Construct a duplicate rule but with the image urls // replaced with retina versions var retinaRule = _postcss.default.rule({ selector: decl.parent.selector }); retinaRule.append(_postcss.default.decl({ prop: 'background-image', value: retinaImages })); if (size) { retinaRule.append(_postcss.default.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 = _postcss.default.atRule({ name: 'media', params }); mediaAtRule.append(retinaRule); ruleContainer.append(mediaAtRule); } function retinaImageExists(retinaUrl, source, resolveImagePath) { var urlValue = extractUrlValue(retinaUrl, source, resolveImagePath); return _fs.default.existsSync(urlValue); } function getBackgroundImageSize(decl, detectImageSize, resolveImagePath, warn) { 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, _pify.default)(_imageSize.default)(urlValue); }).then(function (size) { return _postcss.default.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, _postcssValueParser.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, _isUrl.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 = _path.default.extname(value); if (extension === '.svg') { return image; } // File name without extension var filename = _path.default.basename(_path.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"];