├── .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 | [![Status](https://travis-ci.org/rstacruz/css-condense.svg?branch=master)](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 | [![npm version](https://badge.fury.io/js/css-condense.svg)](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 | --------------------------------------------------------------------------------