snitko-css_dryer_2 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,84 @@
1
+ == DESCRIPTION:
2
+
3
+ This gem is based on Rails plugin 'css_dryer': http://github.com/airblade/css_dryer.
4
+ Unlike 'css_dryer', this one doesn't create any controllers and views.
5
+ Instead, it acts more like SASS, but uses css_dryer parser (which allows you to use completely
6
+ conventional css-synax and write one-line css rules).
7
+
8
+ * Read 'install' section to find out how to use it with Rails or any other Ruby appplication.
9
+ * Read USAGE.markdown file to learn about css_dryer parser (copypasted from css_dryer REAME).
10
+
11
+ == REQUIREMENTS:
12
+
13
+ * ActiveResource gem
14
+
15
+ == INSTALL:
16
+
17
+ 1) sudo gem install css_dryer_2
18
+ 2) If you're planning to use it with rails application, then
19
+ go to rails application folder and run 'script/generate css_dryer'.
20
+ This will generate neccessary rake tasks.
21
+
22
+ 3) Put this is config/environments/development.rb:
23
+
24
+ require 'css_dryer_2'
25
+
26
+ 4) In dev environment, to generate css on any changes in .ncss files, put this in ApplicationController:
27
+
28
+ CssDryer2::FilesHandler.new(:settings => :rails).run if RAILS_ENV == 'development'
29
+
30
+ You can also set custom directories for files, instead of using rails shortcut option:
31
+
32
+ CssDryer2::FilesHandler.new(
33
+ :source_path => 'my_css_dir/ncss',
34
+ :compile_path => 'my_css_dir',
35
+ :tmp_path => '/tmp',
36
+ :force_compile => true
37
+ )
38
+
39
+
40
+ 5) While rails application to production, run rake task:
41
+
42
+ rake ncss:compile
43
+
44
+ 6) Now put your .ncss files in your 'public/stylesheets/ncss/' dir.
45
+ The compiler will put generated css files in 'public/stylesheets/' dir.
46
+
47
+
48
+ == HOW IT WORKS:
49
+
50
+ When you fire 'run' method, CssDryer2::FileHandler object checks if the .ncss files
51
+ changed (it stores their SHA1 in tmp dir). If they did, it recompiles them.
52
+
53
+ For Rails application it assumes that you store your .ncss files in 'public/stylesheets/ncss',
54
+ and it compiles .css files in 'public/stylesheets'.
55
+ You might want to add 'public/stylesheets/*.css' into your '.gitignore' file
56
+
57
+ You can force reompile by providing the run method with':force_recompile => true' argument.
58
+ You can also recompile at any time by running 'rake ncss:compile'
59
+
60
+
61
+ == LICENSE:
62
+
63
+ (The MIT License)
64
+
65
+ Copyright (c) 2009 FIXME full name
66
+
67
+ Permission is hereby granted, free of charge, to any person obtaining
68
+ a copy of this software and associated documentation files (the
69
+ 'Software'), to deal in the Software without restriction, including
70
+ without limitation the rights to use, copy, modify, merge, publish,
71
+ distribute, sublicense, and/or sell copies of the Software, and to
72
+ permit persons to whom the Software is furnished to do so, subject to
73
+ the following conditions:
74
+
75
+ The above copyright notice and this permission notice shall be
76
+ included in all copies or substantial portions of the Software.
77
+
78
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
79
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
80
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
81
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
82
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
83
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
84
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,16 @@
1
+ require 'rbconfig'
2
+
3
+ class CssDryerGenerator < Rails::Generator::Base
4
+
5
+ def initialize(runtime_args, runtime_options = {})
6
+ Dir.mkdir('lib/tasks') unless File.directory?('lib/tasks')
7
+ super
8
+ end
9
+
10
+ def manifest
11
+ record do |m|
12
+ m.file "css_dryer_2.rake", "lib/tasks/css_dryer_2.rake"
13
+ end
14
+ end
15
+
16
+ end
@@ -0,0 +1,10 @@
1
+ require 'css_dryer_2'
2
+
3
+ namespace(:ncss) do
4
+
5
+ desc "compile ncss files into css"
6
+ task :compile do
7
+ CssDryer2::FilesHandler.new(:force_compile => true, :settings => :rails).run
8
+ end
9
+
10
+ end
@@ -0,0 +1,7 @@
1
+ require 'erb'
2
+ require 'activesupport'
3
+ require 'tmpdir'
4
+ require File.dirname(__FILE__) + '/css_dryer_2/processor'
5
+ require File.dirname(__FILE__) + '/css_dryer_2/stylesheets_helper'
6
+ require File.dirname(__FILE__) + '/css_dryer_2/ncss_handler'
7
+ require File.dirname(__FILE__) + '/css_dryer_2/files_handler'
@@ -0,0 +1,63 @@
1
+ require 'digest'
2
+
3
+ module CssDryer2
4
+ # This module is included in rake task or
5
+ # anywhere (for example ApplicationController of Rails app)
6
+ # you need to choose when and which files are compiled.
7
+ class FilesHandler
8
+
9
+ attr_accessor :source_path, :compile_path, :tmp_path, :force_compile
10
+
11
+ def initialize(params = {})
12
+
13
+ @force_compile = false
14
+
15
+ if params[:settings] == :rails
16
+ @source_path = "#{RAILS_ROOT}/public/stylesheets/ncss"
17
+ @compile_path = "#{RAILS_ROOT}/public/stylesheets"
18
+ @tmp_path = "#{RAILS_ROOT}/tmp"
19
+ end
20
+
21
+ @source_path = params[:source_path] if params[:source_path]
22
+ @compile_path = params[:compile_path] if params[:compile_path]
23
+ @tmp_path ||= params[:tmp_path] || Dir.tmpdir + '/css_dryer_2_gem/tmp'
24
+
25
+
26
+ unless @source_path or @compile_path or @tmp_path
27
+ raise "You have to set source_path, compile_path and tmp_path"
28
+ end
29
+
30
+ FileUtils.mkdir_p(@tmp_path + '/css_dryer_2') unless File.exist?(@tmp_path + '/css_dryer_2')
31
+ end
32
+
33
+ def run(params = {})
34
+ files = Dir.new(@source_path + '/').entries
35
+ files.delete_if { |f| !File.file?(@source_path + "/#{f}") or !(f =~ /.ncss$/) }
36
+ files.each { |f| prepare_file(f, params[:force_compile] || @force_compile) }
37
+ end
38
+
39
+ def self.configure
40
+ yield(self)
41
+ end
42
+
43
+ private
44
+
45
+ def prepare_file(file, force_compile)
46
+
47
+ ncss = File.read(@source_path + "/#{file}")
48
+ hashed_content = Digest::SHA1.hexdigest(ncss)
49
+
50
+ # Check if the file has been updated since the last run
51
+ if File.exist?(@tmp_path + "/css_dryer_2/#{file}")
52
+ prev_hashed_content = File.read(@tmp_path + "/css_dryer_2/#{file}")
53
+ return if prev_hashed_content == hashed_content and !@force_compile
54
+ end
55
+
56
+ ncss = File.read(@source_path + "/#{file}")
57
+ css = CssDryer2::NcssHandler.new.compile(ncss)
58
+ File.open(@compile_path + "/#{file.sub(/.ncss$/, '.css')}", 'w') { |f| f.write(css) }
59
+ File.open(@tmp_path + "/css_dryer_2/#{file}", 'w') { |f| f.write(hashed_content) }
60
+ end
61
+
62
+ end
63
+ end
@@ -0,0 +1,19 @@
1
+ # Handler for DRY stylesheets which can be registered with Rails
2
+ # as a new templating system.
3
+ #
4
+ # DRY stylesheets are piped through ERB and then CssDryer#process.
5
+ module CssDryer2
6
+
7
+ class NcssHandler
8
+ include CssDryer::Processor
9
+ # In case user doesn't have helper or hasn't run generator yet.
10
+ include StylesheetsHelper rescue nil
11
+
12
+ def compile(template)
13
+ output_buffer = String.new
14
+ erb_compiled_template = ERB.new(template, nil, nil, "@output_buffer").result(binding)
15
+ process(erb_compiled_template)
16
+ end
17
+ end
18
+
19
+ end
@@ -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.0'
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 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 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| 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