radiant 0.6.6 → 0.6.7

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of radiant might be problematic. Click here for more details.

Files changed (208) hide show
  1. data/CHANGELOG +15 -0
  2. data/CONTRIBUTORS +8 -0
  3. data/app/controllers/admin/page_controller.rb +1 -11
  4. data/app/controllers/admin/welcome_controller.rb +5 -6
  5. data/app/controllers/application.rb +2 -0
  6. data/app/controllers/site_controller.rb +1 -0
  7. data/app/helpers/admin/page_helper.rb +36 -0
  8. data/app/helpers/admin/regions_helper.rb +28 -0
  9. data/app/helpers/admin/user_helper.rb +6 -0
  10. data/app/helpers/application_helper.rb +2 -1
  11. data/app/models/user.rb +7 -8
  12. data/app/views/admin/extension/index.html.haml +28 -0
  13. data/app/views/admin/layout/edit.html.haml +44 -0
  14. data/app/views/admin/layout/index.html.haml +25 -0
  15. data/app/views/admin/layout/remove.html.haml +16 -0
  16. data/app/views/admin/page/_meta_row.html.haml +6 -0
  17. data/app/views/admin/page/_node.html.haml +25 -0
  18. data/app/views/admin/page/_part.html.haml +17 -0
  19. data/app/views/admin/page/_tag_reference.html.haml +3 -0
  20. data/app/views/admin/page/children.html.haml +2 -0
  21. data/app/views/admin/page/edit.html.haml +114 -0
  22. data/app/views/admin/page/index.html.haml +28 -0
  23. data/app/views/admin/page/remove.html.haml +20 -0
  24. data/app/views/admin/snippet/edit.html.haml +36 -0
  25. data/app/views/admin/snippet/index.html.haml +20 -0
  26. data/app/views/admin/snippet/remove.html.haml +19 -0
  27. data/app/views/admin/user/edit.html.haml +80 -0
  28. data/app/views/admin/user/index.html.haml +23 -0
  29. data/app/views/admin/user/preferences.html.haml +28 -0
  30. data/app/views/admin/user/remove.html.haml +18 -0
  31. data/app/views/admin/welcome/login.html.haml +44 -0
  32. data/app/views/layouts/application.html.haml +54 -0
  33. data/app/views/site/not_found.html.haml +3 -0
  34. data/db/migrate/019_add_salt_to_users.rb +11 -0
  35. data/db/schema.rb +2 -1
  36. data/lib/login_system.rb +1 -0
  37. data/lib/radiant.rb +1 -1
  38. data/lib/radiant/admin_ui.rb +65 -0
  39. data/lib/radiant/admin_ui/region_partials.rb +18 -0
  40. data/lib/radiant/admin_ui/region_set.rb +35 -0
  41. data/lib/tasks/extensions.rake +33 -0
  42. data/public/javascripts/admin/admin.js +8 -2
  43. data/public/javascripts/admin/sitemap.js +2 -1
  44. data/public/stylesheets/admin/main.css +4 -0
  45. data/spec/controllers/admin/abstract_model_controller_spec.rb +2 -0
  46. data/spec/controllers/admin/page_controller_spec.rb +3 -28
  47. data/spec/controllers/admin/user_controller_spec.rb +1 -1
  48. data/spec/controllers/admin/welcome_controller_spec.rb +26 -0
  49. data/spec/helpers/admin/page_helper_spec.rb +4 -0
  50. data/spec/helpers/admin/regions_helper_spec.rb +47 -0
  51. data/spec/helpers/admin/user_helper_spec.rb +7 -0
  52. data/spec/helpers/application_helper_spec.rb +7 -3
  53. data/spec/lib/login_system_spec.rb +5 -0
  54. data/spec/lib/radiant/admin_ui/region_partials_spec.rb +35 -0
  55. data/spec/lib/radiant/admin_ui/region_set_spec.rb +61 -0
  56. data/spec/lib/radiant/admin_ui_spec.rb +74 -18
  57. data/spec/models/user_spec.rb +11 -4
  58. data/spec/scenarios/users_scenario.rb +2 -2
  59. data/vendor/plugins/haml/MIT-LICENSE +20 -0
  60. data/vendor/plugins/haml/README.rdoc +319 -0
  61. data/vendor/plugins/haml/Rakefile +158 -0
  62. data/vendor/plugins/haml/TODO +9 -0
  63. data/vendor/plugins/haml/VERSION +1 -0
  64. data/vendor/plugins/haml/bin/css2sass +7 -0
  65. data/vendor/plugins/haml/bin/haml +8 -0
  66. data/vendor/plugins/haml/bin/html2haml +7 -0
  67. data/vendor/plugins/haml/bin/sass +8 -0
  68. data/vendor/plugins/haml/extra/haml-mode.el +328 -0
  69. data/vendor/plugins/haml/extra/sass-mode.el +88 -0
  70. data/vendor/plugins/haml/init.rb +2 -0
  71. data/vendor/plugins/haml/lib/haml.rb +977 -0
  72. data/vendor/plugins/haml/lib/haml/buffer.rb +229 -0
  73. data/vendor/plugins/haml/lib/haml/engine.rb +274 -0
  74. data/vendor/plugins/haml/lib/haml/error.rb +23 -0
  75. data/vendor/plugins/haml/lib/haml/exec.rb +347 -0
  76. data/vendor/plugins/haml/lib/haml/filters.rb +249 -0
  77. data/vendor/plugins/haml/lib/haml/helpers.rb +413 -0
  78. data/vendor/plugins/haml/lib/haml/helpers/action_view_extensions.rb +45 -0
  79. data/vendor/plugins/haml/lib/haml/helpers/action_view_mods.rb +122 -0
  80. data/vendor/plugins/haml/lib/haml/html.rb +188 -0
  81. data/vendor/plugins/haml/lib/haml/precompiler.rb +757 -0
  82. data/vendor/plugins/haml/lib/haml/template.rb +43 -0
  83. data/vendor/plugins/haml/lib/haml/template/patch.rb +58 -0
  84. data/vendor/plugins/haml/lib/haml/template/plugin.rb +72 -0
  85. data/vendor/plugins/haml/lib/sass.rb +833 -0
  86. data/vendor/plugins/haml/lib/sass/constant.rb +245 -0
  87. data/vendor/plugins/haml/lib/sass/constant/color.rb +101 -0
  88. data/vendor/plugins/haml/lib/sass/constant/literal.rb +53 -0
  89. data/vendor/plugins/haml/lib/sass/constant/number.rb +87 -0
  90. data/vendor/plugins/haml/lib/sass/constant/operation.rb +30 -0
  91. data/vendor/plugins/haml/lib/sass/constant/string.rb +22 -0
  92. data/vendor/plugins/haml/lib/sass/css.rb +378 -0
  93. data/vendor/plugins/haml/lib/sass/engine.rb +459 -0
  94. data/vendor/plugins/haml/lib/sass/error.rb +35 -0
  95. data/vendor/plugins/haml/lib/sass/plugin.rb +165 -0
  96. data/vendor/plugins/haml/lib/sass/plugin/merb.rb +56 -0
  97. data/vendor/plugins/haml/lib/sass/plugin/rails.rb +24 -0
  98. data/vendor/plugins/haml/lib/sass/tree/attr_node.rb +53 -0
  99. data/vendor/plugins/haml/lib/sass/tree/comment_node.rb +20 -0
  100. data/vendor/plugins/haml/lib/sass/tree/directive_node.rb +46 -0
  101. data/vendor/plugins/haml/lib/sass/tree/node.rb +42 -0
  102. data/vendor/plugins/haml/lib/sass/tree/rule_node.rb +89 -0
  103. data/vendor/plugins/haml/lib/sass/tree/value_node.rb +16 -0
  104. data/vendor/plugins/haml/test/benchmark.rb +82 -0
  105. data/vendor/plugins/haml/test/haml/engine_test.rb +587 -0
  106. data/vendor/plugins/haml/test/haml/helper_test.rb +187 -0
  107. data/vendor/plugins/haml/test/haml/html2haml_test.rb +60 -0
  108. data/vendor/plugins/haml/test/haml/markaby/standard.mab +52 -0
  109. data/vendor/plugins/haml/test/haml/mocks/article.rb +6 -0
  110. data/vendor/plugins/haml/test/haml/results/content_for_layout.xhtml +16 -0
  111. data/vendor/plugins/haml/test/haml/results/eval_suppressed.xhtml +11 -0
  112. data/vendor/plugins/haml/test/haml/results/filters.xhtml +82 -0
  113. data/vendor/plugins/haml/test/haml/results/helpers.xhtml +94 -0
  114. data/vendor/plugins/haml/test/haml/results/helpful.xhtml +10 -0
  115. data/vendor/plugins/haml/test/haml/results/just_stuff.xhtml +64 -0
  116. data/vendor/plugins/haml/test/haml/results/list.xhtml +12 -0
  117. data/vendor/plugins/haml/test/haml/results/original_engine.xhtml +22 -0
  118. data/vendor/plugins/haml/test/haml/results/partials.xhtml +21 -0
  119. data/vendor/plugins/haml/test/haml/results/silent_script.xhtml +74 -0
  120. data/vendor/plugins/haml/test/haml/results/standard.xhtml +42 -0
  121. data/vendor/plugins/haml/test/haml/results/tag_parsing.xhtml +28 -0
  122. data/vendor/plugins/haml/test/haml/results/very_basic.xhtml +7 -0
  123. data/vendor/plugins/haml/test/haml/results/whitespace_handling.xhtml +94 -0
  124. data/vendor/plugins/haml/test/haml/rhtml/_av_partial_1.rhtml +12 -0
  125. data/vendor/plugins/haml/test/haml/rhtml/_av_partial_2.rhtml +8 -0
  126. data/vendor/plugins/haml/test/haml/rhtml/action_view.rhtml +62 -0
  127. data/vendor/plugins/haml/test/haml/rhtml/standard.rhtml +54 -0
  128. data/vendor/plugins/haml/test/haml/template_test.rb +168 -0
  129. data/vendor/plugins/haml/test/haml/templates/_av_partial_1.haml +9 -0
  130. data/vendor/plugins/haml/test/haml/templates/_av_partial_2.haml +5 -0
  131. data/vendor/plugins/haml/test/haml/templates/_partial.haml +8 -0
  132. data/vendor/plugins/haml/test/haml/templates/_text_area.haml +3 -0
  133. data/vendor/plugins/haml/test/haml/templates/action_view.haml +47 -0
  134. data/vendor/plugins/haml/test/haml/templates/breakage.haml +8 -0
  135. data/vendor/plugins/haml/test/haml/templates/content_for_layout.haml +10 -0
  136. data/vendor/plugins/haml/test/haml/templates/eval_suppressed.haml +11 -0
  137. data/vendor/plugins/haml/test/haml/templates/filters.haml +81 -0
  138. data/vendor/plugins/haml/test/haml/templates/helpers.haml +69 -0
  139. data/vendor/plugins/haml/test/haml/templates/helpful.haml +11 -0
  140. data/vendor/plugins/haml/test/haml/templates/just_stuff.haml +77 -0
  141. data/vendor/plugins/haml/test/haml/templates/list.haml +12 -0
  142. data/vendor/plugins/haml/test/haml/templates/original_engine.haml +17 -0
  143. data/vendor/plugins/haml/test/haml/templates/partialize.haml +1 -0
  144. data/vendor/plugins/haml/test/haml/templates/partials.haml +12 -0
  145. data/vendor/plugins/haml/test/haml/templates/silent_script.haml +40 -0
  146. data/vendor/plugins/haml/test/haml/templates/standard.haml +42 -0
  147. data/vendor/plugins/haml/test/haml/templates/tag_parsing.haml +24 -0
  148. data/vendor/plugins/haml/test/haml/templates/very_basic.haml +4 -0
  149. data/vendor/plugins/haml/test/haml/templates/whitespace_handling.haml +87 -0
  150. data/vendor/plugins/haml/test/haml/test_helper.rb +15 -0
  151. data/vendor/plugins/haml/test/profile.rb +65 -0
  152. data/vendor/plugins/haml/test/sass/engine_test.rb +276 -0
  153. data/vendor/plugins/haml/test/sass/plugin_test.rb +159 -0
  154. data/vendor/plugins/haml/test/sass/results/alt.css +4 -0
  155. data/vendor/plugins/haml/test/sass/results/basic.css +9 -0
  156. data/vendor/plugins/haml/test/sass/results/compact.css +5 -0
  157. data/vendor/plugins/haml/test/sass/results/complex.css +87 -0
  158. data/vendor/plugins/haml/test/sass/results/compressed.css +1 -0
  159. data/vendor/plugins/haml/test/sass/results/constants.css +14 -0
  160. data/vendor/plugins/haml/test/sass/results/expanded.css +19 -0
  161. data/vendor/plugins/haml/test/sass/results/import.css +29 -0
  162. data/vendor/plugins/haml/test/sass/results/mixins.css +95 -0
  163. data/vendor/plugins/haml/test/sass/results/multiline.css +24 -0
  164. data/vendor/plugins/haml/test/sass/results/nested.css +22 -0
  165. data/vendor/plugins/haml/test/sass/results/parent_ref.css +13 -0
  166. data/vendor/plugins/haml/test/sass/results/subdir/nested_subdir/nested_subdir.css +1 -0
  167. data/vendor/plugins/haml/test/sass/results/subdir/subdir.css +1 -0
  168. data/vendor/plugins/haml/test/sass/templates/_partial.sass +2 -0
  169. data/vendor/plugins/haml/test/sass/templates/alt.sass +16 -0
  170. data/vendor/plugins/haml/test/sass/templates/basic.sass +23 -0
  171. data/vendor/plugins/haml/test/sass/templates/bork.sass +2 -0
  172. data/vendor/plugins/haml/test/sass/templates/bork2.sass +2 -0
  173. data/vendor/plugins/haml/test/sass/templates/compact.sass +17 -0
  174. data/vendor/plugins/haml/test/sass/templates/complex.sass +310 -0
  175. data/vendor/plugins/haml/test/sass/templates/compressed.sass +15 -0
  176. data/vendor/plugins/haml/test/sass/templates/constants.sass +97 -0
  177. data/vendor/plugins/haml/test/sass/templates/expanded.sass +17 -0
  178. data/vendor/plugins/haml/test/sass/templates/import.sass +11 -0
  179. data/vendor/plugins/haml/test/sass/templates/importee.sass +14 -0
  180. data/vendor/plugins/haml/test/sass/templates/mixins.sass +76 -0
  181. data/vendor/plugins/haml/test/sass/templates/multiline.sass +20 -0
  182. data/vendor/plugins/haml/test/sass/templates/nested.sass +25 -0
  183. data/vendor/plugins/haml/test/sass/templates/parent_ref.sass +25 -0
  184. data/vendor/plugins/haml/test/sass/templates/subdir/nested_subdir/nested_subdir.sass +3 -0
  185. data/vendor/plugins/haml/test/sass/templates/subdir/subdir.sass +6 -0
  186. metadata +185 -24
  187. data/app/views/admin/extension/index.html.erb +0 -40
  188. data/app/views/admin/layout/edit.html.erb +0 -39
  189. data/app/views/admin/layout/index.html.erb +0 -38
  190. data/app/views/admin/layout/remove.html.erb +0 -17
  191. data/app/views/admin/page/_meta_row.html.erb +0 -4
  192. data/app/views/admin/page/_node.html.erb +0 -28
  193. data/app/views/admin/page/_part.html.erb +0 -13
  194. data/app/views/admin/page/_tag_reference.html.erb +0 -4
  195. data/app/views/admin/page/children.html.erb +0 -4
  196. data/app/views/admin/page/edit.html.erb +0 -140
  197. data/app/views/admin/page/index.html.erb +0 -31
  198. data/app/views/admin/page/remove.html.erb +0 -14
  199. data/app/views/admin/snippet/edit.html.erb +0 -29
  200. data/app/views/admin/snippet/index.html.erb +0 -36
  201. data/app/views/admin/snippet/remove.html.erb +0 -16
  202. data/app/views/admin/user/edit.html.erb +0 -54
  203. data/app/views/admin/user/index.html.erb +0 -43
  204. data/app/views/admin/user/preferences.html.erb +0 -29
  205. data/app/views/admin/user/remove.html.erb +0 -16
  206. data/app/views/admin/welcome/login.html.erb +0 -51
  207. data/app/views/layouts/application.html.erb +0 -83
  208. data/app/views/site/not_found.html.erb +0 -3
