css_dryer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,333 @@
1
+ # Lifted from Rails.
2
+ # "", " ", nil, [], and {} are blank
3
+ class Object #:nodoc:
4
+ def blank?
5
+ if respond_to?(:empty?) && respond_to?(:strip)
6
+ empty? or strip.empty?
7
+ elsif respond_to?(:empty?)
8
+ empty?
9
+ else
10
+ !self
11
+ end
12
+ end
13
+ end
14
+
15
+
16
+ # Converts DRY stylesheets into normal CSS ones.
17
+ module CssDryer
18
+ module Processor
19
+
20
+ VERSION = '0.4.3'
21
+
22
+ # Converts a stylesheet with nested styles into a flattened,
23
+ # normal CSS stylesheet. The original whitespace is preserved
24
+ # as much as possible.
25
+ #
26
+ # For example, the following DRY stylesheet:
27
+ #
28
+ # div {
29
+ # font-family: Verdana;
30
+ # #content {
31
+ # background-color: green;
32
+ # p { color: red; }
33
+ # }
34
+ # }
35
+ #
36
+ # is converted into this CSS:
37
+ #
38
+ # div {
39
+ # font-family: Verdana;
40
+ # }
41
+ # div #content {
42
+ # background-color: green;
43
+ # }
44
+ # div #content p { color: red; }
45
+ #
46
+ # Note, though, that @media blocks are preserved. For example:
47
+ #
48
+ # @media screen, projection {
49
+ # div {font-size:100%;}
50
+ # }
51
+ #
52
+ # is left unchanged.
53
+ #
54
+ # Styles may be nested to an arbitrary level.
55
+ def process(nested_css, indent = 2) #:doc:
56
+ # 'Normalise' comma separated selectors
57
+ nested_css = factor_out_comma_separated_selectors(nested_css, indent)
58
+ structure_to_css(nested_css_to_structure(nested_css), indent)
59
+ end
60
+
61
+ def nested_css_to_structure(css) #:nodoc:
62
+ # Implementation notes:
63
+ # - the correct way to do this would be using a lexer and parser
64
+ # - ironically there is a degree of repetition here
65
+ document = []
66
+ selectors = []
67
+ media_block = false
68
+ css.each_line do |line|
69
+ depth = selectors.length
70
+ case line.chomp!
71
+ # Media block (multiline) opening - treat as plain text but start
72
+ # watching for close of media block.
73
+ # Assume media blocks are never themselves nested.
74
+ # (This must precede the multiline selector condition.)
75
+ when /^(\s*@media.*)[{]\s*$/
76
+ media_block = true
77
+ document << line if depth == 0
78
+ # Media block inline
79
+ # Assume media blocks are never themselves nested.
80
+ when /^\s*@media.*[{].*[}]\s*$/
81
+ document << line if depth == 0
82
+ # Multiline selector opening
83
+ when /^\s*([^{]*?)\s*[{]\s*$/
84
+ hsh = StyleHash[ $1 => [] ]
85
+ hsh.multiline = true
86
+ if depth == 0
87
+ document << hsh
88
+ else
89
+ prev_hsh = selectors.last
90
+ prev_hsh.value << hsh
91
+ end
92
+ selectors << hsh
93
+ # Neither opening nor closing - 'plain text'
94
+ when /^[^{}]*$/
95
+ if depth == 0
96
+ document << line
97
+ else
98
+ hsh = selectors.last
99
+ hsh.value << (depth == 1 ? line : line.strip)
100
+ end
101
+ # Multiline selector closing
102
+ when /^([^{]*)[}]\s*$/
103
+ if media_block
104
+ media_block = false
105
+ if depth == 0
106
+ document << line
107
+ else
108
+ hsh = selectors.last
109
+ hsh.value << line
110
+ end
111
+ else
112
+ selectors.pop
113
+ end
114
+ # Inline selector
115
+ when /^([^{]*?)\s*[{]([^}]*)[}]\s*$/
116
+ key = (depth == 0 ? $1 : $1.strip)
117
+ hsh = StyleHash[ key => [ $2 ] ]
118
+ if depth == 0
119
+ document << hsh
120
+ else
121
+ prev_hsh = selectors.last
122
+ prev_hsh.value << hsh
123
+ end
124
+ end
125
+ end
126
+ document
127
+ end
128
+
129
+ def structure_to_css(structure, indent = 2) #:nodoc:
130
+ # Implementation note: the recursion and the formatting
131
+ # ironically both feel repetitive; DRY them.
132
+ indentation = ' ' * indent
133
+ css = ''
134
+ structure.each do |elem|
135
+ # Top-level hash
136
+ if elem.kind_of? StyleHash
137
+ set_asides = []
138
+ key = elem.key
139
+ if elem.has_non_style_hash_children
140
+ css << "#{key} {"
141
+ css << (elem.multiline ? "\n" : '')
142
+ end
143
+ elem.value.each do |v|
144
+ # Nested hash, depth = 1
145
+ if v.kind_of? StyleHash
146
+ # Set aside
147
+ set_asides << set_aside(combo_key(key, v.key), v.value, v.multiline)
148
+ else
149
+ unless v.blank?
150
+ css << (elem.multiline ? "#{v}" : v)
151
+ css << (elem.multiline ? "\n" : '')
152
+ end
153
+ end
154
+ end
155
+ css << "}\n" if elem.has_non_style_hash_children
156
+ # Now write out the styles that were nested in the above hash
157
+ set_asides.flatten.each { |hsh|
158
+ next unless hsh.has_non_style_hash_children
159
+ css << "#{hsh.key} {"
160
+ css << (hsh.multiline ? "\n" : '')
161
+ hsh.value.each { |item|
162
+ unless item.blank?
163
+ css << (hsh.multiline ? "#{indentation}#{item}" : item)
164
+ css << (hsh.multiline ? "\n" : '')
165
+ end
166
+ }
167
+ css << "}\n"
168
+ }
169
+ set_asides.clear
170
+ else
171
+ css << "#{elem}\n"
172
+ end
173
+ end
174
+ css
175
+ end
176
+
177
+ private
178
+
179
+ def set_aside(key, value, multiline) #:nodoc:
180
+ flattened = []
181
+ hsh = StyleHash[ key => [] ]
182
+ hsh.multiline = multiline
183
+ flattened << hsh
184
+ value.each { |val|
185
+ if val.kind_of? StyleHash
186
+ flattened << set_aside(combo_key(key, val.key), val.value, val.multiline)
187
+ else
188
+ hsh[key] << val
189
+ end
190
+ }
191
+ flattened
192
+ end
193
+
194
+ def combo_key(branch, leaf) #:nodoc:
195
+ (leaf =~ /\A[.:#\[]/) ? "#{branch}#{leaf}" : "#{branch} #{leaf}"
196
+ end
197
+
198
+ def factor_out_comma_separated_selectors(css, indent = 2) #:nodoc:
199
+ # TODO: replace with a nice regex
200
+ commas = false
201
+ css.each_line do |line|
202
+ next if line =~ /@media/
203
+ next if line =~ /,.*;\s*$/ # allow comma separated style values
204
+ commas = true if line =~ /,/
205
+ end
206
+ return css unless commas
207
+
208
+ state_machine = StateMachine.new indent
209
+ css.each_line { |line| state_machine.act_on line }
210
+ factor_out_comma_separated_selectors state_machine.result
211
+ end
212
+
213
+
214
+ class StateMachine #:nodoc:
215
+ def initialize(indent = 2)
216
+ @state = 'flow'
217
+ @depth = 0
218
+ @output = []
219
+ @indent = ' ' * indent
220
+ end
221
+ def result
222
+ @output.join
223
+ end
224
+ def act_on(input)
225
+ # Implementation notes:
226
+ # - the correct way to do this would be to use a lexer and parser
227
+ if @state.eql? 'flow'
228
+ case input
229
+ when %r{/[*]} # open comment
230
+ @state = 'reading_comments'
231
+ act_on input
232
+ when /^[^,]*$/ # no commas
233
+ @output << input
234
+ when /,.*;\s*$/ # comma separated style values
235
+ @output << input
236
+ when /@media/ # @media block
237
+ @output << input
238
+ when /,/ # commas
239
+ @state = 'reading_selectors'
240
+ @selectors = []
241
+ @styles = []
242
+ act_on input
243
+ end
244
+ return
245
+
246
+ elsif @state.eql? 'reading_comments'
247
+ # Dodgy hack: remove commas from comments so that the
248
+ # factor_out_comma_separated_selectors method doesn't
249
+ # go into an infinite loop.
250
+ @output << input.gsub(',', ' ')
251
+ if input =~ %r{[*]/} # close comment
252
+ @state = 'flow'
253
+ end
254
+ return
255
+
256
+ elsif @state.eql? 'reading_selectors'
257
+ if input !~ /[{]/
258
+ @selectors << extract_selectors(input)
259
+ else
260
+ @selectors << extract_selectors($`)
261
+ @state = 'reading_styles'
262
+ act_on input
263
+ end
264
+ return
265
+
266
+ elsif @state.eql? 'reading_styles'
267
+ case input
268
+ when /\A[^{}]*\Z/ # no braces
269
+ @styles << input
270
+ when /\A[^,]*[{](.*)[}]/ # inline styles (no commas)
271
+ @styles << (@depth == 0 ? $1 : input)
272
+ when /[{](.*)[}]/ # inline styles (commas)
273
+ @styles << $1
274
+ when /[{][^}]*\Z/ # open multiline block
275
+ @styles << input unless @depth == 0
276
+ @depth += 1
277
+ when /[^{]*[}]/ # close multiline block
278
+ @depth -= 1
279
+ @styles << input unless @depth == 0
280
+ end
281
+ if @depth == 0 && input =~ /[}]/
282
+ @selectors.flatten.each { |selector|
283
+ # Force each style declaration onto a new line.
284
+ @output << "#{selector} {\n"
285
+ @styles.each { |style| @output << "#{@indent}#{style.chomp.strip}\n" }
286
+ @output << "}\n"
287
+ }
288
+ @state = 'flow'
289
+ end
290
+ return
291
+
292
+ end
293
+ end
294
+
295
+ private
296
+
297
+ def extract_selectors(line)
298
+ line.split(',').map { |selector| selector.strip }.delete_if { |selector| selector =~ /\A\s*\Z/ }
299
+ end
300
+ end
301
+
302
+
303
+ class StyleHash < Hash #:nodoc:
304
+ attr_accessor :multiline
305
+ def initialize *a, &b
306
+ super
307
+ multiline = false
308
+ end
309
+ def has_non_style_hash_children
310
+ value.each do |elem|
311
+ next if elem.kind_of? StyleHash
312
+ return true unless elem.blank?
313
+ end
314
+ false
315
+ end
316
+ # We only ever have one key and one value
317
+ def key
318
+ self.keys.first
319
+ end
320
+ def key=(key)
321
+ self.keys.first = key
322
+ end
323
+ def value
324
+ self.values.first
325
+ end
326
+ def value=(value)
327
+ self.values.first = value
328
+ end
329
+ end
330
+
331
+
332
+ end
333
+ end
@@ -0,0 +1,47 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../css_dryer/processor')
2
+ require 'ERB'
3
+
4
+ # Rack::CssDryer is a Rack middleware that serves de-nested nested
5
+ # stylesheets (.ncss) if available.
6
+ #
7
+ # Nested stylesheets are run through ERB and then denested to convert them
8
+ # into valid CSS.
9
+ module Rack
10
+ class CssDryer
11
+ include ::CssDryer::Processor
12
+ File = ::File
13
+
14
+ # Options:
15
+ #
16
+ # +:url+ the parent URL of the stylesheets. Defaults to 'css'. In a Rails app
17
+ # you probably want to set this to 'stylesheets'.
18
+ #
19
+ # +:path+ the file system directory containing your nested stylesheets (.ncss).
20
+ # In a Rails app you probably want to set this to 'app/views/stylesheets'.
21
+ def initialize(app, options = {})
22
+ @app = app
23
+ @url = options[:url] || 'css'
24
+ @path = options[:path] || 'stylesheets'
25
+ end
26
+
27
+ def call(env)
28
+ path = env['PATH_INFO']
29
+ if path =~ %r{/#{@url}/}
30
+ file = path.sub("/#{@url}", @path)
31
+ ncss = file.sub(/\.css$/, '.ncss')
32
+
33
+ if File.exists?(ncss)
34
+ nested_css = File.read(ncss)
35
+ nested_css = ::ERB.new(nested_css, nil, '-').result
36
+ css = process(nested_css)
37
+ length = ''.respond_to?(:bytesize) ? css.bytesize.to_s : css.size.to_s
38
+ [200, {'Content-Type' => 'text/css', 'Content-Length' => length}, [css]]
39
+ else
40
+ @app.call(env)
41
+ end
42
+ else
43
+ @app.call(env)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,21 @@
1
+ require 'erb'
2
+
3
+ namespace :css_dryer do
4
+
5
+ task :to_css do
6
+ require File.join(RAILS_ROOT, 'app', 'helpers', 'stylesheets_helper')
7
+ include StylesheetsHelper
8
+
9
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'css_dryer', 'processor')
10
+ include CssDryer::Processor
11
+
12
+ Dir.glob(File.join(RAILS_ROOT, 'app', 'views', 'stylesheets', '*')).each do |ncss|
13
+ @output_buffer = ''
14
+ ::ERB.new(File.read(ncss), nil, '-', '@output_buffer').result(binding)
15
+ File.open(File.join(RAILS_ROOT, 'public', 'stylesheets', File.basename(ncss, '.ncss')), 'w') do |f|
16
+ f.write process(@output_buffer)
17
+ end
18
+ end
19
+ end
20
+
21
+ end