eyeliner 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +1 -0
- data/lib/eyeliner.rb +94 -21
- data/lib/eyeliner/version.rb +2 -2
- data/spec/eyeliner_spec.rb +123 -7
- data/spec/spec_helper.rb +16 -1
- metadata +4 -4
data/.gitignore
CHANGED
data/lib/eyeliner.rb
CHANGED
@@ -1,44 +1,117 @@
|
|
1
1
|
require 'nokogiri'
|
2
2
|
require 'css_parser'
|
3
3
|
|
4
|
-
|
4
|
+
class Eyeliner
|
5
5
|
|
6
|
-
|
6
|
+
def initialize(attributes = {})
|
7
|
+
@css = attributes[:css] || ""
|
8
|
+
@stylesheet_base = attributes[:stylesheet_base]
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_accessor :stylesheet_base
|
12
|
+
|
13
|
+
StyleRule = Struct.new(:declarations, :specificity) do
|
14
|
+
|
15
|
+
def <=>(other)
|
16
|
+
specificity <=> other.specificity
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
declarations
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_css(css)
|
26
|
+
@css << css
|
27
|
+
end
|
28
|
+
|
29
|
+
def css
|
30
|
+
@css.dup
|
31
|
+
end
|
32
|
+
|
33
|
+
def apply_to(input)
|
34
|
+
Application.new(self, input).apply
|
35
|
+
end
|
7
36
|
|
8
|
-
|
37
|
+
# encapsulates the application of CSS to some HTML input
|
38
|
+
class Application
|
9
39
|
|
10
|
-
def initialize
|
11
|
-
@
|
40
|
+
def initialize(eyeliner, input)
|
41
|
+
@eyeliner = eyeliner
|
42
|
+
@input = input
|
43
|
+
@css = @eyeliner.css
|
12
44
|
end
|
13
45
|
|
14
|
-
|
46
|
+
def apply
|
47
|
+
parse_input
|
48
|
+
extract_stylesheets
|
49
|
+
parse_css
|
50
|
+
map_styles_to_elements
|
51
|
+
apply_styles_to_elements
|
52
|
+
@doc.to_html
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
15
56
|
|
16
|
-
|
17
|
-
|
57
|
+
def parse_input
|
58
|
+
@doc = case @input
|
59
|
+
when Nokogiri::XML::Node
|
60
|
+
@input
|
61
|
+
else
|
62
|
+
Nokogiri::HTML.parse(@input)
|
18
63
|
end
|
64
|
+
end
|
19
65
|
|
20
|
-
|
21
|
-
|
66
|
+
def extract_stylesheets
|
67
|
+
@doc.css("style, link[rel=stylesheet][type='text/css']").each do |element|
|
68
|
+
case element.name
|
69
|
+
when "style"
|
70
|
+
@css << element.content
|
71
|
+
when "link"
|
72
|
+
@css << read_stylesheet(element["href"])
|
73
|
+
end
|
74
|
+
element.remove
|
22
75
|
end
|
76
|
+
end
|
23
77
|
|
78
|
+
def read_stylesheet(name)
|
79
|
+
full_path = File.join(@eyeliner.stylesheet_base, name)
|
80
|
+
File.read(full_path)
|
24
81
|
end
|
25
82
|
|
26
|
-
def
|
27
|
-
|
28
|
-
css_parser
|
29
|
-
|
30
|
-
styles_by_element = Hash.new do |h,k|
|
83
|
+
def parse_css
|
84
|
+
@css_parser = CssParser::Parser.new
|
85
|
+
@css_parser.add_block!(@css)
|
86
|
+
@styles_by_element = Hash.new do |h,k|
|
31
87
|
h[k] = []
|
32
88
|
end
|
33
|
-
|
34
|
-
|
35
|
-
|
89
|
+
end
|
90
|
+
|
91
|
+
def map_styles_to_elements
|
92
|
+
@css_parser.each_selector do |selector, declarations, specificity|
|
93
|
+
@doc.css(selector, PsuedoClassHandler.new).each do |element|
|
94
|
+
@styles_by_element[element] << StyleRule.new(declarations, specificity)
|
36
95
|
end
|
37
96
|
end
|
38
|
-
|
39
|
-
|
97
|
+
end
|
98
|
+
|
99
|
+
def apply_styles_to_elements
|
100
|
+
@styles_by_element.each do |element, rules|
|
101
|
+
parts = rules.sort
|
102
|
+
parts.push(element["style"]) if element["style"]
|
103
|
+
element["style"] = parts.join(" ")
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
class PsuedoClassHandler
|
110
|
+
|
111
|
+
%w(visited active hover focus).each do |psuedo_class|
|
112
|
+
define_method(psuedo_class) do |node_set|
|
113
|
+
[]
|
40
114
|
end
|
41
|
-
fragment.to_html
|
42
115
|
end
|
43
116
|
|
44
117
|
end
|
data/lib/eyeliner/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
|
2
|
-
VERSION = "0.0.
|
1
|
+
class Eyeliner
|
2
|
+
VERSION = "0.0.2"
|
3
3
|
end
|
data/spec/eyeliner_spec.rb
CHANGED
@@ -1,15 +1,25 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe Eyeliner
|
3
|
+
describe Eyeliner do
|
4
4
|
|
5
|
-
let(:eyeliner) { Eyeliner
|
5
|
+
let(:eyeliner) { Eyeliner.new }
|
6
|
+
|
7
|
+
def parse_fragment(html)
|
8
|
+
Nokogiri::HTML.fragment(html)
|
9
|
+
end
|
6
10
|
|
7
11
|
def should_not_modify(input)
|
8
|
-
eyeliner.
|
12
|
+
eyeliner.apply_to(parse_fragment(input)).should == input
|
9
13
|
end
|
10
14
|
|
11
15
|
def should_modify(input, options)
|
12
|
-
eyeliner.
|
16
|
+
eyeliner.apply_to(parse_fragment(input)).should == options[:to]
|
17
|
+
end
|
18
|
+
|
19
|
+
it "can be initialized with attributes" do
|
20
|
+
eyeliner = Eyeliner.new(:stylesheet_base => "STYLESHEETS", :css => "SOME CSS")
|
21
|
+
eyeliner.stylesheet_base.should == "STYLESHEETS"
|
22
|
+
eyeliner.css.should == "SOME CSS"
|
13
23
|
end
|
14
24
|
|
15
25
|
context "with no CSS" do
|
@@ -39,7 +49,7 @@ describe Eyeliner::Inliner do
|
|
39
49
|
context "with some explicit CSS" do
|
40
50
|
|
41
51
|
before do
|
42
|
-
eyeliner.
|
52
|
+
eyeliner.add_css %{
|
43
53
|
.box { border: 1px solid green; }
|
44
54
|
.small { font-size: 8px; }
|
45
55
|
}
|
@@ -66,14 +76,15 @@ describe Eyeliner::Inliner do
|
|
66
76
|
:to => %(<p class="small box" style="border: 1px solid green; font-size: 8px;">xyz</p>)
|
67
77
|
end
|
68
78
|
|
79
|
+
|
69
80
|
end
|
70
81
|
|
71
82
|
end
|
72
83
|
|
73
|
-
context "where CSS rules
|
84
|
+
context "where multiple CSS rules apply" do
|
74
85
|
|
75
86
|
before do
|
76
|
-
eyeliner.
|
87
|
+
eyeliner.add_css %{
|
77
88
|
p { color: red; }
|
78
89
|
p.small { text-decoration: underline; }
|
79
90
|
.small { font-size: 8px; }
|
@@ -87,6 +98,111 @@ describe Eyeliner::Inliner do
|
|
87
98
|
:to => %(<p class="small" style="color: red; font-size: 8px; text-decoration: underline;">xyz</p>)
|
88
99
|
end
|
89
100
|
|
101
|
+
it "retains contents of existing style attribute" do
|
102
|
+
should_modify %(<p style="border: 1px solid blue;">xyz</p>),
|
103
|
+
:to => %(<p style="color: red; border: 1px solid blue;">xyz</p>)
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
%w(visited active hover focus).each do |pseudo_class|
|
111
|
+
context "when CSS rule contains pseudo-class :#{pseudo_class}" do
|
112
|
+
|
113
|
+
before do
|
114
|
+
eyeliner.add_css %{
|
115
|
+
a:#{pseudo_class} { color: red; }
|
116
|
+
}
|
117
|
+
end
|
118
|
+
|
119
|
+
it "is ignored" do
|
120
|
+
should_not_modify("<a>xyz</a>")
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
context "when the document contains a <style> block" do
|
127
|
+
|
128
|
+
before do
|
129
|
+
@input = <<-HTML
|
130
|
+
<html>
|
131
|
+
<head>
|
132
|
+
<style>
|
133
|
+
strong { text-decoration: underline; }
|
134
|
+
</style>
|
135
|
+
</head>
|
136
|
+
<body>
|
137
|
+
<p>
|
138
|
+
Feeling <strong>STRONG</strong>.
|
139
|
+
</p>
|
140
|
+
</body>
|
141
|
+
</html>
|
142
|
+
HTML
|
143
|
+
end
|
144
|
+
|
145
|
+
describe "#apply_to" do
|
146
|
+
|
147
|
+
before do
|
148
|
+
@output = eyeliner.apply_to(@input)
|
149
|
+
@output_doc = Nokogiri::HTML.fragment(@output)
|
150
|
+
end
|
151
|
+
|
152
|
+
it "inlines the styles" do
|
153
|
+
strong_element = @output_doc.css("strong").first
|
154
|
+
strong_element["style"].should == "text-decoration: underline;"
|
155
|
+
end
|
156
|
+
|
157
|
+
it "removes the <style> element" do
|
158
|
+
@output_doc.css("style").should be_empty
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
|
165
|
+
context "when the document contains a linked stylesheet" do
|
166
|
+
|
167
|
+
before do
|
168
|
+
|
169
|
+
$tmp_dir.mkdir
|
170
|
+
($tmp_dir + "styles.css").open("w") do |css_io|
|
171
|
+
css_io.puts <<-CSS
|
172
|
+
.email h1 { text-decoration: underline; }
|
173
|
+
CSS
|
174
|
+
end
|
175
|
+
|
176
|
+
@input = <<-HTML
|
177
|
+
<html>
|
178
|
+
<head>
|
179
|
+
<link rel="stylesheet" href="styles.css" type="text/css" />
|
180
|
+
</head>
|
181
|
+
<body class="email">
|
182
|
+
<h1>Hello</h1>
|
183
|
+
</body>
|
184
|
+
</html>
|
185
|
+
HTML
|
186
|
+
|
187
|
+
end
|
188
|
+
|
189
|
+
describe "#apply_to" do
|
190
|
+
|
191
|
+
before do
|
192
|
+
eyeliner.stylesheet_base = $tmp_dir.to_s
|
193
|
+
@output = eyeliner.apply_to(@input)
|
194
|
+
@output_doc = Nokogiri::HTML.fragment(@output)
|
195
|
+
end
|
196
|
+
|
197
|
+
it "inlines the styles" do
|
198
|
+
h1_element = @output_doc.css("h1").first
|
199
|
+
h1_element["style"].should == "text-decoration: underline;"
|
200
|
+
end
|
201
|
+
|
202
|
+
it "removes the <link> element" do
|
203
|
+
@output_doc.css("link").should be_empty
|
204
|
+
end
|
205
|
+
|
90
206
|
end
|
91
207
|
|
92
208
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,3 +1,18 @@
|
|
1
1
|
require 'rspec'
|
2
2
|
|
3
|
-
require 'eyeliner'
|
3
|
+
require 'eyeliner'
|
4
|
+
|
5
|
+
require 'pathname'
|
6
|
+
|
7
|
+
$project_dir = Pathname(__FILE__).expand_path.parent.parent
|
8
|
+
$tmp_dir = $project_dir + "tmp"
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
|
12
|
+
config.before(:each) do
|
13
|
+
if $tmp_dir.exist?
|
14
|
+
$tmp_dir.rmtree
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: eyeliner
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.0.
|
5
|
+
version: 0.0.2
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Mike Williams
|
@@ -11,7 +11,7 @@ autorequire:
|
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
13
|
|
14
|
-
date: 2011-07-
|
14
|
+
date: 2011-07-18 00:00:00 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: nokogiri
|
@@ -67,7 +67,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
67
67
|
requirements:
|
68
68
|
- - ">="
|
69
69
|
- !ruby/object:Gem::Version
|
70
|
-
hash:
|
70
|
+
hash: -1764815189923101084
|
71
71
|
segments:
|
72
72
|
- 0
|
73
73
|
version: "0"
|
@@ -76,7 +76,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
76
76
|
requirements:
|
77
77
|
- - ">="
|
78
78
|
- !ruby/object:Gem::Version
|
79
|
-
hash:
|
79
|
+
hash: -1764815189923101084
|
80
80
|
segments:
|
81
81
|
- 0
|
82
82
|
version: "0"
|