Skip to content

Commit aa4ff4a

Browse files
committed
Began making a non-PhantomJS version
1 parent f0f7221 commit aa4ff4a

File tree

10 files changed

+1849
-42
lines changed

10 files changed

+1849
-42
lines changed

lib/domino-chains/extract.coffee

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

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(stylesheets)};"
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)

lib/inliner.coffee

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
async = require("async")
22
_ = require("underscore")
33
Phantom = require("./phantom")
4-
5-
extractChain = require("./chains/extract")
6-
returnChain = require("./chains/return")
4+
domino = require("domino")
5+
request = require("request")
76

87
defaultOptions =
98
# Core
@@ -16,9 +15,14 @@ defaultOptions =
1615
cssId: no
1716
cssExpose: no
1817

18+
# Experimental
19+
useDomino: no
20+
1921
# Miscellaneous
2022
ignoreSheets: []
2123
ignoreSelectors: []
24+
# ...
25+
userAgent: "CSS Inliner for node.js by Nicolai Persson"
2226

2327
module.exports = exports = (options = {}, cb) ->
2428
options = _.extend(defaultOptions, options)
@@ -27,39 +31,73 @@ module.exports = exports = (options = {}, cb) ->
2731
cb("The URL option is required both for 'faking' (i.e. for providing the HTML) and loading an actual website")
2832
return
2933

30-
# Aquire a PhantomJS instance from our pool
31-
Phantom.acquire (error, ph) ->
32-
if error
33-
Phantom.release(ph)
34-
cb(error)
35-
return
36-
37-
# Create a page
38-
ph.createPage (error, page) ->
39-
origCb = cb
40-
cb = (e, cssOrHtml) ->
41-
page.close()
34+
if options.log
35+
console.log "Inlining #{options.url}", if options.html then "(fake URL with content)" else ""
36+
37+
options.cout = (args...) ->
38+
console.log "──", args.join(" ") if options.log
39+
40+
if not options.useDomino
41+
# Aquire a PhantomJS instance from our pool
42+
Phantom.acquire (error, ph) ->
43+
if error
4244
Phantom.release(ph)
43-
origCb(e, cssOrHtml)
45+
cb(error)
46+
return
47+
48+
# Create a page
49+
ph.createPage (error, page) ->
50+
origCb = cb
51+
cb = (e, cssOrHtml) ->
52+
page.close()
53+
Phantom.release(ph)
54+
origCb(e, cssOrHtml)
4455

45-
return cb(error) if error
56+
return cb(error) if error
4657

47-
if options.log
48-
console.log "Inlining #{options.url}", if options.html then "(fake URL with content)" else ""
58+
extractChain = require("./phantom-chains/extract")
59+
for action of extractChain
60+
extractChain[action] = extractChain[action].bind(null, page, options)
4961

50-
options.cout = (args...) ->
51-
console.log "──", args.join(" ") if options.log
62+
async.waterfall _.toArray(extractChain), (error, stylesheets, finalCSS) ->
63+
return cb(error) if error
5264

65+
if options.cssOnly
66+
cb(null, usedCss)
67+
else
68+
returnChain = require("./phantom-chains/return")
69+
70+
for action of returnChain
71+
returnChain[action] = returnChain[action].bind(null, page, options, stylesheets, finalCSS)
72+
73+
async.waterfall _.toArray(returnChain), cb
74+
else
75+
proceed = (html) ->
76+
window = domino.createWindow(html)
77+
document = window.document
78+
79+
window.location.href = options.url
80+
81+
extractChain = require("./domino-chains/extract")
5382
for action of extractChain
54-
extractChain[action] = extractChain[action].bind(null, page, options)
83+
extractChain[action] = extractChain[action].bind(null, window, document, options)
5584

5685
async.waterfall _.toArray(extractChain), (error, stylesheets, finalCSS) ->
5786
return cb(error) if error
5887

5988
if options.cssOnly
6089
cb(null, usedCss)
6190
else
91+
returnChain = require("./domino-chains/return")
92+
6293
for action of returnChain
63-
returnChain[action] = returnChain[action].bind(null, page, options, stylesheets, finalCSS)
94+
returnChain[action] = returnChain[action].bind(null, document, options, stylesheets, finalCSS)
6495

6596
async.waterfall _.toArray(returnChain), cb
97+
98+
if not options.html
99+
request options.url, "User-Agent": options.userAgent, (error, response, body) ->
100+
return cb(error) if error
101+
proceed(body)
102+
else
103+
proceed(options.html)

0 commit comments

Comments
 (0)