Skip to content

feature: added support for custom convertion options #152

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"args": [
"-p",
"-s",
"app-ynet",
// "blazer",
// "ynetnews",
// "vesty",
// "pplus",
// "yplus"
],
"cwd": "C:\\projects\\wcm_front",
"program": "C:\\projects\\wcm_front\\tools\\siteCssBuilder\\siteCssBuilder.js"
}
]
}
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -132,6 +132,53 @@ Default Options:
- `landscape` (Boolean) Adds `@media (orientation: landscape)` with values converted via `landscapeWidth`.
- `landscapeUnit` (String) Expected unit for `landscape` option
- `landscapeWidth` (Number) Viewport width for landscape orientation.
- `customConvertionOptions` (Array of Custom Convertion Objects):
- `viewportWidth` (Number) The width of the viewport.
- `viewportUnit` (String) Expected units.
- `matchSelectors` (Array of Regex) The selector we want to be converted
- `atRule` (Object) used to locate the media query:
- `name` (string) query type. for example 'media'.
- `params` (string) exact query params. for example 'only screen and (max-width: 1040px)'.

#### customConvertionOptions

Use this option if you want to use a different convertion options on the same css file.
You can use:
1. an atRule - object with two fileds (params: string, name: string) - which will convert every rule under this media query.
2. a selector - matchSelectors array with each selector you want to convert in this settings.
3. both - for cases you want to convert specific selector inside the atRule.`

Example:
```js
const pxToViewportOptions = {
viewportWidth: 640,
viewportUnit: 'vw',
propList: ['*'],
selectorBlackList: [/-pf$/],
mediaQuery: true,
customConvertionOptions: [
{
/* Will convert the selector .tablet and all of his sons to 'vw' based on a viewport width of 1536. */
viewportWidth: 1536,
viewportUnit: 'vw',
matchSelectors: [/^.tablet\s/],
},
{
/* Will convert all rules under '@media only screen and (max-width: 961px)' to 'vw' based on a viewport width of 961. */
atRule: { params: 'only screen and (max-width: 961px)', name: 'media' },
viewportWidth: 961,
viewportUnit: 'vw',
},
{
/* Will convert all rules containing the selectors .tablet2 and .tablet3 thats under '@media only screen and (max-width: 961px)' to 'vw' based on a viewport width of 1040. */
atRule: { params: 'only screen and (max-width: 1040px)', name: 'media' },
viewportWidth: 1040,
viewportUnit: 'vw',
matchSelectors: [/^.tablet2\s/, /^.tablet3\s/],
}
]
}
```

> `exclude` and `include` can be set together, and the intersection of the two rules will be taken.
342 changes: 190 additions & 152 deletions index.js
Original file line number Diff line number Diff line change
@@ -6,199 +6,237 @@ var { createPropListMatcher } = require('./src/prop-list-matcher');
var { getUnitRegexp } = require('./src/pixel-unit-regexp');

var defaults = {
unitToConvert: 'px',
viewportWidth: 320,
viewportHeight: 568, // not now used; TODO: need for different units and math for different properties
unitPrecision: 5,
viewportUnit: 'vw',
fontViewportUnit: 'vw', // vmin is more suitable.
selectorBlackList: [],
propList: ['*'],
minPixelValue: 1,
mediaQuery: false,
replace: true,
landscape: false,
landscapeUnit: 'vw',
landscapeWidth: 568
unitToConvert: 'px',
viewportWidth: 320,
viewportHeight: 568, // not now used; TODO: need for different units and math for different properties
unitPrecision: 5,
viewportUnit: 'vw',
fontViewportUnit: 'vw', // vmin is more suitable.
selectorBlackList: [],
propList: ['*'],
minPixelValue: 1,
mediaQuery: false,
replace: true,
landscape: false,
landscapeUnit: 'vw',
landscapeWidth: 568
};

var ignoreNextComment = 'px-to-viewport-ignore-next';
var ignorePrevComment = 'px-to-viewport-ignore';

module.exports = postcss.plugin('postcss-px-to-viewport', function (options) {
var opts = objectAssign({}, defaults, options);

checkRegExpOrArray(opts, 'exclude');
checkRegExpOrArray(opts, 'include');

var pxRegex = getUnitRegexp(opts.unitToConvert);
var satisfyPropList = createPropListMatcher(opts.propList);
var landscapeRules = [];

return function (css, result) {
css.walkRules(function (rule) {
// Add exclude option to ignore some files like 'node_modules'
var file = rule.source && rule.source.input.file;

if (opts.include && file) {
if (Object.prototype.toString.call(opts.include) === '[object RegExp]') {
if (!opts.include.test(file)) return;
} else if (Object.prototype.toString.call(opts.include) === '[object Array]') {
var flag = false;
for (var i = 0; i < opts.include.length; i++) {
if (opts.include[i].test(file)) {
flag = true;
break;
var opts = objectAssign({}, defaults, options);

checkRegExpOrArray(opts, 'exclude');
checkRegExpOrArray(opts, 'include');

var pxRegex = getUnitRegexp(opts.unitToConvert);
var satisfyPropList = createPropListMatcher(opts.propList);
var landscapeRules = [];

return function (css, result) {
css.walkRules(function (rule) {
// Add exclude option to ignore some files like 'node_modules'
var file = rule.source && rule.source.input.file;

if (opts.include && file) {
if (Object.prototype.toString.call(opts.include) === '[object RegExp]') {
if (!opts.include.test(file)) return;
} else if (Object.prototype.toString.call(opts.include) === '[object Array]') {
var flag = false;
for (var i = 0; i < opts.include.length; i++) {
if (opts.include[i].test(file)) {
flag = true;
break;
}
}
if (!flag) return;
}
}
}
if (!flag) return;
}
}

if (opts.exclude && file) {
if (Object.prototype.toString.call(opts.exclude) === '[object RegExp]') {
if (opts.exclude.test(file)) return;
} else if (Object.prototype.toString.call(opts.exclude) === '[object Array]') {
for (var i = 0; i < opts.exclude.length; i++) {
if (opts.exclude[i].test(file)) return;
}
}
}

if (blacklistedSelector(opts.selectorBlackList, rule.selector)) return;

if (opts.landscape && !rule.parent.params) {
var landscapeRule = rule.clone().removeAll();

rule.walkDecls(function(decl) {
if (decl.value.indexOf(opts.unitToConvert) === -1) return;
if (!satisfyPropList(decl.prop)) return;
if (opts.exclude && file) {
if (Object.prototype.toString.call(opts.exclude) === '[object RegExp]') {
if (opts.exclude.test(file)) return;
} else if (Object.prototype.toString.call(opts.exclude) === '[object Array]') {
for (var i = 0; i < opts.exclude.length; i++) {
if (opts.exclude[i].test(file)) return;
}
}
}

landscapeRule.append(decl.clone({
value: decl.value.replace(pxRegex, createPxReplace(opts, opts.landscapeUnit, opts.landscapeWidth))
}));
});
if (blacklistedSelector(opts.selectorBlackList, rule.selector)) return;

if (landscapeRule.nodes.length > 0) {
landscapeRules.push(landscapeRule);
}
}
if (opts.landscape && !rule.parent.params) {
var landscapeRule = rule.clone().removeAll();

if (!validateParams(rule.parent.params, opts.mediaQuery)) return;
rule.walkDecls(function (decl) {
if (decl.value.indexOf(opts.unitToConvert) === -1) return;
if (!satisfyPropList(decl.prop)) return;

rule.walkDecls(function(decl, i) {
if (decl.value.indexOf(opts.unitToConvert) === -1) return;
if (!satisfyPropList(decl.prop)) return;

var prev = decl.prev();
// prev declaration is ignore conversion comment at same line
if (prev && prev.type === 'comment' && prev.text === ignoreNextComment) {
// remove comment
prev.remove();
return;
}
var next = decl.next();
// next declaration is ignore conversion comment at same line
if (next && next.type === 'comment' && next.text === ignorePrevComment) {
if (/\n/.test(next.raws.before)) {
result.warn('Unexpected comment /* ' + ignorePrevComment + ' */ must be after declaration at same line.', { node: next });
} else {
// remove comment
next.remove();
return;
}
}
landscapeRule.append(decl.clone({
value: decl.value.replace(pxRegex, createPxReplace(opts, opts.landscapeUnit, opts.landscapeWidth))
}));
});

var unit;
var size;
var params = rule.parent.params;
if (landscapeRule.nodes.length > 0) {
landscapeRules.push(landscapeRule);
}
}

if (opts.landscape && params && params.indexOf('landscape') !== -1) {
unit = opts.landscapeUnit;
size = opts.landscapeWidth;
} else {
unit = getUnit(decl.prop, opts);
size = opts.viewportWidth;
}
if (!validateParams(rule.parent.params, opts.mediaQuery)) return;

let selectedOptions = { ...opts };

for (const customOption of opts.customConvertionOptions) {
if (customOption.atRule) { /* searching for media query */
if (rule.parent.name === customOption.atRule.name && rule.parent.params === customOption.atRule.params) {
if (customOption.matchSelectors) { /* searching for selector within media query */
if (findMatchingSelectors(customOption.matchSelectors, rule.selector)) {
selectedOptions = { ...selectedOptions, ...customOption };
break;
}
} else {
selectedOptions = { ...selectedOptions, ...customOption };
break;
}
}
} else if (customOption.matchSelectors) { /* searching for selector */
if (findMatchingSelectors(customOption.matchSelectors, rule.selector)) {
selectedOptions = { ...selectedOptions, ...customOption };
break;
}
}
}

var value = decl.value.replace(pxRegex, createPxReplace(opts, unit, size));
rule.walkDecls((decl, i) => walkCustomDecl(rule, decl, i, selectedOptions));
});

if (declarationExists(decl.parent, decl.prop, value)) return;
if (landscapeRules.length > 0) {
var landscapeRoot = new postcss.atRule({ params: '(orientation: landscape)', name: 'media' });

if (opts.replace) {
decl.value = value;
} else {
decl.parent.insertAfter(i, decl.clone({ value: value }));
landscapeRules.forEach(function (rule) {
landscapeRoot.append(rule);
});
css.append(landscapeRoot);
}
});
});

if (landscapeRules.length > 0) {
var landscapeRoot = new postcss.atRule({ params: '(orientation: landscape)', name: 'media' });

landscapeRules.forEach(function(rule) {
landscapeRoot.append(rule);
});
css.append(landscapeRoot);
}
};
};
});

function getUnit(prop, opts) {
return prop.indexOf('font') === -1 ? opts.viewportUnit : opts.fontViewportUnit;
return prop.indexOf('font') === -1 ? opts.viewportUnit : opts.fontViewportUnit;
}

function createPxReplace(opts, viewportUnit, viewportSize) {
return function (m, $1) {
if (!$1) return m;
var pixels = parseFloat($1);
if (pixels <= opts.minPixelValue) return m;
var parsedVal = toFixed((pixels / viewportSize * 100), opts.unitPrecision);
return parsedVal === 0 ? '0' : parsedVal + viewportUnit;
};
return function (m, $1) {
if (!$1) return m;
var pixels = parseFloat($1);
if (pixels <= opts.minPixelValue) return m;
var parsedVal = toFixed((pixels / viewportSize * 100), opts.unitPrecision);
return parsedVal === 0 ? '0' : parsedVal + viewportUnit;
};
}

function error(decl, message) {
throw decl.error(message, { plugin: 'postcss-px-to-viewport' });
throw decl.error(message, { plugin: 'postcss-px-to-viewport' });
}

function checkRegExpOrArray(options, optionName) {
var option = options[optionName];
if (!option) return;
if (Object.prototype.toString.call(option) === '[object RegExp]') return;
if (Object.prototype.toString.call(option) === '[object Array]') {
var bad = false;
for (var i = 0; i < option.length; i++) {
if (Object.prototype.toString.call(option[i]) !== '[object RegExp]') {
bad = true;
break;
}
var option = options[optionName];
if (!option) return;
if (Object.prototype.toString.call(option) === '[object RegExp]') return;
if (Object.prototype.toString.call(option) === '[object Array]') {
var bad = false;
for (var i = 0; i < option.length; i++) {
if (Object.prototype.toString.call(option[i]) !== '[object RegExp]') {
bad = true;
break;
}
}
if (!bad) return;
}
if (!bad) return;
}
throw new Error('options.' + optionName + ' should be RegExp or Array of RegExp.');
throw new Error('options.' + optionName + ' should be RegExp or Array of RegExp.');
}

function toFixed(number, precision) {
var multiplier = Math.pow(10, precision + 1),
wholeNumber = Math.floor(number * multiplier);
return Math.round(wholeNumber / 10) * 10 / multiplier;
var multiplier = Math.pow(10, precision + 1),
wholeNumber = Math.floor(number * multiplier);
return Math.round(wholeNumber / 10) * 10 / multiplier;
}

function blacklistedSelector(blacklist, selector) {
if (typeof selector !== 'string') return;
return blacklist.some(function (regex) {
if (typeof regex === 'string') return selector.indexOf(regex) !== -1;
return selector.match(regex);
});
if (typeof selector !== 'string') return;
return blacklist.some(function (regex) {
if (typeof regex === 'string') return selector.indexOf(regex) !== -1;
return selector.match(regex);
});
}

function declarationExists(decls, prop, value) {
return decls.some(function (decl) {
return (decl.prop === prop && decl.value === value);
});
return decls.some(function (decl) {
return (decl.prop === prop && decl.value === value);
});
}

function validateParams(params, mediaQuery) {
return !params || (params && mediaQuery);
return !params || (params && mediaQuery);
}

function isMatchingSelector(selectedSelector, selector) {
return blacklistedSelector(selectedSelector, selector);
}

function findMatchingSelectors(matchSelectors, selector) {
if (isMatchingSelector(matchSelectors, selector)) return true

return false
}

function walkCustomDecl(rule, decl, i, options) {
var satisfyPropList = createPropListMatcher(options.propList);
var pxRegex = getUnitRegexp(options.unitToConvert);

if (decl.value.indexOf(options.unitToConvert) === -1) return;
if (!satisfyPropList(decl.prop)) return;

var prev = decl.prev();
// prev declaration is ignore conversion comment at same line
if (prev && prev.type === 'comment' && prev.text === ignoreNextComment) {
// remove comment
prev.remove();
return;
}
var next = decl.next();
// next declaration is ignore conversion comment at same line
if (next && next.type === 'comment' && next.text === ignorePrevComment) {
if (/\n/.test(next.raws.before)) {
result.warn('Unexpected comment /* ' + ignorePrevComment + ' */ must be after declaration at same line.', { node: next });
} else {
// remove comment
next.remove();
return;
}
}

var unit;
var size;
var params = rule.parent.params;

if (options.landscape && params && params.indexOf('landscape') !== -1) {
unit = options.landscapeUnit;
size = options.landscapeWidth;
} else {
unit = getUnit(decl.prop, options);
size = options.viewportWidth;
}

var value = decl.value.replace(pxRegex, createPxReplace(options, unit, size));

if (declarationExists(decl.parent, decl.prop, value)) return;

if (options.replace) {
decl.value = value;
} else {
decl.parent.insertAfter(i, decl.clone({ value: value }));
}
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -32,9 +32,9 @@
},
"devDependencies": {
"jest": "^25.4.0",
"postcss": ">=5.0.2"
"postcss": "^7.0.2"
},
"peerDependencies": {
"postcss": ">=5.0.2"
"postcss": "^7.0.2"
}
}