css_dryer 0.0.1

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.
@@ -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