├── .gitignore
├── LICENSE
├── Micro CSS.textmate
├── README.md
├── bin
└── mcss.js
├── example
├── test.css
└── test.mcss
├── h.js
├── index.js
├── lib
└── tokenizer.js
├── package-lock.json
├── package.json
├── query.js
└── test
├── h.test.js
├── micro-css.test.js
├── query.test.js
└── tokenizer.test.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .nyc_output
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Matt McKegg
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Micro CSS.textmate:
--------------------------------------------------------------------------------
1 | { scopeName = 'source.mcss';
2 | comment = '';
3 | fileTypes = ( 'mcss' );
4 | foldingStartMarker = '\{\s*$';
5 | foldingStopMarker = '^\s*\}';
6 | patterns = (
7 |
8 | { match = '([A-Z][A-Za-z\.]+)( )?{';
9 | captures = { 1 = { name = 'variable.mcss'; }; };
10 | },
11 | { match = '(\-[a-z][A-Za-z]+)( )?{';
12 | captures = { 1 = { name = 'support.mcss'; }; };
13 | },
14 | { match = '(_[a-z][A-Za-z]+)( )?{';
15 | captures = { 1 = { name = 'string.mcss'; }; };
16 | },
17 | { match = '([a-z,1-6 ]+[a-z1-6])( )?{';
18 | captures = {
19 | 1 = { name = 'storage.mcss'; };
20 | };
21 | },
22 | { match = '([a-z1-6]+)(\.[a-zA-Z]+)?( )?{';
23 | captures = {
24 | 1 = { name = 'storage.mcss'; };
25 | 2 = { name = 'support.mcss'; };
26 | };
27 | },
28 | { begin = '([a-zA-Z\-]+)(:)';
29 | end = '(;)|\n';
30 | captures = {
31 | 1 = { name = 'keyword.mcss'; };
32 | 2 = { name = 'keyword.operator.mcss'; };
33 | };
34 | patterns = (
35 | { match = "('.+')";
36 | name = 'string.mcss';
37 | },
38 | { match = "[a-zA-Z]+\(|\)";
39 | name = 'storage.mcss';
40 | },
41 | );
42 | name = 'constant.mcss';
43 | },
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Micro CSS
2 |
3 | A CSS preprocessor that provides a simplified object orientated approach to css. The syntax is very similar to CSS but encourages better reuse of classes and discourages high [specificity](http://www.htmldog.com/guides/cssadvanced/specificity/).
4 |
5 | [](https://nodei.co/npm/micro-css/)
6 |
7 | ## BREAKING CHANGES IN v1.0
8 |
9 | - element classes are no longer prefixed with `//.` in generated output
10 | - mixins are now prefixed with `_` instead of `$` to avoid escaping in generated output.
11 |
12 | ## BREAKING CHANGES IN v2.0
13 |
14 | - mixins are back to a `$` prefix and are no longer exported to generated css (can only be used by other styles)
15 |
16 | ## A bit of background
17 |
18 | I think there comes a time in any front-end web developer's life when CSS stops being "the most amazing powerful design language on earth" and changes to become the skeletons in our closet that we prefer not to think about too much. This was certainly my experience.
19 |
20 | CSS is undoubtedly powerful, but it's too powerful. It encourages you to do things that seem efficient and clever at the time but eventually turn in to absolute maintenance nightmares.
21 |
22 | One night I couldn't sleep, I was thinking about the new redesign and how many things would start to break or have to be hacked around to do the new CSS. I realized that I would probably have to completely start again with the CSS on the page. But how should I structure it so that this wouldn't happen again?
23 |
24 | I got reading on the internets, trying to find better ways to do it - preprocessors, frameworks, etc. I'd used SASS/SCSS in the past, and while the nesting functions were nice, it was still far too easy to make a mess. I came across [Stubbornella's](http://www.stubbornella.org/) [Object Oriented CSS project](http://oocss.org/). I really liked some of the ideas and concepts. Things like writing for reuse and avoiding specificity. For me OOCSS it wasn't the answer though, it just didn't click for me, so I started to think about how I could twist CSS to be less unwieldy.
25 |
26 | I wanted to be able to define objects, but not have to worry about where those objects were on the page. I wanted those objects to have multiple elements but not have those styles spill into other objects. I wanted to be able to add classes to objects as tags/flags - only applying if the object were a particular type. I wanted to be able to create mixins that could be applied to multiple elements, but not used on their own. CSS can do all of these things fairly easy, but it just doesn't encourage it.
27 |
28 | So this was the point I realized I was going to have to come up with my own CSS subset that forced these things, but made them far easier to do, and much more manageable/readable. One of my design goals was to make the class attributes in the HTML very easy to understand, using the single class field for assigning object type, meta data flags, and mixins.
29 |
30 | ## Example
31 |
32 | Here's some HTML we want to style:
33 |
34 | ```html
35 |
36 |
37 | HTML5 Rules, but css is still just css...
38 |
39 |
40 |
45 |
46 |
47 |
48 |
49 | Post Title
50 | Subtitle
51 |
52 |
53 |
54 |
57 |
58 |
59 |
60 |
61 | ```
62 |
63 | And some MCSS to do the styling:
64 |
65 | ```scss
66 | Sidebar {
67 | header {
68 | font-weight: bold
69 | color: white
70 | background: #363
71 | }
72 | section {
73 | header {
74 |
75 | }
76 | footer {
77 |
78 | }
79 | }
80 | footer {
81 |
82 | }
83 | }
84 |
85 | Page {
86 | header {
87 |
88 | }
89 | nav {
90 |
91 | }
92 | section {
93 |
94 | }
95 | footer {
96 |
97 | }
98 | }
99 |
100 | Comment {
101 |
102 | }
103 | ```
104 |
105 | ## It's almost a schema for your HTML
106 |
107 | You'll notice that MCSS provides an easy way to view an overall structure of your page. In fact I almost always write the MCSS first to figure out how the page will be laid out then write the HTML to match.
108 |
109 | ## Installation
110 |
111 | ```shell
112 | $ npm install micro-css -g
113 | ```
114 |
115 | ## Usage
116 |
117 | When installed globally execute:
118 |
119 | ```shell
120 | $ mcss filepaths... -o outfile.css
121 | ```
122 |
123 | Or it can be used by API in Node.js:
124 |
125 | ```js
126 | var microCss = require('micro-css')
127 | var finalCss = microCss("Item { color: red } body { font: 80% sans-serif }") // read from file or hardcoded like this
128 | ```
129 |
130 | ## Features
131 |
132 | ### Optional Semicolons
133 |
134 | Because semicolons always annoyed me...
135 |
136 | ### Object classes start with uppercase
137 |
138 | The following MCSS:
139 |
140 | ```scss
141 | Item {
142 | border: 1px solid gray;
143 | background: silver;
144 | }
145 | ```
146 |
147 | Becomes:
148 |
149 | ```css
150 | .Item {
151 | border: 1px solid gray;
152 | background: silver;
153 | }
154 | ```
155 |
156 | ### Good old fashioned element selectors are there - just like normal css
157 |
158 | As long as it starts with a lower case letter... otherwise it'll see it as an Object class.
159 |
160 | ### Nested Rules always use `>`
161 |
162 | ```scss
163 | Item {
164 | h1 {
165 | font-weight: normal
166 | }
167 | p {
168 | margin: 4px 0px
169 | }
170 | }
171 | ```
172 |
173 | Becomes:
174 |
175 | ```css
176 | .Item > h1 {
177 | font-weight: normal
178 | }
179 | .Item > p {
180 | margin: 4px 0px
181 | }
182 | ```
183 |
184 | Makes it not so IE6 friendly, but it's so worth it (then again IE6 doesn't even support multiple classes!)
185 |
186 | ### If you really have to, you can still opt-out of '>'
187 |
188 | ```scss
189 | Item {
190 | (strong) {
191 | font-weight: bold
192 | color: #333
193 | }
194 | }
195 | ```
196 |
197 | Becomes:
198 |
199 | ```css
200 | .Item strong {
201 | font-weight: bold;
202 | color: #333;
203 | }
204 | ```
205 |
206 | ### Add flags to specific types of objects
207 |
208 | ```scss
209 | Item {
210 | color: black
211 | -special {
212 | color: red
213 | }
214 | }
215 | AnotherItem {
216 | color: black
217 | }
218 | ```
219 |
220 | Becomes:
221 |
222 | ```css
223 | .Item {
224 | color: black;
225 | }
226 | .Item.-special {
227 | color: red;
228 | }
229 | .AnotherItem {
230 | color: black;
231 | }
232 | ```
233 |
234 | ```html
235 |
236 | back text
237 |
238 |
239 | red text
240 |
241 |
242 | still black text as '-special' is not defined for 'AnotherItem'
243 |
244 | ```
245 |
246 | So I can use the `-special` flag wherever I like and not worry about stepping on another namespace.
247 |
248 | ### Specify multiple possibilities [OR]...
249 |
250 | ```scss
251 | Item {
252 | -unknown, -disabled{
253 | color:gray
254 | }
255 | -disabled {
256 | opacity:0.5
257 | }
258 | }
259 | h1, h2, h3, h4 {
260 | font-weight: normal
261 | }
262 | ```
263 |
264 | Becomes:
265 |
266 | ```css
267 | .Item.-unknown {
268 | color: gray;
269 | }
270 | .Item.-disabled {
271 | color: gray;
272 | opacity: 0.5;
273 | }
274 | h1, h2, h3, h4 { /* this one is left unchanged - one of the few parts that work exactly like standard css */
275 | font-weight: normal;
276 | }
277 | ```
278 |
279 | ### ... or multiple requirements [AND]
280 |
281 | ```scss
282 | Listing {
283 | -featured {
284 | h1 {
285 | color: orange
286 | }
287 | }
288 | -sold {
289 | h1 {
290 | color: red
291 | }
292 | }
293 | -featured -sold {
294 | h1 {
295 | color: green
296 | }
297 | opacity: 0.5
298 | }
299 | }
300 | ```
301 |
302 | Becomes:
303 |
304 | ```css
305 | .Listing.-featured > h1{
306 | color: orange;
307 | }
308 | .Listing.-sold > h1{
309 | color: red;
310 | }
311 | .Listing.-featured.-sold > h1{
312 | color: green;
313 | }
314 | .Listing.-featured.-sold{
315 | opacity: 0.5;
316 | }
317 | ```
318 |
319 | ```html
320 |
321 |
This text will be orange
322 |
323 |
324 |
This text will be red
325 |
326 |
327 |
This text will be green
328 | And this div will be transparent
329 |
330 | ```
331 |
332 |
333 | ### Mixins for reusing a particular style... when Object classes aren't enough
334 |
335 | A way to reuse styles in multiple places.
336 |
337 | ```scss
338 | $fancyThing {
339 | box-shadow: 10px 10px silver
340 | div {
341 | font-size: 90%
342 | }
343 | }
344 |
345 | Item {
346 | $fancyThing
347 |
348 | border: solid 1px gray
349 | background-color: fuchsia
350 | }
351 | ```
352 |
353 | Becomes:
354 |
355 | ```css
356 | .Item {
357 | border: solid 1px gray
358 | background-color: fuchsia
359 | }
360 | .Item {
361 | box-shadow: 10px 10px silver;
362 | }
363 | .Item > div {
364 | font-size: 90%;
365 | }
366 | ```
367 |
368 | ### Element classes - basically how most people currently use classes
369 |
370 | Except severely limited so you can't hurt yourself.
371 |
372 | ```scss
373 | Item {
374 | color: black
375 | div.main {
376 | font-weight:bold
377 | }
378 | div.extra {
379 | color: gray
380 | }
381 | }
382 | ```
383 |
384 | Becomes
385 |
386 | ```css
387 | .Item {
388 | color: black;
389 | }
390 | .Item > div.main {
391 | font-weight:bold
392 | }
393 | .Item > div.extra {
394 | color: gray
395 | }
396 | ```
397 |
398 | And we can use it like this in our HTML:
399 |
400 | ```html
401 |
402 |
403 | I am some main text
404 |
405 |
406 | I am a standard div
407 |
408 |
411 |
412 | ```
413 |
414 | They can only be used with an element selector, and never on their own. And generally should be avoided if they have a better pure element alternative.
415 |
416 | ### Inline SVG
417 |
418 | ```scss
419 | @svg test {
420 | width: 20px
421 | height: 20px
422 | content: " "
423 |
424 | path {
425 | stroke: #CCC
426 | stroke-width: 3
427 | fill: none
428 | }
429 | }
430 |
431 | Item {
432 | background-image: svg(test)
433 | }
434 | ```
435 |
436 | Becomes
437 |
438 | ```css
439 | Item {
440 | background-image: url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bW...)
441 | }
442 | ```
443 |
444 | The svg is automatically inlined as a data url.
445 |
446 | ### CSS Entities
447 |
448 | ```scss
449 | @keyframes animationName {
450 | from { background-color: red }
451 | 50% { background-color: green }
452 | to { background-color: blue }
453 | }
454 | ```
455 |
456 | Becomes
457 |
458 | ```css
459 | @keyframes animationName {
460 | from { background-color: red; }
461 | 50% { background-color: green; }
462 | to { background-color: blue; }
463 | }
464 | ```
465 |
466 | ## License
467 |
468 | MIT
--------------------------------------------------------------------------------
/bin/mcss.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var microCss = require('../')
4 | var fs = require('fs')
5 |
6 | var argv = require('optimist').usage('Usage: $0 [entry files] {OPTIONS}').wrap(80).option('outfile', {
7 | alias: 'o',
8 | desc: 'Write the generated css to this file.\nIf unspecified, prints to stdout.'
9 | }).argv
10 |
11 | var data = ''
12 | argv._.forEach(function (path) {
13 | data += fs.readFileSync(path, 'utf8') + '\n'
14 | })
15 |
16 | var css = microCss(data)
17 |
18 | if (argv.outfile) {
19 | var maxDate = null
20 | argv._.forEach(function (path) {
21 | var date = fs.statSync(path).mtime
22 | if (!maxDate || date > maxDate) {
23 | maxDate = date
24 | }
25 | })
26 |
27 | fs.writeFileSync(argv.outfile, css)
28 |
29 | if (maxDate) {
30 | fs.utimesSync(argv.outfile, maxDate, maxDate)
31 | }
32 | } else {
33 | console.log(css)
34 | }
35 |
--------------------------------------------------------------------------------
/example/test.css:
--------------------------------------------------------------------------------
1 | .Document {
2 | background-color: silver;
3 | color: gray;
4 | }
5 | .Document.-main {
6 | padding: 30px;
7 | }
8 | .Document.-main > heading {
9 | border-bottom: 1px solid gray;
10 | background-color: silver;
11 | }
12 |
13 | .Document.-main > heading > h1 {
14 | color: black;
15 | }
16 |
17 | .Cat {
18 | color: black;
19 | }
20 |
21 | .\$car {
22 | background-image: url('car.png');
23 | }
--------------------------------------------------------------------------------
/example/test.mcss:
--------------------------------------------------------------------------------
1 | Document {
2 | background-color: silver
3 | color:gray
4 |
5 | -main {
6 | padding:30px
7 |
8 | heading {
9 | border-bottom: 1px solid gray
10 | background-color: silver
11 | h1 {
12 | color: black
13 | }
14 | }
15 |
16 | }
17 |
18 | }
19 |
20 | $car {
21 | background-image: url('car.png')
22 | }
23 |
24 | Cat {
25 | color: black;
26 | }
--------------------------------------------------------------------------------
/h.js:
--------------------------------------------------------------------------------
1 | var tagMatcher = /^(\.?[a-z0-9]+)/
2 |
3 | // pass a hyperscript ctor to wrap with mcss parsing
4 | module.exports = function (h) {
5 | return function (tag, props, children) {
6 | if (!children && props && isChildren(props)) {
7 | children = props
8 | props = null
9 | }
10 |
11 | var parts = tag.split(' ')
12 | var tagName = parts.filter(isTagName)[0] || 'div'
13 | var classes = parts.filter(isNotTagName).join(' ')
14 |
15 | if (classes) {
16 | props = props || {}
17 | if (props.className) {
18 | props.className = classes + ' ' + props.className
19 | } else {
20 | props.className = classes
21 | }
22 | }
23 |
24 | return h(tagName, props, children)
25 | }
26 | }
27 |
28 | function isChildren (object) {
29 | return Array.isArray(object) || !(object instanceof Object) || Object.getPrototypeOf(object) !== Object.prototype
30 | }
31 |
32 | function isTagName (name) {
33 | return !!tagMatcher.exec(name)
34 | }
35 |
36 | function isNotTagName (name) {
37 | return !isTagName(name)
38 | }
39 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | var tokenizer = require('./lib/tokenizer')
2 | var query = require('./query')
3 |
4 | module.exports = function (text) {
5 | var style = tokenizer(text)
6 | var result = getRules(style)
7 |
8 | if (style.entities) {
9 | eachGroup(style.entities, function (name, innerStyle) {
10 | if (!/^@svg /.exec(name)) {
11 | result += getEntityCss(name, innerStyle) + '\n'
12 | }
13 | })
14 | }
15 |
16 | return result
17 | }
18 |
19 | module.exports.query = query
20 |
21 | function addParent (style, parent) {
22 | if (style && style !== parent) {
23 | style.parent = parent
24 | }
25 | }
26 |
27 | function getRules (style, prepend) {
28 | var result = ''
29 |
30 | if (style.objects) {
31 | eachGroup(style.objects, function (name, innerStyle) {
32 | addParent(innerStyle, style)
33 | var selector = getSelector(name)
34 | result += getCssForSelector(selector, innerStyle)
35 | })
36 | }
37 |
38 | if (style.flags) {
39 | eachGroup(style.flags, function (name, innerStyle) {
40 | addParent(innerStyle, style)
41 | name.split(',').forEach(function (n) {
42 | var selector = getSelector(n.trim(), prepend)
43 | result += getCssForSelector(selector, innerStyle)
44 | })
45 | })
46 | }
47 |
48 | if (style.pseudos) {
49 | eachGroup(style.pseudos, function (name, innerStyle) {
50 | addParent(innerStyle, style)
51 | name.split(',').forEach(function (n) {
52 | var selector = getPseudoSelector(n.trim(), prepend)
53 | result += getCssForSelector(selector, innerStyle)
54 | })
55 | })
56 | }
57 |
58 | if (style.elements) {
59 | eachGroup(style.elements, function (name, innerStyle) {
60 | addParent(innerStyle, style)
61 |
62 | var selectors = []
63 | var subItems = ''
64 |
65 | name.split(',').forEach(function (n) {
66 | var parts = n.trim().split('.')
67 | var selector = getElementSelector(parts[0], parts[1], prepend, innerStyle.deep)
68 |
69 | subItems += getRules(innerStyle, selector)
70 | selectors.push(selector)
71 | })
72 |
73 | result += getCssForSelector(selectors.join(', '), innerStyle, subItems)
74 | })
75 | }
76 |
77 | return result
78 | }
79 |
80 | function getCssForSelector (selector, innerStyle, overrideSubItems) {
81 | var result = ''
82 | if (innerStyle.extensions) {
83 | result += getExtensions(selector, innerStyle)
84 | }
85 | if (innerStyle.rules) {
86 | result += getCssBlock(selector, innerStyle)
87 | }
88 | if (overrideSubItems == null) {
89 | result += getRules(innerStyle, selector)
90 | } else {
91 | result += overrideSubItems
92 | }
93 | return result
94 | }
95 |
96 | function getEntityCss (name, style) {
97 | var result = name + ' { '
98 |
99 | if (style.rules) {
100 | eachGroup(style.rules, function (name, value) {
101 | result += name + ': ' + handleValue(value, style) + '; '
102 | })
103 | }
104 |
105 | if (style.elements) {
106 | eachGroup(style.elements, function (name, value) {
107 | result += getEntityCss(name, value) + ' '
108 | })
109 | }
110 |
111 | result += '}'
112 |
113 | return result
114 | }
115 |
116 | function getExtensions (selector, style) {
117 | var result = ''
118 | if (style.extensions) {
119 | style.extensions.forEach(function (name) {
120 | var innerStyle = find('mixins', style, name)
121 | addParent(innerStyle, style)
122 | if (innerStyle) {
123 | selector.split(',').forEach(function (part) {
124 | result += getCssForSelector(part.trim(), innerStyle)
125 | })
126 | }
127 | })
128 | }
129 | return result
130 | }
131 |
132 | function find (key, style, extensionName) {
133 | var result = null
134 | while (style && !result) {
135 | if (style[key] && style[key][extensionName]) {
136 | result = style[key][extensionName]
137 | }
138 | style = style.parent
139 | }
140 | return result
141 | }
142 |
143 | function getCssBlock (selector, style) {
144 | var result = selector + ' { '
145 | eachGroup(style.rules, function (name, value) {
146 | result += name + ': ' + handleValue(value, style) + '; '
147 | })
148 | return result + '}\n'
149 | }
150 |
151 | function handleValue (value, style) {
152 | return value.replace(/(\W|^)(svg)\((.+)\)(\W|$)/g, function (match, prefix, type, name, suffix) {
153 | if (type === 'svg') {
154 | var url = getSvgDataUrl(name, style)
155 | return prefix + 'url("' + url + '")' + suffix
156 | } else {
157 | return ' '
158 | }
159 | })
160 | }
161 |
162 | function getSvgDataUrl (name, style) {
163 | var parts = name.split(' ')
164 | style = find('entities', style, '@svg ' + parts[0])
165 | if (style) {
166 | var innerStyles = getRules(style)
167 | var svg = getSvgBlock(style.rules, innerStyles, parts.slice(1))
168 |
169 | var encoded = Buffer.from(svg).toString('base64')
170 | return 'data:image/svg+xml;charset=utf-8;base64,' + encoded
171 | }
172 | }
173 |
174 | function getSvgBlock (attributes, styles, classes) {
175 | var result = ''
190 |
191 | result += ' '
192 | result += content
193 | result += ' '
194 |
195 | return result
196 | }
197 |
198 | function getSelector (name, prepend) {
199 | var selector = ''
200 | name.split(' ').forEach(function (n) {
201 | if (n) {
202 | selector += '.' + escapeClass(n)
203 | }
204 | })
205 | if (prepend) {
206 | selector = prepend + selector
207 | }
208 | return selector
209 | }
210 |
211 | function getPseudoSelector (name, prepend) {
212 | var selector = ''
213 | name.split(' ').forEach(function (n) {
214 | if (n) {
215 | selector += n
216 | }
217 | })
218 | if (prepend) {
219 | selector = prepend + selector
220 | }
221 | return selector
222 | }
223 |
224 | function getElementSelector (name, filter, prepend, isDeep) {
225 | var selector = escapeClass(name)
226 |
227 | if (filter) {
228 | selector += '.' + escapeClass(filter)
229 | }
230 |
231 | if (prepend) {
232 | if (isDeep) {
233 | selector = prepend + ' ' + selector
234 | } else {
235 | selector = prepend + ' > ' + selector
236 | }
237 | }
238 |
239 | return selector
240 | }
241 |
242 | function escapeClass (name) {
243 | return name.replace(/([$.])/g, '\\$1')
244 | }
245 |
246 | function eachGroup (groups, iterator) {
247 | Object.keys(groups).forEach(function (key) {
248 | iterator(key, groups[key])
249 | })
250 | }
251 |
--------------------------------------------------------------------------------
/lib/tokenizer.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-redeclare */
2 |
3 | module.exports = function (text) {
4 | var rootStyles = {}
5 | eachToken(text, rootHandler, rootStyles)
6 | return rootStyles
7 | }
8 |
9 | function rootHandler (token, target) {
10 | if (isObject(token.group)) {
11 | var styles = define('objects', token.group, target)
12 | eachToken(token.inner, objectHandler, styles)
13 | }
14 |
15 | if (isElement(token.group)) {
16 | var styles = define('elements', token.group, target)
17 | eachToken(token.inner, objectHandler, styles)
18 | }
19 |
20 | if (isMixin(token.group)) {
21 | var styles = define('mixins', token.group, target)
22 | eachToken(token.inner, objectHandler, styles)
23 | }
24 |
25 | if (isEntity(token.group)) {
26 | var styles = define('entities', token.group, target)
27 | eachToken(token.inner, objectHandler, styles)
28 | }
29 |
30 | if (isPseudo(token.group)) {
31 | var styles = define('pseudos', token.group, target)
32 | eachToken(token.inner, objectHandler, styles)
33 | }
34 | }
35 |
36 | function objectHandler (token, target) {
37 | if (!defineRule(token, target)) {
38 | var deep = isDeep(token.group)
39 | if (deep) {
40 | token.group = token.group.slice(1, -1)
41 | }
42 |
43 | if (isPseudo(token.group)) {
44 | var styles = define('pseudos', token.group, target)
45 | eachToken(token.inner, objectHandler, styles)
46 | } else if (isElement(token.group)) {
47 | var styles = define('elements', token.group, target)
48 | eachToken(token.inner, objectHandler, styles)
49 | } else if (isFlag(token.group)) {
50 | var styles = define('flags', token.group, target)
51 | eachToken(token.inner, objectHandler, styles)
52 | } else if (isMixin(token.group)) {
53 | var styles = define('mixins', token.group, target)
54 | eachToken(token.inner, objectHandler, styles)
55 | } else if (isEntity(token.group)) {
56 | var styles = define('entities', token.group, target)
57 | eachToken(token.inner, objectHandler, styles)
58 | }
59 |
60 | if (styles && deep) {
61 | styles.deep = true
62 | }
63 | }
64 | }
65 |
66 | function define (type, name, target) {
67 | target[type] = target[type] || {}
68 | target[type][name] = target[type][name] || {}
69 | return target[type][name]
70 | }
71 |
72 | function defineRule (token, target) {
73 | if (token.rule) {
74 | target.rules = target.rules || {}
75 | target.rules[token.rule] = token.value
76 | return true
77 | } else {
78 | return false
79 | }
80 | }
81 |
82 | function isDeep (name) {
83 | return name && name.charAt(0) === '(' && name.charAt(name.length - 1) === ')'
84 | }
85 |
86 | function isFlag (name) {
87 | return name && name.charAt(0) === '-'
88 | }
89 |
90 | function isObject (name) {
91 | return name && name.charAt(0) !== name.charAt(0).toLowerCase()
92 | }
93 |
94 | function isElement (name) {
95 | return name && (name.charAt(0) !== name.charAt(0).toUpperCase() || name.charAt(0) === '*' || isFinite(name.charAt(0)))
96 | }
97 |
98 | function isPseudo (name) {
99 | // includes attribute selectors
100 | return name && (name.charAt(0) === ':' || (name.charAt(0) === '[' && name.charAt(name.length - 1) === ']'))
101 | }
102 |
103 | function isMixin (name) {
104 | return name && name.charAt(0) === '$'
105 | }
106 |
107 | function isEntity (name) {
108 | return name && name.charAt(0) === '@'
109 | }
110 |
111 | function eachToken (text, each, context) {
112 | var mode = 'search' // name
113 | var capture = { start: 0, end: 0 }
114 | var currentName = null
115 |
116 | function captureName () {
117 | currentName = text.slice(capture.start, capture.end + 1).trim()
118 | }
119 |
120 | for (var i = 0; i < text.length; i++) {
121 | var prevChar = text.charAt(i - 1)
122 | var nextChar = text.charAt(i + 1)
123 | var char = text.charAt(i)
124 |
125 | if (mode === 'search') {
126 | if (isNameChar(char)) {
127 | capture.start = i
128 | mode = 'name'
129 | }
130 | }
131 |
132 | if (mode === 'name') {
133 | var isShortName = isAttrChar(prevChar) && char === ':' && nextChar !== ':'
134 | if (isShortName || (capture.end > capture.start && ((char === ':' && prevChar !== ' ') || char === '{'))) {
135 | captureName()
136 | var start = i + 1
137 | if (char === ':') {
138 | i = endPosition(text, start) || text.length
139 | each({ rule: currentName, value: text.slice(start, i).trim() }, context)
140 | } else if (char === '{') {
141 | i = closePosition(text, start) || text.length
142 | each({ group: currentName, inner: text.slice(start, i).trim() }, context)
143 | }
144 |
145 | mode = 'search'
146 | currentName = null
147 | } else if (char === '\n' || char === ';') {
148 | captureName()
149 | if (isMixin(currentName)) {
150 | context.extensions = context.extensions || []
151 | setAdd(context.extensions, currentName)
152 | }
153 | mode = 'search'
154 | currentName = null
155 | } else {
156 | capture.end = i
157 | }
158 | }
159 | }
160 |
161 | // clean up any dangling names
162 | if (mode === 'name') {
163 | captureName()
164 | if (isMixin(currentName)) {
165 | context.extensions = context.extensions || []
166 | setAdd(context.extensions, currentName)
167 | }
168 | }
169 | }
170 |
171 | function isNameChar (char) {
172 | return char && char !== ' ' && char !== '{' && char !== '}' && char !== '\n' && char !== ';'
173 | }
174 |
175 | function isAttrChar (char) {
176 | return char && isNameChar(char) && char !== ':'
177 | }
178 |
179 | function endPosition (text, start) {
180 | var inParans = false
181 | for (var i = start; i < text.length; i++) {
182 | var char = text.charAt(i)
183 |
184 | // dirty hack for dealing with semicolons in data-urls
185 | if (char === '(') {
186 | inParans = true
187 | } else if (char === ')') {
188 | inParans = false
189 | }
190 |
191 | if ((char === ';' && !inParans) || char === '\n') {
192 | return i
193 | }
194 | }
195 | }
196 |
197 | function closePosition (text, start) {
198 | var depth = 0
199 |
200 | for (var i = start; i < text.length; i++) {
201 | var char = text.charAt(i)
202 | if (char === '{') {
203 | depth += 1
204 | }
205 | if (char === '}') {
206 | if (depth === 0) {
207 | return i
208 | } else {
209 | depth -= 1
210 | }
211 | }
212 | }
213 | }
214 |
215 | function setAdd (array, item) {
216 | if (array.indexOf(item) < 0) {
217 | array.push(item)
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Matt McKegg (https://twitter.com/MattMcKegg)",
3 | "name": "micro-css",
4 | "description": "A CSS preprocessor that provides a simplified object orientated approach to css. The syntax is very similar to CSS but encourages better reuse of classes and discourages high specificity.",
5 | "version": "2.0.2",
6 | "repository": {
7 | "type": "git",
8 | "url": "git://github.com/mmckegg/micro-css.git"
9 | },
10 | "bin": {
11 | "mcss": "bin/mcss.js"
12 | },
13 | "keywords": [
14 | "css",
15 | "preprocessor",
16 | "mcss",
17 | "less",
18 | "stylesheet",
19 | "styling",
20 | "parser",
21 | "svg"
22 | ],
23 | "main": "index.js",
24 | "license": "MIT",
25 | "scripts": {
26 | "test": "standard && tap test/*.js"
27 | },
28 | "dependencies": {
29 | "optimist": "^0.6.1"
30 | },
31 | "devDependencies": {
32 | "standard": "^14.1.0",
33 | "tap": "^14.6.1",
34 | "tape": "*"
35 | },
36 | "optionalDependencies": {},
37 | "engines": {
38 | "node": "*"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/query.js:
--------------------------------------------------------------------------------
1 | module.exports = function (query) {
2 | // objects
3 | query = query.replace(/(^| )([A-Z])/, '$1.$2')
4 |
5 | // flags
6 | query = query.replace(/ -/g, '.-')
7 |
8 | var parts = query.split(',')
9 | var results = parts.map(handleDepth)
10 | return results.join(', ')
11 | }
12 |
13 | function handleDepth (query) {
14 | var result = []
15 | var parts = query.trim().split(' ')
16 | for (var i = 0; i < parts.length; i++) {
17 | var part = parts[i]
18 | var deepQuery = getDeepQuery(part)
19 | part = deepQuery || part
20 |
21 | if (!deepQuery && i > 0) {
22 | result.push('>')
23 | }
24 |
25 | result.push(part)
26 | }
27 | return result.join(' ')
28 | }
29 |
30 | function getDeepQuery (query) {
31 | if (query.slice(0, 1) === '(' && query.slice(-1) === ')') {
32 | return query.slice(1, -1)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/test/h.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-redeclare */
2 |
3 | var test = require('tape')
4 | var ctor = require('../h.js')
5 |
6 | test('parsing classes', function (t) {
7 | var h = ctor(innerH)
8 | var res = h('div.class Object -flag -anotherFlag')
9 |
10 | t.deepEqual(res, [
11 | 'div.class', {
12 | className: 'Object -flag -anotherFlag'
13 | }, undefined
14 | ])
15 |
16 | t.end()
17 | })
18 |
19 | test('add classes to specified', function (t) {
20 | var h = ctor(innerH)
21 | var res = h('div.class', { className: 'another' })
22 |
23 | t.deepEqual(res, [
24 | 'div.class',
25 | { className: 'another' },
26 | undefined
27 | ])
28 |
29 | t.end()
30 | })
31 |
32 | test('no element specified with class', function (t) {
33 | var h = ctor(innerH)
34 | var res = h('.class', { className: 'another' })
35 |
36 | t.deepEqual(res, [
37 | '.class',
38 | { className: 'another' },
39 | undefined
40 | ])
41 |
42 | t.end()
43 | })
44 |
45 | test('children but no properties', function (t) {
46 | var h = ctor(innerH)
47 |
48 | var res = h('div.class', ['children'])
49 | t.deepEqual(res, [
50 | 'div.class', null, ['children']
51 | ])
52 |
53 | var res = h('div.class', new FakeVnode('span'))
54 | t.deepEqual(res, [
55 | 'div.class',
56 | null,
57 | { type: 'vnode', tag: 'span' }
58 | ])
59 |
60 | var res = h('div.class', 'text')
61 | t.deepEqual(res, [
62 | 'div.class',
63 | null,
64 | 'text'
65 | ])
66 |
67 | var res = h('div', 'text')
68 | t.deepEqual(res, [
69 | 'div', null, 'text'
70 | ])
71 |
72 | t.end()
73 | })
74 |
75 | function innerH (tagName, properties, children) {
76 | return [tagName, properties, children]
77 | }
78 |
79 | function FakeVnode (tag) {
80 | this.tag = tag
81 | this.type = 'vnode'
82 | }
83 |
--------------------------------------------------------------------------------
/test/micro-css.test.js:
--------------------------------------------------------------------------------
1 | var test = require('tape')
2 |
3 | var microCss = require('../')
4 |
5 | test('object with rules', function (t) {
6 | var mcss = 'Document {\n background-color: silver\n color:gray\n }'
7 | var expected = '.Document { background-color: silver; color: gray; }\n'
8 |
9 | t.equal(microCss(mcss), expected)
10 |
11 | t.end()
12 | })
13 |
14 | test('root element with rules', function (t) {
15 | var mcss = ('h1 {\n font-size: 10pt\n color:#356\n }')
16 | var expected = 'h1 { font-size: 10pt; color: #356; }\n'
17 |
18 | t.equal(microCss(mcss), expected)
19 |
20 | t.end()
21 | })
22 |
23 | test('svg element with short attributes', function (t) {
24 | var mcss = ('rect {\n y: 50%\n }')
25 | var expected = 'rect { y: 50%; }\n'
26 |
27 | t.equal(microCss(mcss), expected)
28 |
29 | t.end()
30 | })
31 |
32 | test('test preceded by operator', function (t) {
33 | var mcss = ('p + p {\n margin-top: 10px\n }')
34 | var expected = 'p + p { margin-top: 10px; }\n'
35 |
36 | t.equal(microCss(mcss), expected)
37 |
38 | t.end()
39 | })
40 |
41 | test('test multiple elements', function (t) {
42 | var mcss = ('input, select {\n width: 100px\n }')
43 | var expected = 'input, select { width: 100px; }\n'
44 |
45 | t.equal(microCss(mcss), expected)
46 |
47 | t.end()
48 | })
49 |
50 | test('test multiple elements with nesting', function (t) {
51 | var mcss = 'input, select {\n' +
52 | '[disabled] {\n opacity: 0.5\n }\n' +
53 | '}'
54 | var expected = 'input[disabled] { opacity: 0.5; }\nselect[disabled] { opacity: 0.5; }\n'
55 |
56 | t.equal(microCss(mcss), expected)
57 |
58 | t.end()
59 | })
60 |
61 | test('test multiple elements with mixin', function (t) {
62 | var mcss = 'input, select {\n' +
63 | ' $mixin\n' +
64 | '}\n' +
65 | '$mixin {\n' +
66 | ' [disabled]{ opacity: 0.5 }\n' +
67 | '}'
68 | var expected = 'input[disabled] { opacity: 0.5; }\nselect[disabled] { opacity: 0.5; }\n'
69 |
70 | t.equal(microCss(mcss), expected)
71 |
72 | t.end()
73 | })
74 |
75 | test('nested mixins', function (t) {
76 | var mcss = 'div {\n' +
77 | ' $mixin\n' +
78 | '}\n' +
79 | '$mixin {\n' +
80 | ' background: green\n' +
81 | ' $innerMixin\n' +
82 | '}\n' +
83 | '$innerMixin {\n' +
84 | ' div{ color: red }\n' +
85 | '}'
86 | var expected = 'div > div { color: red; }\ndiv { background: green; }\n'
87 |
88 | t.equal(microCss(mcss), expected)
89 |
90 | t.end()
91 | })
92 |
93 | test('test nested preceded by operator', function (t) {
94 | var mcss = 'Object {\n' +
95 | 'p + p {\n margin-top: 10px\n }\n' +
96 | '}'
97 | var expected = '.Object > p + p { margin-top: 10px; }\n'
98 |
99 | t.equal(microCss(mcss), expected)
100 |
101 | t.end()
102 | })
103 |
104 | test('element with pseudo class', function (t) {
105 | var mcss = ('a {\n :hover {\n text-decoration: underline\n }\n}')
106 | var expected = 'a:hover { text-decoration: underline; }\n'
107 |
108 | t.equal(microCss(mcss), expected)
109 |
110 | t.end()
111 | })
112 |
113 | test('element with multi pseudo class', function (t) {
114 | var mcss = ("a {\n :before, :after {\n content: '-'\n }\n}")
115 | var expected = "a:before { content: '-'; }\na:after { content: '-'; }\n"
116 |
117 | t.equal(microCss(mcss), expected)
118 |
119 | t.end()
120 | })
121 |
122 | test('root multi element with rules', function (t) {
123 | var mcss = ('h1, h2, h3, h4, h5, h6 {\n margin-bottom: 10px }')
124 | var expected = 'h1, h2, h3, h4, h5, h6 { margin-bottom: 10px; }\n'
125 |
126 | t.equal(microCss(mcss), expected)
127 | t.end()
128 | })
129 |
130 | test('mixin not in output', function (t) {
131 | var mcss = ('$noticeMe {\n background-color: fuchsia\n color:lime\n }')
132 | var expected = ''
133 |
134 | t.equal(microCss(mcss), expected)
135 | t.end()
136 | })
137 |
138 | test('mixin to another rule', function (t) {
139 | var mcss = (
140 | '$noticeMe { ' +
141 | '-fancy { ' +
142 | 'background: green \n' +
143 | 'div.stuff { color: white }\n' +
144 | '}\n' +
145 | 'color: green ' +
146 | '}' +
147 | 'Item { ' +
148 | '$noticeMe \n' +
149 | 'border: solid gray 1px \n' +
150 | 'div { ' +
151 | 'color: gray ' +
152 | '}' +
153 | '}'
154 | )
155 |
156 | var expected = (
157 | '.Item { color: green; }\n' +
158 | '.Item.-fancy { background: green; }\n' +
159 | '.Item.-fancy > div.stuff { color: white; }\n' +
160 | '.Item { border: solid gray 1px; }\n' +
161 | '.Item > div { color: gray; }\n'
162 | )
163 |
164 | t.equal(microCss(mcss), expected)
165 |
166 | t.end()
167 | })
168 |
169 | test('nested mixin', function (t) {
170 | var mcss = (
171 | 'Item { ' +
172 | 'div { ' +
173 | '$noticeMe \n' +
174 | '}\n' +
175 | '$noticeMe { ' +
176 | '-fancy { ' +
177 | 'background: green \n' +
178 | '}\n' +
179 | 'color: gray ' +
180 | '}' +
181 | '}'
182 | )
183 |
184 | var expected = (
185 | '.Item > div { color: gray; }\n' +
186 | '.Item > div.-fancy { background: green; }\n'
187 | )
188 |
189 | t.equal(microCss(mcss), expected)
190 |
191 | t.end()
192 | })
193 |
194 | test('object with flags', function (t) {
195 | var mcss = (
196 | 'Document {\n' +
197 | ' background-color: silver\n' +
198 | ' color:gray\n' +
199 | ' -wide {\n' +
200 | ' width: 700px\n' +
201 | ' padding:30px\n' +
202 | ' }\n' +
203 | '}'
204 | )
205 |
206 | var expected = '.Document { background-color: silver; color: gray; }\n.Document.-wide { width: 700px; padding: 30px; }\n'
207 |
208 | t.equal(microCss(mcss), expected)
209 |
210 | t.end()
211 | })
212 |
213 | test('object with flags and nested elements', function (t) {
214 | var mcss = (
215 | 'Document {\n' +
216 | ' background-color: silver\n' +
217 | ' color:gray\n' +
218 | ' -main {\n' +
219 | ' padding:30px\n' +
220 | ' heading {\n' +
221 | ' background-color: silver\n' +
222 | ' color: black\n' +
223 | ' }\n' +
224 | ' }\n' +
225 | '}'
226 | )
227 |
228 | var expected = '.Document { background-color: silver; color: gray; }\n' +
229 | '.Document.-main { padding: 30px; }\n' +
230 | '.Document.-main > heading { background-color: silver; color: black; }\n'
231 |
232 | t.equal(microCss(mcss), expected)
233 |
234 | t.end()
235 | })
236 |
237 | test('object with flags and multiple nested elements', function (t) {
238 | var mcss = (
239 | 'Document {\n' +
240 | ' background-color: silver\n' +
241 | ' color:gray\n' +
242 | ' -main {\n' +
243 | ' padding:30px\n' +
244 | ' heading {\n' +
245 | ' border-bottom: 1px solid gray\n' +
246 | ' background-color: silver\n' +
247 | ' h1 {\n' +
248 | ' color: black\n' +
249 | ' }\n' +
250 | ' }\n' +
251 | ' }\n' +
252 | '}'
253 | )
254 |
255 | var expected = '.Document { background-color: silver; color: gray; }\n' +
256 | '.Document.-main { padding: 30px; }\n' +
257 | '.Document.-main > heading { border-bottom: 1px solid gray; background-color: silver; }\n' +
258 | '.Document.-main > heading > h1 { color: black; }\n'
259 |
260 | t.equal(microCss(mcss), expected)
261 |
262 | t.end()
263 | })
264 |
265 | test('object with filtered elements', function (t) {
266 | var mcss = (
267 | 'Document {\n' +
268 | ' span.name {\n' +
269 | ' color: red\n' +
270 | ' }\n' +
271 | '}'
272 | )
273 |
274 | var expected = '.Document > span.name { color: red; }\n'
275 |
276 | t.equal(microCss(mcss), expected)
277 |
278 | t.end()
279 | })
280 |
281 | test('object with deep element', function (t) {
282 | var mcss = (
283 | 'Document {\n' +
284 | ' (strong) {\n' +
285 | ' font-weight: bold\n' +
286 | ' color: blue\n' +
287 | ' }\n' +
288 | '}'
289 | )
290 |
291 | var expected = '.Document strong { font-weight: bold; color: blue; }\n'
292 |
293 | t.equal(microCss(mcss), expected)
294 |
295 | t.end()
296 | })
297 |
298 | test('element with attribute match', function (t) {
299 | var mcss = (
300 | 'div {\n' +
301 | ' [contenteditable] {\n' +
302 | ' outline: dotted 1px silver\n' +
303 | ' }\n' +
304 | '}'
305 | )
306 |
307 | var expected = 'div[contenteditable] { outline: dotted 1px silver; }\n'
308 |
309 | t.equal(microCss(mcss), expected)
310 |
311 | t.end()
312 | })
313 |
314 | test('element with attribute equal match', function (t) {
315 | var mcss = (
316 | 'input {\n' +
317 | " [type='date'] {\n" +
318 | ' outline: dotted 1px silver\n' +
319 | ' }\n' +
320 | '}'
321 | )
322 |
323 | var expected = "input[type='date'] { outline: dotted 1px silver; }\n"
324 |
325 | t.equal(microCss(mcss), expected)
326 |
327 | t.end()
328 | })
329 |
330 | test('element with attribute match', function (t) {
331 | var mcss = (
332 | '[hidden] {\n' +
333 | ' display: none\n' +
334 | '}'
335 | )
336 |
337 | var expected = '[hidden] { display: none; }\n'
338 |
339 | t.equal(microCss(mcss), expected)
340 |
341 | t.end()
342 | })
343 |
344 | test('object with multi flags', function (t) {
345 | var mcss = (
346 | 'Document {\n' +
347 | ' -large -red, -notice {\n' +
348 | ' color: red\n' +
349 | ' }\n' +
350 | '}'
351 | )
352 |
353 | var expected = '.Document.-large.-red { color: red; }\n.Document.-notice { color: red; }\n'
354 |
355 | t.equal(microCss(mcss), expected)
356 |
357 | t.end()
358 | })
359 |
360 | test('test inline svg', function (t) {
361 | var mcss = (
362 | '@svg test {\n' +
363 | " content: ' '\n" +
364 | ' width: 16px\n' +
365 | ' height: 16px \n' +
366 | ' ellipse { \n' +
367 | ' fill: green \n ' +
368 | ' } \n' +
369 | ' -sub { \n' +
370 | ' ellipse {\n' +
371 | ' fill: blue \n' +
372 | ' }\n' +
373 | ' }\n' +
374 | '} \n' +
375 | 'body {\n' +
376 | ' background: svg(test) no-repeat left \n' +
377 | '}\n' +
378 | 'div {\n' +
379 | ' background: svg(test -sub) \n' +
380 | '}'
381 | )
382 |
383 | var expected = 'body { background: url("data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgd2lkdGg9IjE2cHgiIGhlaWdodD0iMTZweCI+PGRlZnM+PHN0eWxlIHR5cGU9InRleHQvY3NzIj48IVtDREFUQVsuLXN1YiA+IGVsbGlwc2UgeyBmaWxsOiBibHVlOyB9CmVsbGlwc2UgeyBmaWxsOiBncmVlbjsgfQpdXT48L3N0eWxlPjwvZGVmcz48ZWxsaXBzZS8+PC9zdmc+") no-repeat left; }\n' +
384 | 'div { background: url("data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgd2lkdGg9IjE2cHgiIGhlaWdodD0iMTZweCIgY2xhc3M9Ii1zdWIiPjxkZWZzPjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+PCFbQ0RBVEFbLi1zdWIgPiBlbGxpcHNlIHsgZmlsbDogYmx1ZTsgfQplbGxpcHNlIHsgZmlsbDogZ3JlZW47IH0KXV0+PC9zdHlsZT48L2RlZnM+PGVsbGlwc2UvPjwvc3ZnPg=="); }\n'
385 |
386 | t.equal(microCss(mcss), expected)
387 |
388 | t.end()
389 | })
390 |
391 | test('test nested inline svg', function (t) {
392 | var mcss = (
393 | 'Object {\n' +
394 | ' @svg test {\n' +
395 | " content: ' '\n" +
396 | ' }\n' +
397 | ' div {\n' +
398 | ' background: svg(test) \n' +
399 | ' }\n' +
400 | '}'
401 | )
402 |
403 | var expected = '.Object > div { background: url("data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSI+PGRlZnM+PHN0eWxlIHR5cGU9InRleHQvY3NzIj48IVtDREFUQVtdXT48L3N0eWxlPjwvZGVmcz48ZWxsaXBzZS8+PC9zdmc+"); }\n'
404 |
405 | t.equal(microCss(mcss), expected)
406 |
407 | t.end()
408 | })
409 |
410 | test('test keyframes', function (t) {
411 | var mcss = (
412 | '@keyframes test {\n' +
413 | ' from { background-color: red }\n' +
414 | ' 50% { background-color: green }\n' +
415 | ' to { background-color: blue }\n' +
416 | '}'
417 | )
418 |
419 | var expected = '@keyframes test { from { background-color: red; } 50% { background-color: green; } to { background-color: blue; } }\n'
420 |
421 | t.equal(microCss(mcss), expected)
422 |
423 | t.end()
424 | })
425 |
--------------------------------------------------------------------------------
/test/query.test.js:
--------------------------------------------------------------------------------
1 | var test = require('tape')
2 | var query = require('../query')
3 |
4 | test('test', function (t) {
5 | t.equal(query('Object'), '.Object')
6 | t.equal(query('Object -cat'), '.Object.-cat')
7 | t.equal(query('Object -cat div.title'), '.Object.-cat > div.title')
8 | t.equal(
9 | query('Object -cat div.title, div, div.thing, span -large'),
10 | '.Object.-cat > div.title, div, div.thing, span.-large'
11 | )
12 | t.end()
13 | })
14 |
--------------------------------------------------------------------------------
/test/tokenizer.test.js:
--------------------------------------------------------------------------------
1 | var test = require('tape')
2 |
3 | var tokenizer = require('../lib/tokenizer')
4 |
5 | test('object with rules', function (t) {
6 | var tokens = tokenizer('Document {\n background-color: silver\n color:gray\n }')
7 |
8 | t.deepEquals({
9 | objects: {
10 | Document: {
11 | rules: {
12 | 'background-color': 'silver',
13 | color: 'gray'
14 | }
15 | }
16 | }
17 | }, tokens)
18 |
19 | t.end()
20 | })
21 |
22 | test('root element with rules', function (t) {
23 | var tokens = tokenizer('h1 {\n font-size: 10pt\n color:#356\n }')
24 |
25 | t.deepEquals({
26 | elements: {
27 | h1: {
28 | rules: {
29 | 'font-size': '10pt',
30 | color: '#356'
31 | }
32 | }
33 | }
34 | }, tokens)
35 |
36 | t.end()
37 | })
38 |
39 | test('test preceded by operator', function (t) {
40 | var tokens = tokenizer('p + p {\n font-size: 10pt\n color:#356\n }')
41 |
42 | t.deepEquals({
43 | elements: {
44 | 'p + p': {
45 | rules: {
46 | 'font-size': '10pt',
47 | color: '#356'
48 | }
49 | }
50 | }
51 | }, tokens)
52 |
53 | t.end()
54 | })
55 |
56 | test('element with pseudo class', function (t) {
57 | var tokens = tokenizer('a {\n :hover {\n text-decoration: underline\n }\n}')
58 |
59 | t.deepEquals(tokens, {
60 | elements: {
61 | a: {
62 | pseudos: {
63 | ':hover': {
64 | rules: {
65 | 'text-decoration': 'underline'
66 | }
67 | }
68 | }
69 | }
70 | }
71 | })
72 |
73 | t.end()
74 | })
75 |
76 | test('element with multiple pseudo classes', function (t) {
77 | var tokens = tokenizer("a {\n :before, :after {\n content: '-'\n }\n}")
78 |
79 | t.deepEquals(tokens, {
80 | elements: {
81 | a: {
82 | pseudos: {
83 | ':before, :after': {
84 | rules: {
85 | content: "'-'"
86 | }
87 | }
88 | }
89 | }
90 | }
91 | })
92 |
93 | t.end()
94 | })
95 |
96 | test('mixin with rules', function (t) {
97 | var tokens = tokenizer('$noticeMe {\n background-color: fuchsia\n color:lime\n }')
98 |
99 | t.deepEquals({
100 | mixins: {
101 | $noticeMe: {
102 | rules: {
103 | 'background-color': 'fuchsia',
104 | color: 'lime'
105 | }
106 | }
107 | }
108 | }, tokens)
109 |
110 | t.end()
111 | })
112 |
113 | test('nested mixin', function (t) {
114 | var tokens = tokenizer('Object { div { $mixin } $mixin { color: red } }')
115 |
116 | t.deepEquals(tokens, {
117 | objects: {
118 | Object: {
119 | elements: {
120 | div: {
121 | extensions: ['$mixin']
122 | }
123 | },
124 | mixins: {
125 | $mixin: {
126 | rules: {
127 | color: 'red'
128 | }
129 | }
130 | }
131 | }
132 | }
133 | })
134 |
135 | t.end()
136 | })
137 |
138 | test('mixin to another rule', function (t) {
139 | var tokens = tokenizer(
140 | '$noticeMe { ' +
141 | '-fancy { ' +
142 | 'background: green \n' +
143 | 'div.stuff { color: white } ' +
144 | '} ' +
145 | '}' +
146 | 'Item { ' +
147 | '$noticeMe \n' +
148 | 'div { ' +
149 | 'color: gray ' +
150 | '}' +
151 | '}'
152 | )
153 |
154 | t.deepEquals({
155 | mixins: {
156 | $noticeMe: {
157 | flags: {
158 | '-fancy': {
159 | rules: {
160 | background: 'green'
161 | },
162 | elements: {
163 | 'div.stuff': {
164 | rules: {
165 | color: 'white'
166 | }
167 | }
168 | }
169 | }
170 | }
171 | }
172 | },
173 | objects: {
174 | Item: {
175 | extensions: ['$noticeMe'],
176 | elements: {
177 | div: {
178 | rules: {
179 | color: 'gray'
180 | }
181 | }
182 | }
183 | }
184 | }
185 | }, tokens)
186 |
187 | t.end()
188 | })
189 |
190 | test('mixin extending element with no rules', function (t) {
191 | var tokens = tokenizer('div { $mixin }')
192 | t.deepEquals(tokens, {
193 | elements: {
194 | div: {
195 | extensions: ['$mixin']
196 | }
197 | }
198 | })
199 | t.end()
200 | })
201 |
202 | test('mixin with flags and inner rule', function (t) {
203 | var tokens = tokenizer('$noticeMe { -fancy { background: green \n div.stuff { color: white } } }')
204 |
205 | t.deepEquals(tokens, {
206 | mixins: {
207 | $noticeMe: {
208 | flags: {
209 | '-fancy': {
210 | rules: {
211 | background: 'green'
212 | },
213 | elements: {
214 | 'div.stuff': {
215 | rules: {
216 | color: 'white'
217 | }
218 | }
219 | }
220 | }
221 | }
222 | }
223 | }
224 | })
225 |
226 | t.end()
227 | })
228 |
229 | test('object with flags', function (t) {
230 | var tokens = tokenizer(
231 | 'Document {\n' +
232 | ' background-color: silver\n' +
233 | ' color:gray\n' +
234 | ' -wide {\n' +
235 | ' width: 700px\n' +
236 | ' padding:30px\n' +
237 | ' }\n' +
238 | '}'
239 | )
240 |
241 | t.deepEquals({
242 | objects: {
243 | Document: {
244 | rules: {
245 | 'background-color': 'silver',
246 | color: 'gray'
247 | },
248 | flags: {
249 | '-wide': {
250 | rules: {
251 | width: '700px',
252 | padding: '30px'
253 | }
254 | }
255 | }
256 | }
257 | }
258 | }, tokens)
259 |
260 | t.end()
261 | })
262 |
263 | test('object with deep element', function (t) {
264 | var tokens = tokenizer(
265 | 'Document {\n' +
266 | ' (strong) {\n' +
267 | ' font-weight: bold\n' +
268 | ' color: blue\n' +
269 | ' }\n' +
270 | '}'
271 | )
272 |
273 | t.deepEquals({
274 | objects: {
275 | Document: {
276 | elements: {
277 | strong: {
278 | deep: true,
279 | rules: {
280 | 'font-weight': 'bold',
281 | color: 'blue'
282 | }
283 | }
284 | }
285 | }
286 | }
287 | }, tokens)
288 |
289 | t.end()
290 | })
291 |
292 | test('element with attribute match', function (t) {
293 | var tokens = tokenizer(
294 | 'div {\n' +
295 | ' [contenteditable] {\n' +
296 | ' outline: dotted 1px silver\n' +
297 | ' }\n' +
298 | '}'
299 | )
300 |
301 | t.deepEquals({
302 | elements: {
303 | div: {
304 | pseudos: {
305 | '[contenteditable]': {
306 | rules: {
307 | outline: 'dotted 1px silver'
308 | }
309 | }
310 | }
311 | }
312 | }
313 | }, tokens)
314 |
315 | t.end()
316 | })
317 |
318 | test('root attribute match', function (t) {
319 | var tokens = tokenizer(
320 | '[hidden] {\n' +
321 | ' display: none\n' +
322 | '}'
323 | )
324 |
325 | t.deepEquals({
326 | pseudos: {
327 | '[hidden]': {
328 | rules: {
329 | display: 'none'
330 | }
331 | }
332 | }
333 | }, tokens)
334 |
335 | t.end()
336 | })
337 |
338 | test('object with flags and nested elements', function (t) {
339 | var tokens = tokenizer(
340 | 'Document {\n' +
341 | ' background-color: silver\n' +
342 | ' color:gray\n' +
343 | ' -main {\n' +
344 | ' padding:30px\n' +
345 | ' heading {\n' +
346 | ' border-bottom: 1px solid gray\n' +
347 | ' background-color: silver\n' +
348 | ' color: black\n' +
349 | ' }\n' +
350 | ' }\n' +
351 | '}'
352 | )
353 |
354 | t.deepEquals({
355 | objects: {
356 | Document: {
357 | rules: {
358 | 'background-color': 'silver',
359 | color: 'gray'
360 | },
361 | flags: {
362 | '-main': {
363 | rules: {
364 | padding: '30px'
365 | },
366 | elements: {
367 | heading: {
368 | rules: {
369 | 'border-bottom': '1px solid gray',
370 | 'background-color': 'silver',
371 | color: 'black'
372 | }
373 | }
374 | }
375 | }
376 | }
377 | }
378 | }
379 | }, tokens)
380 |
381 | t.end()
382 | })
383 |
384 | test('object with flags and multiple nested elements', function (t) {
385 | var tokens = tokenizer(
386 | 'Document {\n' +
387 | ' background-color: silver\n' +
388 | ' color:gray\n' +
389 | ' -main {\n' +
390 | ' padding:30px\n' +
391 | ' heading {\n' +
392 | ' border-bottom: 1px solid gray\n' +
393 | ' background-color: silver\n' +
394 | ' h1 {\n' +
395 | ' color: black\n' +
396 | ' }\n' +
397 | ' }\n' +
398 | ' }\n' +
399 | '}'
400 | )
401 |
402 | t.deepEquals({
403 | objects: {
404 | Document: {
405 | rules: {
406 | 'background-color': 'silver',
407 | color: 'gray'
408 | },
409 | flags: {
410 | '-main': {
411 | rules: {
412 | padding: '30px'
413 | },
414 | elements: {
415 | heading: {
416 | rules: {
417 | 'border-bottom': '1px solid gray',
418 | 'background-color': 'silver'
419 | },
420 | elements: {
421 | h1: {
422 | rules: {
423 | color: 'black'
424 | }
425 | }
426 | }
427 | }
428 | }
429 | }
430 | }
431 | }
432 | }
433 | }, tokens)
434 |
435 | t.end()
436 | })
437 |
438 | test('object with filtered elements', function (t) {
439 | var tokens = tokenizer(
440 | 'Document {\n' +
441 | ' span.name {\n' +
442 | ' color: red\n' +
443 | ' }\n' +
444 | '}'
445 | )
446 |
447 | t.deepEquals({
448 | objects: {
449 | Document: {
450 | elements: {
451 | 'span.name': {
452 | rules: {
453 | color: 'red'
454 | }
455 | }
456 | }
457 | }
458 | }
459 | }, tokens)
460 |
461 | t.end()
462 | })
463 |
464 | test('object with multi flags', function (t) {
465 | var tokens = tokenizer(
466 | 'Document {\n' +
467 | ' -large -red, -notice {\n' +
468 | ' color: red\n' +
469 | ' }\n' +
470 | '}'
471 | )
472 |
473 | t.deepEquals({
474 | objects: {
475 | Document: {
476 | flags: {
477 | '-large -red, -notice': {
478 | rules: {
479 | color: 'red'
480 | }
481 | }
482 | }
483 | }
484 | }
485 | }, tokens)
486 |
487 | t.end()
488 | })
489 |
490 | test('wildcard root', function (t) {
491 | var tokens = tokenizer(
492 | '* {\n' +
493 | ' box-sizing: border-box\n' +
494 | '}'
495 | )
496 |
497 | t.deepEquals(tokens, {
498 | elements: {
499 | '*': {
500 | rules: {
501 | 'box-sizing': 'border-box'
502 | }
503 | }
504 | }
505 | })
506 |
507 | t.end()
508 | })
509 |
510 | test('pseudos elements', function (t) {
511 | var tokens = tokenizer(
512 | 'Document {\n' +
513 | ' ::-webkit-placeholder {\n' +
514 | ' color: red\n' +
515 | ' }\n' +
516 | '}'
517 | )
518 |
519 | t.deepEquals(tokens, {
520 | objects: {
521 | Document: {
522 | pseudos: {
523 | '::-webkit-placeholder': {
524 | rules: {
525 | color: 'red'
526 | }
527 | }
528 | }
529 | }
530 | }
531 | })
532 |
533 | t.end()
534 | })
535 |
536 | test('svg entity', function (t) {
537 | var tokens = tokenizer(
538 | '@svg test {\n' +
539 | " content: ' '\n" +
540 | ' width: 16px\n' +
541 | ' height: 16px \n' +
542 | ' ellipse { \n' +
543 | ' fill: green \n ' +
544 | ' } \n' +
545 | '}'
546 | )
547 |
548 | t.deepEquals({
549 | entities: {
550 | '@svg test': {
551 | rules: {
552 | width: '16px',
553 | height: '16px',
554 | content: "' '"
555 | },
556 | elements: {
557 | ellipse: {
558 | rules: {
559 | fill: 'green'
560 | }
561 | }
562 | }
563 | }
564 | }
565 | }, tokens)
566 |
567 | t.end()
568 | })
569 |
570 | test('nested svg entity', function (t) {
571 | var tokens = tokenizer(
572 | 'Object {\n' +
573 | ' @svg test {\n' +
574 | " content: ' '\n" +
575 | ' }\n' +
576 | '}'
577 | )
578 |
579 | t.deepEquals({
580 | objects: {
581 | Object: {
582 | entities: {
583 | '@svg test': {
584 | rules: {
585 | content: "' '"
586 | }
587 | }
588 | }
589 | }
590 | }
591 | }, tokens)
592 |
593 | t.end()
594 | })
595 |
596 | test('css entity with nesting', function (t) {
597 | var tokens = tokenizer(
598 | '@keyframes test {\n' +
599 | ' from { background-color: red }\n' +
600 | ' 50% { background-color: green }\n' +
601 | ' to { background-color: blue }\n' +
602 | '}'
603 | )
604 |
605 | t.deepEquals({
606 | entities: {
607 | '@keyframes test': {
608 | elements: {
609 | from: {
610 | rules: {
611 | 'background-color': 'red'
612 | }
613 | },
614 | '50%': {
615 | rules: {
616 | 'background-color': 'green'
617 | }
618 | },
619 | to: {
620 | rules: {
621 | 'background-color': 'blue'
622 | }
623 | }
624 | }
625 | }
626 | }
627 | }, tokens)
628 |
629 | t.end()
630 | })
631 |
--------------------------------------------------------------------------------