added backup and email client
This commit is contained in:
+114
@@ -0,0 +1,114 @@
|
||||
import { css, glob, keyframes } from '../css';
|
||||
import { hash } from '../core/hash';
|
||||
import { compile } from '../core/compile';
|
||||
import { getSheet } from '../core/get-sheet';
|
||||
|
||||
jest.mock('../core/hash', () => ({
|
||||
hash: jest.fn().mockReturnValue('hash()')
|
||||
}));
|
||||
|
||||
jest.mock('../core/compile', () => ({
|
||||
compile: jest.fn().mockReturnValue('compile()')
|
||||
}));
|
||||
|
||||
jest.mock('../core/get-sheet', () => ({
|
||||
getSheet: jest.fn().mockReturnValue('getSheet()')
|
||||
}));
|
||||
|
||||
describe('css', () => {
|
||||
beforeEach(() => {
|
||||
hash.mockClear();
|
||||
compile.mockClear();
|
||||
getSheet.mockClear();
|
||||
});
|
||||
|
||||
it('type', () => {
|
||||
expect(typeof css).toEqual('function');
|
||||
});
|
||||
|
||||
it('args: tagged', () => {
|
||||
const out = css`base${1}`;
|
||||
|
||||
expect(compile).toBeCalledWith(['base', ''], [1], undefined);
|
||||
expect(getSheet).toBeCalled();
|
||||
expect(hash).toBeCalledWith('compile()', 'getSheet()', undefined, undefined, undefined);
|
||||
expect(out).toEqual('hash()');
|
||||
});
|
||||
|
||||
it('args: object', () => {
|
||||
const out = css({ foo: 1 });
|
||||
|
||||
expect(hash).toBeCalledWith({ foo: 1 }, 'getSheet()', undefined, undefined, undefined);
|
||||
expect(compile).not.toBeCalled();
|
||||
expect(getSheet).toBeCalled();
|
||||
expect(out).toEqual('hash()');
|
||||
});
|
||||
|
||||
it('args: array', () => {
|
||||
const propsBased = jest.fn().mockReturnValue({
|
||||
backgroundColor: 'gold'
|
||||
});
|
||||
const payload = [{ foo: 1 }, { baz: 2 }, { opacity: 0, color: 'red' }, propsBased];
|
||||
const out = css(payload);
|
||||
|
||||
expect(propsBased).toHaveBeenCalled();
|
||||
expect(hash).toBeCalledWith(
|
||||
{ foo: 1, baz: 2, opacity: 0, color: 'red', backgroundColor: 'gold' },
|
||||
'getSheet()',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
expect(compile).not.toBeCalled();
|
||||
expect(getSheet).toBeCalled();
|
||||
expect(out).toEqual('hash()');
|
||||
});
|
||||
|
||||
it('args: function', () => {
|
||||
const incoming = { foo: 'foo' };
|
||||
const out = css.call({ p: incoming }, (props) => ({ foo: props.foo }));
|
||||
|
||||
expect(hash).toBeCalledWith(incoming, 'getSheet()', undefined, undefined, undefined);
|
||||
expect(compile).not.toBeCalled();
|
||||
expect(getSheet).toBeCalled();
|
||||
expect(out).toEqual('hash()');
|
||||
});
|
||||
|
||||
it('bind', () => {
|
||||
const target = '';
|
||||
const p = {};
|
||||
const g = true;
|
||||
const out = css.bind({
|
||||
target,
|
||||
p,
|
||||
g
|
||||
})`foo: 1`;
|
||||
|
||||
expect(hash).toBeCalledWith('compile()', 'getSheet()', true, undefined, undefined);
|
||||
expect(compile).toBeCalledWith(['foo: 1'], [], p);
|
||||
expect(getSheet).toBeCalledWith(target);
|
||||
expect(out).toEqual('hash()');
|
||||
});
|
||||
});
|
||||
|
||||
describe('glob', () => {
|
||||
it('type', () => {
|
||||
expect(typeof glob).toEqual('function');
|
||||
});
|
||||
|
||||
it('args: g', () => {
|
||||
glob`a:b`;
|
||||
expect(hash).toBeCalledWith('compile()', 'getSheet()', 1, undefined, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyframes', () => {
|
||||
it('type', () => {
|
||||
expect(typeof keyframes).toEqual('function');
|
||||
});
|
||||
|
||||
it('args: k', () => {
|
||||
keyframes`a:b`;
|
||||
expect(hash).toBeCalledWith('compile()', 'getSheet()', undefined, undefined, 1);
|
||||
});
|
||||
});
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import * as goober from '../index';
|
||||
|
||||
describe('goober', () => {
|
||||
it('exports', () => {
|
||||
expect(Object.keys(goober).sort()).toEqual([
|
||||
'css',
|
||||
'extractCss',
|
||||
'glob',
|
||||
'keyframes',
|
||||
'setup',
|
||||
'styled'
|
||||
]);
|
||||
});
|
||||
});
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
import { h, createContext, render } from 'preact';
|
||||
import { useContext, forwardRef } from 'preact/compat';
|
||||
import { setup, css, styled, keyframes } from '../index';
|
||||
import { extractCss } from '../core/update';
|
||||
|
||||
describe('integrations', () => {
|
||||
it('preact', () => {
|
||||
const ThemeContext = createContext();
|
||||
const useTheme = () => useContext(ThemeContext);
|
||||
|
||||
setup(h, null, useTheme);
|
||||
|
||||
const target = document.createElement('div');
|
||||
|
||||
const Span = styled('span', forwardRef)`
|
||||
color: red;
|
||||
`;
|
||||
|
||||
const SpanWrapper = styled('div')`
|
||||
color: cyan;
|
||||
|
||||
${Span} {
|
||||
border: 1px solid red;
|
||||
}
|
||||
`;
|
||||
|
||||
const BoxWithColor = styled('div')`
|
||||
color: ${(props) => props.color};
|
||||
`;
|
||||
|
||||
const BoxWithColorFn = styled('div')(
|
||||
(props) => `
|
||||
color: ${props.color};
|
||||
`
|
||||
);
|
||||
|
||||
const BoxWithThemeColor = styled('div')`
|
||||
color: ${(props) => props.theme.color};
|
||||
`;
|
||||
|
||||
const BoxWithThemeColorFn = styled('div')(
|
||||
(props) => `
|
||||
color: ${props.theme.color};
|
||||
`
|
||||
);
|
||||
|
||||
const fadeAnimation = keyframes`
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
99% {
|
||||
opacity: 1;
|
||||
color: dodgerblue;
|
||||
}
|
||||
`;
|
||||
|
||||
const BoxWithAnimation = styled('span')`
|
||||
opacity: 0;
|
||||
animation: ${fadeAnimation} 500ms ease-in-out;
|
||||
`;
|
||||
|
||||
const BoxWithConditionals = styled('div')([
|
||||
{ foo: 1 },
|
||||
(props) => ({ color: props.isActive ? 'red' : 'tomato' }),
|
||||
null,
|
||||
{ baz: 0 },
|
||||
false,
|
||||
{ baz: 0 }
|
||||
]);
|
||||
|
||||
const shared = { opacity: 0 };
|
||||
const BoxWithShared = styled('div')(shared);
|
||||
const BoxWithSharedAndConditional = styled('div')([shared, { baz: 0 }]);
|
||||
|
||||
const BoxWithHas = styled('div')`
|
||||
label:has(input, select),
|
||||
:has(foo, boo) {
|
||||
color: red;
|
||||
}
|
||||
`;
|
||||
|
||||
const refSpy = jest.fn();
|
||||
|
||||
render(
|
||||
<ThemeContext.Provider value={{ color: 'blue' }}>
|
||||
<div>
|
||||
<Span ref={refSpy} />
|
||||
<Span as={'div'} />
|
||||
<SpanWrapper>
|
||||
<Span />
|
||||
</SpanWrapper>
|
||||
<BoxWithColor color={'red'} />
|
||||
<BoxWithColorFn color={'red'} />
|
||||
<BoxWithThemeColor />
|
||||
<BoxWithThemeColorFn />
|
||||
<BoxWithThemeColor theme={{ color: 'green' }} />
|
||||
<BoxWithThemeColorFn theme={{ color: 'orange' }} />
|
||||
<BoxWithAnimation />
|
||||
<BoxWithConditionals isActive />
|
||||
<BoxWithShared />
|
||||
<BoxWithSharedAndConditional />
|
||||
<div className={css([shared, { background: 'cyan' }])} />
|
||||
<BoxWithHas />
|
||||
</div>
|
||||
</ThemeContext.Provider>,
|
||||
target
|
||||
);
|
||||
|
||||
expect(extractCss()).toMatchInlineSnapshot(
|
||||
[
|
||||
'"',
|
||||
' ', // Empty white space that holds the textNode that the styles are appended
|
||||
'@keyframes go384228713{0%{opacity:0;}99%{opacity:1;color:dodgerblue;}}',
|
||||
'.go1127809067{opacity:0;background:cyan;}',
|
||||
'.go3865451590{color:red;}',
|
||||
'.go3991234422{color:cyan;}',
|
||||
'.go3991234422 .go3865451590{border:1px solid red;}',
|
||||
'.go1925576363{color:blue;}',
|
||||
'.go3206651468{color:green;}',
|
||||
'.go4276997079{color:orange;}',
|
||||
'.go2069586824{opacity:0;animation:go384228713 500ms ease-in-out;}',
|
||||
'.go631307347{foo:1;color:red;baz:0;}',
|
||||
'.go3865943372{opacity:0;}',
|
||||
'.go1162430001{opacity:0;baz:0;}',
|
||||
'.go2602823658 label:has(input, select),.go2602823658 :has(foo, boo){color:red;}',
|
||||
'"'
|
||||
].join('')
|
||||
);
|
||||
|
||||
expect(refSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
tagName: 'SPAN'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('support extending with as', () => {
|
||||
const list = ['p', 'm', 'as', 'bg'];
|
||||
setup(h, undefined, undefined, (props) => {
|
||||
for (let prop in props) {
|
||||
if (list.indexOf(prop) !== -1) {
|
||||
delete props[prop];
|
||||
}
|
||||
}
|
||||
});
|
||||
const target = document.createElement('div');
|
||||
|
||||
const Base = styled('div')(({ p = 0, m }) => [
|
||||
{
|
||||
color: 'white',
|
||||
padding: p + 'em'
|
||||
},
|
||||
m != null && { margin: m + 'em' }
|
||||
]);
|
||||
|
||||
const Super = styled(Base)`
|
||||
background: ${(p) => p.bg || 'none'};
|
||||
`;
|
||||
|
||||
render(
|
||||
<div>
|
||||
<Base />
|
||||
<Base p={2} />
|
||||
<Base m={1} p={3} as={'span'} />
|
||||
<Super m={1} bg={'dodgerblue'} as={'button'} />
|
||||
</div>,
|
||||
target
|
||||
);
|
||||
|
||||
// Makes sure the resulting DOM does not contain any props
|
||||
expect(target.innerHTML).toEqual(
|
||||
[
|
||||
'<div>',
|
||||
'<div class="go103173764"></div>',
|
||||
'<div class="go103194166"></div>',
|
||||
'<span class="go2081835032"></span>',
|
||||
'<button class="go1969245729 go1824201605"></button>',
|
||||
'</div>'
|
||||
].join('')
|
||||
);
|
||||
|
||||
expect(extractCss()).toMatchInlineSnapshot(
|
||||
[
|
||||
'"',
|
||||
'.go1969245729{color:white;padding:0em;margin:1em;}',
|
||||
'.go103173764{color:white;padding:0em;}',
|
||||
'.go103194166{color:white;padding:2em;}',
|
||||
'.go2081835032{color:white;padding:3em;margin:1em;}',
|
||||
'.go1824201605{background:dodgerblue;}',
|
||||
'"'
|
||||
].join('')
|
||||
);
|
||||
});
|
||||
|
||||
it('shouldForwardProps', () => {
|
||||
const list = ['p', 'm', 'as'];
|
||||
setup(h, undefined, undefined, (props) => {
|
||||
for (let prop in props) {
|
||||
if (list.indexOf(prop) !== -1) {
|
||||
delete props[prop];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const target = document.createElement('div');
|
||||
|
||||
const Base = styled('div')(({ p = 0, m }) => [
|
||||
{
|
||||
color: 'white',
|
||||
padding: p + 'em'
|
||||
},
|
||||
m != null && { margin: m + 'em' }
|
||||
]);
|
||||
|
||||
render(
|
||||
<div>
|
||||
<Base />
|
||||
<Base p={2} />
|
||||
<Base m={1} p={3} as={'span'} />
|
||||
</div>,
|
||||
target
|
||||
);
|
||||
|
||||
// Makes sure the resulting DOM does not contain any props
|
||||
expect(target.innerHTML).toEqual(
|
||||
[
|
||||
'<div>',
|
||||
'<div class="go103173764"></div>',
|
||||
'<div class="go103194166"></div>',
|
||||
'<span class="go2081835032"></span>',
|
||||
'</div>'
|
||||
].join(''),
|
||||
`"<div><div class=\\"go103173764\\"></div><div class=\\"go103194166\\"></div><span class=\\"go2081835032\\"></span></div>"`
|
||||
);
|
||||
|
||||
expect(extractCss()).toMatchInlineSnapshot(
|
||||
[
|
||||
'"',
|
||||
'.go103173764{color:white;padding:0em;}',
|
||||
'.go103194166{color:white;padding:2em;}',
|
||||
'.go2081835032{color:white;padding:3em;margin:1em;}',
|
||||
'"'
|
||||
].join('')
|
||||
);
|
||||
});
|
||||
});
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
import { styled, setup } from '../styled';
|
||||
import { extractCss } from '../core/update';
|
||||
|
||||
const pragma = jest.fn((tag, props) => {
|
||||
return { tag, props: { ...props, className: props.className.replace(/go\d+/g, 'go') } };
|
||||
});
|
||||
|
||||
expect.extend({
|
||||
toMatchVNode(received, tag, props) {
|
||||
expect(received.tag).toEqual(tag);
|
||||
expect(received.props).toEqual(props);
|
||||
return {
|
||||
message: 'Expected vnode to match vnode',
|
||||
pass: true
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
describe('styled', () => {
|
||||
beforeEach(() => {
|
||||
pragma.mockClear();
|
||||
setup(pragma);
|
||||
extractCss();
|
||||
});
|
||||
|
||||
it('calls pragma', () => {
|
||||
setup(undefined);
|
||||
expect(() => styled()()()).toThrow();
|
||||
|
||||
setup(pragma);
|
||||
const vnode = styled('div')``();
|
||||
|
||||
expect(pragma).toBeCalledTimes(1);
|
||||
expect(vnode).toMatchVNode('div', {
|
||||
className: 'go'
|
||||
});
|
||||
});
|
||||
|
||||
it('extend props', () => {
|
||||
const vnode = styled('tag')`
|
||||
color: peachpuff;
|
||||
`({ bar: 1 });
|
||||
|
||||
expect(vnode).toMatchVNode('tag', {
|
||||
bar: 1,
|
||||
className: 'go'
|
||||
});
|
||||
expect(extractCss()).toEqual('.go3183460609{color:peachpuff;}');
|
||||
});
|
||||
|
||||
it('concat className if present in props', () => {
|
||||
const vnode = styled('tag')`
|
||||
color: peachpuff;
|
||||
`({ bar: 1, className: 'existing' });
|
||||
|
||||
expect(vnode).toMatchVNode('tag', {
|
||||
bar: 1,
|
||||
className: 'go existing'
|
||||
});
|
||||
});
|
||||
|
||||
it('pass template function', () => {
|
||||
const vnode = styled('tag')((props) => ({ color: props.color }))({ color: 'red' });
|
||||
|
||||
expect(vnode).toMatchVNode('tag', {
|
||||
className: 'go',
|
||||
color: 'red'
|
||||
});
|
||||
expect(extractCss()).toEqual('.go3433634237{color:red;}');
|
||||
});
|
||||
|
||||
it('change tag via "as" prop', () => {
|
||||
const Tag = styled('tag')`
|
||||
color: red;
|
||||
`;
|
||||
|
||||
// Simulate a render
|
||||
let vnode = Tag();
|
||||
expect(vnode).toMatchVNode('tag', { className: 'go' });
|
||||
|
||||
// Simulate a render with
|
||||
vnode = Tag({ as: 'foo' });
|
||||
// Expect it to be changed to foo
|
||||
expect(vnode).toMatchVNode('foo', { className: 'go' });
|
||||
|
||||
// Simulate a render
|
||||
vnode = Tag();
|
||||
expect(vnode).toMatchVNode('tag', { className: 'go' });
|
||||
});
|
||||
|
||||
it('support forwardRef', () => {
|
||||
const forwardRef = jest.fn((fn) => (props) => fn(props, 'ref'));
|
||||
const vnode = styled('tag', forwardRef)`
|
||||
color: red;
|
||||
`({ bar: 1 });
|
||||
|
||||
expect(vnode).toMatchVNode('tag', {
|
||||
bar: 1,
|
||||
className: 'go',
|
||||
ref: 'ref'
|
||||
});
|
||||
});
|
||||
|
||||
it('setup useTheme', () => {
|
||||
setup(pragma, null, () => 'theme');
|
||||
|
||||
const styleFn = jest.fn(() => ({}));
|
||||
const vnode = styled('tag')(styleFn)({ bar: 1 });
|
||||
|
||||
expect(styleFn).toBeCalledWith({ bar: 1, theme: 'theme' });
|
||||
expect(vnode).toMatchVNode('tag', {
|
||||
bar: 1,
|
||||
className: 'go'
|
||||
});
|
||||
});
|
||||
|
||||
it('setup useTheme with theme prop override', () => {
|
||||
setup(pragma, null, () => 'theme');
|
||||
|
||||
const styleFn = jest.fn(() => ({}));
|
||||
const vnode = styled('tag')(styleFn)({ theme: 'override' });
|
||||
|
||||
expect(styleFn).toBeCalledWith({ theme: 'override' });
|
||||
expect(vnode).toMatchVNode('tag', { className: 'go', theme: 'override' });
|
||||
});
|
||||
|
||||
it('uses babel compiled classNames', () => {
|
||||
const Comp = styled('tag')``;
|
||||
Comp.className = 'foobar';
|
||||
const vnode = Comp({});
|
||||
expect(vnode).toMatchVNode('tag', { className: 'go foobar' });
|
||||
});
|
||||
|
||||
it('omits css prop with falsy should forward prop function', () => {
|
||||
const shouldForwardProp = (props) => {
|
||||
for (let prop in props) {
|
||||
if (prop.includes('$')) delete props[prop];
|
||||
}
|
||||
};
|
||||
// Overwrite setup for this test
|
||||
setup(pragma, undefined, undefined, shouldForwardProp);
|
||||
|
||||
const vnode = styled('tag')`
|
||||
color: peachpuff;
|
||||
`({ bar: 1, $templateColumns: '1fr 1fr' });
|
||||
|
||||
expect(vnode).toMatchVNode('tag', { className: 'go', bar: 1 });
|
||||
});
|
||||
|
||||
it('pass truthy logical and operator', () => {
|
||||
const Tag = styled('tag')((props) => props.draw && { color: 'yellow' });
|
||||
|
||||
// Simulate a render
|
||||
let vnode = Tag({ draw: true });
|
||||
|
||||
expect(vnode).toMatchVNode('tag', { className: 'go', draw: true });
|
||||
expect(extractCss()).toEqual('.go2986228274{color:yellow;}');
|
||||
});
|
||||
});
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
import { astish } from '../astish';
|
||||
|
||||
describe('astish', () => {
|
||||
it('regular', () => {
|
||||
expect(
|
||||
astish(`
|
||||
prop: value;
|
||||
`)
|
||||
).toEqual({
|
||||
prop: 'value'
|
||||
});
|
||||
});
|
||||
|
||||
it('nested', () => {
|
||||
expect(
|
||||
astish(`
|
||||
prop: value;
|
||||
@keyframes foo {
|
||||
0% {
|
||||
attr: value;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
foo: baz;
|
||||
}
|
||||
}
|
||||
named {
|
||||
background-image: url('/path-to-jpg.png');
|
||||
}
|
||||
opacity: 0;
|
||||
.class,
|
||||
&:hover {
|
||||
-webkit-touch: none;
|
||||
}
|
||||
`)
|
||||
).toEqual({
|
||||
prop: 'value',
|
||||
opacity: '0',
|
||||
'.class, &:hover': {
|
||||
'-webkit-touch': 'none'
|
||||
},
|
||||
'@keyframes foo': {
|
||||
'0%': {
|
||||
attr: 'value'
|
||||
},
|
||||
'50%': {
|
||||
opacity: '1'
|
||||
},
|
||||
|
||||
'100%': {
|
||||
foo: 'baz'
|
||||
}
|
||||
},
|
||||
named: {
|
||||
'background-image': "url('/path-to-jpg.png')"
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('merging', () => {
|
||||
expect(
|
||||
astish(`
|
||||
.c {
|
||||
font-size:24px;
|
||||
}
|
||||
|
||||
.c {
|
||||
color:red;
|
||||
}
|
||||
`)
|
||||
).toEqual({
|
||||
'.c': {
|
||||
'font-size': '24px',
|
||||
color: 'red'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('regression', () => {
|
||||
expect(
|
||||
astish(`
|
||||
&.g0ssss {
|
||||
aa: foo;
|
||||
box-shadow: 0 1px rgba(0, 2, 33, 4) inset;
|
||||
}
|
||||
named {
|
||||
transform: scale(1.2), rotate(1, 1);
|
||||
}
|
||||
@media screen and (some-rule: 100px) {
|
||||
foo: baz;
|
||||
opacity: 1;
|
||||
level {
|
||||
one: 1;
|
||||
level {
|
||||
two: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
.a{
|
||||
color: red;
|
||||
}
|
||||
.b {
|
||||
color: blue;
|
||||
}
|
||||
`)
|
||||
).toEqual({
|
||||
'&.g0ssss': {
|
||||
aa: 'foo',
|
||||
'box-shadow': '0 1px rgba(0, 2, 33, 4) inset'
|
||||
},
|
||||
'.a': {
|
||||
color: 'red'
|
||||
},
|
||||
'.b': {
|
||||
color: 'blue'
|
||||
},
|
||||
named: {
|
||||
transform: 'scale(1.2), rotate(1, 1)'
|
||||
},
|
||||
'@media screen and (some-rule: 100px)': {
|
||||
foo: 'baz',
|
||||
opacity: '1',
|
||||
level: {
|
||||
one: '1',
|
||||
|
||||
level: {
|
||||
two: '2'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should strip comments', () => {
|
||||
expect(
|
||||
astish(`
|
||||
color: red;
|
||||
/*
|
||||
some comment
|
||||
*/
|
||||
transform: translate3d(0, 0, 0);
|
||||
/**
|
||||
* other comment
|
||||
*/
|
||||
background: peachpuff;
|
||||
font-size: xx-large; /* inline comment */
|
||||
/* foo: bar */
|
||||
font-weight: bold;
|
||||
`)
|
||||
).toEqual({
|
||||
color: 'red',
|
||||
transform: 'translate3d(0, 0, 0)',
|
||||
background: 'peachpuff',
|
||||
'font-size': 'xx-large',
|
||||
'font-weight': 'bold'
|
||||
});
|
||||
});
|
||||
|
||||
// for reference on what is valid:
|
||||
// https://www.w3.org/TR/CSS22/syndata.html#value-def-identifier
|
||||
it('should not mangle valid css identifiers', () => {
|
||||
expect(
|
||||
astish(`
|
||||
:root {
|
||||
--azAZ-_中文09: 0;
|
||||
}
|
||||
`)
|
||||
).toEqual({
|
||||
':root': {
|
||||
'--azAZ-_中文09': '0'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse multiline background declaration', () => {
|
||||
expect(
|
||||
astish(`
|
||||
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="white"><path d="M7.5 36.7h58.4v10.6H7.5V36.7zm0-15.9h58.4v10.6H7.5V20.8zm0 31.9h58.4v10.6H7.5V52.7zm0 15.9h58.4v10.6H7.5V68.6zm63.8-15.9l10.6 15.9 10.6-15.9H71.3zm21.2-5.4L81.9 31.4 71.3 47.3h21.2z"/></svg>')
|
||||
center/contain;
|
||||
`)
|
||||
).toEqual({
|
||||
background: `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="white"><path d="M7.5 36.7h58.4v10.6H7.5V36.7zm0-15.9h58.4v10.6H7.5V20.8zm0 31.9h58.4v10.6H7.5V52.7zm0 15.9h58.4v10.6H7.5V68.6zm63.8-15.9l10.6 15.9 10.6-15.9H71.3zm21.2-5.4L81.9 31.4 71.3 47.3h21.2z"/></svg>') center/contain`
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle inline @media block', () => {
|
||||
expect(
|
||||
astish(
|
||||
`h1 { font-size: 1rem; } @media only screen and (min-width: 850px) { h1 { font-size: 2rem; } }`
|
||||
)
|
||||
).toEqual({
|
||||
h1: {
|
||||
'font-size': '1rem'
|
||||
},
|
||||
'@media only screen and (min-width: 850px)': {
|
||||
h1: {
|
||||
'font-size': '2rem'
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle newlines as part of the rule value', () => {
|
||||
expect(
|
||||
astish(
|
||||
`tag {
|
||||
font-size: first
|
||||
second;
|
||||
}`
|
||||
)
|
||||
).toEqual({
|
||||
tag: {
|
||||
'font-size': 'first second'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import { compile } from '../compile';
|
||||
|
||||
const template = (str, ...defs) => {
|
||||
return (props) => compile(str, defs, props);
|
||||
};
|
||||
|
||||
describe('compile', () => {
|
||||
it('simple', () => {
|
||||
expect(template`prop: 1;`({})).toEqual('prop: 1;');
|
||||
});
|
||||
|
||||
it('vnode', () => {
|
||||
expect(template`prop: 1; ${() => ({ props: { className: 'class' } })}`({})).toEqual(
|
||||
'prop: 1; .class'
|
||||
);
|
||||
|
||||
// Empty or falsy
|
||||
expect(template`prop: 1; ${() => ({ props: { foo: 1 } })}`({})).toEqual('prop: 1; ');
|
||||
});
|
||||
|
||||
it('vanilla classname', () => {
|
||||
expect(template`prop: 1; ${() => 'go0ber'}`({})).toEqual('prop: 1; .go0ber');
|
||||
});
|
||||
|
||||
it('value interpolations', () => {
|
||||
// This interpolations are testing the ability to interpolate thruty and falsy values
|
||||
expect(template`prop: 1; ${() => 0},${() => undefined},${() => null},${2}`({})).toEqual(
|
||||
'prop: 1; 0,,,2'
|
||||
);
|
||||
|
||||
const tmpl = template`
|
||||
background: dodgerblue;
|
||||
${(props) =>
|
||||
props.padding === 'bloo' &&
|
||||
`
|
||||
padding: ${props.padding}px;
|
||||
`}
|
||||
border: 1px solid blue;
|
||||
`;
|
||||
expect(tmpl({})).toEqual(`
|
||||
background: dodgerblue;
|
||||
|
||||
border: 1px solid blue;
|
||||
`);
|
||||
});
|
||||
|
||||
describe('objects', () => {
|
||||
it('normal', () => {
|
||||
expect(template`prop: 1;${(p) => ({ color: p.color })}`({ color: 'red' })).toEqual(
|
||||
'prop: 1;color:red;'
|
||||
);
|
||||
});
|
||||
|
||||
it('styled-system', () => {
|
||||
const color = (p) => ({ color: p.color });
|
||||
const background = (p) => ({ backgroundColor: p.backgroundColor });
|
||||
|
||||
const props = { color: 'red', backgroundColor: 'blue' };
|
||||
const res = template`
|
||||
prop: 1;
|
||||
${color}
|
||||
${background}
|
||||
`(props);
|
||||
|
||||
expect(res.replace(/([\s|\n]+)/gm, '').trim()).toEqual(
|
||||
'prop:1;color:red;background-color:blue;'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import { getSheet } from '../get-sheet';
|
||||
|
||||
describe('getSheet', () => {
|
||||
it('regression', () => {
|
||||
const target = getSheet();
|
||||
expect(target.nodeType).toEqual(3);
|
||||
});
|
||||
|
||||
it('custom target', () => {
|
||||
const custom = document.createElement('div');
|
||||
const sheet = getSheet(custom);
|
||||
|
||||
expect(sheet.nodeType).toEqual(3);
|
||||
expect(sheet.parentElement.nodeType).toEqual(1);
|
||||
expect(sheet.parentElement.getAttribute('id')).toEqual('_goober');
|
||||
});
|
||||
|
||||
it('reuse sheet', () => {
|
||||
const custom = document.createElement('div');
|
||||
const sheet = getSheet(custom);
|
||||
const second = getSheet(custom);
|
||||
|
||||
expect(sheet === second).toBeTruthy();
|
||||
});
|
||||
|
||||
it('applies nonce from window.__nonce__', () => {
|
||||
const sheet = getSheet();
|
||||
const style = sheet.parentElement;
|
||||
const prevAttr = style.getAttribute('nonce');
|
||||
const hadAttr = style.hasAttribute('nonce');
|
||||
const prevNonce = window.__nonce__;
|
||||
|
||||
style.removeAttribute('nonce');
|
||||
delete window.__nonce__;
|
||||
|
||||
window.__nonce__ = 'secure-nonce';
|
||||
getSheet();
|
||||
|
||||
expect(style.getAttribute('nonce')).toEqual('secure-nonce');
|
||||
|
||||
if (prevAttr != null) style.setAttribute('nonce', prevAttr);
|
||||
else if (!hadAttr) style.removeAttribute('nonce');
|
||||
|
||||
if (prevNonce === undefined) delete window.__nonce__;
|
||||
else window.__nonce__ = prevNonce;
|
||||
});
|
||||
|
||||
it('server side', () => {
|
||||
const bkp = global.document;
|
||||
delete global.document;
|
||||
|
||||
expect(() => getSheet()).not.toThrow();
|
||||
|
||||
global.document = bkp;
|
||||
});
|
||||
|
||||
it('server side with custom collector', () => {
|
||||
const bkp = global.document;
|
||||
const win = global.window;
|
||||
delete global.document;
|
||||
delete global.window;
|
||||
|
||||
const collector = { data: '' };
|
||||
|
||||
expect(collector === getSheet(collector)).toBeTruthy();
|
||||
|
||||
global.document = bkp;
|
||||
global.window = win;
|
||||
});
|
||||
});
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
import { hash } from '../hash';
|
||||
import { toHash } from '../to-hash';
|
||||
import { update } from '../update';
|
||||
import { parse } from '../parse';
|
||||
import { astish } from '../astish';
|
||||
|
||||
jest.mock('../astish', () => ({
|
||||
astish: jest.fn().mockReturnValue('astish()')
|
||||
}));
|
||||
|
||||
jest.mock('../parse', () => ({
|
||||
parse: jest.fn().mockReturnValue('parse()')
|
||||
}));
|
||||
|
||||
jest.mock('../to-hash', () => ({
|
||||
toHash: jest.fn().mockReturnValue('toHash()')
|
||||
}));
|
||||
|
||||
jest.mock('../update', () => ({
|
||||
update: jest.fn().mockReturnValue('update()')
|
||||
}));
|
||||
|
||||
jest.mock('../astish', () => ({
|
||||
astish: jest.fn().mockReturnValue('astish()')
|
||||
}));
|
||||
|
||||
jest.mock('../parse', () => ({
|
||||
parse: jest.fn().mockReturnValue('parse()')
|
||||
}));
|
||||
|
||||
describe('hash', () => {
|
||||
beforeEach(() => {
|
||||
toHash.mockClear();
|
||||
update.mockClear();
|
||||
parse.mockClear();
|
||||
astish.mockClear();
|
||||
});
|
||||
|
||||
it('regression', () => {
|
||||
const res = hash('compiled', 'target');
|
||||
|
||||
expect(toHash).toBeCalledWith('compiled');
|
||||
expect(update).toBeCalledWith('parse()', 'target', undefined, null);
|
||||
expect(astish).toBeCalledWith('compiled');
|
||||
expect(parse).toBeCalledWith('astish()', '.toHash()');
|
||||
|
||||
expect(res).toEqual('toHash()');
|
||||
});
|
||||
|
||||
it('regression: cache', () => {
|
||||
const res = hash('compiled', 'target');
|
||||
|
||||
expect(toHash).not.toBeCalled();
|
||||
expect(astish).not.toBeCalled();
|
||||
expect(parse).not.toBeCalled();
|
||||
expect(update).toBeCalledWith('parse()', 'target', undefined, null);
|
||||
|
||||
expect(res).toEqual('toHash()');
|
||||
});
|
||||
|
||||
it('regression: global', () => {
|
||||
const res = hash('global', 'target', true);
|
||||
|
||||
expect(toHash).toBeCalledWith('global');
|
||||
expect(astish).not.toBeCalled();
|
||||
expect(parse).not.toBeCalled();
|
||||
expect(update).toBeCalledWith('parse()', 'target', undefined, null);
|
||||
|
||||
expect(res).toEqual('toHash()');
|
||||
});
|
||||
|
||||
it('regression: global-style-replace', () => {
|
||||
const res = hash('global', 'target', true);
|
||||
|
||||
expect(toHash).not.toBeCalled();
|
||||
expect(astish).not.toBeCalled();
|
||||
expect(parse).not.toBeCalled();
|
||||
expect(update).toBeCalledWith('parse()', 'target', undefined, 'parse()');
|
||||
|
||||
expect(res).toEqual('toHash()');
|
||||
});
|
||||
|
||||
it('regression: keyframes', () => {
|
||||
const res = hash('keyframes', 'target', undefined, undefined, 1);
|
||||
|
||||
expect(toHash).toBeCalledWith('keyframes');
|
||||
expect(astish).not.toBeCalled();
|
||||
expect(parse).not.toBeCalled();
|
||||
expect(update).toBeCalledWith('parse()', 'target', undefined, null);
|
||||
|
||||
expect(res).toEqual('toHash()');
|
||||
});
|
||||
|
||||
it('regression: object', () => {
|
||||
const className = Math.random() + 'unique';
|
||||
toHash.mockReturnValue(className);
|
||||
|
||||
const res = hash({ baz: 1 }, 'target');
|
||||
|
||||
expect(toHash).toBeCalledWith('baz1');
|
||||
expect(astish).not.toBeCalled();
|
||||
expect(parse).toBeCalledWith({ baz: 1 }, '.' + className);
|
||||
expect(update).toBeCalledWith('parse()', 'target', undefined, null);
|
||||
|
||||
expect(res).toEqual(className);
|
||||
});
|
||||
|
||||
it('regression: cache-object', () => {
|
||||
const className = Math.random() + 'unique';
|
||||
toHash.mockReturnValue(className);
|
||||
|
||||
// Since it's not yet cached
|
||||
hash({ cacheObject: 1 }, 'target');
|
||||
expect(toHash).toBeCalledWith('cacheObject1');
|
||||
toHash.mockClear();
|
||||
|
||||
// Different object
|
||||
hash({ foo: 2 }, 'target');
|
||||
expect(toHash).toBeCalledWith('foo2');
|
||||
toHash.mockClear();
|
||||
|
||||
// First object should not call .toHash
|
||||
hash({ cacheObject: 1 }, 'target');
|
||||
expect(toHash).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
+327
@@ -0,0 +1,327 @@
|
||||
import { parse } from '../parse';
|
||||
|
||||
describe('parse', () => {
|
||||
it('regular', () => {
|
||||
const out = parse(
|
||||
{
|
||||
display: 'value',
|
||||
button: {
|
||||
border: '0'
|
||||
},
|
||||
'&.nested': {
|
||||
foo: '1px',
|
||||
baz: 'scale(1), translate(1)'
|
||||
}
|
||||
},
|
||||
'hush'
|
||||
);
|
||||
|
||||
expect(out).toEqual(
|
||||
[
|
||||
'hush{display:value;}',
|
||||
'hush button{border:0;}',
|
||||
'hush.nested{foo:1px;baz:scale(1), translate(1);}'
|
||||
].join('')
|
||||
);
|
||||
});
|
||||
|
||||
it('camelCase', () => {
|
||||
const out = parse(
|
||||
{
|
||||
fooBarProperty: 'value',
|
||||
button: {
|
||||
webkitPressSomeButton: '0'
|
||||
},
|
||||
'&.nested': {
|
||||
foo: '1px',
|
||||
backgroundEffect: 'scale(1), translate(1)'
|
||||
}
|
||||
},
|
||||
'hush'
|
||||
);
|
||||
|
||||
expect(out).toEqual(
|
||||
[
|
||||
'hush{foo-bar-property:value;}',
|
||||
'hush button{webkit-press-some-button:0;}',
|
||||
'hush.nested{foo:1px;background-effect:scale(1), translate(1);}'
|
||||
].join('')
|
||||
);
|
||||
});
|
||||
|
||||
it('keyframes', () => {
|
||||
const out = parse(
|
||||
{
|
||||
'@keyframes superAnimation': {
|
||||
'11.1%': {
|
||||
opacity: '0.9999'
|
||||
},
|
||||
'111%': {
|
||||
opacity: '1'
|
||||
}
|
||||
},
|
||||
'@keyframes foo': {
|
||||
to: {
|
||||
baz: '1px',
|
||||
foo: '1px'
|
||||
}
|
||||
},
|
||||
'@keyframes complex': {
|
||||
'from, 20%, 53%, 80%, to': {
|
||||
transform: 'translate3d(0,0,0)'
|
||||
},
|
||||
'40%, 43%': {
|
||||
transform: 'translate3d(0, -30px, 0)'
|
||||
},
|
||||
'70%': {
|
||||
transform: 'translate3d(0, -15px, 0)'
|
||||
},
|
||||
'90%': {
|
||||
transform: 'translate3d(0,-4px,0)'
|
||||
}
|
||||
}
|
||||
},
|
||||
'hush'
|
||||
);
|
||||
|
||||
expect(out).toEqual(
|
||||
[
|
||||
'@keyframes superAnimation{11.1%{opacity:0.9999;}111%{opacity:1;}}',
|
||||
'@keyframes foo{to{baz:1px;foo:1px;}}',
|
||||
'@keyframes complex{from, 20%, 53%, 80%, to{transform:translate3d(0,0,0);}40%, 43%{transform:translate3d(0, -30px, 0);}70%{transform:translate3d(0, -15px, 0);}90%{transform:translate3d(0,-4px,0);}}'
|
||||
].join('')
|
||||
);
|
||||
});
|
||||
|
||||
it('font-face', () => {
|
||||
const out = parse(
|
||||
{
|
||||
'@font-face': {
|
||||
'font-weight': 100
|
||||
}
|
||||
},
|
||||
'FONTFACE'
|
||||
);
|
||||
|
||||
expect(out).toEqual(['@font-face{font-weight:100;}'].join(''));
|
||||
});
|
||||
|
||||
it('@media', () => {
|
||||
const out = parse(
|
||||
{
|
||||
'@media any all (no-really-anything)': {
|
||||
position: 'super-absolute'
|
||||
}
|
||||
},
|
||||
'hush'
|
||||
);
|
||||
|
||||
expect(out).toEqual(
|
||||
['@media any all (no-really-anything){hush{position:super-absolute;}}'].join('')
|
||||
);
|
||||
});
|
||||
|
||||
it('@import', () => {
|
||||
const out = parse(
|
||||
{
|
||||
'@import': "url('https://domain.com/path?1=s')"
|
||||
},
|
||||
'hush'
|
||||
);
|
||||
|
||||
expect(out).toEqual(["@import url('https://domain.com/path?1=s');"].join(''));
|
||||
});
|
||||
|
||||
it('cra', () => {
|
||||
expect(
|
||||
parse(
|
||||
{
|
||||
'@import': "url('path/to')",
|
||||
'@font-face': {
|
||||
'font-weight': 100
|
||||
},
|
||||
'text-align': 'center',
|
||||
'.logo': {
|
||||
animation: 'App-logo-spin infinite 20s linear',
|
||||
height: '40vmin',
|
||||
'pointer-events': 'none'
|
||||
},
|
||||
'.header': {
|
||||
'background-color': '#282c34',
|
||||
'min-height': '100vh',
|
||||
display: 'flex',
|
||||
'flex-direction': 'column',
|
||||
'align-items': 'center',
|
||||
'justify-content': 'center',
|
||||
'font-size': 'calc(10px + 2vmin)',
|
||||
color: 'white'
|
||||
},
|
||||
'.link': {
|
||||
color: '#61dafb'
|
||||
},
|
||||
'@keyframes App-logo-spin': {
|
||||
from: {
|
||||
transform: 'rotate(0deg)'
|
||||
},
|
||||
to: {
|
||||
transform: 'rotate(360deg)'
|
||||
}
|
||||
}
|
||||
},
|
||||
'App'
|
||||
)
|
||||
).toEqual(
|
||||
[
|
||||
"@import url('path/to');",
|
||||
'App{text-align:center;}',
|
||||
'@font-face{font-weight:100;}',
|
||||
'App .logo{animation:App-logo-spin infinite 20s linear;height:40vmin;pointer-events:none;}',
|
||||
'App .header{background-color:#282c34;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;font-size:calc(10px + 2vmin);color:white;}',
|
||||
'App .link{color:#61dafb;}',
|
||||
'@keyframes App-logo-spin{from{transform:rotate(0deg);}to{transform:rotate(360deg);}}'
|
||||
].join('')
|
||||
);
|
||||
});
|
||||
|
||||
it('@supports', () => {
|
||||
expect(
|
||||
parse(
|
||||
{
|
||||
'@supports (some: 1px)': {
|
||||
'@media (s: 1)': {
|
||||
display: 'flex'
|
||||
}
|
||||
},
|
||||
'@supports': {
|
||||
opacity: 1
|
||||
}
|
||||
},
|
||||
'hush'
|
||||
)
|
||||
).toEqual(
|
||||
[
|
||||
'@supports (some: 1px){@media (s: 1){hush{display:flex;}}}',
|
||||
'@supports{hush{opacity:1;}}'
|
||||
].join('')
|
||||
);
|
||||
});
|
||||
|
||||
it('unwrapp', () => {
|
||||
expect(
|
||||
parse(
|
||||
{
|
||||
'--foo': 1,
|
||||
opacity: 1,
|
||||
'@supports': {
|
||||
'--bar': 'none'
|
||||
},
|
||||
html: {
|
||||
background: 'goober'
|
||||
}
|
||||
},
|
||||
''
|
||||
)
|
||||
).toEqual(
|
||||
['--foo:1;opacity:1;', '@supports{--bar:none;}', 'html{background:goober;}'].join('')
|
||||
);
|
||||
});
|
||||
|
||||
it('nested with multiple selector', () => {
|
||||
const out = parse(
|
||||
{
|
||||
display: 'value',
|
||||
'&:hover,&:focus': {
|
||||
border: '0',
|
||||
span: {
|
||||
index: 'unset'
|
||||
}
|
||||
},
|
||||
'p,b,i': {
|
||||
display: 'block',
|
||||
'&:focus,input': {
|
||||
opacity: 1,
|
||||
'div,span': {
|
||||
opacity: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'hush'
|
||||
);
|
||||
expect(out).toEqual(
|
||||
[
|
||||
'hush{display:value;}',
|
||||
'hush:hover,hush:focus{border:0;}',
|
||||
'hush:hover span,hush:focus span{index:unset;}',
|
||||
'hush p,hush b,hush i{display:block;}',
|
||||
'hush p:focus,hush p input,hush b:focus,hush b input,hush i:focus,hush i input{opacity:1;}',
|
||||
'hush p:focus div,hush p:focus span,hush p input div,hush p input span,hush b:focus div,hush b:focus span,hush b input div,hush b input span,hush i:focus div,hush i:focus span,hush i input div,hush i input span{opacity:0;}'
|
||||
].join('')
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle the :where(a,b) cases', () => {
|
||||
expect(
|
||||
parse(
|
||||
{
|
||||
div: {
|
||||
':where(a, b)': {
|
||||
color: 'blue'
|
||||
}
|
||||
}
|
||||
},
|
||||
''
|
||||
)
|
||||
).toEqual('div :where(a, b){color:blue;}');
|
||||
});
|
||||
|
||||
it('should handle null and undefined values', () => {
|
||||
expect(
|
||||
parse(
|
||||
{
|
||||
div: {
|
||||
opacity: 0,
|
||||
color: null
|
||||
}
|
||||
},
|
||||
''
|
||||
)
|
||||
).toEqual('div{opacity:0;}');
|
||||
expect(
|
||||
parse(
|
||||
{
|
||||
div: {
|
||||
opacity: 0,
|
||||
color: undefined // or `void 0` when minified
|
||||
}
|
||||
},
|
||||
''
|
||||
)
|
||||
).toEqual('div{opacity:0;}');
|
||||
});
|
||||
|
||||
it('does not transform the case of custom CSS variables', () => {
|
||||
expect(
|
||||
parse({
|
||||
'--cP': 'red'
|
||||
})
|
||||
).toEqual('--cP:red;');
|
||||
expect(
|
||||
parse({
|
||||
'--c-P': 'red'
|
||||
})
|
||||
).toEqual('--c-P:red;');
|
||||
expect(
|
||||
parse({
|
||||
'--cp': 'red'
|
||||
})
|
||||
).toEqual('--cp:red;');
|
||||
expect(
|
||||
parse({
|
||||
':root': {
|
||||
'--cP': 'red'
|
||||
}
|
||||
})
|
||||
).toEqual(':root{--cP:red;}');
|
||||
});
|
||||
});
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { toHash } from '../to-hash';
|
||||
|
||||
describe('to-hash', () => {
|
||||
it('regression', () => {
|
||||
const res = toHash('goober');
|
||||
|
||||
expect(res).toEqual('go1990315141');
|
||||
expect(toHash('goober')).toEqual('go1990315141');
|
||||
});
|
||||
|
||||
it('collision', () => {
|
||||
const a = toHash('background:red;color:black;');
|
||||
const b = toHash('background:black;color:red;');
|
||||
|
||||
expect(a === b).toBeFalsy();
|
||||
});
|
||||
});
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import { update, extractCss } from '../update';
|
||||
import { getSheet } from '../get-sheet';
|
||||
|
||||
describe('update', () => {
|
||||
it('regression', () => {
|
||||
const t = { data: '' };
|
||||
|
||||
update('css', t);
|
||||
expect(t.data).toEqual('css');
|
||||
});
|
||||
|
||||
it('regression: duplicate', () => {
|
||||
const t = { data: '' };
|
||||
|
||||
update('css', t);
|
||||
update('foo', t);
|
||||
update('css', t);
|
||||
|
||||
expect(t.data).toEqual('cssfoo');
|
||||
});
|
||||
|
||||
it('regression: extract and flush', () => {
|
||||
update('filled', getSheet());
|
||||
expect(extractCss()).toEqual(' filled');
|
||||
expect(extractCss()).toEqual('');
|
||||
});
|
||||
|
||||
it('regression: extract and flush without DOM', () => {
|
||||
const bkp = global.self;
|
||||
delete global.self;
|
||||
update('filled', getSheet());
|
||||
expect(extractCss()).toEqual('filled');
|
||||
expect(extractCss()).toEqual('');
|
||||
global.self = bkp;
|
||||
});
|
||||
|
||||
it('regression: extract and flush from custom target', () => {
|
||||
const target = document.createElement('div');
|
||||
update('filled', getSheet());
|
||||
update('filledbody', getSheet(target));
|
||||
expect(extractCss(target)).toEqual(' filledbody');
|
||||
expect(extractCss(target)).toEqual('');
|
||||
});
|
||||
|
||||
it('regression: append or prepend', () => {
|
||||
extractCss();
|
||||
update('end', getSheet());
|
||||
update('start', getSheet(), true);
|
||||
expect(extractCss()).toEqual('startend');
|
||||
});
|
||||
|
||||
it('regression: global style replacement', () => {
|
||||
const t = { data: 'html, body { background-color: white; }' };
|
||||
|
||||
update(
|
||||
'html, body { background-color: black; }',
|
||||
t,
|
||||
undefined,
|
||||
'html, body { background-color: white; }'
|
||||
);
|
||||
|
||||
expect(t.data).toEqual('html, body { background-color: black; }');
|
||||
});
|
||||
});
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
let newRule = /(?:([\u0080-\uFFFF\w-%@]+) *:? *([^{;]+?);|([^;}{]*?) *{)|(}\s*)/g;
|
||||
let ruleClean = /\/\*[^]*?\*\/| +/g;
|
||||
let ruleNewline = /\n+/g;
|
||||
let empty = ' ';
|
||||
|
||||
/**
|
||||
* Convert a css style string into a object
|
||||
* @param {String} val
|
||||
* @returns {Object}
|
||||
*/
|
||||
export let astish = (val) => {
|
||||
let tree = [{}];
|
||||
let block, left;
|
||||
|
||||
while ((block = newRule.exec(val.replace(ruleClean, '')))) {
|
||||
// Remove the current entry
|
||||
if (block[4]) {
|
||||
tree.shift();
|
||||
} else if (block[3]) {
|
||||
left = block[3].replace(ruleNewline, empty).trim();
|
||||
tree.unshift((tree[0][left] = tree[0][left] || {}));
|
||||
} else {
|
||||
tree[0][block[1]] = block[2].replace(ruleNewline, empty).trim();
|
||||
}
|
||||
}
|
||||
|
||||
return tree[0];
|
||||
};
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { parse } from './parse';
|
||||
|
||||
/**
|
||||
* Can parse a compiled string, from a tagged template
|
||||
* @param {String} value
|
||||
* @param {Object} [props]
|
||||
*/
|
||||
export let compile = (str, defs, data) => {
|
||||
return str.reduce((out, next, i) => {
|
||||
let tail = defs[i];
|
||||
|
||||
// If this is a function we need to:
|
||||
if (tail && tail.call) {
|
||||
// 1. Call it with `data`
|
||||
let res = tail(data);
|
||||
|
||||
// 2. Grab the className
|
||||
let className = res && res.props && res.props.className;
|
||||
|
||||
// 3. If there's none, see if this is basically a
|
||||
// previously styled className by checking the prefix
|
||||
let end = className || (/^go/.test(res) && res);
|
||||
|
||||
if (end) {
|
||||
// If the `end` is defined means it's a className
|
||||
tail = '.' + end;
|
||||
} else if (res && typeof res == 'object') {
|
||||
// If `res` it's an object, we're either dealing with a vnode
|
||||
// or an object returned from a function interpolation
|
||||
tail = res.props ? '' : parse(res, '');
|
||||
} else {
|
||||
// Regular value returned. Can be falsy as well.
|
||||
// Here we check if this is strictly a boolean with false value
|
||||
// define it as `''` to be picked up as empty, otherwise use
|
||||
// res value
|
||||
tail = res === false ? '' : res;
|
||||
}
|
||||
}
|
||||
return out + next + (tail == null ? '' : tail);
|
||||
}, '');
|
||||
};
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
let GOOBER_ID = '_goober';
|
||||
let ssr = {
|
||||
data: ''
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the _commit_ target
|
||||
* @param {Object} [target]
|
||||
* @returns {HTMLStyleElement|{data: ''}}
|
||||
*/
|
||||
export let getSheet = (target) => {
|
||||
if (typeof window === 'object') {
|
||||
// Querying the existing target for a previously defined <style> tag
|
||||
// We're doing a querySelector because the <head> element doesn't implemented the getElementById api
|
||||
let el =
|
||||
(target ? target.querySelector('#' + GOOBER_ID) : window[GOOBER_ID]) ||
|
||||
Object.assign(document.createElement('style'), {
|
||||
innerHTML: ' ',
|
||||
id: GOOBER_ID
|
||||
});
|
||||
el.nonce = window.__nonce__;
|
||||
if (!el.parentNode) (target || document.head).appendChild(el);
|
||||
return el.firstChild;
|
||||
}
|
||||
|
||||
return target || ssr;
|
||||
};
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import { toHash } from './to-hash';
|
||||
import { update } from './update';
|
||||
import { astish } from './astish';
|
||||
import { parse } from './parse';
|
||||
|
||||
/**
|
||||
* In-memory cache.
|
||||
*/
|
||||
let cache = {};
|
||||
|
||||
/**
|
||||
* Stringifies a object structure
|
||||
* @param {Object} data
|
||||
* @returns {String}
|
||||
*/
|
||||
let stringify = (data) => {
|
||||
if (typeof data == 'object') {
|
||||
let out = '';
|
||||
for (let p in data) out += p + stringify(data[p]);
|
||||
return out;
|
||||
} else {
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates the needed className
|
||||
* @param {String|Object} compiled
|
||||
* @param {Object} sheet StyleSheet target
|
||||
* @param {Object} global Global flag
|
||||
* @param {Boolean} append Append or not
|
||||
* @param {Boolean} keyframes Keyframes mode. The input is the keyframes body that needs to be wrapped.
|
||||
* @returns {String}
|
||||
*/
|
||||
export let hash = (compiled, sheet, global, append, keyframes) => {
|
||||
// Get a string representation of the object or the value that is called 'compiled'
|
||||
let stringifiedCompiled = stringify(compiled);
|
||||
|
||||
// Retrieve the className from cache or hash it in place
|
||||
let className =
|
||||
cache[stringifiedCompiled] || (cache[stringifiedCompiled] = toHash(stringifiedCompiled));
|
||||
|
||||
// If there's no entry for the current className
|
||||
if (!cache[className]) {
|
||||
// Build the _ast_-ish structure if needed
|
||||
let ast = stringifiedCompiled !== compiled ? compiled : astish(compiled);
|
||||
|
||||
// Parse it
|
||||
cache[className] = parse(
|
||||
// For keyframes
|
||||
keyframes ? { ['@keyframes ' + className]: ast } : ast,
|
||||
global ? '' : '.' + className
|
||||
);
|
||||
}
|
||||
|
||||
// If the global flag is set, save the current stringified and compiled CSS to `cache.g`
|
||||
// to allow replacing styles in <style /> instead of appending them.
|
||||
// This is required for using `createGlobalStyles` with themes
|
||||
let cssToReplace = global && cache.g ? cache.g : null;
|
||||
if (global) cache.g = cache[className];
|
||||
|
||||
// add or update
|
||||
update(cache[className], sheet, append, cssToReplace);
|
||||
|
||||
// return hash
|
||||
return className;
|
||||
};
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Parses the object into css, scoped, blocks
|
||||
* @param {Object} obj
|
||||
* @param {String} selector
|
||||
* @param {String} wrapper
|
||||
*/
|
||||
export let parse = (obj, selector) => {
|
||||
let outer = '';
|
||||
let blocks = '';
|
||||
let current = '';
|
||||
|
||||
for (let key in obj) {
|
||||
let val = obj[key];
|
||||
|
||||
if (key[0] == '@') {
|
||||
// If these are the `@` rule
|
||||
if (key[1] == 'i') {
|
||||
// Handling the `@import`
|
||||
outer = key + ' ' + val + ';';
|
||||
} else if (key[1] == 'f') {
|
||||
// Handling the `@font-face` where the
|
||||
// block doesn't need the brackets wrapped
|
||||
blocks += parse(val, key);
|
||||
} else {
|
||||
// Regular at rule block
|
||||
blocks += key + '{' + parse(val, key[1] == 'k' ? '' : selector) + '}';
|
||||
}
|
||||
} else if (typeof val == 'object') {
|
||||
// Call the parse for this block
|
||||
blocks += parse(
|
||||
val,
|
||||
selector
|
||||
? // Go over the selector and replace the matching multiple selectors if any
|
||||
selector.replace(/([^,])+/g, (sel) => {
|
||||
// Return the current selector with the key matching multiple selectors if any
|
||||
return key.replace(/([^,]*:\S+\([^)]*\))|([^,])+/g, (k) => {
|
||||
// If the current `k`(key) has a nested selector replace it
|
||||
if (/&/.test(k)) return k.replace(/&/g, sel);
|
||||
|
||||
// If there's a current selector concat it
|
||||
return sel ? sel + ' ' + k : k;
|
||||
});
|
||||
})
|
||||
: key
|
||||
);
|
||||
} else if (val != undefined) {
|
||||
// Convert all but CSS variables
|
||||
key = /^--/.test(key) ? key : key.replace(/[A-Z]/g, '-$&').toLowerCase();
|
||||
// Push the line for this property
|
||||
current += parse.p
|
||||
? // We have a prefixer and we need to run this through that
|
||||
parse.p(key, val)
|
||||
: // Nope no prefixer just append it
|
||||
key + ':' + val + ';';
|
||||
}
|
||||
}
|
||||
|
||||
// If we have properties apply standard rule composition
|
||||
return outer + (selector && current ? selector + '{' + current + '}' : current) + blocks;
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Transforms the input into a className.
|
||||
* The multiplication constant 101 is selected to be a prime,
|
||||
* as is the initial value of 11.
|
||||
* The intermediate and final results are truncated into 32-bit
|
||||
* unsigned integers.
|
||||
* @param {String} str
|
||||
* @returns {String}
|
||||
*/
|
||||
export let toHash = (str) => {
|
||||
let i = 0,
|
||||
out = 11;
|
||||
while (i < str.length) out = (101 * out + str.charCodeAt(i++)) >>> 0;
|
||||
return 'go' + out;
|
||||
};
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { getSheet } from './get-sheet';
|
||||
/**
|
||||
* Extracts and wipes the cache
|
||||
* @returns {String}
|
||||
*/
|
||||
export let extractCss = (target) => {
|
||||
let sheet = getSheet(target);
|
||||
let out = sheet.data;
|
||||
sheet.data = '';
|
||||
return out;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the target and keeps a local cache
|
||||
* @param {String} css
|
||||
* @param {Object} sheet
|
||||
* @param {Boolean} append
|
||||
* @param {?String} cssToReplace
|
||||
*/
|
||||
export let update = (css, sheet, append, cssToReplace) => {
|
||||
cssToReplace
|
||||
? (sheet.data = sheet.data.replace(cssToReplace, css))
|
||||
: sheet.data.indexOf(css) === -1 &&
|
||||
(sheet.data = append ? css + sheet.data : sheet.data + css);
|
||||
};
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { hash } from './core/hash';
|
||||
import { compile } from './core/compile';
|
||||
import { getSheet } from './core/get-sheet';
|
||||
|
||||
/**
|
||||
* css entry
|
||||
* @param {String|Object|Function} val
|
||||
*/
|
||||
function css(val) {
|
||||
let ctx = this || {};
|
||||
let _val = val.call ? val(ctx.p) : val;
|
||||
|
||||
return hash(
|
||||
_val.unshift
|
||||
? _val.raw
|
||||
? // Tagged templates
|
||||
compile(_val, [].slice.call(arguments, 1), ctx.p)
|
||||
: // Regular arrays
|
||||
_val.reduce((o, i) => Object.assign(o, i && i.call ? i(ctx.p) : i), {})
|
||||
: _val,
|
||||
getSheet(ctx.target),
|
||||
ctx.g,
|
||||
ctx.o,
|
||||
ctx.k
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS Global function to declare global styles
|
||||
* @type {Function}
|
||||
*/
|
||||
let glob = css.bind({ g: 1 });
|
||||
|
||||
/**
|
||||
* `keyframes` function for defining animations
|
||||
* @type {Function}
|
||||
*/
|
||||
let keyframes = css.bind({ k: 1 });
|
||||
|
||||
export { css, glob, keyframes };
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export { styled, setup } from './styled';
|
||||
export { extractCss } from './core/update';
|
||||
export { css, glob, keyframes } from './css';
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import { css } from './css';
|
||||
import { parse } from './core/parse';
|
||||
|
||||
let h, useTheme, fwdProp;
|
||||
function setup(pragma, prefix, theme, forwardProps) {
|
||||
// This one needs to stay in here, so we won't have cyclic dependencies
|
||||
parse.p = prefix;
|
||||
|
||||
// These are scope to this context
|
||||
h = pragma;
|
||||
useTheme = theme;
|
||||
fwdProp = forwardProps;
|
||||
}
|
||||
|
||||
/**
|
||||
* styled function
|
||||
* @param {string} tag
|
||||
* @param {function} forwardRef
|
||||
*/
|
||||
function styled(tag, forwardRef) {
|
||||
let _ctx = this || {};
|
||||
|
||||
return function wrapper() {
|
||||
let _args = arguments;
|
||||
|
||||
function Styled(props, ref) {
|
||||
// Grab a shallow copy of the props
|
||||
let _props = Object.assign({}, props);
|
||||
|
||||
// Keep a local reference to the previous className
|
||||
let _previousClassName = _props.className || Styled.className;
|
||||
|
||||
// _ctx.p: is the props sent to the context
|
||||
_ctx.p = Object.assign({ theme: useTheme && useTheme() }, _props);
|
||||
|
||||
// Set a flag if the current components had a previous className
|
||||
// similar to goober. This is the append/prepend flag
|
||||
// The _empty_ space compresses better than `\s`
|
||||
_ctx.o = / *go\d+/.test(_previousClassName);
|
||||
|
||||
_props.className =
|
||||
// Define the new className
|
||||
css.apply(_ctx, _args) + (_previousClassName ? ' ' + _previousClassName : '');
|
||||
|
||||
// If the forwardRef fun is defined we have the ref
|
||||
if (forwardRef) {
|
||||
_props.ref = ref;
|
||||
}
|
||||
|
||||
// Assign the _as with the provided `tag` value
|
||||
let _as = tag;
|
||||
|
||||
// If this is a string -- checking that is has a first valid char
|
||||
if (tag[0]) {
|
||||
// Try to assign the _as with the given _as value if any
|
||||
_as = _props.as || tag;
|
||||
// And remove it
|
||||
delete _props.as;
|
||||
}
|
||||
|
||||
// Handle the forward props filter if defined and _as is a string
|
||||
if (fwdProp && _as[0]) {
|
||||
fwdProp(_props);
|
||||
}
|
||||
|
||||
return h(_as, _props);
|
||||
}
|
||||
|
||||
return forwardRef ? forwardRef(Styled) : Styled;
|
||||
};
|
||||
}
|
||||
|
||||
export { styled, setup };
|
||||
Reference in New Issue
Block a user