diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..1789940 --- /dev/null +++ b/.vscode/launch.json @@ -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": [ + "/**" + ], + "args": [ + "-p", + "-s", + "app-ynet", + // "blazer", + // "ynetnews", + // "vesty", + // "pplus", + // "yplus" + ], + "cwd": "C:\\projects\\wcm_front", + "program": "C:\\projects\\wcm_front\\tools\\siteCssBuilder\\siteCssBuilder.js" + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 234df59..658f3e7 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/index.js b/index.js index 6419322..31b8be7 100755 --- a/index.js +++ b/index.js @@ -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 })); + } +} \ No newline at end of file diff --git a/package.json b/package.json index deec944..70e9e84 100755 --- a/package.json +++ b/package.json @@ -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" } }