Skip to content

Commit

Permalink
🔨 Adds support for namespacing keyframes and media queries
Browse files Browse the repository at this point in the history
  • Loading branch information
danieldelcore committed Oct 29, 2020
1 parent d58e262 commit 4b8e5b3
Show file tree
Hide file tree
Showing 7 changed files with 362 additions and 260 deletions.
17 changes: 17 additions & 0 deletions packages/core/src/namespace.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,23 @@ describe('namespace', () => {
});
});

it('merges duplicate & alike selectors', () => {
const result = namespace('.my-id', {
'& button': {
background: 'violet',
},
button: {
color: 'green',
},
});
expect(result).toEqual({
'.my-id button': {
background: 'violet',
color: 'green',
},
});
});

it('namespaces nested selectors', () => {
const result = namespace('.my-id', {
background: 'red',
Expand Down
88 changes: 60 additions & 28 deletions packages/core/src/namespace.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,70 @@
type RuleSet = Record<string, Record<string, number | string>>;
export type RuleSet = Record<string, Record<string, number | string>>;

function namespace(id: string, style: Record<string, any>): RuleSet {
const ruleSet: RuleSet = {};

(function pushRules(nestedId: string, nestedStyle: Record<string, any>) {
Object.entries(nestedStyle).forEach(([property, value]) => {
if (typeof value === 'string' || typeof value === 'number') {
if (!ruleSet[nestedId]) ruleSet[nestedId] = {};
ruleSet[nestedId][property] = value;
return;
}
function isObject(item: any) {
return item && typeof item === 'object' && !Array.isArray(item);
}

let newRuleKey = property;
function mergeDeep(target: any, ...sources: any): any {
if (!sources.length) return target;
const source = sources.shift();

if (property.includes('&')) {
newRuleKey = property.replace(new RegExp(/&/, 'g'), nestedId);
} else if (property.includes(':')) {
newRuleKey = property.replace(
new RegExp(/:/, 'g'),
nestedId + ':',
);
if (isObject(target) && isObject(source)) {
for (const key in source) {
if (isObject(source[key])) {
if (!target[key]) Object.assign(target, { [key]: {} });
mergeDeep(target[key], source[key]);
} else {
newRuleKey = property.replace(
new RegExp(/,/, 'g'),
', ' + nestedId,
);
newRuleKey = nestedId + ' ' + newRuleKey;
Object.assign(target, { [key]: source[key] });
}
}
}

return mergeDeep(target, ...sources);
}

function namespace(id: string, style: Record<string, any>): RuleSet {
return Object.entries(style).reduce<RuleSet>((accum, [property, value]) => {
if (typeof value !== 'object') {
if (!accum[id]) accum[id] = {};
accum[id][property] = value;
return accum;
}

if (property.includes('@keyframe')) {
return mergeDeep(accum, {
[property]: Object.entries(value).reduce<RuleSet>(
(
nestedAccum,
[nestedProp, nestedValue]: [string, any],
) => ({
...nestedAccum,
...namespace(nestedProp, nestedValue),
}),
{},
),
});
} else if (property.includes('@media')) {
return mergeDeep(accum, {
[property]: namespace(id, value),
});
}

let selector = '';

pushRules(`${newRuleKey}`, value);
});
})(id, style);
if (property.includes('&')) {
selector = property.replace(new RegExp(/&/, 'g'), id);
} else if (property.includes('::')) {
selector = property.replace(new RegExp(/::/, 'g'), id + '::');
} else if (property.includes(':')) {
selector = property.replace(new RegExp(/:/, 'g'), id + ':');
} else {
selector =
id + ' ' + property.replace(new RegExp(/,/, 'g'), ', ' + id);
}

return ruleSet;
// TODO: deep merge is a shame
return mergeDeep(accum, namespace(selector, value));
}, {});
}

export default namespace;
4 changes: 2 additions & 2 deletions packages/core/src/prefix.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ describe('prefix', () => {
it('prefixes simple css property', () => {
const result = prefix('appearance', 'none');
expect(result).toEqual(
'appearance: none;\n-moz-appearance: none;\n-webkit-appearance: none;\n-moz-appearance: none;\n',
'appearance: none;-moz-appearance: none;-webkit-appearance: none;-moz-appearance: none;',
);
});

it('does not prefix well supported rules', () => {
const result = prefix('color', 'red');
expect(result).toEqual('color: red;\n');
expect(result).toEqual('color: red;');
});
});
2 changes: 1 addition & 1 deletion packages/core/src/prefix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { prefixProperty } from 'tiny-css-prefixer';
// TODO might be good to return an object here instead.
const prefix = (prop: string, value: string | number) => {
const flag = prefixProperty(prop);
let css = `${prop}: ${value};\n`;
let css = `${prop}: ${value};`;
if (flag & 0b001) css += `-ms-${css}`;
if (flag & 0b010) css += `-moz-${css}`;
if (flag & 0b100) css += `-webkit-${css}`;
Expand Down
70 changes: 58 additions & 12 deletions packages/core/src/process.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ describe('process', () => {
color: 'white',
});
expect(result).toEqual({
button: 'background-color: #b3cde8;\ncolor: white;\n',
button: 'background-color: #b3cde8;color: white;',
});
});

Expand All @@ -17,7 +17,7 @@ describe('process', () => {
color: 'white',
});
expect(result).toEqual({
button: 'background-color: #b3cde8;\ncolor: white;\n',
button: 'background-color: #b3cde8;color: white;',
});
});

Expand All @@ -30,8 +30,8 @@ describe('process', () => {
},
});
expect(result).toEqual({
'.my-id': 'background-color: red;\n',
'.my-id button': 'background-color: violet;\n',
'.my-id': 'background-color: red;',
'.my-id button': 'background-color: violet;',
});
});

