Skip to content

Commit

Permalink
Inline CSS imports, preserving media queries
Browse files Browse the repository at this point in the history
  • Loading branch information
magnars committed Jan 9, 2014
1 parent fb4e76c commit 9edc0da
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 11 deletions.
6 changes: 4 additions & 2 deletions README.md
Expand Up @@ -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:

Expand Down Expand Up @@ -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)))
Expand Down
19 changes: 13 additions & 6 deletions src/optimus/assets/load_css.clj
Expand Up @@ -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]]
Expand All @@ -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)]
Expand Down
5 changes: 4 additions & 1 deletion src/optimus/optimizations.clj
Expand Up @@ -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)))
Expand Down
29 changes: 29 additions & 0 deletions 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))
2 changes: 1 addition & 1 deletion test/optimus/assets_test.clj
Expand Up @@ -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
Expand Down
46 changes: 46 additions & 0 deletions 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';"}])
12 changes: 11 additions & 1 deletion test/optimus/optimizations/minify_test.clj
Expand Up @@ -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."

Expand All @@ -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."
Expand Down

0 comments on commit 9edc0da

Please sign in to comment.