Skip to content

Commit 2371388

Browse files
committed
Merge pull request #2 from spookd/feature-no-phantomjs
New chain of command, without PhantomJS
2 parents f0f7221 + afe73b7 commit 2371388

File tree

9 files changed

+396
-78
lines changed

9 files changed

+396
-78
lines changed

README.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,20 @@ inliner({
2020
```
2121

2222
#### Options
23-
| Option | Default | Description |
24-
| ------------------- | :-------: | ---------------- |
25-
| **url** | `nil` | **Requred:** The URL to be loaded (or faked, if the HTML is specified). |
26-
| **html** | `nil` | The HTML content to use, instead of loading the URL specified. |
27-
| **cssMedia** | `nil` | By default it only processes stylesheets with media query `all`, `screen`, and those without one. Specify here which others to include. |
28-
| **cssMinify** | `false` | Can be either a boolean (`true`) or an object with parameters to pass on to the [clean-css](https://github.com/GoalSmashers/clean-css) module. |
29-
| **cssOnly** | `false` | If `true`, it'll return the CSS only. No HTML. |
30-
| **cssId** | `nil` | The `id`-attribute to be set on the `<style>`-tag. |
31-
| **cssExpose** | `false` | All the stripped stylesheets can be exposed in a JavaScript-variable, at the bottom of the `<body>`-tag, for you to use (i.e. loading the stylesheets when the page has finished loading). |
32-
| **ignoreSheets** | `nil` | An array of stylesheets to ignore. Can be either exact strings or regular expressions (or a mix of those). |
33-
| **ignoreSelectors** | `nil` | An array of selectors/rules to keep at all times. These will not be stripped. Can be either exact strings or regular expressions (or a mix of those). |
34-
| **log** | `false` | Whether or not to print out log messages. |
23+
| Option | Default | Description |
24+
| ------------------------ | :-------: | ---------------- |
25+
| **url** | `null` | **Requred:** The URL to be loaded (or faked, if the HTML is specified). |
26+
| **html** | `null` | The HTML content to use, instead of loading the URL specified. |
27+
| **cssMedia** | `null` | By default it only processes stylesheets with media query `all`, `screen`, and those without one. Specify here which others to include. |
28+
| **cssMinify** | `false` | Can be either a boolean (`true`) or an object with parameters to pass on to the [clean-css](https://github.com/GoalSmashers/clean-css) module. |
29+
| **cssOnly** | `false` | If `true`, it'll return the CSS only. No HTML. |
30+
| **cssId** | `null` | The `id`-attribute to be set on the `<style>`-tag. |
31+
| **cssExpose** | `false` | All the stripped stylesheets can be exposed in a JavaScript-variable, at the bottom of the `<body>`-tag, for you to use (i.e. loading the stylesheets when the page has finished loading). |
32+
| **ignoreSheets** | `null` | An array of stylesheets to ignore. Can be either exact strings or regular expressions (or a mix of those). |
33+
| **ignoreSelectors** | `null` | An array of selectors/rules to keep at all times. These will not be stripped. Can be either exact strings or regular expressions (or a mix of those). |
34+
| **ignoreExternalSheets** | `false` | If `true`, all external stylesheets are ignored. |
35+
| **useDomino** | `false` | **Experimental:** If `true`, [domino](https://github.com/fgnass/domino) is used instead of [PhantomJS](http://phantomjs.org). |
36+
| **log** | `false` | Whether or not to print out log messages. |
3537

3638
## Contributing or contact me
3739
Any contributions are welcome. Simply fork the repository and make a pull request describing what you added/changed/fixed.

lib/domino-chains/extract.coffee

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
async = require("async")
2+
request = require("request")
3+
css = require("css")
4+
CleanCSS = require("clean-css")
5+
_ = require("lodash")
6+
url = require("url")
7+
8+
getElementAttributes = (el, ignore...) ->
9+
attrs = {}
10+
11+
for attr of el.attributes when typeof el.attributes[attr] is "object" and typeof el.attributes[attr].data is "string"
12+
continue if ignore.indexOf(attr) isnt -1
13+
attrs[attr] = el.attributes[attr].data
14+
15+
return attrs
16+
17+
module.exports = exports =
18+
extractStylesheets: (window, document, options, next) ->
19+
options.cout "Extracting stylesheets from page"
20+
21+
media = _.union(["", "all", "screen"], options.cssMedia)
22+
links = document.querySelectorAll("link[rel='stylesheet']")
23+
sheets = []
24+
25+
for link in links
26+
attrs = getElementAttributes(link, "media", "rel")
27+
continue unless typeof attrs.href is "string"
28+
29+
# Domino bug/incorrect usage?
30+
realHref = attrs.href
31+
32+
delete attrs.href
33+
34+
sheets.push
35+
href: url.resolve(options.url, realHref)
36+
media: link.media
37+
attributes: attrs
38+
39+
urlParsed = url.parse(options.url)
40+
41+
sheets = sheets.filter (sheet) ->
42+
_.every options.ignoreSheets, (ignore) ->
43+
if ignore instanceof RegExp and ignore.test(sheet.href)
44+
options.cout "|> Ignoring stylesheet (#{sheet.href}) because of ignore-rule #{ignore.toString()}"
45+
return no
46+
47+
return sheet.href isnt ignore
48+
49+
if options.ignoreExternalSheets and urlParsed.hostname isnt url.parse(sheet.href).hostname
50+
options.cout "|> Ignoring stylesheet (#{sheet.href}) because external stylesheets are ignored"
51+
return no
52+
53+
return yes
54+
55+
sheets = sheets.filter((sheet) -> return media.indexOf(sheet.media) isnt -1)
56+
57+
next(null, sheets)
58+
59+
downloadStylesheets: (window, document, options, sheets, next) ->
60+
if not sheets or sheets.length is 0
61+
options.cout "Refusing to continue: 0 stylesheets"
62+
return next("Refusing to continue: 0 stylesheets")
63+
64+
options.cout "Downloading #{sheets.length} stylesheet(s)"
65+
66+
mapFn = (sheet, done) ->
67+
requestOptions =
68+
headers:
69+
"User-Agent": options.userAgent
70+
71+
request sheet.href, requestOptions, (error, response, body) ->
72+
if error
73+
options.cout "Failed to download stylesheet #{sheet.href}: #{error.toString()}"
74+
else if response.statusCode < 200 or response.statusCode > 399
75+
options.cout "Failed to download stylesheet #{sheet.href}: Status code not in valid range #{response.statusCode.toString()}"
76+
else
77+
options.cout "|> Stylesheet downloaded: #{sheet.href}"
78+
79+
body = "" if not error and (response.statusCode < 200 or response.statusCode > 399)
80+
81+
# Rebase url()'s
82+
body = body.replace /url\((["']|)([^"'\(\)]+)\1\)/g, (m, quote, uri) ->
83+
return "url(#{quote}#{url.resolve(sheet.href, uri)}#{quote})"
84+
85+
sheet.body = body
86+
87+
done(error, sheet)
88+
89+
async.map(sheets, mapFn, next)
90+
91+
parseStylesheets: (window, document, options, sheets, next) ->
92+
if sheets
93+
cssString = ""
94+
95+
for sheet in sheets
96+
cssString += sheet.body
97+
98+
options.cout "Parsing #{sheets.length} stylesheet(s) - #{cssString.split("\n").length} lines"
99+
100+
try
101+
styles = css.parse(cssString).stylesheet
102+
catch e
103+
if e.line
104+
line = cssString.split("\n")[e.line - 1]
105+
line = line.substring(e.column - 40, e.column) if line.length > 40 and e.column
106+
e.message = "node-css-inliner: #{e.message}\n\t -> #{line.substring(0, 40)}"
107+
108+
return next(e)
109+
110+
next(null, sheets, styles)
111+
else
112+
next("Failed to download stylesheets")
113+
114+
filter: (window, document, options, sheets, styles, next) ->
115+
options.cout "Starting to filter out rules ..."
116+
117+
filterChain = require("./filter")
118+
119+
for action of filterChain
120+
filterChain[action] = filterChain[action].bind(null, document, options, styles)
121+
122+
async.waterfall _.toArray(filterChain), (error, usedStyles) ->
123+
next(error, sheets, styles, usedStyles)
124+
125+
generateStyles: (window, document, options, sheets, styles, usedStyles, next) ->
126+
try
127+
finalCSS = css.stringify(stylesheet: rules: usedStyles)
128+
129+
if options.cssMinify isnt no
130+
finalCSS = new CleanCSS(if typeof options.cssMinify is "object" then options.cssMinify else {}).minify(finalCSS)
131+
132+
sheets = sheets.filter (sheet) ->
133+
delete sheet["body"]
134+
return yes
135+
136+
next(null, sheets, finalCSS)
137+
catch e
138+
next(error)
139+
140+

lib/domino-chains/filter.coffee

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
async = require("async")
2+
css = require("css")
3+
_ = require("lodash")
4+
5+
dePseudify = (->
6+
ignoredPseudos = [
7+
#/* link */
8+
':link', ':visited',
9+
#/* user action */
10+
':hover', ':active', ':focus',
11+
#/* UI element states */
12+
':enabled', ':disabled', ':checked', ':indeterminate',
13+
#/* pseudo elements */
14+
'::first-line', '::first-letter', '::selection', '::before', '::after',
15+
#/* CSS2 pseudo elements */
16+
':before', ':after'
17+
]
18+
19+
regex = new RegExp(ignoredPseudos.join('|'), 'g');
20+
21+
return (selector) ->
22+
return selector.replace(regex, "")
23+
)()
24+
25+
module.exports = exports =
26+
findSelectorsUsed: (document, options, styles, next, test) ->
27+
options.cout "|> Finding selectors used", test
28+
29+
filterUsed = (rules, cb, recursive = no) ->
30+
concatFn = (rule, done) ->
31+
if rule.type is "rule"
32+
return done(null, rule.selectors)
33+
else if rule.type is "media"
34+
return filterUsed(rule.rules, done, yes)
35+
36+
return done(null, [])
37+
38+
filterFn = (error, selectors) ->
39+
return cb(error, selectors) if recursive
40+
41+
selectors = selectors.filter (selector) ->
42+
try
43+
return yes if document.querySelector(selector)
44+
catch e
45+
return yes
46+
47+
cb(null, selectors)
48+
49+
return async.concat(rules, concatFn, filterFn)
50+
51+
filterUsed styles.rules, (error, usedSelectors) ->
52+
next(null, usedSelectors)
53+
54+
removeUnusedRules: (document, options, styles, usedSelectors, next) ->
55+
options.cout "|> Removing unused selectors"
56+
selectorsToIgnore = options.ignoreSelectors
57+
58+
filterEmptyRules = (rules) ->
59+
return rules.filter (rule) ->
60+
return rule.selectors.length isnt 0 if rule.type is "rule"
61+
62+
if rule.type is "media"
63+
rule.rules = filterEmptyRules(rule.rules)
64+
return rule.rules.length isnt 0
65+
66+
return yes
67+
68+
filterSelectors = (selectors) ->
69+
return selectors.filter (selector) ->
70+
return yes if selector[0] is "@"
71+
72+
for ignoreRule in selectorsToIgnore
73+
return ignoreRule.test(selector) if ignoreRule instanceof RegExp
74+
return yes if ignoreRule is selector
75+
76+
return usedSelectors.indexOf(selector) isnt -1
77+
78+
filterRules = (rules) ->
79+
for rule in rules
80+
if rule.type is "rule"
81+
rule.selectors = filterSelectors(rule.selectors)
82+
else if rule.type is "media"
83+
rule.rules = filterRules(rule.rules)
84+
85+
return filterEmptyRules(rules)
86+
87+
next(null, filterRules(styles.rules))

lib/domino-chains/return.coffee

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
module.exports = exports =
2+
exposeStylesheets: (document, options, sheets, finalCSS, next) ->
3+
return next(null) if not options.cssExpose or typeof options.cssExpose isnt "string"
4+
5+
name = options.cssExpose
6+
7+
script = document.createElement("script")
8+
script.innerHTML = "#{name} = #{JSON.stringify(sheets)};"
9+
script.innerHTML = "var " + script.innerHTML if name.indexOf(".") < 0
10+
document.getElementsByTagName("body")[0].appendChild(script)
11+
12+
next(null)
13+
14+
removeStylesheetsAndInjectUsedStyles: (document, options, sheets, finalCSS, next) ->
15+
links = document.querySelectorAll("link[rel='stylesheet']")
16+
linkForHref = (href) ->
17+
for link in links
18+
return link if link.href is href
19+
20+
for sheet in sheets
21+
el = linkForHref(sheet.href)
22+
continue unless el
23+
24+
if first?
25+
el.parentNode.removeChild(el)
26+
else
27+
first = el
28+
29+
style = document.createElement("style")
30+
style.setAttribute("type", "text/css")
31+
style.setAttribute("id", options.cssId) if options.cssId? and typeof options.cssId is "string"
32+
style.innerHTML = finalCSS
33+
34+
if first?
35+
first.parentNode.replaceChild(style, first)
36+
else
37+
document.getElementsByTagName("head")[0].appendChild(style)
38+
39+
next(null, document.documentElement.outerHTML)

0 commit comments

Comments
 (0)