@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
JavaScript
;
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"];