Expand All @@ -47,9 +47,9 @@ describe('process', () => {
},
});
expect(result).toEqual({
'.my-id': 'background-color: red;\n',
'.my-id button': 'background-color: violet;\n',
'.my-id button span': 'background-color: green;\n',
'.my-id': 'background-color: red;',
'.my-id button': 'background-color: violet;',
'.my-id button span': 'background-color: green;',
});
});

Expand All @@ -65,9 +65,9 @@ describe('process', () => {
},
});
expect(result).toEqual({
'.my-id': 'background-color: red;\n',
'.my-id #MyButton': 'background-color: violet;\n',
'.my-id #MyButton .myDiv': 'background-color: green;\n',
'.my-id': 'background-color: red;',
'.my-id #MyButton': 'background-color: violet;',
'.my-id #MyButton .myDiv': 'background-color: green;',
});
});

Expand All @@ -79,8 +79,54 @@ describe('process', () => {
'& > .myButton': { color: 'violet' },
});
expect(result).toEqual({
'.my-id': 'background-color: red;\n',
'.my-id > .myButton': 'background-color: violet;\ncolor: violet;\n',
'.my-id': 'background-color: red;',
'.my-id > .myButton': 'background-color: violet;color: violet;',
});
});

it('stringifies keyframe animations', () => {
const result = process('.my-id', {
// @ts-ignore
'@keyframes mymove': {
from: { top: '0px' },
to: { top: '200px' },
},
});
expect(result).toEqual({
'@keyframes mymove': 'from{top: 0px;}to{top: 200px;}',
});
});

it('stringifies media queries', () => {
const result = process('.my-id', {
// @ts-ignore
'@media screen and (max-width: 992px)': {
'& button': {
background: 'violet',
},
},
});
expect(result).toEqual({
'@media screen and (max-width: 992px)':
'.my-id button{background: violet;}',
});
});

it('stringifies media queries with deeply nested selectors', () => {
const result = process('.my-id', {
// @ts-ignore
'@media screen and (max-width: 992px)': {
'& button': {
background: 'violet',
'& span': {
color: 'red',
},
},
},
});
expect(result).toEqual({
'@media screen and (max-width: 992px)':
'.my-id button{background: violet;}.my-id button span{color: red;}',
});
});
});
29 changes: 18 additions & 11 deletions packages/core/src/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,27 @@ import hyphenate from './hyphenate';
import prefix from './prefix';
import { CSSObject } from './types';

type RuleSet = Record<string, string>;
type ProcessedRuleSet = Record<string, string>;

function process(id: string, styles: CSSObject) {
const namespaced = namespace(id, styles);
function stringify(styles: CSSObject): string {
return Object.entries(styles).reduce((accum, [key, properties]) => {
if (typeof properties === 'object') {
return `${accum}${key}{${stringify(properties)}}`;
}

return Object.keys(namespaced).reduce<RuleSet>((accum, key) => {
accum[key] = Object.keys(namespaced[key]).reduce(
(nestedAccum, prop) =>
nestedAccum + prefix(hyphenate(prop), namespaced[key][prop]),
'',
);
// @ts-ignore
return accum + prefix(hyphenate(key), styles[key]);
}, '');
}

return accum;
}, {});
function process(id: string, styles: CSSObject) {
return Object.entries(namespace(id, styles)).reduce<ProcessedRuleSet>(
(accum, [key, value]) => {
accum[key] = stringify(value);
return accum;
},
{},
);
}

export default process;
Loading

0 comments on commit 4b8e5b3

Please sign in to comment.