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

Implements nesting in template string parser #1103

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
6 changes: 5 additions & 1 deletion changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
### Bug fixes

- [jss-plugin-expand] Fix attributes spread for `border-bottom`, `border-top`, `border-left` and `border-right` ([#1083](https://github.com/cssinjs/jss/pull/1083))
- [jss-plugin-props-sort] Fix sorting in Node 11 ([#1084](https://github.com/cssinjs/jss/pull/1083))
- [jss-plugin-props-sort] Fix sorting in Node 11 ([#1085](https://github.com/cssinjs/jss/pull/1085))
- [jss] Fix escaping keyframes names ([#1100](https://github.com/cssinjs/jss/pull/1100))

### Improvements

- [jss-plugin-template] Add nesting support ([#1103](https://github.com/cssinjs/jss/pull/1103))

## 10.0.0-alpha.16 (2019-3-24)

### Bug fixes
Expand Down
27 changes: 23 additions & 4 deletions docs/jss-plugin-template.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
## Enables string templates

Allows you to use string templates to declare CSS rules. It implements a **very naive** but **very fast (~42000 ops/sec)** runtime CSS parser, with certain limitations:
This parser is not meant to be a complete one but to enable authoring styles using a template string with nesting syntax support, fastest parse performance and small footprint.

- Supports only rule body (no selectors)
- Requires semicolon and a new line after the value (except the last line)
- No nested rules support
Design of this parser has two main principles:

1. It does not parse entire CSS. It uses only specific markers to separate selectors from props and values.
1. It uses warnings to make sure expected syntax is used instead of supporting the full syntax.

To do that it requires some constraints:

- Parser expects a new line after each declaration (`color: red;\n`).
- Parser expects an ampersand, selector and opening curly brace for nesting syntax on a single line (`& selector {`).
- Parser expects a closing curly brace on a separate line.

```js
const styles = {
Expand All @@ -14,6 +21,9 @@ const styles = {
color: red;
margin: 20px 40px;
padding: 10px;
&:hover span {
color: green;
}
`,
'@media print': {
button: `color: black`
Expand All @@ -24,3 +34,12 @@ const styles = {
}
}
```

### Benchmark

```
Chrome 74.0.3729 (Mac OS X 10.14.3) Parse: parse() at 122983 ops/sec
Chrome 74.0.3729 (Mac OS X 10.14.3) Parse: stylis() at 47582 ops/sec
Chrome 74.0.3729 (Mac OS X 10.14.3)
Parse: parse() at 122983 ops/sec (2.58x faster than stylis())
```
2 changes: 1 addition & 1 deletion karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ module.exports = config => {
frameworks: ['benchmark'],
// Using a fixed position for a file name, m.b. should use an args parser later.
files: [process.argv[4] || 'packages/jss/benchmark/**/*.js'],
preprocessors: {'packages/jss/benchmark/**/*.js': ['webpack']},
preprocessors: {'packages/**/benchmark/**/*.js': ['webpack']},
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was broken

reporters: ['benchmark'],
// Some tests are slow.
browserNoActivityTimeout: 20000
Expand Down
24 changes: 12 additions & 12 deletions packages/jss-plugin-template/.size-snapshot.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
{
"dist/jss-plugin-template.js": {
"bundled": 1777,
"minified": 730,
"gzipped": 453
"bundled": 3694,
"minified": 1128,
"gzipped": 603
},
"dist/jss-plugin-template.min.js": {
"bundled": 1418,
"minified": 564,
"gzipped": 355
"bundled": 3066,
"minified": 820,
"gzipped": 474
},
"dist/jss-plugin-template.cjs.js": {
"bundled": 1341,
"minified": 686,
"gzipped": 423
"bundled": 3289,
"minified": 1194,
"gzipped": 579
},
"dist/jss-plugin-template.esm.js": {
"bundled": 1123,
"minified": 518,
"gzipped": 341,
"bundled": 3066,
"minified": 1020,
"gzipped": 501,
"treeshaked": {
"rollup": {
"code": 21,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import template from '../../src/index'
import parse from '../../src/parse'

const options = {Renderer: null}
const jss = create(options).use(template())
const jss = create(options).use(template({cache: false}))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

disabling cache here to have a realistic comparison without caching, I didn't document it, because I don't think user should need it, so for now it's a private one, just for tests


const css = `
color: rgb(77, 77, 77);
Expand Down
20 changes: 19 additions & 1 deletion packages/jss-plugin-template/benchmark/tests/parse.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import parse from '../../src/parse'
import stylis from 'stylis'
import parse, {parse2} from '../../src/parse'

const css = `
color: rgb(77, 77, 77);
Expand Down Expand Up @@ -37,8 +38,25 @@ const css = `
font-variant: normal normal;
border-spacing: 0px;
`

stylis.set({
global: false,
keyframe: false,
prefix: false,
compress: false,
semicolon: true
})

suite('Parse', () => {
benchmark('parse()', () => {
parse(css)
})

benchmark('parse2()', () => {
parse2(css)
})

benchmark('stylis()', () => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using stylis parser just to see a comparable other value in perspective

stylis('#id', css)
})
})
4 changes: 4 additions & 0 deletions packages/jss-plugin-template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,9 @@
"@babel/runtime": "^7.3.1",
"jss": "10.0.0-alpha.16",
"tiny-warning": "^1.0.2"
},
"devDependencies": {
"jss-plugin-nested": "^10.0.0-alpha.16",
"stylis": "^3.5.4"
}
}
28 changes: 21 additions & 7 deletions packages/jss-plugin-template/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,27 @@
import {type Plugin} from 'jss'
import parse from './parse'

const onProcessRule = rule => {
if (typeof rule.style === 'string') {
// $FlowFixMe: We can safely assume that rule has the style property
rule.style = parse(rule.style)
export const cache = {}

type Options = {|cache: boolean|}

export default function templatePlugin(options: Options = {cache: true}): Plugin {
const onProcessStyle = style => {
if (typeof style !== 'string') {
return style
}

if (style in cache) {
return cache[style]
HenriBeck marked this conversation as resolved.
Show resolved Hide resolved
}

if (options.cache) {
cache[style] = parse(style)
return cache[style]
}

return parse(style)
}
}

export default function templatePlugin(): Plugin {
return {onProcessRule}
return {onProcessStyle}
}
114 changes: 22 additions & 92 deletions packages/jss-plugin-template/src/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,118 +3,49 @@
import expect from 'expect.js'
import {stripIndent} from 'common-tags'
import {create} from 'jss'
import sinon from 'sinon'
import template from '.'
import nested from 'jss-plugin-nested'
import template, {cache} from '.'

const settings = {
createGenerateId: () => rule => `${rule.key}-id`
}

describe('jss-plugin-template', () => {
let spy
let jss

beforeEach(() => {
spy = sinon.spy(console, 'warn')
jss = create(settings).use(template())
jss = create(settings).use(template(), nested())
})

afterEach(() => {
console.warn.restore()
it('should cache parsed template', () => {
const a = `color: red`
jss.createStyleSheet({a})
expect(cache[a]).to.eql({color: 'red'})
})

describe('template literals', () => {
it('should convert a single single property/value', () => {
const sheet = jss.createStyleSheet({
a: `
color: red;
`
})
expect(sheet.toString()).to.be(stripIndent`
.a-id {
color: red;
}
`)
})

it('should parse multiple props/values', () => {
const sheet = jss.createStyleSheet({
a: `
color: red;
float: left;
`
})
expect(sheet.toString()).to.be(stripIndent`
.a-id {
color: red;
float: left;
}
`)
expect(spy.callCount).to.be(0)
})

it('should warn when there is no colon found', () => {
jss.createStyleSheet({
a: 'color red;'
})

expect(spy.callCount).to.be(1)
expect(spy.calledWithExactly('Warning: [JSS] Malformed CSS string "color red;"')).to.be(true)
})

it('should strip spaces', () => {
const sheet = jss.createStyleSheet({
a: `
color: red ;
float: left ;
`
})
expect(sheet.toString()).to.be(stripIndent`
.a-id {
color: red;
float: left;
}
`)
})

it('should allow skiping last semicolon', () => {
const sheet = jss.createStyleSheet({
a: `
color: red;
float: left
`
})
expect(sheet.toString()).to.be(stripIndent`
.a-id {
color: red;
float: left;
}
`)
it('should support @media', () => {
const sheet = jss.createStyleSheet({
'@media print': {
button: 'color: black;'
}
})

it('should support @media', () => {
const sheet = jss.createStyleSheet({
'@media print': {
button: 'color: black'
}
})
expect(sheet.toString()).to.be(stripIndent`
expect(sheet.toString()).to.be(stripIndent`
@media print {
.button-id {
color: black;
}
}
`)
})
})

it('should support @keyframes', () => {
const sheet = jss.createStyleSheet({
'@keyframes a': {
from: 'opacity: 0',
to: 'opacity: 1'
}
})
expect(sheet.toString()).to.be(stripIndent`
it('should support @keyframes', () => {
const sheet = jss.createStyleSheet({
'@keyframes a': {
from: 'opacity: 0;',
to: 'opacity: 1;'
}
})
expect(sheet.toString()).to.be(stripIndent`
@keyframes keyframes-a-id {
from {
opacity: 0;
Expand All @@ -124,6 +55,5 @@ describe('jss-plugin-template', () => {
}
}
`)
})
})
})
Loading