Skip to content
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

feat(transformer): px2rem supports more options #174

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@emotion/unitless": "^0.7.5",
"classnames": "^2.3.1",
"csstype": "^3.1.3",
"defu": "^6.1.4",
"rc-util": "^5.35.0",
"stylis": "^4.0.13"
},
Expand Down
290 changes: 264 additions & 26 deletions src/transformers/px2rem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@
*/
// @ts-ignore
import unitless from '@emotion/unitless';
import { defuArrayFn } from 'defu';
import type { CSSObject } from '..';
import type { Transformer } from './interface';

interface ConvertUnit {
source: string | RegExp;
target: string;
}

interface Options {
/**
* The root font size.
Expand All @@ -22,49 +28,281 @@ interface Options {
* @default false
*/
mediaQuery?: boolean;
/**
* The selector blackList.
*
*/
selectorBlackList?: {
/**
* The selector black list.
*/
match?: (string | RegExp)[];
/**
* Whether to deep into the children.
* @default true
*/
deep?: boolean;
};
/**
* The property list to convert.
* @default ['*']
* @example
* ['font-size', 'margin']
*/
propList?: string[];
/**
* The minimum pixel value to transform.
* @default 1
*/
minPixelValue?: number;
/**
* Convert unit on end.
* @default null
* @example
* ```js
* {
* source: /px$/i,
* target: 'px'
* }
* ```
*/
convertUnit?: ConvertUnit | ConvertUnit[] | false | null;
}

const pxRegex = /url\([^)]+\)|var\([^)]+\)|(\d*\.?\d+)px/g;
const pxRegex = /"[^"]+"|'[^']+'|url\([^)]+\)|--[\w-]+|(\d*\.?\d+)px/g;

export const filterPropList = {
exact(list: string[]) {
return list.filter((m) => m.match(/^[^!*]+$/));
},
contain(list: string[]) {
return list.filter((m) => m.match(/^\*.+\*$/)).map((m) => m.slice(1, -1));
},
endWith(list: string[]) {
return list.filter((m) => m.match(/^\*[^*]+$/)).map((m) => m.slice(1));
},
startWith(list: string[]) {
return list
.filter((m) => m.match(/^[^!*]+\*$/))
.map((m) => m.slice(0, Math.max(0, m.length - 1)));
},
notExact(list: string[]) {
return list.filter((m) => m.match(/^![^*].*$/)).map((m) => m.slice(1));
},
notContain(list: string[]) {
return list.filter((m) => m.match(/^!\*.+\*$/)).map((m) => m.slice(2, -1));
},
notEndWith(list: string[]) {
return list.filter((m) => m.match(/^!\*[^*]+$/)).map((m) => m.slice(2));
},
notStartWith(list: string[]) {
return list.filter((m) => m.match(/^![^*]+\*$/)).map((m) => m.slice(1, -1));
},
};

function createPropListMatcher(propList: string[]) {
const hasWild = propList.includes('*');
const matchAll = hasWild && propList.length === 1;
const lists = {
exact: filterPropList.exact(propList),
contain: filterPropList.contain(propList),
startWith: filterPropList.startWith(propList),
endWith: filterPropList.endWith(propList),
notExact: filterPropList.notExact(propList),
notContain: filterPropList.notContain(propList),
notStartWith: filterPropList.notStartWith(propList),
notEndWith: filterPropList.notEndWith(propList),
};
return function (prop: string) {
if (matchAll) return true;
return (
(hasWild ||
lists.exact.includes(prop) ||
lists.contain.some((m) => prop.includes(m)) ||
lists.startWith.some((m) => prop.indexOf(m) === 0) ||
lists.endWith.some(
(m) => prop.indexOf(m) === prop.length - m.length,
)) &&
!(
lists.notExact.includes(prop) ||
lists.notContain.some((m) => prop.includes(m)) ||
lists.notStartWith.some((m) => prop.indexOf(m) === 0) ||
lists.notEndWith.some((m) => prop.indexOf(m) === prop.length - m.length)
)
);
};
}

function createPxReplace(
rootValue: number,
precision: NonNullable<Options['precision']>,
minPixelValue: NonNullable<Options['minPixelValue']>,
) {
return (m: string, $1: string | null) => {
if (!$1) return m;
const pixels = Number.parseFloat($1);
if (pixels <= minPixelValue) return m;
const fixedVal = toFixed(pixels / rootValue, precision);
return fixedVal === 0 ? '0' : `${fixedVal}rem`;
};
}

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

function is(val: unknown, type: string) {
return Object.prototype.toString.call(val) === `[object ${type}]`;
}

