Permalink
Browse files

Inline CSS imports, preserving media queries

  • Loading branch information...
1 parent fb4e76c commit 9edc0da33326723ef1bad300553794655f843ae2 @magnars committed Jan 9, 2014
View
6 README.md
@@ -25,10 +25,11 @@ increase.
Depending on how you use it, Optimus:
- concatenates your JavaScript and CSS files into bundles.
-- minifies your JavaScript with [UglifyJS 2](https://github.com/mishoo/UglifyJS2)
-- minifies your CSS with [CSSO](http://bem.info/tools/optimizers/csso/)
- adds cache-busters to your static asset URLs
- adds [far future Expires headers](http://developer.yahoo.com/performance/rules.html#expires)
+- minifies your JavaScript with [UglifyJS 2](https://github.com/mishoo/UglifyJS2)
+- minifies your CSS with [CSSO](http://bem.info/tools/optimizers/csso/)
+- inlines CSS imports while preserving media queries
You might also be interested in:
@@ -213,6 +214,7 @@ If you want to mix and match optimizations, here's how you do that:
(-> assets
(optimizations/minify-js-assets options)
(optimizations/minify-css-assets options)
+ (optimizations/inline-css-imports)
(optimizations/concatenate-bundles)
(optimizations/add-cache-busted-expires-headers)
(optimizations/add-last-modified-headers)))
View
19 src/optimus/assets/load_css.clj
@@ -21,7 +21,7 @@
(defn- data-url? [#^String url]
(.startsWith url "data:"))
-(defn- external-url? [#^String url]
+(defn external-url? [#^String url]
(re-matches #"^(?://|http://|https://).*" url))
(defn- url-match [[match & urls]]
@@ -48,13 +48,20 @@
(map url-match)
(map remove-url-appendages)
(remove data-url?)
- (remove external-url?)))
+ (remove external-url?)
+ (set)))
+
+(defn update-css-references [asset]
+ (let [paths (paths-in-css asset)]
+ (if (empty? paths)
+ (dissoc asset :references)
+ (assoc asset :references paths))))
(defn create-css-asset [path contents last-modified]
- (let [asset (-> (create-asset path contents
- :last-modified last-modified)
- (make-css-urls-absolute))]
- (assoc asset :references (set (paths-in-css asset)))))
+ (-> (create-asset path contents
+ :last-modified last-modified)
+ (make-css-urls-absolute)
+ (update-css-references)))
(defn load-css-asset [public-dir path]
(let [resource (existing-resource public-dir path)]
View
5 src/optimus/optimizations.clj
@@ -2,18 +2,21 @@
(:require [optimus.optimizations.add-cache-busted-expires-headers]
[optimus.optimizations.concatenate-bundles]
[optimus.optimizations.minify]
- [optimus.optimizations.add-last-modified-headers]))
+ [optimus.optimizations.add-last-modified-headers]
+ [optimus.optimizations.inline-css-imports]))
(def minify-js-assets optimus.optimizations.minify/minify-js-assets)
(def minify-css-assets optimus.optimizations.minify/minify-css-assets)
(def concatenate-bundles optimus.optimizations.concatenate-bundles/concatenate-bundles)
(def add-cache-busted-expires-headers optimus.optimizations.add-cache-busted-expires-headers/add-cache-busted-expires-headers)
(def add-last-modified-headers optimus.optimizations.add-last-modified-headers/add-last-modified-headers)
+(def inline-css-imports optimus.optimizations.inline-css-imports/inline-css-imports)
(defn all [assets options]
(-> assets
(minify-js-assets options)
(minify-css-assets options)
+ (inline-css-imports)
(concatenate-bundles)
(add-cache-busted-expires-headers)
(add-last-modified-headers)))
View
29 src/optimus/optimizations/inline_css_imports.clj
@@ -0,0 +1,29 @@
+(ns optimus.optimizations.inline-css-imports
+ (:require [clojure.string :as str]
+ [optimus.assets.load-css :refer [update-css-references external-url?]]))
+
+(def import-re #"@import (?:url)?[('\"]{1,2}([^']+?)[)'\"]{1,2} ?([^;]*);")
+
+(defn- by-path [path assets]
+ (first (filter #(= path (:path %)) assets)))
+
+(defn- inline-import-match [asset assets [match path media]]
+ (if (external-url? path)
+ (throw (Exception. "Import of external URL http://external.css in /main.css is strongly adviced against. It's a performance killer. In fact, there's no option to allow this. Use a link in your HTML instead. Open an issue if you really, really need it."))
+ (let [contents (:contents (by-path path assets))]
+ (if (empty? media)
+ contents
+ (str "@media " media " { " contents " }")))))
+
+(defn- is-css [#^String path]
+ (.endsWith path ".css"))
+
+(defn- inline-css-imports-1 [asset assets]
+ (if-not (is-css (:path asset))
+ asset
+ (-> asset
+ (assoc :contents (str/replace (:contents asset) import-re (partial inline-import-match asset assets)))
+ (update-css-references))))
+
+(defn inline-css-imports [assets]
+ (map #(inline-css-imports-1 % assets) assets))
View
2 test/optimus/assets_test.clj
@@ -106,7 +106,7 @@
["/other.css" "#id {}"]]
(->> (load-assets public-dir ["/main.css"])
(map #(select-keys % #{:path :references}))) => [{:path "/main.css" :references #{"/other.css"}}
- {:path "/other.css" :references #{}}]))
+ {:path "/other.css"}]))
(with-files [["/main.css" "#id { background: url(data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)}"]]
(fact
View
46 test/optimus/optimizations/inline_css_imports_test.clj
@@ -0,0 +1,46 @@
+(ns optimus.optimizations.inline-css-imports-test
+ (:require [optimus.optimizations.inline-css-imports :refer :all]
+ [midje.sweet :refer :all]))
+
+(fact
+ "CSS imports are inlined from the asset list, and removed from the
+ references."
+
+ (inline-css-imports [{:path "/main.css" :contents "@import '/other.css'; #id {}" :references #{"/other.css"}}
+ {:path "/other.css" :contents ".class {}"}])
+ => [{:path "/main.css" :contents ".class {} #id {}"}
+ {:path "/other.css" :contents ".class {}"}])
+
+(fact
+ "CSS imports can take many forms."
+
+ (let [assets [{:path "/main.css" :contents "@import '/other.css';"}
+ {:path "/other.css" :contents ".class {}"}]
+ expected [{:path "/main.css" :contents ".class {}"}
+ {:path "/other.css" :contents ".class {}"}]]
+
+ (inline-css-imports (assoc-in assets [0 :contents] "@import '/other.css';")) => expected
+ (inline-css-imports (assoc-in assets [0 :contents] "@import \"/other.css\";")) => expected
+ (inline-css-imports (assoc-in assets [0 :contents] "@import url('/other.css');")) => expected
+ (inline-css-imports (assoc-in assets [0 :contents] "@import url(\"/other.css\");")) => expected
+ (inline-css-imports (assoc-in assets [0 :contents] "@import url(/other.css);")) => expected))
+
+(fact
+ "Media queries are conserved."
+
+ (inline-css-imports [{:path "/main.css" :contents "@import '/other.css' screen and (orientation:landscape);"}
+ {:path "/other.css" :contents ".class {}"}])
+ => [{:path "/main.css" :contents "@media screen and (orientation:landscape) { .class {} }"}
+ {:path "/other.css" :contents ".class {}"}])
+
+(fact
+ "External URLs for @imports are not tolerated. It's disastrous for frontend performance."
+
+ (inline-css-imports [{:path "/main.css" :contents "@import 'http://external.css';"}])
+ => (throws Exception "Import of external URL http://external.css in /main.css is strongly adviced against. It's a performance killer. In fact, there's no option to allow this. Use a link in your HTML instead. Open an issue if you really, really need it."))
+
+(fact
+ "Non-CSS assets are left to their own devices."
+
+ (inline-css-imports [{:path "/main.js" :contents "@import '/other.css';"}])
+ => [{:path "/main.js" :contents "@import '/other.css';"}])
View
12 test/optimus/optimizations/minify_test.clj
@@ -72,7 +72,6 @@
(fact (minify-css "body {\n color: red;\n}") => "body{color:red}")
(fact (minify-css "body {\n color: red") => (throws Exception "Please check the validity of the CSS block starting from the line #1"))
-
(fact
"You can turn off structural optimizations."
@@ -82,6 +81,17 @@
(minify-css "body { color: red; } body { font-size: 10px; }" {:optimize-css-structure false})
=> "body{color:red}body{font-size:10px}")
+(fact
+ "CSSO doesn't mess up percentages after rgb-colors. It used to tho.
+ Don't want any regressions."
+
+ (minify-css "body { background: -webkit-linear-gradient(bottom, rgb(209,209,209) 10%, rgb(250,250,250) 55%);}")
+ => "body{background:-webkit-linear-gradient(bottom,#d1d1d1 10%,#fafafa 55%)}")
+
+#_(fact
+ "CSSO doesn't mess up media queries." ;; well, it does now, if you have multiple and statements.
+ (minify-css "@media screen and (orientation:landscape) {#id{color:red}}") => "@media screen and (orientation:landscape){#id{color:red}}"
+ (minify-css "@import 'abc.css' screen and (min-width:7) and (max-width:9);") => "@import 'abc.css' screen and (min-width:7) and (max-width:9);")
(fact
"It minifies a list of CSS assets."

0 comments on commit 9edc0da

Please sign in to comment.