├── .gitignore
├── test
├── cases
│ ├── rgba.out.css
│ ├── colors.out.css
│ ├── page.out.css
│ ├── colors.in.css
│ ├── dupe_selectors.out.css
│ ├── page.in.css
│ ├── important.out.css
│ ├── point_numbers.out.css
│ ├── property_whitespace.out.css
│ ├── rgba.in.css
│ ├── comment.out.css
│ ├── dupe_selectors.in.css
│ ├── empty_rules.out.css
│ ├── hack_sorting.out.css
│ ├── important.in.css
│ ├── point_numbers.in.css
│ ├── semicolons.out.css
│ ├── at_font_face.out.css
│ ├── hack_sorting.in.css
│ ├── ie5_mac_hack.out.css
│ ├── media_query.out.css
│ ├── semicolons.in.css
│ ├── dupe_selectors_via_consolidation.out.css
│ ├── extra_semicolons.out.css
│ ├── media_query.in.css
│ ├── page_consolidation.out.css
│ ├── property_whitespace.in.css
│ ├── sort_declarations.out.css
│ ├── extra_semicolons.in.css
│ ├── page_order.out.css
│ ├── quotes_in_selector.out.css
│ ├── sort_declarations.in.css
│ ├── at_font_face.in.css
│ ├── dupe_at_charset.out.css
│ ├── font_face_punting.out.css
│ ├── padding.out.css
│ ├── quotes_in_selector.in.css
│ ├── selector_consolidation.out.css
│ ├── declarations_consolidation.out.css
│ ├── duplicate_font_face.out.css
│ ├── empty_keyframes.out.css
│ ├── empty_rules.in.css
│ ├── font_face_compression.out.css
│ ├── selector_consolidation_2.out.css
│ ├── selector_consolidation_3.out.css
│ ├── at_charset_in_wrong_place.out.css
│ ├── none_to_zero.out.css
│ ├── quotes.out.css
│ ├── unquote_urls.out.css
│ ├── bang_comments.out.css
│ ├── compress_selectors.out.css
│ ├── font_face_compression.in.css
│ ├── selector_compression.out.css
│ ├── selector_consolidation.in.css
│ ├── at_keyframes.out.css
│ ├── dupe_selectors_via_consolidation.in.css
│ ├── font_family.out.css
│ ├── hello.out.css
│ ├── media_query_basic_consolidation.out.css
│ ├── media_query_order.out.css
│ ├── bang_comments.in.css
│ ├── commas_in_values.out.css
│ ├── page_order.in.css
│ ├── unquote_urls.in.css
│ ├── at_charset_in_wrong_place.in.css
│ ├── page_consolidation.in.css
│ ├── vendor_prefix_sorting.out.css
│ ├── commas_in_values.in.css
│ ├── font_face_punting.in.css
│ ├── parentheses.out.css
│ ├── keyframes_prefix_optimization.out.css
│ ├── multiple_font_face.out.css
│ ├── none_to_zero.in.css
│ ├── quotes.in.css
│ ├── reordering.out.css
│ ├── voice_family_hack.out.css
│ ├── compress_selectors.in.css
│ ├── dupe_at_charset.in.css
│ ├── media_query_consolidation.out.css
│ ├── selector_compression.in.css
│ ├── selector_consolidation_3.in.css
│ ├── declarations_consolidation.in.css
│ ├── padding.in.css
│ ├── parentheses.in.css
│ ├── at_keyframes_with_vendor_2.out.css
│ ├── comment.in.css
│ ├── duplicate_font_face.in.css
│ ├── font_family.in.css
│ ├── at_keyframes_with_vendor.out.css
│ ├── media_query_order.in.css
│ ├── page_pseudoclasses.out.css
│ ├── selector_consolidation_2.in.css
│ ├── dont_munge_properties.out.css
│ ├── hello.in.css
│ ├── ie5_mac_hack.in.css
│ ├── reordering.in.css
│ ├── vendor_prefix_sorting.in.css
│ ├── media_query_basic_consolidation.in.css
│ ├── at_keyframes_with_vendor_2.in.css
│ ├── font_face_duplicates.out.css
│ ├── at_keyframes_with_vendor.in.css
│ ├── reordering_advanced.out.css
│ ├── multiple_font_face.in.css
│ ├── empty_keyframes.in.css
│ ├── voice_family_hack.in.css
│ ├── dont_munge_properties.in.css
│ ├── at_keyframes.in.css
│ ├── keyframes_prefix_optimization.in.css
│ ├── reordering_advanced.in.css
│ ├── media_query_consolidation.in.css
│ ├── page_pseudoclasses.in.css
│ └── font_face_duplicates.in.css
└── test.js
├── .travis.yml
├── dist
├── index.html
├── css-condense.min.js
└── css-condense.js
├── TODO.md
├── package.json
├── Makefile
├── HISTORY.md
├── bin
└── cssc
├── README.md
└── index.js
/.gitignore:
--------------------------------------------------------------------------------
1 | docs
2 | node_modules
3 |
--------------------------------------------------------------------------------
/test/cases/rgba.out.css:
--------------------------------------------------------------------------------
1 | a{color:#369}
2 |
--------------------------------------------------------------------------------
/test/cases/colors.out.css:
--------------------------------------------------------------------------------
1 | .red{color:#f00}
2 |
--------------------------------------------------------------------------------
/test/cases/page.out.css:
--------------------------------------------------------------------------------
1 | @page{margin:1cm}
2 |
--------------------------------------------------------------------------------
/test/cases/colors.in.css:
--------------------------------------------------------------------------------
1 | .red { color: #ff0000; }
2 |
--------------------------------------------------------------------------------
/test/cases/dupe_selectors.out.css:
--------------------------------------------------------------------------------
1 | div{color:blue}
2 |
--------------------------------------------------------------------------------
/test/cases/page.in.css:
--------------------------------------------------------------------------------
1 | @page { margin: 1cm; }
2 |
--------------------------------------------------------------------------------
/test/cases/important.out.css:
--------------------------------------------------------------------------------
1 | a{margin:0!important}
2 |
--------------------------------------------------------------------------------
/test/cases/point_numbers.out.css:
--------------------------------------------------------------------------------
1 | div{margin:.03em}
2 |
--------------------------------------------------------------------------------
/test/cases/property_whitespace.out.css:
--------------------------------------------------------------------------------
1 | div{color:blue}
2 |
--------------------------------------------------------------------------------
/test/cases/rgba.in.css:
--------------------------------------------------------------------------------
1 | a { color: rgb(51, 102, 153); }
2 |
--------------------------------------------------------------------------------
/test/cases/comment.out.css:
--------------------------------------------------------------------------------
1 | div{color:blue}a{color:green}
2 |
--------------------------------------------------------------------------------
/test/cases/dupe_selectors.in.css:
--------------------------------------------------------------------------------
1 | div,div { color: blue; }
2 |
--------------------------------------------------------------------------------
/test/cases/empty_rules.out.css:
--------------------------------------------------------------------------------
1 | a{color:green}p{color:blue}
2 |
--------------------------------------------------------------------------------
/test/cases/hack_sorting.out.css:
--------------------------------------------------------------------------------
1 | div{border:10px;*border:0}
2 |
--------------------------------------------------------------------------------
/test/cases/important.in.css:
--------------------------------------------------------------------------------
1 | a { margin: 0 !important; }
2 |
--------------------------------------------------------------------------------
/test/cases/point_numbers.in.css:
--------------------------------------------------------------------------------
1 | div { margin: 00.030em; }
2 |
--------------------------------------------------------------------------------
/test/cases/semicolons.out.css:
--------------------------------------------------------------------------------
1 | div{color:red;margin:20px}
2 |
--------------------------------------------------------------------------------
/test/cases/at_font_face.out.css:
--------------------------------------------------------------------------------
1 | @font-face{font-family:avenir}
2 |
--------------------------------------------------------------------------------
/test/cases/hack_sorting.in.css:
--------------------------------------------------------------------------------
1 | div { border: 10px; *border: 0; }
2 |
--------------------------------------------------------------------------------
/test/cases/ie5_mac_hack.out.css:
--------------------------------------------------------------------------------
1 | /*\*/.selector{color:khaki}/**/
2 |
--------------------------------------------------------------------------------
/test/cases/media_query.out.css:
--------------------------------------------------------------------------------
1 | @media screen{div{color:red}}
2 |
--------------------------------------------------------------------------------
/test/cases/semicolons.in.css:
--------------------------------------------------------------------------------
1 | div { color: red; margin: 20px; }
2 |
--------------------------------------------------------------------------------
/test/cases/dupe_selectors_via_consolidation.out.css:
--------------------------------------------------------------------------------
1 | div,x{color:blue}
2 |
--------------------------------------------------------------------------------
/test/cases/extra_semicolons.out.css:
--------------------------------------------------------------------------------
1 | div{azimuth:0;background:blue}
2 |
--------------------------------------------------------------------------------
/test/cases/media_query.in.css:
--------------------------------------------------------------------------------
1 | @media screen { div { color: red } }
2 |
--------------------------------------------------------------------------------
/test/cases/page_consolidation.out.css:
--------------------------------------------------------------------------------
1 | @page{margin:1cm;padding:2cm}
2 |
--------------------------------------------------------------------------------
/test/cases/property_whitespace.in.css:
--------------------------------------------------------------------------------
1 | div {
2 | color : blue;
3 | }
4 |
--------------------------------------------------------------------------------
/test/cases/sort_declarations.out.css:
--------------------------------------------------------------------------------
1 | div{background:green;z-index:0}
2 |
--------------------------------------------------------------------------------
/test/cases/extra_semicolons.in.css:
--------------------------------------------------------------------------------
1 | div { azimuth: 0;;; background: blue; }
2 |
--------------------------------------------------------------------------------
/test/cases/page_order.out.css:
--------------------------------------------------------------------------------
1 | p{color:black}@page{margin:1cm;padding:2cm}
2 |
--------------------------------------------------------------------------------
/test/cases/quotes_in_selector.out.css:
--------------------------------------------------------------------------------
1 | div[title="\"hello\""]{color:blue}
2 |
--------------------------------------------------------------------------------
/test/cases/sort_declarations.in.css:
--------------------------------------------------------------------------------
1 | div { z-index: 0; background: green; }
2 |
--------------------------------------------------------------------------------
/test/cases/at_font_face.in.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: avenir;
3 | }
4 |
--------------------------------------------------------------------------------
/test/cases/dupe_at_charset.out.css:
--------------------------------------------------------------------------------
1 | @charset "utf-8";div{color:blue}a{color:red}
2 |
--------------------------------------------------------------------------------
/test/cases/font_face_punting.out.css:
--------------------------------------------------------------------------------
1 | @font-face{font-family:geneva;src:url(y)}
2 |
--------------------------------------------------------------------------------
/test/cases/padding.out.css:
--------------------------------------------------------------------------------
1 | div{padding:10px}a{margin:10px 3px}span{padding:1px}
2 |
--------------------------------------------------------------------------------
/test/cases/quotes_in_selector.in.css:
--------------------------------------------------------------------------------
1 | div[title="\"hello\""] { color: blue; }
2 |
--------------------------------------------------------------------------------
/test/cases/selector_consolidation.out.css:
--------------------------------------------------------------------------------
1 | div{color:blue;position:absolute}
2 |
--------------------------------------------------------------------------------
/test/cases/declarations_consolidation.out.css:
--------------------------------------------------------------------------------
1 | .foo,a,div{background:green;color:red}
2 |
--------------------------------------------------------------------------------
/test/cases/duplicate_font_face.out.css:
--------------------------------------------------------------------------------
1 | @font-face{font-family:avenir;src:url(x)}
2 |
--------------------------------------------------------------------------------
/test/cases/empty_keyframes.out.css:
--------------------------------------------------------------------------------
1 | @keyframes shift{0%{color:blue}100%{color:red}}
2 |
--------------------------------------------------------------------------------
/test/cases/empty_rules.in.css:
--------------------------------------------------------------------------------
1 | a { color: green }
2 | div { }
3 | p { color: blue }
4 |
--------------------------------------------------------------------------------
/test/cases/font_face_compression.out.css:
--------------------------------------------------------------------------------
1 | @font-face{font-family:avenir;src:url(x)}
2 |
--------------------------------------------------------------------------------
/test/cases/selector_consolidation_2.out.css:
--------------------------------------------------------------------------------
1 | .text,div{color:blue;position:absolute}
2 |
--------------------------------------------------------------------------------
/test/cases/selector_consolidation_3.out.css:
--------------------------------------------------------------------------------
1 | .text,div{background:green;color:blue}
2 |
--------------------------------------------------------------------------------
/test/cases/at_charset_in_wrong_place.out.css:
--------------------------------------------------------------------------------
1 | @charset "utf-8";div{color:blue}a{color:red}
2 |
--------------------------------------------------------------------------------
/test/cases/none_to_zero.out.css:
--------------------------------------------------------------------------------
1 | div{background:0}a{box-shadow:none}span{-moz-outline:0}
2 |
--------------------------------------------------------------------------------
/test/cases/quotes.out.css:
--------------------------------------------------------------------------------
1 | div{color:'#ff0000';content:'x';font-family:Lucida Console}
2 |
--------------------------------------------------------------------------------
/test/cases/unquote_urls.out.css:
--------------------------------------------------------------------------------
1 | div{background:url(yes.png)}a{background:url(no.png)}
2 |
--------------------------------------------------------------------------------
/test/cases/bang_comments.out.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Hiyooo
3 | */
4 | div{color:blue}a{color:red}
5 |
--------------------------------------------------------------------------------
/test/cases/compress_selectors.out.css:
--------------------------------------------------------------------------------
1 | .a>.b{color:blue}.c+.d{color:red}.e~.f{color:yellow}
2 |
--------------------------------------------------------------------------------
/test/cases/font_face_compression.in.css:
--------------------------------------------------------------------------------
1 | @font-face { font-family: avenir; src: url('x'); }
2 |
--------------------------------------------------------------------------------
/test/cases/selector_compression.out.css:
--------------------------------------------------------------------------------
1 | div a{color:blue}.a>.b{color:green}.c .d{color:red}
2 |
--------------------------------------------------------------------------------
/test/cases/selector_consolidation.in.css:
--------------------------------------------------------------------------------
1 | div { color: blue; }
2 | div { position: absolute; }
3 |
--------------------------------------------------------------------------------
/test/cases/at_keyframes.out.css:
--------------------------------------------------------------------------------
1 | @keyframes foo{0%{color:#f00;opacity:0}100%{color:#f0f;opacity:1}}
2 |
--------------------------------------------------------------------------------
/test/cases/dupe_selectors_via_consolidation.in.css:
--------------------------------------------------------------------------------
1 | div { color: blue; }
2 | x,div { color: blue; }
3 |
--------------------------------------------------------------------------------
/test/cases/font_family.out.css:
--------------------------------------------------------------------------------
1 | div{font-family:Lucida Grande,sans-serif}span{font-family:serif}
2 |
--------------------------------------------------------------------------------
/test/cases/hello.out.css:
--------------------------------------------------------------------------------
1 | h1{color:#f00 1px}.fonts{text-shadow:0 0 20px}.foobar,div,x{color:red}
2 |
--------------------------------------------------------------------------------
/test/cases/media_query_basic_consolidation.out.css:
--------------------------------------------------------------------------------
1 | @media screen{div{color:blue}span{color:red}}
2 |
--------------------------------------------------------------------------------
/test/cases/media_query_order.out.css:
--------------------------------------------------------------------------------
1 | p{color:black}@media screen{div{color:blue}p{color:green}}
2 |
--------------------------------------------------------------------------------
/test/cases/bang_comments.in.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Hiyooo
3 | */
4 | div { color: blue }
5 | a { color: red }
6 |
--------------------------------------------------------------------------------
/test/cases/commas_in_values.out.css:
--------------------------------------------------------------------------------
1 | div{box-shadow:inset 0 1px 0 0 linear-gradient(hsla(0,0%,100%,.9))}
2 |
--------------------------------------------------------------------------------
/test/cases/page_order.in.css:
--------------------------------------------------------------------------------
1 | @page { margin:1cm; }
2 | p { color: black; }
3 | @page { padding:2cm }
4 |
--------------------------------------------------------------------------------
/test/cases/unquote_urls.in.css:
--------------------------------------------------------------------------------
1 | div { background: url('yes.png'); }
2 | a { background: url('no.png'); }
3 |
--------------------------------------------------------------------------------
/test/cases/at_charset_in_wrong_place.in.css:
--------------------------------------------------------------------------------
1 | div { color: blue; }
2 | @charset "utf-8";
3 | a { color: red; }
4 |
--------------------------------------------------------------------------------
/test/cases/page_consolidation.in.css:
--------------------------------------------------------------------------------
1 | @page {
2 | margin:1cm;
3 | }
4 | @page {
5 | padding:2cm;
6 | }
7 |
--------------------------------------------------------------------------------
/test/cases/vendor_prefix_sorting.out.css:
--------------------------------------------------------------------------------
1 | div{azimuth:0;-webkit-box-shadow:0;-moz-box-shadow:0;box-shadow:0}
2 |
--------------------------------------------------------------------------------
/test/cases/commas_in_values.in.css:
--------------------------------------------------------------------------------
1 | div { box-shadow: inset 0 1px 0 0 linear-gradient( hsla(0, 0%, 100%, .9) ); }
2 |
--------------------------------------------------------------------------------
/test/cases/font_face_punting.in.css:
--------------------------------------------------------------------------------
1 | @font-face { src: url(x) }
2 | @font-face { font-family: geneva; src: url(y) }
3 |
--------------------------------------------------------------------------------
/test/cases/parentheses.out.css:
--------------------------------------------------------------------------------
1 | .foo{background:url(x) no-repeat}a{background:linear-gradient(rgba(0,0,0,0.25))}
2 |
--------------------------------------------------------------------------------
/test/cases/keyframes_prefix_optimization.out.css:
--------------------------------------------------------------------------------
1 | @-moz-keyframes x{0%{-moz-transform:scale(0.5);transform:scale(0.5)}}
2 |
--------------------------------------------------------------------------------
/test/cases/multiple_font_face.out.css:
--------------------------------------------------------------------------------
1 | @font-face{font-family:avenir;src:url(x)}@font-face{font-family:lato;src:url(y)}
2 |
--------------------------------------------------------------------------------
/test/cases/none_to_zero.in.css:
--------------------------------------------------------------------------------
1 | div { background: none; }
2 | a { box-shadow: none; }
3 | span { -moz-outline: none; }
4 |
--------------------------------------------------------------------------------
/test/cases/quotes.in.css:
--------------------------------------------------------------------------------
1 | div {
2 | color: '#ff0000';
3 | content: 'x';
4 | font-family: 'Lucida Console';
5 | }
6 |
--------------------------------------------------------------------------------
/test/cases/reordering.out.css:
--------------------------------------------------------------------------------
1 | @keyframes boo{0%{color:blue}}@font-face{font-family:avenir;src:url(x)}div{color:blue}
2 |
--------------------------------------------------------------------------------
/test/cases/voice_family_hack.out.css:
--------------------------------------------------------------------------------
1 | #elem{voice-family:"\"}\"";voice-family:'\'}\'';voice-family:inherit;width:100px}
2 |
--------------------------------------------------------------------------------
/test/cases/compress_selectors.in.css:
--------------------------------------------------------------------------------
1 | .a > .b { color: blue; }
2 | .c + .d { color: red; }
3 | .e ~ .f { color: yellow; }
4 |
--------------------------------------------------------------------------------
/test/cases/dupe_at_charset.in.css:
--------------------------------------------------------------------------------
1 | @charset "utf-8";
2 | div { color: blue; }
3 | @charset "utf-8";
4 | a { color: red; }
5 |
--------------------------------------------------------------------------------
/test/cases/media_query_consolidation.out.css:
--------------------------------------------------------------------------------
1 | @media screen{.foo{background:red}a,div{color:green}.bar{background:yellow}}
2 |
--------------------------------------------------------------------------------
/test/cases/selector_compression.in.css:
--------------------------------------------------------------------------------
1 | div a { color: blue; }
2 | .a > .b { color: green; }
3 | .c
4 | .d { color: red; }
5 |
--------------------------------------------------------------------------------
/test/cases/selector_consolidation_3.in.css:
--------------------------------------------------------------------------------
1 | div { color: blue; }
2 | .text { color: blue; }
3 | div, .text { background: green; }
4 |
--------------------------------------------------------------------------------
/test/cases/declarations_consolidation.in.css:
--------------------------------------------------------------------------------
1 | div, a { background: green; color: red; }
2 | .foo { background: green; color: red; }
3 |
--------------------------------------------------------------------------------
/test/cases/padding.in.css:
--------------------------------------------------------------------------------
1 | div { padding: 10px 10px 10px 10px; }
2 | a { margin: 10px 3px 10px 3px; }
3 | span { padding: 1px 1px; }
4 |
--------------------------------------------------------------------------------
/test/cases/parentheses.in.css:
--------------------------------------------------------------------------------
1 | .foo { background: url(x) no-repeat; }
2 | a { background: linear-gradient( rgba( 0, 0, 0, 0.25 ) ); }
3 |
--------------------------------------------------------------------------------
/test/cases/at_keyframes_with_vendor_2.out.css:
--------------------------------------------------------------------------------
1 | @keyframes rotate{from{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(-360deg)}}
2 |
--------------------------------------------------------------------------------
/test/cases/comment.in.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Multiline
3 | */
4 | div { color: blue /* Single line */ }
5 | a { colo/* In property */r: green }
6 |
--------------------------------------------------------------------------------
/test/cases/duplicate_font_face.in.css:
--------------------------------------------------------------------------------
1 | @font-face { font-family: avenir; src: url(x); }
2 | @font-face { font-family: avenir; src: url(y); }
3 |
--------------------------------------------------------------------------------
/test/cases/font_family.in.css:
--------------------------------------------------------------------------------
1 | div {
2 | font-family: 'Lucida Grande', 'sans-serif';
3 | }
4 | span {
5 | font-family: 'serif';
6 | }
7 |
--------------------------------------------------------------------------------
/test/cases/at_keyframes_with_vendor.out.css:
--------------------------------------------------------------------------------
1 | @-webkit-keyframes rotate{from{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(-360deg)}}
2 |
--------------------------------------------------------------------------------
/test/cases/media_query_order.in.css:
--------------------------------------------------------------------------------
1 | @media screen { div { color: blue; } }
2 | p { color: black; }
3 | @media screen { p { color: green; } }
4 |
--------------------------------------------------------------------------------
/test/cases/page_pseudoclasses.out.css:
--------------------------------------------------------------------------------
1 | @page{color:blue;size:portrait}@page :first{margin-top:1cm}@page rotated{size:landscape}table{page:rotated}
--------------------------------------------------------------------------------
/test/cases/selector_consolidation_2.in.css:
--------------------------------------------------------------------------------
1 | div { color: blue; }
2 | div { position: absolute; }
3 | .text { color: blue; position: absolute; }
4 |
--------------------------------------------------------------------------------
/test/cases/dont_munge_properties.out.css:
--------------------------------------------------------------------------------
1 | div{background:-moz-linear-gradient(x);background:-webkit-linear-gradient(x);background:linear-gradient(x)}
2 |
--------------------------------------------------------------------------------
/test/cases/hello.in.css:
--------------------------------------------------------------------------------
1 | div, x { color: red; }
2 | h1 { color: #ff0000 1px; }
3 | .fonts { text-shadow: 0 0 20px; }
4 | .foobar { color: red }
5 |
--------------------------------------------------------------------------------
/test/cases/ie5_mac_hack.in.css:
--------------------------------------------------------------------------------
1 | /* Ignore the next rule in IE mac \*/
2 | .selector {
3 | color: khaki;
4 | }
5 | /* Stop ignoring in IE mac */
6 |
--------------------------------------------------------------------------------
/test/cases/reordering.in.css:
--------------------------------------------------------------------------------
1 | div { color: blue; }
2 | @font-face { font-family: avenir; src: url(x); }
3 | @keyframes boo { 0% { color: blue; } }
4 |
--------------------------------------------------------------------------------
/test/cases/vendor_prefix_sorting.in.css:
--------------------------------------------------------------------------------
1 | div {
2 | azimuth: 0;
3 | -webkit-box-shadow: 0;
4 | -moz-box-shadow: 0;
5 | box-shadow: 0;
6 | }
7 |
--------------------------------------------------------------------------------
/test/cases/media_query_basic_consolidation.in.css:
--------------------------------------------------------------------------------
1 | @media screen {
2 | div { color: blue; }
3 | }
4 | @media screen {
5 | span { color: red; }
6 | }
7 |
--------------------------------------------------------------------------------
/test/cases/at_keyframes_with_vendor_2.in.css:
--------------------------------------------------------------------------------
1 | @keyframes rotate {
2 | from{-webkit-transform:rotate(0deg)}
3 | to{-webkit-transform:rotate(-360deg)}
4 | }
5 |
--------------------------------------------------------------------------------
/test/cases/font_face_duplicates.out.css:
--------------------------------------------------------------------------------
1 | @font-face{font-family:avenir;font-weight:bold;src:url(x)}@font-face{font-family:avenir;font-weight:normal;src:url(z)}
2 |
--------------------------------------------------------------------------------
/test/cases/at_keyframes_with_vendor.in.css:
--------------------------------------------------------------------------------
1 | @-webkit-keyframes rotate {
2 | from{-webkit-transform:rotate(0deg)}
3 | to{-webkit-transform:rotate(-360deg)}
4 | }
5 |
--------------------------------------------------------------------------------
/test/cases/reordering_advanced.out.css:
--------------------------------------------------------------------------------
1 | @keyframes boo{0%{color:blue}}@-webkit-keyframes boo{0%{color:blue}}@font-face{font-family:avenir;src:url(x)}div{color:blue}
2 |
--------------------------------------------------------------------------------
/test/cases/multiple_font_face.in.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: avenir;
3 | src: url(x);
4 | }
5 | @font-face {
6 | font-family: lato;
7 | src: url(y);
8 | }
9 |
--------------------------------------------------------------------------------
/test/cases/empty_keyframes.in.css:
--------------------------------------------------------------------------------
1 | @keyframes shift {
2 | 0% {
3 | color: blue;
4 | }
5 | 50% {
6 | }
7 | 100% {
8 | color: red;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/cases/voice_family_hack.in.css:
--------------------------------------------------------------------------------
1 | #elem {
2 | width: 100px; /* IE */
3 | voice-family: "\"}\"";
4 | voice-family: '\'}\'';
5 | voice-family:inherit;
6 | }
7 |
--------------------------------------------------------------------------------
/test/cases/dont_munge_properties.in.css:
--------------------------------------------------------------------------------
1 | div {
2 | background: -moz-linear-gradient(x);
3 | background: -webkit-linear-gradient(x);
4 | background: linear-gradient(x);
5 | }
6 |
--------------------------------------------------------------------------------
/test/cases/at_keyframes.in.css:
--------------------------------------------------------------------------------
1 | @keyframes foo{
2 | 0% {
3 | color: #ff0000;
4 | opacity: 0;
5 | }
6 | 100% {
7 | color: #ff00ff;
8 | opacity: 1;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/cases/keyframes_prefix_optimization.in.css:
--------------------------------------------------------------------------------
1 | @-moz-keyframes x {
2 | 0% {
3 | -webkit-transform: scale(0.5);
4 | -moz-transform: scale(0.5);
5 | transform: scale(0.5);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/test/cases/reordering_advanced.in.css:
--------------------------------------------------------------------------------
1 | div { color: blue; }
2 | @font-face { font-family: avenir; src: url(x); }
3 | @keyframes boo { 0% { color: blue; } }
4 | @-webkit-keyframes boo { 0% { color: blue; } }
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js: ["0.10"]
3 |
4 | notifications:
5 | email:
6 | recipients:
7 | - dropbox+travis@ricostacruz.com
8 | on_success: change
9 | on_failure: change
10 |
--------------------------------------------------------------------------------
/test/cases/media_query_consolidation.in.css:
--------------------------------------------------------------------------------
1 | @media screen {
2 | div { color: green; }
3 | .foo { background: red; }
4 | }
5 | @media screen {
6 | a { color: green; }
7 | .bar { background: yellow; }
8 | }
9 |
--------------------------------------------------------------------------------
/test/cases/page_pseudoclasses.in.css:
--------------------------------------------------------------------------------
1 | @page {
2 | color: blue;
3 | size: portrait
4 | }
5 | @page :first {
6 | margin-top:1cm;
7 | }
8 | @page rotated {
9 | size: landscape
10 | }
11 | table {
12 | page: rotated;
13 | }
14 |
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | Todo
2 | ----
3 |
4 | * Remove duplicate keyframes
5 | * Remove duplicate font faces
6 | * `[type="submit"]` to `[type=submit]`
7 | * `black` to `#000`
8 | * `#f00` to `red`, `#d2b48c` to `tan`
9 | * `*.foo` to `.foo`
10 |
11 | '#f00': 'red'
12 | '#d2b48c': 'tan'
13 | '#fffafa': 'snow'
14 |
15 |
--------------------------------------------------------------------------------
/test/cases/font_face_duplicates.in.css:
--------------------------------------------------------------------------------
1 | /* The declaration [2] should be removed because it's defining the
2 | * same font as [1]. */
3 | @font-face { font-family: avenir; font-weight: bold; src: url('x'); } /* 1 */
4 | @font-face { font-family: avenir; font-weight: bold; src: url('y'); } /* 2 */
5 | @font-face { font-family: avenir; font-weight: normal; src: url('z'); }
6 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var files = process.argv.slice(2);
3 | var compress = require('../index').compress;
4 |
5 | var failed = 0;
6 |
7 | files.forEach(function(file) {
8 |
9 | var input = fs.readFileSync(file, 'utf-8');
10 | var control = fs.readFileSync(file.replace('.in.', '.out.'), 'utf-8').trim();
11 | var output = compress(input).trim();
12 |
13 | if (output != control) {
14 | console.log("==> FAIL: "+file);
15 | console.log(" expected:", control);
16 | console.log(" actual: ", output);
17 | failed += 1;
18 | }
19 | });
20 |
21 | if (failed > 0) {
22 | console.log("" + failed + " failures.");
23 | process.exit(1);
24 | } else {
25 | console.log("OK!");
26 | }
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "css-condense",
3 | "description": "CSS compressor.",
4 | "keywords": ["css", "compress", "compression", "minify", "stylesheet"],
5 | "author": "Rico Sta. Cruz",
6 | "version": "0.1.1",
7 | "engines": {
8 | "node": ">=0.4.0"
9 | },
10 | "main": "./index.js",
11 | "bin": {
12 | "cssc": "./bin/cssc"
13 | },
14 | "homepage": "http://github.com/rstacruz/css-condense",
15 | "repository": {
16 | "type": "git",
17 | "url": "https://github.com/rstacruz/css-condense.git"
18 | },
19 | "dependencies": {
20 | "css-parse": "1.1.0",
21 | "css-stringify": "1.2.0",
22 | "commander": "^2.3.0"
23 | },
24 | "scripts": {
25 | "test": "make test"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | LIDOC ?= lidoc
2 |
3 | JS_BEAUTIFIER ?= uglifyjs -b -i 2 -nm -ns
4 | JS_COMPILER ?= uglifyjs
5 |
6 | docs: \
7 | index.js
8 | $(LIDOC) $^ -o $@
9 |
10 | test: test/cases/*.in.css
11 | node test/test.js $^
12 |
13 | dist: \
14 | dist/css-condense.js \
15 | dist/css-condense.min.js
16 |
17 | dist/css-condense.raw.js:
18 | mkdir -p dist
19 | echo '(function() {' > $@
20 | echo 'var modules = {};' >> $@
21 | echo 'function require(mod) { return modules[mod]; }' >> $@
22 | #
23 | echo 'modules["debug"] = function() {};' >> $@
24 | #
25 | echo 'modules["css-parse"] = (function() { var module = {};' >> $@
26 | cat node_modules/css-parse/index.js >> $@
27 | echo 'return module.exports; })();' >> $@
28 | #
29 | echo 'modules["css-stringify"] = (function() { var module = {};' >> $@
30 | cat node_modules/css-stringify/index.js >> $@
31 | echo 'return module.exports; })();' >> $@
32 | #
33 | echo 'modules["css-condense"] = (function() { var module = {};' >> $@
34 | cat index.js >> $@
35 | echo 'return module.exports; })();' >> $@
36 | #
37 | echo 'this.CssCondense = modules["css-condense"];' >> $@
38 | echo '})();' >> $@
39 |
40 | dist/css-condense.js: dist/css-condense.raw.js
41 |
42 | dist/css-condense.min.js: dist/css-condense.js
43 |
44 | %.min.js: %.js
45 | cat $^ | $(JS_COMPILER) > $@
46 |
47 | %.js: %.raw.js
48 | cat $^ | $(JS_BEAUTIFIER) > $@
49 | rm $^
50 |
51 | clean:
52 | rm -rf dist
53 |
54 | dist.commit: dist
55 | git add dist
56 | git commit -m "Update distribution." --author "Nobody "
57 |
58 | .PHONY: test clean dist.commit
59 |
--------------------------------------------------------------------------------
/HISTORY.md:
--------------------------------------------------------------------------------
1 | v0.1.1 - Aug 2, 2014
2 | --------------------
3 |
4 | * Fixes errors about undefined rules. (#8, @benjamine)
5 |
6 | v0.1.0 - Aug 2, 2014
7 | --------------------
8 |
9 | * Update dependencies. Likely fixes many things. It now uses css-parse 1.1.0
10 | and css-stringify 1.2.0. (#6, @gbouthenot)
11 | * Fixes bootstrap compatibility. (#5)
12 |
13 | v0.0.6 - Nov 02, 2012
14 | ---------------------
15 |
16 | ### Fixed:
17 | * font-face wrongly identifying duplicates.
18 |
19 | ### Changed:
20 | * Handle @charset.
21 | * Implement --debug.
22 | * Reorder the document to put keyframes and @font-faces on top.
23 | * Update distribution.
24 | * Update stringify.
25 |
26 | v0.0.5 - Sep 05, 2012
27 | ---------------------
28 |
29 | * Fixed `@font-face` being consolidated wrong. [#2]
30 | * Fixed wrongly compressing `background:none scroll` to `background:0 scroll`.
31 | * Compress border-left (et al) none to 0.
32 | * Compress rgb() to hex code syntax.
33 | * Compress uppercase colors property (`#FFFFFF` => `#fff`).
34 | * Handle parentheses in values more effectively.
35 | * Improve selector compression.
36 | * Support minifying identifiers with the `pc` and `ex` property.
37 | * Trim off whitespaces in properties (`color : blue`).
38 | * Strip whitespaces from commas and parentheses in values better.
39 |
40 | [#2]: https://github.com/rstacruz/css-condense/issues/2
41 |
42 | v0.0.4 - Sep 03, 2012
43 | ---------------------
44 |
45 | * Fix `--safe` not turning off consolidation properly.
46 |
47 | v0.0.3 - Sep 02, 2012
48 | ---------------------
49 |
50 | * Compress margin/padding values (`10px 10px` => `10px`).
51 | * Allow calling `compress()` without options.
52 | * Always sort `font-face` after `font`.
53 | * Added a browser-compatible distribution.
54 | * Implemented many command options:
55 | - `--no-consolidate-via-declarations`
56 | - `--no-consolidate-via-selectors`
57 | - `--no-consolidate-media-queries`
58 | - `--no-sort-selectors`
59 | - `--no-sort-declarations`
60 | - `--no-compress`
61 | - `--no-sort`
62 | - `--line-breaks`
63 | - `--sort`
64 |
65 | v0.0.2 - Sep 2, 2012
66 | --------------------
67 |
68 | Initial release.
69 |
--------------------------------------------------------------------------------
/bin/cssc:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var compress = require('../index').compress;
4 | var fs = require('fs');
5 | var path = require('path');
6 |
7 | function version() {
8 | var pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
9 | return pkg.version;
10 | }
11 |
12 | function getOptions() {
13 | return require('commander')
14 | .version("css-condense version "+version())
15 | .usage("[] [options]")
16 | .option("--no-consolidate-via-declarations", "Don't consolidate rules via declarations")
17 | .option("--no-consolidate-via-selectors", "Don't consolidate rules via selectors")
18 | .option("--no-consolidate-media-queries", "Don't consolidate media queries together")
19 | .option("--no-sort-selectors", "Don't sort selectors in a rule")
20 | .option("--no-sort-declarations", "Don't sort declarations in a rule")
21 | .option("--no-compress", "Don't strip whitespaces from output")
22 | .option("--no-sort", "Turn off sorting")
23 | .option("--line-breaks", "Add linebreaks")
24 | .option("--debug", "Debug mode")
25 | .option("-S, --safe", "Don't do unsafe operations")
26 | .on('--help', function() {
27 | console.log(" The --no-sort switch turns off all sorting (ie, it implies --no-sort-*).");
28 | console.log(" The --safe switch turns off all consolidation behavior (ie, it implies --no-consolidate-*).");
29 | console.log("");
30 | console.log(" If a is not specified, input from stdin is read instead.");
31 | console.log(" Examples:");
32 | console.log("");
33 | console.log(" $ cssc style.css > style.min.css");
34 | console.log(" $ cat style.css | cssc > style.min.css");
35 | });
36 | }
37 |
38 | var options = getOptions().parse(process.argv);
39 |
40 | var input;
41 | if (options.args.length === 0) {
42 | var input = '';
43 | process.stdin.resume();
44 | process.stdin.on('data', function (chunk) { input += chunk; });
45 | process.stdin.on('end', function () { console.log(compress(input, options)); });
46 | } else {
47 | input = "";
48 | options.args.forEach(function(fname) {
49 | input += "\n" + fs.readFileSync(fname, 'utf-8');
50 | });
51 | console.log(compress(input, options));
52 | }
53 |
--------------------------------------------------------------------------------
/dist/css-condense.min.js:
--------------------------------------------------------------------------------
1 | (function(){function b(b){return a[b]}var a={};a.debug=function(){},a["css-parse"]=function(){var a={},c=b("debug")("css-parse");return a.exports=function(a){function b(){return{stylesheet:{rules:e()}}}function c(){return f(/^{\s*/)}function d(){return f(/^}\s*/)}function e(){var b,c=[];g(),h();while(a[0]!="}"&&(b=q()||r()))h(),c.push(b);return c}function f(b){var c=b.exec(a);if(!c)return;return a=a.slice(c[0].length),c}function g(){f(/^\s*/)}function h(){while(i());}function i(){if("/"==a[0]&&"*"==a[1]){var b=2;while("*"!=a[b]&&"/"!=a[b+1])++b;return b+=2,a=a.slice(b),g(),!0}}function j(){var a=f(/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|[^{])+)/);if(!a)return;var b=a[0].trim().match(/((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|[^,])+)/g);for(var c=0,d=b.length;cc(b)?1:-1}))}function p(a,b,d,e){if(c.consolidateViaDeclarations===!1)return;s("selectors","declarations","last",a,b,d,e),a.selectors=n(a.selectors)}function q(a,b,d,e){if(c.consolidateViaSelectors===!1)return;s("declarations","selectors","last",a,b,d,e),a.declarations=o(a.declarations)}function r(a,b,d,e){if(c.consolidateMediaQueries===!1)return;s("rules","media","last",a,b,d,e)}function s(a,b,c,d,e,f,g){var h=JSON.stringify(d[b]);if(c=="first")typeof g[h]!="undefined"?(g[h][a]=g[h][a].concat(d[a]),delete e[f]):g[h]=d;else{if(typeof g[h]!="undefined"){var i=g[h];d[a]=i.rule[a].concat(d[a]),delete e[i.index]}g[h]={rule:d,index:f}}}function t(a,b,c){if(a.declarations.length===0){delete b[c];return}return g(a)&&(a.selectors=n(a.selectors.map(z))),a.declarations=o(a.declarations),a.declarations.forEach(function(a){u(a)}),a}function u(a){var b=this,c=a.value;a.property=a.property.trim(),c.indexOf("'")===-1&&c.indexOf('"')===-1&&(c=c.replace(/\s*,\s*/g,",").replace(/(\(\s*)+/g,function(a){return a.replace(/\s/g,"")}).replace(/(\s*\))+/g,function(a){return a.replace(/\s/g,"")}));var d=A(a.property,c);d=d.map(function(b){return v(b,a.property,d.length)});if(a.property==="margin"||a.property==="padding")d=y(d);return a.property==="font-family"?c=d.join(","):c=d.join(" "),c=c.replace(/\s*!important$/,"!important"),a.value=c,a}function v(a,b,c){var d=["background","border","border-left","border-right","border-top","border-bottom","outline","outline-left","outline-right","outline-top","outline-bottom"],e;if(a==="none"&&d.indexOf(w(b))>-1&&c===1)return"0";e=a.match(/^url\(["'](.*?)["']\)$/);if(e)return"url("+e[1]+")";e=a.match(/^(\.?[0-9]+|[0-9]+\.[0-9]+)?(%|em|ex|in|cm|mm|pt|pc|px)$/);if(e){var f=e[1],g=e[2];return f.match(/^0*\.?0*$/)?"0":(f=f.replace(/^0+/,""),f.indexOf(".")>-1&&(f=f.replace(/0+$/,"")),f+g)}return e=a.match(/^rgb\(([0-9]+),([0-9]+),([0-9]+)\)$/i),e&&(a=x([e[1],e[2],e[3]])),a.match(/^#[0-9a-f]+$/i)?(a=a.toLowerCase(),a[1]===a[2]&&a[3]===a[4]&&a[5]===a[6]?"#"+a[1]+a[3]+a[5]:a):a}function w(a){var b=a.match(/^(?:_|\*|-[a-z]+-)(.*)$/);return b?b[1]:a}function x(a){return a=a.map(function(a){var b=parseInt(a,10).toString(16).toLowerCase();return b.length===1&&(b="0"+b),b}),"#"+a.join("")}function y(a){return a.length===4&&a[0]===a[2]&&a[1]===a[3]&&(a=[a[0],a[1]]),a.length===2&&a[0]===a[1]&&(a=[a[0]]),a}function z(a){var b=a;return b=b.replace(/\s+/g," "),b=b.replace(/ ?([\+>~]) ?/g,"$1"),b}function A(a,b){var c;return a==="font-family"?c=b.split(","):c=b.match(/"(?:\\"|.)*?"|'(?:\\'|.)*?'|[^ ]+/g),c=c.map(function(a){return a.trim()}),a==="font-family"&&(c=c.map(function(a){if(a.charAt(0)==='"'||a.charAt(0)==="'")a=a.substr(1,a.length-2);return a})),c}function B(a){return a.replace(/\/\*[\s\S]*?\*\//g,"")}function C(a){var b=[],c=a.replace(/\/\*![\s\S]*?\*\//g,function(a){return b.push(a.trim()+"\n"),""});return{comments:b,code:c}}c||(c={});var d={parse:b("css-parse"),stringify:b("css-stringify")};return c.safe===!0&&(c.consolidateMediaQueries=!1,c.consolidateViaSelectors=!1,c.consolidateViaDeclarations=!1),c.sort===!1&&(c.sortSelectors=!1,c.sortDeclarations=!1),e(a)}var a={};return a.exports={compress:c},a.exports}(),this.CssCondense=a["css-condense"]})()
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | css-condense
2 | ============
3 |
4 | Compresses CSS, and isn't conservative about it.
5 |
6 | [](https://travis-ci.org/rstacruz/css-condense)
7 |
8 | Installation
9 | ------------
10 |
11 | Install [NodeJS](http://nodejs.org/), and:
12 |
13 | $ npm install -g css-condense
14 |
15 | Usage
16 | -----
17 |
18 | $ cssc file.css > file.min.css
19 |
20 | Or via NodeJS:
21 |
22 | ``` js
23 | require('css-condense').compress("div {color: red}")
24 | ```
25 |
26 | [](https://npmjs.org/package/css-condense "View this project on npm")
27 |
28 | What it does
29 | ------------
30 |
31 | Well, it does a lot of things. The most common of which is:
32 |
33 | #### Whitespace removal
34 |
35 | It strips whitespaces. Yeah, well, every CSS compressor out there does that,
36 | right?
37 |
38 | ``` css
39 | div {
40 | color: red;
41 | width: 100%;
42 | }
43 | ```
44 |
45 | Becomes:
46 |
47 | ``` css
48 | div{color:red;width:100%}
49 | ```
50 |
51 | #### Identifier compression
52 |
53 | Some identifiers, like pixel values or colors, can be trimmed to save on space.
54 |
55 | ``` css
56 | div { color: #ff0000; }
57 | span { margin: 1px !important; }
58 | h1 { background: none; }
59 | a { padding: 0.30em; }
60 | p { font-family: "Arial Black", sans-serif; }
61 | abbr { background: url("tile.jpg"); }
62 | ```
63 |
64 | Can be: (newlines added for readability)
65 |
66 | ``` css
67 | div{color:#f00} /* Collapsing 6-digit hex colors to 3 */
68 | span{margin:1px!important} /* Strip space before !important */
69 | h1{background:0} /* Change border/background/outline 'none' to '0' */
70 | a{padding:.3em} /* Removing trailing zeroes from numbers */
71 | p{font-family: Arial Black,sans-serif} /* Font family unquoting */
72 | abbr{background:url(tile.jpg)} /* URL unquoting */
73 | ```
74 |
75 | #### More compressions
76 |
77 | ``` css
78 | ul { padding: 30px 30px 30px 30px; }
79 | li { margin: 0 auto 0 auto; }
80 | .zero { outline: 0px; }
81 | a + .b { color: blue; }
82 | .color { background: rgb(51,51,51); }
83 | ```
84 |
85 | Output:
86 |
87 | ``` css
88 | ul{padding:30px} /* Collapsing border/padding values */
89 | li{margin:0 auto} /* Same as above */,
90 | .zero{outline:0} /* Removing units from zeros */
91 | a+.b{color:blue} /* Collapse + and > in selectors */
92 | .color{background:#333} /* Converting rgb() values to hex */
93 | ```
94 |
95 | #### Keyframe compressions
96 |
97 | css-condense will trim out any unneeded vendor prefixes from keyframes.
98 |
99 | ``` css
100 | @-moz-keyframes twist {
101 | 0% {
102 | -webkit-transform: rotate(30deg);
103 | -moz-transform: rotate(30deg);
104 | -o-transform: rotate(30deg);
105 | }
106 | 100% {
107 | -webkit-transform: rotate(0);
108 | -moz-transform: rotate(0);
109 | -o-transform: rotate(0);
110 | }
111 | }
112 | ```
113 |
114 | Output:
115 |
116 | ``` css
117 | @-moz-keyframes twist{
118 | 0%{-moz-transform:rotate(30deg)}
119 | 100%{-moz-transform:rotate(0)}
120 | }
121 | ```
122 |
123 | #### Selector/declaration sorting
124 |
125 | Each rule has its selectors and declarations sorted. This may not seem like it
126 | will net any effect, but (1) it increases the likelihood that consecutive
127 | properties will be gzipped, and (2) it will help consolidation (more on that
128 | later).
129 |
130 | ``` css
131 | div, a { z-index: 10; background: green; }
132 | ```
133 |
134 | becomes:
135 |
136 | ``` css
137 | a,div{background:green;z-index:10}
138 | ```
139 |
140 | The dangerous things it does
141 | ----------------------------
142 |
143 | But that's not all! Here's where things get exciting!
144 | (Don't worry, you can turn these off with the `--safe` flag.)
145 |
146 | #### Consolidation via selectors
147 |
148 | Rules with same selectors can be consolidated.
149 |
150 | ``` css
151 | div { color: blue; }
152 | div { cursor: pointer; }
153 | ```
154 |
155 | Can be consolidated into:
156 |
157 | ``` css
158 | div{color:blue;cursor:pointer}
159 | ```
160 |
161 | #### Consolidation via definitions
162 |
163 | Rules with same definitions will be consolidated too. Great if you use
164 | mixins in your favorite CSS preprocessor mercilessly. (Those clearfixes will
165 | totally add up like crazy)
166 |
167 | ``` css
168 | div { color: blue; }
169 | p { color: blue; }
170 | ```
171 |
172 | Becomes:
173 |
174 | ``` css
175 | div,p{color:blue}
176 | ```
177 |
178 | #### Media query consolidation
179 |
180 | Rules with the same media query will be merged into one. Say:
181 |
182 | ``` css
183 | @media screen and (min-width: 780px) {
184 | div { width: 100%; }
185 | }
186 | @media screen and (min-width: 780px) {
187 | p { width: 50%; }
188 | }
189 | ```
190 |
191 | Becomes:
192 |
193 | ``` css
194 | @media screen and (min-width:780px){div{width:100%}p{width:50%}}
195 | ```
196 |
197 | Command line usage
198 | ------------------
199 |
200 | ```
201 | $ cssc --help
202 |
203 | Usage: cssc [] [options]
204 |
205 | Options:
206 |
207 | -h, --help output usage information
208 | -V, --version output the version number
209 | --no-consolidate-via-declarations Don't consolidate rules via declarations
210 | --no-consolidate-via-selectors Don't consolidate rules via selectors
211 | --no-consolidate-media-queries Don't consolidate media queries together
212 | --no-sort-selectors Don't sort selectors in a rule
213 | --no-sort-declarations Don't sort declarations in a rule
214 | --no-compress Don't strip whitespaces from output
215 | --no-sort Turn off sorting
216 | --line-breaks Add linebreaks
217 | -S, --safe Don't do unsafe operations
218 |
219 | The --no-sort switch turns off all sorting (ie, it implies --no-sort-*).
220 | The --safe switch turns off all consolidation behavior (ie, it implies --no-consolidate-*).
221 |
222 | If a is not specified, input from stdin is read instead.
223 | Examples:
224 |
225 | $ cssc style.css > style.min.css
226 | $ cat style.css | cssc > style.min.css
227 | ```
228 |
229 | Programatic usage
230 | -----------------
231 |
232 | You can use the `css-condense` NodeJS package, or you can use
233 | `dist/css-condense.js` for the browser.
234 |
235 | NodeJS:
236 |
237 | ``` javascript
238 | var cssc = require('css-condense');
239 | var str = "div { color: blue; }";
240 |
241 | cssc.compress(str);
242 | cssc.compress(str, {
243 | sortSelectors: false,
244 | lineBreaks: true
245 | });
246 | ```
247 |
248 | Or with `css-condense.js`:
249 |
250 | ``` javascript
251 | CssCondense.compress(str);
252 | ```
253 |
254 | But you'll risk breaking things!
255 | --------------------------------
256 |
257 | Well, yes. You want a safe approach? Use `--safe` or go with [YUI
258 | Compressor](http://developer.yahoo.com/yui/compressor/).
259 |
260 | But hey, css-condense tries its best to make assumptions to ensure that no
261 | breakage (or at least minimal breakage) will happen.
262 |
263 | For instance, consolidating media queries can go wrong in this case:
264 |
265 | ``` css
266 | /* Restrict height on phones */
267 | @media screen and (max-width: 480px) {
268 | .box { max-height: 10px; } /* [1] */
269 | }
270 | .box {
271 | padding: 20px; /* [2] */
272 | }
273 | /* Small screens = less spacing */
274 | @media screen and (max-width: 480px) {
275 | .box { padding: 10px; } /* [3] */
276 | }
277 | div { color: blue; }
278 | ```
279 |
280 | The two media queries have the same query, and will be subject to consolidation.
281 | However, if the `[3]` is to be consolidated into `[1]`, you will not get the
282 | effect you want.
283 |
284 | ``` css
285 | /* Bad :( */
286 | @media screen and (max-width:480px){.box{max-height:10px;padding:10px}}
287 | .box{padding:20px}
288 | div{color:blue}
289 | ```
290 |
291 | `.box`'s padding is supposed to be overridden to `10px`, which in this case,
292 | doesn't happen anymore.
293 |
294 | css-condense then makes the assumption is that media queries are usually used to
295 | override "normal" rules. The effect is that in cases like these, consolidated
296 | rels are placed at its last appearance:
297 |
298 | ``` css
299 | /* Good -- css-condense does things this way! */
300 | .box{padding:20px}
301 | @media screen and (max-width:480px){.box{max-height:10px;padding:10px}}
302 | div{color:blue}
303 | ```
304 |
305 | However, it indeed isn't perfectly safe: if you have a `max-height` rule on the
306 | regular `.box`, you're gonna have a bad time.
307 |
308 | What about with CSS rules?
309 | --------------------------
310 |
311 | css-condense also goes by the assumption that most people put their least
312 | specific things on top (like resets).
313 |
314 | ``` css
315 | body, div, h1, p { margin: 0; padding: 0; }
316 | .listing h1 { padding: 10px; }
317 | .item h1 { margin: 0; padding: 0; }
318 | ```
319 |
320 | Now if `.item` is inside `.listing`, all of these rules affect `.listing h1`.
321 | The final effect is that the `h1` will have a padding of `0`.
322 |
323 | If the consolidation puts things on top, `h1` will get a padding of `10px`. Not
324 | good.
325 |
326 | ``` css
327 | /* Bad :( */
328 | body,div,h1,p,.item h1 { margin: 0; padding: 0; }
329 | .listing h1 { padding: 10px; }
330 | ```
331 |
332 | ...which is why css-condense assumes that the more specific things are usually
333 | at the bottom. This then compresses nicely to:
334 |
335 | ``` css
336 | /* Good -- css-condense knows what's good for you. */
337 | .listing h1 { padding: 10px; }
338 | body,div,h1,p,.item h1 { margin: 0; padding: 0; }
339 | ```
340 |
341 | ...giving your H1 the right padding: `0`.
342 |
343 | How's the real-world performance?
344 | ---------------------------------
345 |
346 | I ran it through some real-world CSS files that have already been compressed,
347 | and usually get around 5% to 25% more compression out of it.
348 |
349 | Example: https://gist.github.com/3583505
350 |
351 | But gzip will compress that for you anyway!
352 | -------------------------------------------
353 |
354 | Yes, but css-condense will also reduce the number of rules (usually around 10%
355 | to 40% less rules!), which can hypothetically make page rendering faster :)
356 |
357 | Acknowledgements
358 | ----------------
359 |
360 | Special thanks to [TJ Holowaychuk] for
361 | [css-parse] which this project uses to parse CSS, and [css-stringify] which is
362 | used to build the final output.
363 |
364 | [TJ Holowaychuk]: https://github.com/visionmedia
365 | [css-parse]: https://github.com/visionmedia/node-css-parse
366 | [css-stringify]: https://github.com/visionmedia/node-css-stringify
367 |
368 | ## Thanks
369 |
370 | **css-condense** © 2012+, Rico Sta. Cruz. Released under the [MIT License].
371 | Authored and maintained by Rico Sta. Cruz with help from [contributors].
372 |
373 | > [ricostacruz.com](http://ricostacruz.com) ·
374 | > GitHub [@rstacruz](https://github.com/rstacruz) ·
375 | > Twitter [@rstacruz](https://twitter.com/rstacruz)
376 |
377 | [MIT License]: http://mit-license.org/
378 | [contributors]: http://github.com/rstacruz/css-condense/contributors
379 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | // # CssCrunch
2 | // Yes! This only has one entry point, `compress`:
3 | //
4 | // var CC = require('css_crunch')
5 | //
6 | // var output = CC.compress(string);
7 | //
8 |
9 | function compress(str, options) {
10 | options || (options = {});
11 |
12 | var css = { parse: require('css-parse'), stringify: require('css-stringify') };
13 |
14 | // Handle the `safe: true` preset.
15 | if (options.safe === true) {
16 | options.consolidateMediaQueries = false;
17 | options.consolidateViaSelectors = false;
18 | options.consolidateViaDeclarations = false;
19 | }
20 |
21 | // Handle the `sort: false` preset.
22 | if (options.sort === false) {
23 | options.sortSelectors = false;
24 | options.sortDeclarations = false;
25 | }
26 |
27 | return compressCode(str);
28 |
29 | function compressCode(str) {
30 | // Get important comments before stripping.
31 | var parts = getBangComments(str);
32 |
33 | str = parts.code;
34 |
35 | // Crappy way of accounting for the IE5/Mac hack.
36 | var i=0;
37 | str = str.replace(/\/\*[\s\S]*?\\\*\/([\s\S]+?)\/\*[\s\S]*?\*\//g, function(content) {
38 | return '#x'+i+'ie5machack{start:1}' + content + '#x'+(++i)+'ie5machack{end:1}';
39 | });
40 |
41 | // Strip comments before compressing.
42 | str = stripComments(str);
43 |
44 | //- Get the AST.
45 | var tree = css.parse(str);
46 |
47 | //- Transform the AST.
48 | transform(tree);
49 |
50 | //- Stringify using node-css-stringify.
51 | var output;
52 | if (options.compress === false) {
53 | output = css.stringify(tree).trim();
54 | } else {
55 | output = css.stringify(tree, { compress: true });
56 | }
57 |
58 | //- Heh, replace back the sentinels we made
59 | output = output
60 | .replace(/\s*#x[0-9]+ie5machack\{start:1\}\s*/g, '/*\\*/')
61 | .replace(/\s*#x[0-9]+ie5machack\{end:1\}\s*/g, '/**/');
62 |
63 | //- Add line breaks if you want.
64 | if (options.lineBreaks === true) {
65 | output = output.replace(/\}/g, "}\n");
66 | }
67 |
68 | //- Combine the bang comments with the stringified output.
69 | output = parts.comments.join("") + output;
70 |
71 | //- Debug mode? Add comments at the beginning
72 | if (options.debug) {
73 | output = "/* Transformed AST:\n" + JSON.stringify(tree, null, 2) + "\n*/\n" + output;
74 | }
75 |
76 | return output;
77 | }
78 |
79 | // ### transform
80 | // Transforms a stylesheet.
81 |
82 | function transform(tree) {
83 | context(tree.stylesheet);
84 | }
85 |
86 | // ### isStyleRule
87 | // Helper to check if a rule is a normal style rule.
88 |
89 | function isStyleRule(rule) {
90 | return ((typeof rule.declarations !== 'undefined') &&
91 | (typeof rule.selectors !== 'undefined') &&
92 | (rule.selectors[0] !== '@font-face'));
93 | }
94 |
95 | function isMediaRule(rule) {
96 | return (typeof rule.media !== 'undefined');
97 | }
98 |
99 | function isFontfaceRule(rule) {
100 | return ((typeof rule.declarations !== 'undefined') &&
101 | (typeof rule.selectors !== 'undefined') &&
102 | (rule.selectors[0] === '@font-face'));
103 | }
104 |
105 | function isKeyframesRule(rule) {
106 | return (typeof rule.keyframes !== 'undefined');
107 | }
108 |
109 | function isCharsetRule(rule) {
110 | return (typeof rule.charset !== 'undefined');
111 | }
112 |
113 |
114 | function getFontID(rule) {
115 | function get(key) {
116 | var re;
117 | rule.declarations.forEach(function(declaration, i) {
118 | if (declaration.property.trim() === key) re = declaration.value.trim();
119 | });
120 | return re;
121 | }
122 |
123 | var id = [
124 | (get('font-family') || ""),
125 | (get('font-weight') || ""),
126 | (get('font-style') || "")
127 | ].join("");
128 |
129 | return (id === "") ? null : id;
130 | }
131 |
132 | // ### context
133 | // Transforms a given tree `context`. A `context` can usually be a media query
134 | // definition, or a stylesheet itself.
135 | //
136 | // tree == {
137 | // rules: [ { StyleRule, MediaRule, KeyframeRule, FontfaceRule, ? } ]
138 | // }
139 | //
140 | function context(tree) {
141 | var mediaCache = {};
142 | var valueCache = {};
143 | var selectorCache = {};
144 |
145 | // __Pass #0__
146 |
147 | var parts = { charsets: [], keyframes: [], fonts: [], other: [] };
148 | var fonts = {};
149 |
150 | tree.rules.forEach(function(rule, i) {
151 | //- Sort everything into `parts`...
152 | if (isCharsetRule(rule)) {
153 | parts.charsets = [rule];
154 | } else if (isKeyframesRule(rule)) {
155 | parts.keyframes.push(rule);
156 | } else if (isFontfaceRule(rule)) {
157 | var fontname = getFontID(rule);
158 | if (fontname && !fonts[fontname]) {
159 | parts.fonts.push(rule);
160 | fonts[fontname] = true;
161 | }
162 | } else {
163 | parts.other.push(rule);
164 | }
165 | });
166 |
167 | //- And put them back in this particular order.
168 | tree.rules = parts.charsets.concat(parts.keyframes).concat(parts.fonts).concat(parts.other);
169 |
170 | // __Pass #1__
171 |
172 | tree.rules.forEach(function(rule, i) {
173 | //- Optimize keyframes.
174 | if (isKeyframesRule(rule)) {
175 | rule.keyframes.forEach(function(keyframe, i) {
176 | filterDeclarationsByPrefix(keyframe, rule.vendor);
177 | });
178 | }
179 |
180 | //- Consolidate media queries.
181 | if (isMediaRule(rule)) {
182 | consolidateMediaQueries(rule, tree.rules, i, mediaCache);
183 | }
184 |
185 | if (isFontfaceRule(rule) || isStyleRule(rule)) {
186 | styleRule(rule, tree.rules, i);
187 | }
188 |
189 | //- Compress selectors and declarations.
190 | //- Consolidate rules with same definitions.
191 | if (isStyleRule(rule)) {
192 | consolidateViaDeclarations(rule, tree.rules, i, valueCache);
193 | }
194 | });
195 |
196 | // __Pass #2__
197 |
198 | tree.rules.forEach(function(rule, i) {
199 | //- Consolidate rules with same selectors.
200 | if (isStyleRule(rule)) {
201 | consolidateViaSelectors(rule, tree.rules, i, selectorCache);
202 | }
203 | });
204 |
205 | // __Pass #3__
206 |
207 | valueCache = {};
208 | tree.rules.forEach(function(rule, i) {
209 | //- Consolidate rules with same definitions. Again, to account for the
210 | // updated rules in Pass #2.
211 | if (isStyleRule(rule)) {
212 | consolidateViaDeclarations(rule, tree.rules, i, valueCache);
213 | rule.selectors = undupeSelectors(rule.selectors);
214 | }
215 |
216 | //- Recurse through media queries.
217 | if (isMediaRule(rule)) {
218 | rule = context(rule);
219 | }
220 |
221 | //- Recurse through at keyframes.
222 | if (isKeyframesRule(rule)) {
223 | rule.keyframes.forEach(function(keyframe, i) {
224 | styleRule(keyframe, rule.keyframes, i);
225 | });
226 | }
227 | });
228 |
229 | // filter removed rules
230 | tree.rules = tree.rules.filter(function(rule) { return !!rule; });
231 |
232 | return tree;
233 | }
234 |
235 | // ### undupeSelectors
236 | // Removes duplicate selectors
237 |
238 | function undupeSelectors(selectors) {
239 | var cache = {}, output = [];
240 |
241 | selectors.forEach(function(selector) {
242 | if (!cache[selector]) {
243 | cache[selector] = true;
244 | output.push(selector);
245 | }
246 | });
247 |
248 | return output;
249 | }
250 |
251 | // ### sortSelectors
252 | // Sorts selectors.
253 |
254 | function sortSelectors(selectors) {
255 | if (options.sortSelectors === false) return selectors;
256 | if (selectors.length <= 1) return selectors;
257 |
258 | return selectors.sort();
259 | }
260 |
261 | // ### sortDeclarations
262 | // Sorts a given list of declarations.
263 | //
264 | // This accounts for IE hacks and vendor prefixes *[1]* to make sure that
265 | // they're after the declarations it hacks. eg, it will ensure the order of
266 | // `border: 0; *border: 0;`.
267 | //
268 | // Also, this will preserve the order of declarations with the same property.
269 | // For instance, `background: -moz-linear-gradient(); background:
270 | // -linear-gradient()` will have its order preserved.
271 |
272 | function sortDeclarations(declarations) {
273 | if (options.sortDeclarations === false) return declarations;
274 | if (declarations.length <= 1) return declarations;
275 |
276 | declarations.forEach(function(decl, i) {
277 | decl.index = i;
278 | });
279 |
280 | return declarations.sort(function(a, b) {
281 | function toIndex(decl) {
282 | var prop = unvendor(decl.property);
283 | return prop + "-" + (1000+decl.index); /* [2] */
284 | }
285 |
286 | return toIndex(a) > toIndex(b) ? 1 : -1;
287 | });
288 | }
289 |
290 | // ### consolidateViaDeclarations
291 | // Consolidate rules with same definitions.
292 | //
293 | // Takes a rule `rule` and checks if it's in `cache`. If it is, it consolidates
294 | // it and removes it from `context`/`i`.
295 |
296 | function consolidateViaDeclarations(rule, context, i, cache) {
297 | if (options.consolidateViaDeclarations === false) return;
298 |
299 | consolidate('selectors', 'declarations', 'last', rule, context, i, cache);
300 | rule.selectors = sortSelectors(rule.selectors);
301 | }
302 |
303 | // ### consolidateViaSelectors
304 | // Consolidate rules with same selectors. See `consalidateViaDeclarations` for description.
305 |
306 | function consolidateViaSelectors(rule, context, i, cache) {
307 | if (options.consolidateViaSelectors === false) return;
308 |
309 | consolidate('declarations', 'selectors', 'last', rule, context, i, cache);
310 | rule.declarations = sortDeclarations(rule.declarations);
311 | }
312 |
313 | // ### consolidateMediaQueries
314 | // Consolidate media queries
315 |
316 | function consolidateMediaQueries(rule, context, i, cache) {
317 | if (options.consolidateMediaQueries === false) return;
318 | consolidate('rules', 'media', 'last', rule, context, i, cache);
319 | }
320 |
321 | // ### consolidate
322 | // What the other consolidate thingies use.
323 | // Consolidate the rule's `what` for the other rules that has the same `via`.
324 |
325 | function consolidate(what, via, direction, rule, context, i, cache) {
326 | var value = JSON.stringify(rule[via]);
327 |
328 | if (direction == 'first') {
329 | if (typeof cache[value] !== 'undefined') {
330 | cache[value][what] =
331 | cache[value][what].concat(rule[what]);
332 |
333 | delete context[i];
334 | } else {
335 | cache[value] = rule;
336 | }
337 | }
338 |
339 | else {
340 | if (typeof cache[value] !== 'undefined') {
341 | var last = cache[value];
342 | rule[what] = last.rule[what].concat(rule[what]);
343 |
344 | delete context[last.index];
345 | }
346 | cache[value] = { rule: rule, index: i };
347 | }
348 | }
349 |
350 | // ### styleRule
351 | //
352 | // Transforms given `rule`, which is part of rules array `context` at index `i`.
353 | //
354 | // A `rule` can either be a CSS rule, or a keyframe.
355 | //
356 | // /* Rule */
357 | // rule == {
358 | // selectors: [ '.red' ]
359 | // declarations: [ { Declaration } ]
360 | // }
361 | //
362 | // /* Keyframe */
363 | // rule == {
364 | // values: [ '100%' ]
365 | // declarations: [ { Declaration } ]
366 | // }
367 |
368 | function styleRule(rule, context, i) {
369 | //- If the rule doesn't have anything, delete it.
370 | if (rule.declarations.length === 0) {
371 | delete(context[i]);
372 | return;
373 | }
374 |
375 | //- Compress its selectors.
376 | if (isStyleRule(rule)) {
377 | rule.selectors = sortSelectors(rule.selectors.map(compressSelector));
378 | }
379 |
380 | //- Sort declarations.
381 | rule.declarations = sortDeclarations(rule.declarations);
382 |
383 | //- Compress its declarations.
384 | rule.declarations.forEach(function(declaration) {
385 | compressDeclaration(declaration);
386 | });
387 |
388 | return rule;
389 | }
390 |
391 | /*
392 | { property: 'color', value: '#ff0000' }
393 | */
394 | function compressDeclaration(declaration) {
395 | var self = this;
396 | var val = declaration.value;
397 |
398 | //- Trim off whitespace in property.
399 | declaration.property = declaration.property.trim();
400 |
401 | //- Naively strip whitespaces from commas and parentheses.
402 | // Only do it if there's no quoted string in there.
403 | if ((val.indexOf("'") === -1) && (val.indexOf('"') === -1)) {
404 | val = val
405 | .replace(/\s*,\s*/g, ',')
406 | .replace(/(\(\s*)+/g, function(str) { return str.replace(/\s/g, ''); })
407 | .replace(/(\s*\))+/g, function(str) { return str.replace(/\s/g, ''); });
408 | }
409 |
410 | //- Split the values according to quotes, etc.
411 | var values = valueSplit(declaration.property, val);
412 |
413 | //- Compress each of the values if possible
414 | values = values.map(function(identifier) {
415 | return compressIdentifier(identifier, declaration.property, values.length);
416 | });
417 |
418 | //- Consolidate 10px 10px 10px 10px -> 10px.
419 | if ((declaration.property === 'margin') || (declaration.property === 'padding')) {
420 | values = compressPadding(values);
421 | }
422 |
423 | if (declaration.property === 'font-family') {
424 | val = values.join(',');
425 | } else {
426 | val = values.join(' ');
427 | }
428 |
429 | //- Strip whitespace on important
430 | val = val.replace(/\s*!important$/, '!important');
431 | declaration.value = val;
432 |
433 | return declaration;
434 | }
435 |
436 | // ### compressIdentifier
437 | // Compresses a given identifier.
438 | // Returns a string.
439 |
440 | function compressIdentifier(identifier, property, count) {
441 | var zeroableProperties = [
442 | 'background', 'border', 'border-left', 'border-right', 'border-top', 'border-bottom',
443 | 'outline', 'outline-left', 'outline-right', 'outline-top', 'outline-bottom'
444 | ];
445 |
446 | var m;
447 | //- Compress `none` to `0`.
448 | if ((identifier === 'none') && (zeroableProperties.indexOf(unvendor(property)) > -1) && (count === 1)) {
449 | return "0";
450 | }
451 |
452 | //- Remove quotes from urls.
453 | m = identifier.match(/^url\(["'](.*?)["']\)$/);
454 |
455 | if (m) {
456 | return "url(" + m[1] + ")";
457 | }
458 |
459 | //- Compress `0px` to `0`.
460 | m = identifier.match(/^(\.?[0-9]+|[0-9]+\.[0-9]+)(%|em|ex|in|cm|mm|pt|pc|px)$/);
461 | if (m) {
462 | var num = m[1];
463 | var unit = m[2];
464 |
465 | if (num.match(/^0*\.?0*$/)) {
466 | return "0";
467 | } else {
468 | num = num.replace(/^0+/, '');
469 | if (num.indexOf('.') > -1) num = num.replace(/0+$/, '');
470 | return num + unit;
471 | }
472 | }
473 |
474 | m = identifier.match(/^rgb\(([0-9]+),([0-9]+),([0-9]+)\)$/i);
475 | if (m) {
476 | identifier = rgbToHex([ m[1], m[2], m[3] ]);
477 | }
478 |
479 | //- Compress `#ff2288` to `#f28`. Also, lowercase all hex codes.
480 | if (identifier.match(/^#[0-9a-f]+$/i)) {
481 | identifier = identifier.toLowerCase();
482 | if (identifier[1] === identifier[2] && identifier[3] === identifier[4] && identifier[5] === identifier[6]) {
483 | return '#' + identifier[1] + identifier[3] + identifier[5];
484 | } else {
485 | return identifier;
486 | }
487 | }
488 |
489 | // Else, just return it.
490 | return identifier;
491 | }
492 |
493 | // ### unvendor()
494 | // Removes a vendor prefix from a property name.
495 |
496 | function unvendor(prop) {
497 | var m = prop.match(/^(?:_|\*|-[a-z]+-)(.*)$/);
498 | if (m) {
499 | return m[1];
500 | } else {
501 | return prop;
502 | }
503 | }
504 |
505 | // ### rgbToHex()
506 | // Converts a rgb triplet `rgb` to a hex string.
507 |
508 | function rgbToHex(rgb) {
509 | rgb = rgb.map(function(num) {
510 | //- "126" => "7e"
511 | var str = parseInt(num, 10).toString(16).toLowerCase();
512 | if (str.length === 1) str = "0" + str;
513 |
514 | return str;
515 | });
516 |
517 | return '#' + rgb.join("");
518 | }
519 |
520 | // ### compressPadding
521 | // Compresses padding values, eg, `10px 10px 10px 10px` => `10px`.
522 |
523 | function compressPadding(values) {
524 | //- Compress `10px 3px 10px 3px` => `10px 3px`.
525 | if ((values.length === 4) && (values[0] === values[2]) && (values[1] === values[3])) {
526 | values = [values[0], values[1]];
527 | }
528 |
529 | //- Compress `10px 10px` => `10px`.
530 | if ((values.length === 2) && (values[0] === values[1])) {
531 | values = [values[0]];
532 | }
533 |
534 | return values;
535 | }
536 |
537 | // ### compressSelector
538 | // Compresses a selector string.
539 | // Returns the compressed string.
540 |
541 | function compressSelector(selector) {
542 | var re = selector;
543 |
544 | re = re.replace(/\s+/g, ' ');
545 | re = re.replace(/ ?([\+>~]) ?/g, '$1');
546 |
547 | return re;
548 | }
549 |
550 | // ### filterDeclarationsByPrefix()
551 | // Find CSS property declarations that have vendor prefixes that don't
552 | // match the given vendor prefix, and remove them.
553 | function filterDeclarationsByPrefix(context, prefix) {
554 | if ((!prefix) || (prefix.length === 0)) return;
555 |
556 | var decls = [];
557 | context.declarations.forEach(function(decl, j) {
558 | if ((decl.property.substr(0, 1) !== '-') || (decl.property.substr(0, prefix.length) === prefix)) {
559 | decls.push(decl);
560 | }
561 | });
562 | context.declarations = decls;
563 | }
564 |
565 |
566 | // ### valueSplit
567 | // Split a value into an array. Takes a string `values`, along with the property name `prop`.
568 | //
569 | // Returns an array.
570 |
571 | function valueSplit(prop, values) {
572 | var re;
573 |
574 | //- Split accordingly. Fonts are parsed differently from others.
575 | if (prop === 'font-family') {
576 | re = values.split(',');
577 | } else {
578 | re = values.match(/"(?:\\"|.)*?"|'(?:\\'|.)*?'|[^ ]+/g);
579 | }
580 |
581 | //- Trim out surrounding whitespace.
582 | re = re.map(function(s) { return s.trim(); });
583 |
584 | // Remove the quotes from those that don't need it.
585 | if (prop === 'font-family') {
586 | re = re.map(function(value) {
587 | if ((value.charAt(0) === '"') || (value.charAt(0) === "'")) {
588 | value = value.substr(1, value.length - 2);
589 | }
590 | return value;
591 | });
592 | }
593 |
594 | return re;
595 | }
596 |
597 | // ### stripComments
598 | // Helper to remove the comments out of a string `str`.
599 |
600 | function stripComments(str) {
601 | return str.replace(/\/\*[\s\S]*?\*\//g, '');
602 | }
603 |
604 | // ### getBangComments
605 | // Returns an array of bang comments in `comments`, and the rest of the code
606 | // without the comments in `code`.
607 |
608 | function getBangComments(str) {
609 | var comments = [];
610 | var code = str.replace(/\/\*![\s\S]*?\*\//g, function(str) {
611 | comments.push(str.trim() + "\n");
612 | return '';
613 | });
614 | return { comments: comments, code: code };
615 | }
616 | }
617 |
618 | module.exports = {
619 | compress: compress
620 | };
621 |
--------------------------------------------------------------------------------
/dist/css-condense.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | var modules = {};
3 | function require(mod) {
4 | return modules[mod];
5 | }
6 | modules["debug"] = function() {};
7 | modules["css-parse"] = function() {
8 | var module = {};
9 | var debug = require("debug")("css-parse");
10 | module.exports = function(css) {
11 | function stylesheet() {
12 | return {
13 | stylesheet: {
14 | rules: rules()
15 | }
16 | };
17 | }
18 | function open() {
19 | return match(/^{\s*/);
20 | }
21 | function close() {
22 | return match(/^}\s*/);
23 | }
24 | function rules() {
25 | var node;
26 | var rules = [];
27 | whitespace();
28 | comments();
29 | while (css[0] != "}" && (node = atrule() || rule())) {
30 | comments();
31 | rules.push(node);
32 | }
33 | return rules;
34 | }
35 | function match(re) {
36 | var m = re.exec(css);
37 | if (!m) return;
38 | css = css.slice(m[0].length);
39 | return m;
40 | }
41 | function whitespace() {
42 | match(/^\s*/);
43 | }
44 | function comments() {
45 | while (comment()) ;
46 | }
47 | function comment() {
48 | if ("/" == css[0] && "*" == css[1]) {
49 | var i = 2;
50 | while ("*" != css[i] && "/" != css[i + 1]) ++i;
51 | i += 2;
52 | css = css.slice(i);
53 | whitespace();
54 | return true;
55 | }
56 | }
57 | function selector() {
58 | var m = match(/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|[^{])+)/);
59 | if (!m) return;
60 | var matches = m[0].trim().match(/((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|[^,])+)/g);
61 | for (var i = 0, len = matches.length; i < len; ++i) {
62 | matches[i] = matches[i].trim();
63 | }
64 | return matches;
65 | }
66 | function declaration() {
67 | var prop = match(/^(\*?[-\w]+)\s*/);
68 | if (!prop) return;
69 | prop = prop[0];
70 | if (!match(/^:\s*/)) return;
71 | var val = match(/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+)\s*/);
72 | if (!val) return;
73 | val = val[0].trim();
74 | match(/^[;\s]*/);
75 | return {
76 | property: prop,
77 | value: val
78 | };
79 | }
80 | function keyframe() {
81 | var m;
82 | var vals = [];
83 | while (m = match(/^(from|to|\d+%)\s*/)) {
84 | vals.push(m[1]);
85 | match(/^,\s*/);
86 | }
87 | if (!vals.length) return;
88 | return {
89 | values: vals,
90 | declarations: declarations()
91 | };
92 | }
93 | function keyframes() {
94 | var m = match(/^@([-\w]+)?keyframes */);
95 | if (!m) return;
96 | var vendor = m[1];
97 | var m = match(/^([-\w]+)\s*/);
98 | if (!m) return;
99 | var name = m[1];
100 | if (!open()) return;
101 | comments();
102 | var frame;
103 | var frames = [];
104 | while (frame = keyframe()) {
105 | frames.push(frame);
106 | comments();
107 | }
108 | if (!close()) return;
109 | return {
110 | name: name,
111 | vendor: vendor,
112 | keyframes: frames
113 | };
114 | }
115 | function media() {
116 | var m = match(/^@media *([^{]+)/);
117 | if (!m) return;
118 | var media = m[1].trim();
119 | if (!open()) return;
120 | comments();
121 | var style = rules();
122 | if (!close()) return;
123 | return {
124 | media: media,
125 | rules: style
126 | };
127 | }
128 | function atimport() {
129 | var m = match(/^@import *([^;\n]+);\s*/);
130 | if (!m) return;
131 | return {
132 | "import": m[1].trim()
133 | };
134 | }
135 | function declarations() {
136 | var decls = [];
137 | if (!open()) return;
138 | comments();
139 | var decl;
140 | while (decl = declaration()) {
141 | decls.push(decl);
142 | comments();
143 | }
144 | if (!close()) return;
145 | return decls;
146 | }
147 | function atrule() {
148 | return keyframes() || media() || atimport();
149 | }
150 | function rule() {
151 | var sel = selector();
152 | if (!sel) return;
153 | comments();
154 | return {
155 | selectors: sel,
156 | declarations: declarations()
157 | };
158 | }
159 | return stylesheet();
160 | };
161 | return module.exports;
162 | }();
163 | modules["css-stringify"] = function() {
164 | var module = {};
165 | module.exports = function(node, options) {
166 | return (new Compiler(options)).compile(node);
167 | return options.compress ? node.stylesheet.rules.map(visit(options)).join("") : node.stylesheet.rules.map(visit(options)).join("\n\n");
168 | };
169 | function Compiler(options) {
170 | options = options || {};
171 | this.compress = options.compress;
172 | }
173 | Compiler.prototype.compile = function(node) {
174 | return node.stylesheet.rules.map(this.visit.bind(this)).join(this.compress ? "" : "\n\n");
175 | };
176 | Compiler.prototype.visit = function(node) {
177 | if (node.keyframes) return this.keyframes(node);
178 | if (node.media) return this.media(node);
179 | if (node.import) return this.import(node);
180 | return this.rule(node);
181 | };
182 | Compiler.prototype.import = function(node) {
183 | return "@import " + node.import + ";";
184 | };
185 | Compiler.prototype.media = function(node) {
186 | var self = this;
187 | if (this.compress) {
188 | return "@media " + node.media + "{" + node.rules.map(this.visit.bind(this)).join("") + "}";
189 | }
190 | return "@media " + node.media + " {\n" + node.rules.map(function(node) {
191 | return " " + self.visit(node);
192 | }).join("\n\n") + "\n}";
193 | };
194 | Compiler.prototype.keyframes = function(node) {
195 | if (this.compress) {
196 | return "@" + (node.vendor || "") + "keyframes " + node.name + "{" + node.keyframes.map(this.keyframe.bind(this)).join("") + "}";
197 | }
198 | return "@" + (node.vendor || "") + "keyframes " + node.name + " {\n" + node.keyframes.map(this.keyframe.bind(this)).join("\n") + "}";
199 | };
200 | Compiler.prototype.keyframe = function(node) {
201 | var self = this;
202 | if (this.compress) {
203 | return node.values.join(",") + "{" + node.declarations.map(this.declaration.bind(this)).join(";") + "}";
204 | }
205 | return " " + node.values.join(", ") + " {\n" + node.declarations.map(function(node) {
206 | return " " + self.declaration(node);
207 | }).join(";\n") + "\n }\n";
208 | };
209 | Compiler.prototype.rule = function(node) {
210 | if (this.compress) {
211 | return node.selectors.join(",") + "{" + node.declarations.map(this.declaration.bind(this)).join(";") + "}";
212 | }
213 | return node.selectors.join(",\n") + " {\n" + node.declarations.map(this.declaration.bind(this)).join(";\n") + "\n}";
214 | };
215 | Compiler.prototype.declaration = function(node) {
216 | if (this.compress) {
217 | return node.property + ":" + node.value;
218 | }
219 | return " " + node.property + ": " + node.value;
220 | };
221 | return module.exports;
222 | }();
223 | modules["css-condense"] = function() {
224 | var module = {};
225 | function compress(str, options) {
226 | options || (options = {});
227 | var css = {
228 | parse: require("css-parse"),
229 | stringify: require("css-stringify")
230 | };
231 | if (options.safe === true) {
232 | options.consolidateMediaQueries = false;
233 | options.consolidateViaSelectors = false;
234 | options.consolidateViaDeclarations = false;
235 | }
236 | if (options.sort === false) {
237 | options.sortSelectors = false;
238 | options.sortDeclarations = false;
239 | }
240 | return compressCode(str);
241 | function compressCode(str) {
242 | var parts = getBangComments(str);
243 | str = parts.code;
244 | var i = 0;
245 | str = str.replace(/\/\*[\s\S]*?\\\*\/([\s\S]+?)\/\*[\s\S]*?\*\//g, function(content) {
246 | return "#x" + i + "ie5machack{start:1}" + content + "#x" + ++i + "ie5machack{end:1}";
247 | });
248 | str = stripComments(str);
249 | var tree = css.parse(str);
250 | transform(tree);
251 | var output;
252 | if (options.compress === false) {
253 | output = css.stringify(tree).trim();
254 | } else {
255 | output = css.stringify(tree, {
256 | compress: true
257 | });
258 | }
259 | output = output.replace(/\s*#x[0-9]+ie5machack\{start:1\}\s*/g, "/*\\*/").replace(/\s*#x[0-9]+ie5machack\{end:1\}\s*/g, "/**/");
260 | if (options.lineBreaks === true) {
261 | output = output.replace(/\}/g, "}\n");
262 | }
263 | output = parts.comments.join("") + output;
264 | return output;
265 | }
266 | function transform(tree) {
267 | context(tree.stylesheet);
268 | }
269 | function isStyleRule(rule) {
270 | return typeof rule.declarations !== "undefined" && typeof rule.selectors !== "undefined" && rule.selectors[0] !== "@font-face";
271 | }
272 | function isMediaRule(rule) {
273 | return typeof rule.media !== "undefined";
274 | }
275 | function isFontfaceRule(rule) {
276 | return typeof rule.declarations !== "undefined" && typeof rule.selectors !== "undefined" && rule.selectors[0] === "@font-face";
277 | }
278 | function isKeyframesRule(rule) {
279 | return typeof rule.keyframes !== "undefined";
280 | }
281 | function getFontName(rule) {
282 | var output;
283 | rule.declarations.forEach(function(declaration, i) {
284 | if (!output && declaration.property.trim() === "font-family") {
285 | output = declaration.value.trim();
286 | }
287 | });
288 | return output;
289 | }
290 | function context(tree) {
291 | var mediaCache = {};
292 | var valueCache = {};
293 | var selectorCache = {};
294 | var parts = {
295 | keyframes: [],
296 | fonts: [],
297 | other: []
298 | };
299 | var fonts = {};
300 | tree.rules.forEach(function(rule, i) {
301 | if (isKeyframesRule(rule)) {
302 | parts.keyframes.push(rule);
303 | } else if (isFontfaceRule(rule)) {
304 | var fontname = getFontName(rule);
305 | if (fontname && !fonts[fontname]) {
306 | parts.fonts.push(rule);
307 | fonts[fontname] = true;
308 | }
309 | } else {
310 | parts.other.push(rule);
311 | }
312 | });
313 | tree.rules = parts.keyframes.concat(parts.fonts).concat(parts.other);
314 | tree.rules.forEach(function(rule, i) {
315 | if (isMediaRule(rule)) {
316 | consolidateMediaQueries(rule, tree.rules, i, mediaCache);
317 | }
318 | if (isFontfaceRule(rule) || isStyleRule(rule)) {
319 | styleRule(rule, tree.rules, i);
320 | }
321 | if (isStyleRule(rule)) {
322 | consolidateViaDeclarations(rule, tree.rules, i, valueCache);
323 | }
324 | });
325 | tree.rules.forEach(function(rule, i) {
326 | if (isStyleRule(rule)) {
327 | consolidateViaSelectors(rule, tree.rules, i, selectorCache);
328 | }
329 | });
330 | valueCache = {};
331 | tree.rules.forEach(function(rule, i) {
332 | if (isStyleRule(rule)) {
333 | consolidateViaDeclarations(rule, tree.rules, i, valueCache);
334 | rule.selectors = undupeSelectors(rule.selectors);
335 | }
336 | if (isMediaRule(rule)) {
337 | rule = context(rule);
338 | }
339 | if (isKeyframesRule(rule)) {
340 | rule.keyframes.forEach(function(keyframe, i) {
341 | styleRule(keyframe, rule.keyframes, i);
342 | });
343 | }
344 | });
345 | return tree;
346 | }
347 | function undupeSelectors(selectors) {
348 | var cache = {}, output = [];
349 | selectors.forEach(function(selector) {
350 | if (!cache[selector]) {
351 | cache[selector] = true;
352 | output.push(selector);
353 | }
354 | });
355 | return output;
356 | }
357 | function sortSelectors(selectors) {
358 | if (options.sortSelectors === false) return selectors;
359 | if (selectors.length <= 1) return selectors;
360 | return selectors.sort();
361 | }
362 | function sortDeclarations(declarations) {
363 | if (options.sortDeclarations === false) return declarations;
364 | if (declarations.length <= 1) return declarations;
365 | declarations.forEach(function(decl, i) {
366 | decl.index = i;
367 | });
368 | return declarations.sort(function(a, b) {
369 | function toIndex(decl) {
370 | var prop = unvendor(decl.property);
371 | return prop + "-" + (1e3 + decl.index);
372 | }
373 | return toIndex(a) > toIndex(b) ? 1 : -1;
374 | });
375 | }
376 | function consolidateViaDeclarations(rule, context, i, cache) {
377 | if (options.consolidateViaDeclarations === false) return;
378 | consolidate("selectors", "declarations", "last", rule, context, i, cache);
379 | rule.selectors = sortSelectors(rule.selectors);
380 | }
381 | function consolidateViaSelectors(rule, context, i, cache) {
382 | if (options.consolidateViaSelectors === false) return;
383 | consolidate("declarations", "selectors", "last", rule, context, i, cache);
384 | rule.declarations = sortDeclarations(rule.declarations);
385 | }
386 | function consolidateMediaQueries(rule, context, i, cache) {
387 | if (options.consolidateMediaQueries === false) return;
388 | consolidate("rules", "media", "last", rule, context, i, cache);
389 | }
390 | function consolidate(what, via, direction, rule, context, i, cache) {
391 | var value = JSON.stringify(rule[via]);
392 | if (direction == "first") {
393 | if (typeof cache[value] !== "undefined") {
394 | cache[value][what] = cache[value][what].concat(rule[what]);
395 | delete context[i];
396 | } else {
397 | cache[value] = rule;
398 | }
399 | } else {
400 | if (typeof cache[value] !== "undefined") {
401 | var last = cache[value];
402 | rule[what] = last.rule[what].concat(rule[what]);
403 | delete context[last.index];
404 | }
405 | cache[value] = {
406 | rule: rule,
407 | index: i
408 | };
409 | }
410 | }
411 | function styleRule(rule, context, i) {
412 | if (rule.declarations.length === 0) {
413 | delete context[i];
414 | return;
415 | }
416 | if (isStyleRule(rule)) {
417 | rule.selectors = sortSelectors(rule.selectors.map(compressSelector));
418 | }
419 | rule.declarations = sortDeclarations(rule.declarations);
420 | rule.declarations.forEach(function(declaration) {
421 | compressDeclaration(declaration);
422 | });
423 | return rule;
424 | }
425 | function compressDeclaration(declaration) {
426 | var self = this;
427 | var val = declaration.value;
428 | declaration.property = declaration.property.trim();
429 | if (val.indexOf("'") === -1 && val.indexOf('"') === -1) {
430 | val = val.replace(/\s*,\s*/g, ",").replace(/(\(\s*)+/g, function(str) {
431 | return str.replace(/\s/g, "");
432 | }).replace(/(\s*\))+/g, function(str) {
433 | return str.replace(/\s/g, "");
434 | });
435 | }
436 | var values = valueSplit(declaration.property, val);
437 | values = values.map(function(identifier) {
438 | return compressIdentifier(identifier, declaration.property, values.length);
439 | });
440 | if (declaration.property === "margin" || declaration.property === "padding") {
441 | values = compressPadding(values);
442 | }
443 | if (declaration.property === "font-family") {
444 | val = values.join(",");
445 | } else {
446 | val = values.join(" ");
447 | }
448 | val = val.replace(/\s*!important$/, "!important");
449 | declaration.value = val;
450 | return declaration;
451 | }
452 | function compressIdentifier(identifier, property, count) {
453 | var zeroableProperties = [ "background", "border", "border-left", "border-right", "border-top", "border-bottom", "outline", "outline-left", "outline-right", "outline-top", "outline-bottom" ];
454 | var m;
455 | if (identifier === "none" && zeroableProperties.indexOf(unvendor(property)) > -1 && count === 1) {
456 | return "0";
457 | }
458 | m = identifier.match(/^url\(["'](.*?)["']\)$/);
459 | if (m) {
460 | return "url(" + m[1] + ")";
461 | }
462 | m = identifier.match(/^(\.?[0-9]+|[0-9]+\.[0-9]+)(%|em|ex|in|cm|mm|pt|pc|px)$/);
463 | if (m) {
464 | var num = m[1];
465 | var unit = m[2];
466 | if (num.match(/^0*\.?0*$/)) {
467 | return "0";
468 | } else {
469 | num = num.replace(/^0+/, "");
470 | if (num.indexOf(".") > -1) num = num.replace(/0+$/, "");
471 | return num + unit;
472 | }
473 | }
474 | m = identifier.match(/^rgb\(([0-9]+),([0-9]+),([0-9]+)\)$/i);
475 | if (m) {
476 | identifier = rgbToHex([ m[1], m[2], m[3] ]);
477 | }
478 | if (identifier.match(/^#[0-9a-f]+$/i)) {
479 | identifier = identifier.toLowerCase();
480 | if (identifier[1] === identifier[2] && identifier[3] === identifier[4] && identifier[5] === identifier[6]) {
481 | return "#" + identifier[1] + identifier[3] + identifier[5];
482 | } else {
483 | return identifier;
484 | }
485 | }
486 | return identifier;
487 | }
488 | function unvendor(prop) {
489 | var m = prop.match(/^(?:_|\*|-[a-z]+-)(.*)$/);
490 | if (m) {
491 | return m[1];
492 | } else {
493 | return prop;
494 | }
495 | }
496 | function rgbToHex(rgb) {
497 | rgb = rgb.map(function(num) {
498 | var str = parseInt(num, 10).toString(16).toLowerCase();
499 | if (str.length === 1) str = "0" + str;
500 | return str;
501 | });
502 | return "#" + rgb.join("");
503 | }
504 | function compressPadding(values) {
505 | if (values.length === 4 && values[0] === values[2] && values[1] === values[3]) {
506 | values = [ values[0], values[1] ];
507 | }
508 | if (values.length === 2 && values[0] === values[1]) {
509 | values = [ values[0] ];
510 | }
511 | return values;
512 | }
513 | function compressSelector(selector) {
514 | var re = selector;
515 | re = re.replace(/\s+/g, " ");
516 | re = re.replace(/ ?([\+>~]) ?/g, "$1");
517 | return re;
518 | }
519 | function valueSplit(prop, values) {
520 | var re;
521 | if (prop === "font-family") {
522 | re = values.split(",");
523 | } else {
524 | re = values.match(/"(?:\\"|.)*?"|'(?:\\'|.)*?'|[^ ]+/g);
525 | }
526 | re = re.map(function(s) {
527 | return s.trim();
528 | });
529 | if (prop === "font-family") {
530 | re = re.map(function(value) {
531 | if (value.charAt(0) === '"' || value.charAt(0) === "'") {
532 | value = value.substr(1, value.length - 2);
533 | }
534 | return value;
535 | });
536 | }
537 | return re;
538 | }
539 | function stripComments(str) {
540 | return str.replace(/\/\*[\s\S]*?\*\//g, "");
541 | }
542 | function getBangComments(str) {
543 | var comments = [];
544 | var code = str.replace(/\/\*![\s\S]*?\*\//g, function(str) {
545 | comments.push(str.trim() + "\n");
546 | return "";
547 | });
548 | return {
549 | comments: comments,
550 | code: code
551 | };
552 | }
553 | }
554 | module.exports = {
555 | compress: compress
556 | };
557 | return module.exports;
558 | }();
559 | this.CssCondense = modules["css-condense"];
560 | })();
561 |
--------------------------------------------------------------------------------