function isRegExp(data: unknown): data is RegExp {
return is(data, 'RegExp');
}

function isString(data: unknown): data is string {
return is(data, 'String');
}

function isObject(data: unknown): data is object {
return is(data, 'Object');
}

function isNumber(data: unknown): data is number {
return is(data, 'Number');
}

function blacklistedSelector(blacklist: (string | RegExp)[], selector: string) {
if (!isString(selector)) return;
return blacklist.some((t) => {
if (isString(t)) {
return selector.includes(t);
}
return selector.match(t);
});
}

const SKIP_SYMBOL = Symbol('skip_transform');

function defineSkipSymbol(obj: object) {
Reflect.defineProperty(obj, SKIP_SYMBOL, {
value: true,
enumerable: false,
writable: false,
configurable: false,
});
}

function getSkipSymbol(obj: object) {
return Reflect.get(obj, SKIP_SYMBOL);
}

const uppercasePattern = /([A-Z])/g;
function hyphenateStyleName(name: string): string {
return name.replace(uppercasePattern, '-$1').toLowerCase();
}

function convertUnitFn(value: string, convert: ConvertUnit) {
const { source, target } = convert;
if (isRegExp(source)) {
return value.replace(new RegExp(source), target);
}
return value.replace(new RegExp(`${source}$`), target);
}

const DEFAULT_OPTIONS: Required<Options> = {
rootValue: 16,
precision: 5,
mediaQuery: false,
minPixelValue: 1,
propList: ['*'],
selectorBlackList: { match: [], deep: true },
convertUnit: null,
};

function resolveOptions(options: Options, defaults = DEFAULT_OPTIONS) {
return defuArrayFn(options, defaults);
}

const transform = (options: Options = {}): Transformer => {
const { rootValue = 16, precision = 5, mediaQuery = false } = options;
const opts = resolveOptions(options);

const pxReplace = (m: string, $1: any) => {
if (!$1) return m;
const pixels = parseFloat($1);
// covenant: pixels <= 1, not transform to rem @zombieJ
if (pixels <= 1) return m;
const fixedVal = toFixed(pixels / rootValue, precision);
return `${fixedVal}rem`;
};
const {
rootValue,
precision,
minPixelValue,
propList,
mediaQuery,
convertUnit,
selectorBlackList,
} = opts;

const pxReplace = createPxReplace(rootValue, precision, minPixelValue);
const satisfyPropList = createPropListMatcher(propList);

const visit = (cssObj: CSSObject): CSSObject => {
const skip = getSkipSymbol(cssObj);

const clone: CSSObject = { ...cssObj };

Object.entries(cssObj).forEach(([key, value]) => {
if (typeof value === 'string' && value.includes('px')) {
const newValue = value.replace(pxRegex, pxReplace);
clone[key] = newValue;
if (skip) {
if (selectorBlackList.deep) {
Object.values(clone).forEach((value) => {
if (value && isObject(value)) {
defineSkipSymbol(value);
}
});
}
return clone;
}

// no unit
if (!unitless[key] && typeof value === 'number' && value !== 0) {
clone[key] = `${value}px`.replace(pxRegex, pxReplace);
}
Object.entries(cssObj).forEach(([key, value]) => {
if (!isObject(value)) {
if (!satisfyPropList(hyphenateStyleName(key))) {
// Current style property is not in the propList
// Skip
return;
}

if (isString(value) && value.includes('px')) {
const newValue = value.replace(pxRegex, pxReplace);
clone[key] = newValue;
}

// no unit
if (!unitless[key] && isNumber(value) && value !== 0) {
clone[key] = `${value}px`.replace(pxRegex, pxReplace);
}

if (convertUnit && isString(clone[key])) {
const newValue = clone[key] as string;
if (Array.isArray(convertUnit)) {
clone[key] = convertUnit.reduce((c, conv) => {
return convertUnitFn(c, conv);
}, newValue);
} else {
clone[key] = convertUnitFn(newValue, convertUnit);
}
}
} else {
if (blacklistedSelector(selectorBlackList.match || [], key)) {
defineSkipSymbol(value);
return;
}

// Media queries
const mergedKey = key.trim();
if (mergedKey.startsWith('@') && mergedKey.includes('px') && mediaQuery) {
const newKey = key.replace(pxRegex, pxReplace);
// Media queries
const mergedKey = key.trim();
if (
mergedKey.startsWith('@') &&
mergedKey.includes('px') &&
mediaQuery
) {
const newKey = key.replace(pxRegex, pxReplace);

clone[newKey] = clone[key];
delete clone[key];
clone[newKey] = clone[key];
delete clone[key];
}
}
});

Expand Down
Loading
Loading