@@ -0,0 +1,30 @@
1
+ require 'sass/constant/string'
2
+ require 'sass/constant/number'
3
+ require 'sass/constant/color'
4
+
5
+ module Sass::Constant # :nodoc:
6
+ class Operation # :nodoc:
7
+ def initialize(operand1, operand2, operator)
8
+ @operand1 = operand1
9
+ @operand2 = operand2
10
+ @operator = operator
11
+ end
12
+
13
+ def to_s
14
+ self.perform.to_s
15
+ end
16
+
17
+ protected
18
+
19
+ def perform
20
+ literal1 = @operand1.perform
21
+ literal2 = @operand2.perform
22
+ begin
23
+ literal1.send(@operator, literal2)
24
+ rescue NoMethodError => e
25
+ raise e unless e.name.to_s == @operator.to_s
26
+ raise Sass::SyntaxError.new("Undefined operation: \"#{literal1} #{@operator} #{literal2}\".")
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,22 @@
1
+ require 'sass/constant/literal'
2
+
3
+ module Sass::Constant # :nodoc:
4
+ class String < Literal # :nodoc:
5
+
6
+ def parse(value)
7
+ @value = value
8
+ end
9
+
10
+ def plus(other)
11
+ Sass::Constant::String.from_value(self.to_s + other.to_s)
12
+ end
13
+
14
+ def funcall(other)
15
+ Sass::Constant::String.from_value("#{self.to_s}(#{other.to_s})")
16
+ end
17
+
18
+ def to_s
19
+ @value
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,378 @@
1
+ require File.dirname(__FILE__) + '/../sass'
2
+ require 'sass/tree/node'
3
+ require 'strscan'
4
+
5
+ module Sass
6
+ # :stopdoc:
7
+ module Tree
8
+ class Node
9
+ def to_sass(opts = {})
10
+ result = ''
11
+
12
+ children.each do |child|
13
+ result << "#{child.to_sass(0, opts)}\n"
14
+ end
15
+
16
+ result
17
+ end
18
+ end
19
+
20
+ class ValueNode
21
+ def to_sass(tabs)
22
+ "#{value}\n"
23
+ end
24
+ end
25
+
26
+ class RuleNode
27
+ def to_sass(tabs, opts = {})
28
+ str = "\n#{' ' * tabs}#{rule}#{children.any? { |c| c.is_a? AttrNode } ? "\n" : ''}"
29
+
30
+ children.each do |child|
31
+ str << "#{child.to_sass(tabs + 1, opts)}"
32
+ end
33
+
34
+ str
35
+ end
36
+ end
37
+
38
+ class AttrNode
39
+ def to_sass(tabs, opts = {})
40
+ "#{' ' * tabs}#{opts[:alternate] ? '' : ':'}#{name}#{opts[:alternate] ? ':' : ''} #{value}\n"
41
+ end
42
+ end
43
+ end
44
+
45
+ # This class is based on the Ruby 1.9 ordered hashes.
46
+ # It keeps the semantics and most of the efficiency of normal hashes
47
+ # while also keeping track of the order in which elements were set.
48
+ class OrderedHash
49
+ Node = Struct.new('Node', :key, :value, :next)
50
+ include Enumerable
51
+
52
+ def initialize
53
+ @hash = {}
54
+ end
55
+
56
+ def [](key)
57
+ @hash[key] && @hash[key].value
58
+ end
59
+
60
+ def []=(key, value)
61
+ node = Node.new(key, value, nil)
62
+ if @first.nil?
63
+ @first = @last = node
64
+ else
65
+ @last.next = node
66
+ @last = node
67
+ end
68
+ @hash[key] = node
69
+ value
70
+ end
71
+
72
+ def each
73
+ return unless @first
74
+ yield [@first.key, @first.value]
75
+ node = @first
76
+ yield [node.key, node.value] while node = node.next
77
+ self
78
+ end
79
+
80
+ def values
81
+ self.map { |k, v| v }
82
+ end
83
+ end
84
+
85
+ # :startdoc:
86
+
87
+ # This class contains the functionality used in the +css2sass+ utility,
88
+ # namely converting CSS documents to Sass templates.
89
+ class CSS
90
+
91
+ # Creates a new instance of Sass::CSS that will compile the given document
92
+ # to a Sass string when +render+ is called.
93
+ def initialize(template, options = {})
94
+ if template.is_a? IO
95
+ template = template.read
96
+ end
97
+
98
+ @options = options
99
+ @template = StringScanner.new(template)
100
+ end
101
+
102
+ # Processes the document and returns the result as a string
103
+ # containing the CSS template.
104
+ def render
105
+ begin
106
+ build_tree.to_sass(@options).lstrip
107
+ rescue Exception => err
108
+ line = @template.string[0...@template.pos].split("\n").size
109
+
110
+ err.backtrace.unshift "(css):#{line}"
111
+ raise err
112
+ end
113
+ end
114
+
115
+ private
116
+
117
+ def build_tree
118
+ root = Tree::Node.new(nil)
119
+ whitespace
120
+ directives root
121
+ rules root
122
+ expand_commas root
123
+ parent_ref_rules root
124
+ remove_parent_refs root
125
+ flatten_rules root
126
+ fold_commas root
127
+ root
128
+ end
129
+
130
+ def directives(root)
131
+ while @template.scan(/@/)
132
+ name = @template.scan /[^\s;]+/
133
+ whitespace
134
+ value = @template.scan /[^;]+/
135
+ assert_match /;/
136
+ whitespace
137
+
138
+ if name == "import" && value =~ /^(url\()?"?([^\s\(\)\"]+)\.css"?\)?$/
139
+ value = $2
140
+ end
141
+
142
+ root << Tree::ValueNode.new("@#{name} #{value};", nil)
143
+ end
144
+ end
145
+
146
+ def rules(root)
147
+ rules = []
148
+ while @template.scan(/[^\{\s]+/)
149
+ rules << @template[0]
150
+ whitespace
151
+
152
+ if @template.scan(/\{/)
153
+ result = Tree::RuleNode.new(rules.join(' '), nil)
154
+ root << result
155
+ rules = []
156
+
157
+ whitespace
158
+ attributes(result)
159
+ end
160
+ end
161
+ end
162
+
163
+ def attributes(rule)
164
+ while @template.scan(/[^:\}\s]+/)
165
+ name = @template[0]
166
+ whitespace
167
+
168
+ assert_match /:/
169
+
170
+ value = ''
171
+ while @template.scan(/[^;\s\}]+/)
172
+ value << @template[0] << whitespace
173
+ end
174
+
175
+ assert_match /(;|(?=\}))/
176
+ rule << Tree::AttrNode.new(name, value, nil)
177
+ end
178
+
179
+ assert_match /\}/
180
+ end
181
+
182
+ def whitespace
183
+ space = @template.scan(/\s*/) || ''
184
+
185
+ # If we've hit a comment,
186
+ # go past it and look for more whitespace
187
+ if @template.scan(/\/\*/)
188
+ @template.scan_until(/\*\//)
189
+ return space + whitespace
190
+ end
191
+ return space
192
+ end
193
+
194
+ def assert_match(re)
195
+ if !@template.scan(re)
196
+ line = @template.string[0..@template.pos].count "\n"
197
+ # Display basic regexps as plain old strings
198
+ expected = re.source == Regexp.escape(re.source) ? "\"#{re.source}\"" : re.inspect
199
+ raise Exception.new("Invalid CSS on line #{line}: expected #{expected}")
200
+ end
201
+ whitespace
202
+ end
203
+
204
+ # Transform
205
+ #
206
+ # foo, bar, baz
207
+ # color: blue
208
+ #
209
+ # into
210
+ #
211
+ # foo
212
+ # color: blue
213
+ # bar
214
+ # color: blue
215
+ # baz
216
+ # color: blue
217
+ #
218
+ # Yes, this expands the amount of code,
219
+ # but it's necessary to get nesting to work properly.
220
+ def expand_commas(root)
221
+ root.children.map! do |child|
222
+ next child unless Tree::RuleNode === child && child.rule.include?(',')
223
+ child.rule.split(',').map do |rule|
224
+ node = Tree::RuleNode.new(rule, nil)
225
+ node.children = child.children
226
+ node
227
+ end
228
+ end
229
+ root.children.flatten!
230
+ end
231
+
232
+ # Make rules use parent refs so that
233
+ #
234
+ # foo
235
+ # color: green
236
+ # foo.bar
237
+ # color: blue
238
+ #
239
+ # becomes
240
+ #
241
+ # foo
242
+ # color: green
243
+ # &.bar
244
+ # color: blue
245
+ #
246
+ # This has the side effect of nesting rules,
247
+ # so that
248
+ #
249
+ # foo
250
+ # color: green
251
+ # foo bar
252
+ # color: red
253
+ # foo baz
254
+ # color: blue
255
+ #
256
+ # becomes
257
+ #
258
+ # foo
259
+ # color: green
260
+ # & bar
261
+ # color: red
262
+ # & baz
263
+ # color: blue
264
+ #
265
+ def parent_ref_rules(root)
266
+ rules = OrderedHash.new
267
+ root.children.select { |c| Tree::RuleNode === c }.each do |child|
268
+ root.children.delete child
269
+ first, rest = child.rule.scan(/^(&?(?: .|[^ ])[^.#: \[]*)([.#: \[].*)?$/).first
270
+ rules[first] ||= Tree::RuleNode.new(first, nil)
271
+ if rest
272
+ child.rule = "&" + rest
273
+ rules[first] << child
274
+ else
275
+ rules[first].children += child.children
276
+ end
277
+ end
278
+
279
+ rules.values.each { |v| parent_ref_rules(v) }
280
+ root.children += rules.values
281
+ end
282
+
283
+ # Remove useless parent refs so that
284
+ #
285
+ # foo
286
+ # & bar
287
+ # color: blue
288
+ #
289
+ # becomes
290
+ #
291
+ # foo
292
+ # bar
293
+ # color: blue
294
+ #
295
+ def remove_parent_refs(root)
296
+ root.children.each do |child|
297
+ if child.is_a?(Tree::RuleNode)
298
+ child.rule.gsub! /^& /, ''
299
+ remove_parent_refs child
300
+ end
301
+ end
302
+ end
303
+
304
+ # Flatten rules so that
305
+ #
306
+ # foo
307
+ # bar
308
+ # baz
309
+ # color: red
310
+ #
311
+ # becomes
312
+ #
313
+ # foo bar baz
314
+ # color: red
315
+ #
316
+ # and
317
+ #
318
+ # foo
319
+ # &.bar
320
+ # color: blue
321
+ #
322
+ # becomes
323
+ #
324
+ # foo.bar
325
+ # color: blue
326
+ #
327
+ def flatten_rules(root)
328
+ root.children.each { |child| flatten_rule(child) if child.is_a?(Tree::RuleNode) }
329
+ end
330
+
331
+ def flatten_rule(rule)
332
+ while rule.children.size == 1 && rule.children.first.is_a?(Tree::RuleNode)
333
+ child = rule.children.first
334
+
335
+ if child.rule[0] == ?&
336
+ rule.rule = child.rule.gsub /^&/, rule.rule
337
+ else
338
+ rule.rule = "#{rule.rule} #{child.rule}"
339
+ end
340
+
341
+ rule.children = child.children
342
+ end
343
+
344
+ flatten_rules(rule)
345
+ end
346
+
347
+ # Transform
348
+ #
349
+ # foo
350
+ # bar
351
+ # color: blue
352
+ # baz
353
+ # color: blue
354
+ #
355
+ # into
356
+ #
357
+ # foo
358
+ # bar, baz
359
+ # color: blue
360
+ #
361
+ def fold_commas(root)
362
+ prev_rule = nil
363
+ root.children.map! do |child|
364
+ next child unless Tree::RuleNode === child
365
+
366
+ if prev_rule && prev_rule.children == child.children
367
+ prev_rule.rule << ", #{child.rule}"
368
+ next nil
369
+ end
370
+
371
+ fold_commas(child)
372
+ prev_rule = child
373
+ child
374
+ end
375
+ root.children.compact!
376
+ end
377
+ end
378
+ end
@@ -0,0 +1,459 @@
1
+ require 'sass/tree/node'
2
+ require 'sass/tree/value_node'
3
+ require 'sass/tree/rule_node'
4
+ require 'sass/tree/comment_node'
5
+ require 'sass/tree/attr_node'
6
+ require 'sass/tree/directive_node'
7
+ require 'sass/constant'
8
+ require 'sass/error'
9
+
10
+ module Sass
11
+ # This is the class where all the parsing and processing of the Sass
12
+ # template is done. It can be directly used by the user by creating a
13
+ # new instance and calling <tt>render</tt> to render the template. For example:
14
+ #
15
+ # template = File.load('stylesheets/sassy.sass')
16
+ # sass_engine = Sass::Engine.new(template)
17
+ # output = sass_engine.render
18
+ # puts output
19
+ class Engine
20
+ # The character that begins a CSS attribute.
21
+ ATTRIBUTE_CHAR = ?:
22
+
23
+ # The character that designates that
24
+ # an attribute should be assigned to the result of constant arithmetic.
25
+ SCRIPT_CHAR = ?=
26
+
27
+ # The character that designates the beginning of a comment,
28
+ # either Sass or CSS.
29
+ COMMENT_CHAR = ?/
30
+
31
+ # The character that follows the general COMMENT_CHAR and designates a Sass comment,
32
+ # which is not output as a CSS comment.
33
+ SASS_COMMENT_CHAR = ?/
34
+
35
+ # The character that follows the general COMMENT_CHAR and designates a CSS comment,
36
+ # which is embedded in the CSS document.
37
+ CSS_COMMENT_CHAR = ?*
38
+
39
+ # The character used to denote a compiler directive.
40
+ DIRECTIVE_CHAR = ?@
41
+
42
+ # Designates a non-parsed rule.
43
+ ESCAPE_CHAR = ?\\
44
+
45
+ # Designates block as mixin definition rather than CSS rules to output
46
+ MIXIN_DEFINITION_CHAR = ?=
47
+
48
+ # Includes named mixin declared using MIXIN_DEFINITION_CHAR
49
+ MIXIN_INCLUDE_CHAR = ?+
50
+
51
+ # The regex that matches and extracts data from
52
+ # attributes of the form <tt>:name attr</tt>.
53
+ ATTRIBUTE = /^:([^\s=:]+)\s*(=?)(?:\s+|$)(.*)/
54
+
55
+ # The regex that matches attributes of the form <tt>name: attr</tt>.
56
+ ATTRIBUTE_ALTERNATE_MATCHER = /^[^\s:]+\s*[=:](\s|$)/
57
+
58
+ # The regex that matches and extracts data from
59
+ # attributes of the form <tt>name: attr</tt>.
60
+ ATTRIBUTE_ALTERNATE = /^([^\s=:]+)(\s*=|:)(?:\s+|$)(.*)/
61
+
62
+ # Creates a new instace of Sass::Engine that will compile the given
63
+ # template string when <tt>render</tt> is called.
64
+ # See README.rdoc for available options.
65
+ #
66
+ #--
67
+ #
68
+ # TODO: Add current options to REFRENCE. Remember :filename!
69
+ #
70
+ # When adding options, remember to add information about them
71
+ # to README.rdoc!
72
+ #++
73
+ #
74
+ def initialize(template, options={})
75
+ @options = {
76
+ :style => :nested,
77
+ :load_paths => ['.']
78
+ }.merge! options
79
+ @template = template.split(/\n?\r|\r?\n/)
80
+ @lines = []
81
+ @constants = {"important" => "!important"}
82
+ @mixins = {}
83
+ end
84
+
85
+ # Processes the template and returns the result as a string.
86
+ def render
87
+ begin
88
+ render_to_tree.to_s
89
+ rescue SyntaxError => err
90
+ unless err.sass_filename
91
+ err.add_backtrace_entry(@options[:filename])
92
+ end
93
+ raise err
94
+ end
95
+ end
96
+
97
+ alias_method :to_css, :render
98
+
99
+ protected
100
+
101
+ def constants
102
+ @constants
103
+ end
104
+
105
+ def mixins
106
+ @mixins
107
+ end
108
+
109
+ def render_to_tree
110
+ split_lines
111
+
112
+ root = Tree::Node.new(@options[:style])
113
+ index = 0
114
+ while @lines[index]
115
+ child, index = build_tree(index)
116
+
117
+ if child.is_a? Tree::Node
118
+ child.line = index
119
+ root << child
120
+ elsif child.is_a? Array
121
+ child.each do |c|
122
+ root << c
123
+ end
124
+ end
125
+ end
126
+ @lines.clear
127
+
128
+ root
129
+ end
130
+
131
+ private
132
+
133
+ # Readies each line in the template for parsing,
134
+ # and computes the tabulation of the line.
135
+ def split_lines
136
+ @line = 0
137
+ old_tabs = 0
138
+ @template.each_with_index do |line, index|
139
+ @line += 1
140
+
141
+ tabs = count_tabs(line)
142
+
143
+ if line[0] == COMMENT_CHAR && line[1] == SASS_COMMENT_CHAR && tabs == 0
144
+ tabs = old_tabs
145
+ end
146
+
147
+ if tabs # if line isn't blank
148
+ if tabs - old_tabs > 1
149
+ raise SyntaxError.new("#{tabs * 2} spaces were used for indentation. Sass must be indented using two spaces.", @line)
150
+ end
151
+ @lines << [line.strip, tabs]
152
+
153
+ old_tabs = tabs
154
+ else
155
+ @lines << ['//', old_tabs]
156
+ end
157
+ end
158
+
159
+ @line = nil
160
+ end
161
+
162
+ # Counts the tabulation of a line.
163
+ def count_tabs(line)
164
+ return nil if line.strip.empty?
165
+ return nil unless spaces = line.index(/[^ ]/)
166
+
167
+ if spaces % 2 == 1
168
+ raise SyntaxError.new(<<END.strip, @line)
169
+ #{spaces} space#{spaces == 1 ? ' was' : 's were'} used for indentation. Sass must be indented using two spaces.
170
+ END
171
+ elsif line[spaces] == ?\t
172
+ raise SyntaxError.new(<<END.strip, @line)
173
+ A tab character was used for indentation. Sass must be indented using two spaces.
174
+ Are you sure you have soft tabs enabled in your editor?
175
+ END
176
+ end
177
+ spaces / 2
178
+ end
179
+
180
+ def build_tree(index)
181
+ line, tabs = @lines[index]
182
+ index += 1
183
+ @line = index
184
+ node = parse_line(line)
185
+
186
+ has_children = has_children?(index, tabs)
187
+
188
+ # Node is a symbol if it's non-outputting, like a constant assignment
189
+ unless node.is_a? Tree::Node
190
+ if has_children
191
+ if node == :constant
192
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath constants.", @line + 1)
193
+ elsif node.is_a? Array
194
+ # arrays can either be full of import statements
195
+ # or attributes from mixin includes
196
+ # in either case they shouldn't have children.
197
+ # Need to peek into the array in order to give meaningful errors
198
+ directive_type = (node.first.is_a?(Tree::DirectiveNode) ? "import" : "mixin")
199
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath #{directive_type} directives.", @line + 1)
200
+ end
201
+ end
202
+
203
+ index = @line if node == :mixin
204
+ return node, index
205
+ end
206
+
207
+ node.line = @line
208
+
209
+ if node.is_a? Tree::CommentNode
210
+ while has_children
211
+ line, index = raw_next_line(index)
212
+ node << line
213
+
214
+ has_children = has_children?(index, tabs)
215
+ end
216
+
217
+ return node, index
218
+ end
219
+
220
+ # Resolve multiline rules
221
+ if node.is_a?(Tree::RuleNode)
222
+ if node.continued?
223
+ child, index = build_tree(index) if @lines[old_index = index]
224
+ if @lines[old_index].nil? || has_children?(old_index, tabs) || !child.is_a?(Tree::RuleNode)
225
+ raise SyntaxError.new("Rules can't end in commas.", @line)
226
+ end
227
+
228
+ node.add_rules child
229
+ end
230
+ node.children = child.children if child
231
+ end
232
+
233
+ while has_children
234
+ child, index = build_tree(index)
235
+
236
+ validate_and_append_child(node, child)
237
+
238
+ has_children = has_children?(index, tabs)
239
+ end
240
+
241
+ return node, index
242
+ end
243
+
244
+ def validate_and_append_child(parent, child)
245
+ case child
246
+ when :constant
247
+ raise SyntaxError.new("Constants may only be declared at the root of a document.", @line)
248
+ when :mixin
249
+ raise SyntaxError.new("Mixins may only be defined at the root of a document.", @line)
250
+ when Array
251
+ child.each do |c|
252
+ if c.is_a?(Tree::DirectiveNode)
253
+ raise SyntaxError.new("Import directives may only be used at the root of a document.", @line)
254
+ end
255
+ parent << c
256
+ end
257
+ when Tree::Node
258
+ parent << child
259
+ end
260
+ end
261
+
262
+ def has_children?(index, tabs)
263
+ next_line = ['//', 0]
264
+ while !next_line.nil? && next_line[0] == '//' && next_line[1] = 0
265
+ next_line = @lines[index]
266
+ index += 1
267
+ end
268
+ next_line && next_line[1] > tabs
269
+ end
270
+
271
+ def raw_next_line(index)
272
+ [@lines[index][0], index + 1]
273
+ end
274
+
275
+ def parse_line(line)
276
+ case line[0]
277
+ when ATTRIBUTE_CHAR
278
+ parse_attribute(line, ATTRIBUTE)
279
+ when Constant::CONSTANT_CHAR
280
+ parse_constant(line)
281
+ when COMMENT_CHAR
282
+ parse_comment(line)
283
+ when DIRECTIVE_CHAR
284
+ parse_directive(line)
285
+ when ESCAPE_CHAR
286
+ Tree::RuleNode.new(line[1..-1], @options[:style])
287
+ when MIXIN_DEFINITION_CHAR
288
+ parse_mixin_definition(line)
289
+ when MIXIN_INCLUDE_CHAR
290
+ parse_mixin_include(line)
291
+ else
292
+ if line =~ ATTRIBUTE_ALTERNATE_MATCHER
293
+ parse_attribute(line, ATTRIBUTE_ALTERNATE)
294
+ else
295
+ Tree::RuleNode.new(line, @options[:style])
296
+ end
297
+ end
298
+ end
299
+
300
+ def parse_attribute(line, attribute_regx)
301
+ if @options[:attribute_syntax] == :normal &&
302
+ attribute_regx == ATTRIBUTE_ALTERNATE
303
+ raise SyntaxError.new("Illegal attribute syntax: can't use alternate syntax when :attribute_syntax => :normal is set.")
304
+ elsif @options[:attribute_syntax] == :alternate &&
305
+ attribute_regx == ATTRIBUTE
306
+ raise SyntaxError.new("Illegal attribute syntax: can't use normal syntax when :attribute_syntax => :alternate is set.")
307
+ end
308
+
309
+ name, eq, value = line.scan(attribute_regx)[0]
310
+
311
+ if name.nil? || value.nil?
312
+ raise SyntaxError.new("Invalid attribute: \"#{line}\".", @line)
313
+ end
314
+
315
+ if eq.strip[0] == SCRIPT_CHAR
316
+ value = Sass::Constant.parse(value, @constants, @line).to_s
317
+ end
318
+
319
+ Tree::AttrNode.new(name, value, @options[:style])
320
+ end
321
+
322
+ def parse_constant(line)
323
+ name, op, value = line.scan(Sass::Constant::MATCH)[0]
324
+ unless name && value
325
+ raise SyntaxError.new("Invalid constant: \"#{line}\".", @line)
326
+ end
327
+
328
+ constant = Sass::Constant.parse(value, @constants, @line)
329
+ if op == '||='
330
+ @constants[name] ||= constant
331
+ else
332
+ @constants[name] = constant
333
+ end
334
+
335
+ :constant
336
+ end
337
+
338
+ def parse_comment(line)
339
+ if line[1] == SASS_COMMENT_CHAR
340
+ :comment
341
+ elsif line[1] == CSS_COMMENT_CHAR
342
+ Tree::CommentNode.new(line, @options[:style])
343
+ else
344
+ Tree::RuleNode.new(line, @options[:style])
345
+ end
346
+ end
347
+
348
+ def parse_directive(line)
349
+ directive, value = line[1..-1].split(/\s+/, 2)
350
+
351
+ # If value begins with url( or ",
352
+ # it's a CSS @import rule and we don't want to touch it.
353
+ if directive == "import" && value !~ /^(url\(|")/
354
+ import(value)
355
+ else
356
+ Tree::DirectiveNode.new(line, @options[:style])
357
+ end
358
+ end
359
+
360
+ def parse_mixin_definition(line)
361
+ mixin_name = line[1..-1]
362
+ @mixins[mixin_name] = []
363
+ index = @line
364
+ line, tabs = @lines[index]
365
+ while !line.nil? && tabs > 0
366
+ child, index = build_tree(index)
367
+ validate_and_append_child(@mixins[mixin_name], child)
368
+ line, tabs = @lines[index]
369
+ end
370
+ :mixin
371
+ end
372
+
373
+ def parse_mixin_include(line)
374
+ mixin_name = line[1..-1]
375
+ unless @mixins.has_key?(mixin_name)
376
+ raise SyntaxError.new("Undefined mixin '#{mixin_name}'.", @line)
377
+ end
378
+ @mixins[mixin_name]
379
+ end
380
+
381
+ def import(files)
382
+ nodes = []
383
+
384
+ files.split(/,\s*/).each do |filename|
385
+ engine = nil
386
+
387
+ begin
388
+ filename = self.class.find_file_to_import(filename, @options[:load_paths])
389
+ rescue Exception => e
390
+ raise SyntaxError.new(e.message, @line)
391
+ end
392
+
393
+ if filename =~ /\.css$/
394
+ nodes << Tree::DirectiveNode.new("@import url(#{filename})", @options[:style])
395
+ else
396
+ File.open(filename) do |file|
397
+ new_options = @options.dup
398
+ new_options[:filename] = filename
399
+ engine = Sass::Engine.new(file.read, @options)
400
+ end
401
+
402
+ engine.constants.merge! @constants
403
+ engine.mixins.merge! @mixins
404
+
405
+ begin
406
+ root = engine.render_to_tree
407
+ rescue Sass::SyntaxError => err
408
+ err.add_backtrace_entry(filename)
409
+ raise err
410
+ end
411
+ root.children.each do |child|
412
+ child.filename = filename
413
+ nodes << child
414
+ end
415
+ @constants = engine.constants
416
+ @mixins = engine.mixins
417
+ end
418
+ end
419
+
420
+ nodes
421
+ end
422
+
423
+ def self.find_file_to_import(filename, load_paths)
424
+ was_sass = false
425
+ original_filename = filename
426
+
427
+ if filename[-5..-1] == ".sass"
428
+ filename = filename[0...-5]
429
+ was_sass = true
430
+ elsif filename[-4..-1] == ".css"
431
+ return filename
432
+ end
433
+
434
+ new_filename = find_full_path("#{filename}.sass", load_paths)
435
+
436
+ if new_filename.nil?
437
+ if was_sass
438
+ raise Exception.new("File to import not found or unreadable: #{original_filename}.")
439
+ else
440
+ return filename + '.css'
441
+ end
442
+ else
443
+ new_filename
444
+ end
445
+ end
446
+
447
+ def self.find_full_path(filename, load_paths)
448
+ load_paths.each do |path|
449
+ ["_#{filename}", filename].each do |name|
450
+ full_path = File.join(path, name)
451
+ if File.readable?(full_path)
452
+ return full_path
453
+ end
454
+ end
455
+ end
456
+ nil
457
+ end
458
+ end
459
+ end