snitko-css_dryer_2 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.
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