'use strict'; var css = require('css'); var extend = require('extend'); var defaultConfig = { baseDpr: 2, // base device pixel ratio (default: 2) remUnit: 75, // rem unit value (default: 75) remPrecision: 6, // rem value precision (default: 6) forcePxComment: 'px', // force px comment (default: `px`) keepComment: 'no' // no transform value comment (default: `no`) }; var pxRegExp = /\b(\d+(\.\d+)?)px\b/; function Px2rem(options) { this.config = {}; extend(this.config, defaultConfig, options); } // generate @1x, @2x and @3x version stylesheet Px2rem.prototype.generateThree = function (cssText, dpr) { dpr = dpr || 2; var self = this; var config = self.config; var astObj = css.parse(cssText); function processRules(rules) { for (var i = 0; i < rules.length; i++) { var rule = rules[i]; if (rule.type === 'media') { processRules(rule.rules); // recursive invocation while dealing with media queries continue; } else if (rule.type === 'keyframes') { processRules(rule.keyframes); // recursive invocation while dealing with keyframes continue; } else if (rule.type !== 'rule' && rule.type !== 'keyframe') { continue; } var declarations = rule.declarations; for (var j = 0; j < declarations.length; j++) { var declaration = declarations[j]; // need transform: declaration && has 'px' if (declaration.type === 'declaration' && pxRegExp.test(declaration.value)) { var nextDeclaration = rule.declarations[j + 1]; if (nextDeclaration && nextDeclaration.type === 'comment') { // next next declaration is comment if (nextDeclaration.comment.trim() === config.keepComment) { // no transform declarations.splice(j + 1, 1); // delete corresponding comment continue; } else if (nextDeclaration.comment.trim() === config.forcePxComment) { // force px declarations.splice(j + 1, 1); // delete corresponding comment } } declaration.value = self._getCalcValue('px', declaration.value, dpr); // common transform } } } } processRules(astObj.stylesheet.rules); return css.stringify(astObj); }; // generate rem version stylesheet Px2rem.prototype.generateRem = function (cssText) { var self = this; var config = self.config; var astObj = css.parse(cssText); function processRules(rules, noDealPx) { // FIXME: keyframes do not support `force px` comment for (var i = 0; i < rules.length; i++) { var rule = rules[i]; if (rule.type === 'media') { processRules(rule.rules); // recursive invocation while dealing with media queries continue; } else if (rule.type === 'keyframes') { processRules(rule.keyframes, true); // recursive invocation while dealing with keyframes continue; } else if (rule.type !== 'rule' && rule.type !== 'keyframe') { continue; } if (!noDealPx) { // generate 3 new rules which has [data-dpr] var newRules = []; for (var dpr = 1; dpr <= 3; dpr++) { var newRule = {}; newRule.type = rule.type; newRule.selectors = rule.selectors.map(function (sel) { return '[data-dpr="' + dpr + '"] ' + sel; }); newRule.declarations = []; newRules.push(newRule); } } var declarations = rule.declarations; for (var j = 0; j < declarations.length; j++) { var declaration = declarations[j]; // need transform: declaration && has 'px' if (declaration.type === 'declaration' && pxRegExp.test(declaration.value)) { var nextDeclaration = rule.declarations[j + 1]; if (nextDeclaration && nextDeclaration.type === 'comment') { // next next declaration is comment if (nextDeclaration.comment.trim() === config.forcePxComment) { // force px // do not transform `0px` if (declaration.value === '0px') { declaration.value = '0'; declarations.splice(j + 1, 1); // delete corresponding comment continue; } if (!noDealPx) { // generate 3 new declarations and put them in the new rules which has [data-dpr] for (var dpr = 1; dpr <= 3; dpr++) { var newDeclaration = {}; extend(true, newDeclaration, declaration); newDeclaration.value = self._getCalcValue('px', newDeclaration.value, dpr); newRules[dpr - 1].declarations.push(newDeclaration); } declarations.splice(j, 2); // delete this rule and corresponding comment j--; } else { // FIXME: keyframes do not support `force px` comment declaration.value = self._getCalcValue('rem', declaration.value); // common transform declarations.splice(j + 1, 1); // delete corresponding comment } } else if (nextDeclaration.comment.trim() === config.keepComment) { // no transform declarations.splice(j + 1, 1); // delete corresponding comment } else { declaration.value = self._getCalcValue('rem', declaration.value); // common transform } } else { declaration.value = self._getCalcValue('rem', declaration.value); // common transform } } } // if the origin rule has no declarations, delete it if (!rules[i].declarations.length) { rules.splice(i, 1); i--; } if (!noDealPx) { // add the new rules which contain declarations that are forced to use px if (newRules[0].declarations.length) { rules.splice(i + 1, 0, newRules[0], newRules[1], newRules[2]); i += 3; // skip the added new rules } } } } processRules(astObj.stylesheet.rules); return css.stringify(astObj); }; // get calculated value of px or rem Px2rem.prototype._getCalcValue = function (type, value, dpr) { var config = this.config; var pxGlobalRegExp = new RegExp(pxRegExp.source, 'g'); function getValue(val) { val = parseFloat(val.toFixed(config.remPrecision)); // control decimal precision of the calculated value return val == 0 ? val : val + type; } return value.replace(pxGlobalRegExp, function ($0, $1) { return type === 'px' ? getValue($1 * dpr / config.baseDpr) : getValue($1 / config.remUnit); }); }; module.exports = Px2rem;