zena 1.2.1 → 1.2.2

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.
Files changed (202) hide show
  1. data/History.txt +38 -1
  2. data/app/controllers/documents_controller.rb +7 -5
  3. data/app/controllers/nodes_controller.rb +47 -6
  4. data/app/controllers/user_sessions_controller.rb +12 -3
  5. data/app/controllers/virtual_classes_controller.rb +8 -2
  6. data/app/models/acl.rb +5 -2
  7. data/app/models/cached_page.rb +5 -5
  8. data/app/models/column.rb +27 -4
  9. data/app/models/group.rb +1 -1
  10. data/app/models/node.rb +106 -24
  11. data/app/models/note.rb +2 -1
  12. data/app/models/relation.rb +9 -4
  13. data/app/models/relation_proxy.rb +2 -2
  14. data/app/models/role.rb +12 -5
  15. data/app/models/site.rb +10 -9
  16. data/app/models/skin.rb +8 -0
  17. data/app/models/string_hash.rb +65 -0
  18. data/app/models/text_document.rb +1 -1
  19. data/app/models/user.rb +2 -0
  20. data/app/models/virtual_class.rb +43 -10
  21. data/app/views/comments/create.rjs +1 -32
  22. data/app/views/comments/edit.rjs +1 -1
  23. data/app/views/comments/update.rjs +1 -1
  24. data/app/views/documents/show.rhtml +1 -1
  25. data/app/views/groups/_form.rhtml +7 -0
  26. data/app/views/groups/_li.rhtml +1 -1
  27. data/app/views/nodes/500.html +2 -1
  28. data/app/views/nodes/destroy.rjs +2 -0
  29. data/app/views/sites/jobs.erb +2 -3
  30. data/app/views/templates/document_create_tabs/_file.rhtml +1 -1
  31. data/app/views/templates/document_create_tabs/_import.rhtml +4 -1
  32. data/app/views/templates/document_create_tabs/_template.rhtml +3 -0
  33. data/app/views/templates/document_create_tabs/_text_document.rhtml +3 -0
  34. data/app/views/versions/custom_tab.rhtml +1 -1
  35. data/app/views/versions/edit.rhtml +1 -1
  36. data/bricks/acls/lib/bricks/acls.rb +3 -3
  37. data/bricks/acls/zena/test/unit/acl_test.rb +15 -0
  38. data/bricks/fs_skin/lib/bricks/fs_skin.rb +190 -0
  39. data/bricks/fs_skin/zena/init.rb +1 -0
  40. data/bricks/fs_skin/zena/migrate/20110702010330_add_fs_skin_to_idx_templates.rb +12 -0
  41. data/bricks/{static → fs_skin}/zena/skins/blog/Image-edit.zafu +0 -0
  42. data/bricks/{static → fs_skin}/zena/skins/blog/Image.zafu +0 -0
  43. data/bricks/{static → fs_skin}/zena/skins/blog/Node-+index.zafu +0 -0
  44. data/bricks/{static → fs_skin}/zena/skins/blog/Node-+notFound.zafu +0 -0
  45. data/bricks/{static → fs_skin}/zena/skins/blog/Node-+search.zafu +0 -0
  46. data/bricks/{static → fs_skin}/zena/skins/blog/Node.zafu +1 -1
  47. data/bricks/{static → fs_skin}/zena/skins/blog/Post.zafu +0 -0
  48. data/bricks/{static → fs_skin}/zena/skins/blog/Project--kml.zafu +0 -0
  49. data/bricks/{static → fs_skin}/zena/skins/blog/Project.zafu +0 -0
  50. data/bricks/{static → fs_skin}/zena/skins/blog/comments.zafu +0 -0
  51. data/bricks/{static → fs_skin}/zena/skins/blog/dict.yml +0 -0
  52. data/bricks/{static → fs_skin}/zena/skins/blog/img/dateBg.jpg +0 -0
  53. data/bricks/{static → fs_skin}/zena/skins/blog/img/header.png +0 -0
  54. data/bricks/{static → fs_skin}/zena/skins/blog/img/mapPin.png +0 -0
  55. data/bricks/{static → fs_skin}/zena/skins/blog/img/menu.gif +0 -0
  56. data/bricks/{static → fs_skin}/zena/skins/blog/img/menuover.gif +0 -0
  57. data/bricks/{static → fs_skin}/zena/skins/blog/img/style.css +0 -0
  58. data/bricks/fs_skin/zena/tasks.rb +26 -0
  59. data/bricks/{static/zena/test/integration/static_integration_test.rb → fs_skin/zena/test/integration/fs_skin_integration_test.rb} +6 -6
  60. data/bricks/fs_skin/zena/test/unit/fs_skin_test.rb +33 -0
  61. data/bricks/grid/lib/bricks/grid.rb +4 -3
  62. data/bricks/tags/lib/bricks/tags.rb +1 -7
  63. data/bricks/zena/zena/migrate/20120605091558_add_ssl_login_to_site.rb +7 -0
  64. data/bricks/zena/zena/migrate/20120630123551_add_auto_publish_to_group.rb +9 -0
  65. data/config/bricks.yml +3 -3
  66. data/config/gems.yml +2 -3
  67. data/lib/tasks/zena.rake +7 -3
  68. data/lib/zafu.rb +7 -0
  69. data/lib/zafu/all.rb +21 -0
  70. data/lib/zafu/compiler.rb +7 -0
  71. data/lib/zafu/controller_methods.rb +58 -0
  72. data/lib/zafu/handler.rb +57 -0
  73. data/lib/zafu/info.rb +4 -0
  74. data/lib/zafu/markup.rb +309 -0
  75. data/lib/zafu/mock_helper.rb +42 -0
  76. data/lib/zafu/node_context.rb +203 -0
  77. data/lib/zafu/ordered_hash.rb +53 -0
  78. data/lib/zafu/parser.rb +676 -0
  79. data/lib/zafu/parsing_rules.rb +382 -0
  80. data/lib/zafu/process/ajax.rb +530 -0
  81. data/lib/zafu/process/conditional.rb +92 -0
  82. data/lib/zafu/process/context.rb +186 -0
  83. data/lib/zafu/process/forms.rb +143 -0
  84. data/lib/zafu/process/html.rb +186 -0
  85. data/lib/zafu/process/ruby_less_processing.rb +321 -0
  86. data/lib/zafu/security.rb +15 -0
  87. data/lib/zafu/template.rb +25 -0
  88. data/lib/zafu/test_helper.rb +19 -0
  89. data/lib/zafu/view_methods.rb +6 -0
  90. data/lib/zena.rb +1 -1
  91. data/lib/zena/acts/enrollable.rb +1 -1
  92. data/lib/zena/app.rb +4 -17
  93. data/lib/zena/console.rb +18 -1
  94. data/lib/zena/core_ext/file_utils.rb +13 -1
  95. data/lib/zena/core_ext/fixnum.rb +4 -0
  96. data/lib/zena/core_ext/float.rb +7 -0
  97. data/lib/zena/deploy.rb +4 -2
  98. data/lib/zena/deploy/app_init.rhtml +2 -1
  99. data/lib/zena/deploy/database.rhtml +1 -1
  100. data/lib/zena/info.rb +1 -1
  101. data/lib/zena/parser/zazen_rules.rb +4 -4
  102. data/lib/zena/routes.rb +1 -1
  103. data/lib/zena/test_controller.rb +1 -1
  104. data/lib/zena/use.rb +14 -1
  105. data/lib/zena/use/action.rb +4 -2
  106. data/lib/zena/use/ajax.rb +86 -38
  107. data/lib/zena/use/authlogic.rb +16 -1
  108. data/lib/zena/use/calendar.rb +37 -17
  109. data/lib/zena/use/conditional.rb +2 -2
  110. data/lib/zena/use/context.rb +30 -9
  111. data/lib/zena/use/dates.rb +39 -3
  112. data/lib/zena/use/display.rb +6 -19
  113. data/lib/zena/use/forms.rb +100 -79
  114. data/lib/zena/use/i18n.rb +40 -16
  115. data/lib/zena/use/query_builder.rb +0 -6
  116. data/lib/zena/use/query_node.rb +17 -4
  117. data/lib/zena/use/relations.rb +1 -3
  118. data/lib/zena/use/rendering.rb +10 -8
  119. data/lib/zena/use/scope_index.rb +5 -1
  120. data/lib/zena/use/search.rb +2 -1
  121. data/lib/zena/use/urls.rb +82 -77
  122. data/lib/zena/use/workflow.rb +12 -4
  123. data/lib/zena/use/zafu_safe_definitions.rb +37 -9
  124. data/lib/zena/use/zafu_templates.rb +49 -20
  125. data/lib/zena/use/zazen.rb +6 -2
  126. data/locale/it/LC_MESSAGES/zena.mo +0 -0
  127. data/locale/it/zena.mo +0 -0
  128. data/locale/it/zena.po +1982 -0
  129. data/public/images/arrow_back.png +0 -0
  130. data/public/images/remove_tag.png +0 -0
  131. data/public/javascripts/grid.js +800 -199
  132. data/public/javascripts/window.js +1 -1
  133. data/public/javascripts/zena.js +130 -21
  134. data/public/stylesheets/grid.css +11 -2
  135. data/public/stylesheets/zena.css +2 -1
  136. data/test/custom_queries/complex.host.yml +5 -0
  137. data/test/fixtures/files/TestNode.zafu +36 -0
  138. data/test/functional/nodes_controller_test.rb +18 -1
  139. data/test/integration/zafu_compiler/action.yml +2 -2
  140. data/test/integration/zafu_compiler/ajax.yml +44 -26
  141. data/test/integration/zafu_compiler/asset.yml +12 -2
  142. data/test/integration/zafu_compiler/basic.yml +0 -16
  143. data/test/integration/zafu_compiler/calendar.yml +6 -6
  144. data/test/integration/zafu_compiler/complex_ok.yml +23 -1
  145. data/test/integration/zafu_compiler/conditional.yml +5 -5
  146. data/test/integration/zafu_compiler/context.yml +6 -5
  147. data/test/integration/zafu_compiler/dates.yml +23 -2
  148. data/test/integration/zafu_compiler/display.yml +46 -2
  149. data/test/integration/zafu_compiler/errors.yml +2 -2
  150. data/test/integration/zafu_compiler/eval.yml +35 -7
  151. data/test/integration/zafu_compiler/forms.yml +47 -13
  152. data/test/integration/zafu_compiler/i18n.yml +2 -2
  153. data/test/integration/zafu_compiler/meta.yml +35 -1
  154. data/test/integration/zafu_compiler/query.yml +23 -4
  155. data/test/integration/zafu_compiler/relations.yml +10 -6
  156. data/test/integration/zafu_compiler/roles.yml +4 -4
  157. data/test/integration/zafu_compiler/rubyless.yml +11 -1
  158. data/test/integration/zafu_compiler/safe_definitions.yml +23 -5
  159. data/test/integration/zafu_compiler/security.yml +10 -6
  160. data/test/integration/zafu_compiler/urls.yml +23 -6
  161. data/test/integration/zafu_compiler/zafu_attributes.yml +1 -1
  162. data/test/integration/zafu_compiler/zazen.yml +14 -0
  163. data/test/selenium/Add/add3.rsel +8 -8
  164. data/test/selenium/Destroy/0setup.rsel +12 -0
  165. data/test/selenium/Destroy/destroy1.rsel +16 -0
  166. data/test/selenium/Edit/edit2.rsel +9 -9
  167. data/test/selenium/Edit/edit5.rsel +9 -9
  168. data/test/selenium/Edit/edit6.rsel +9 -9
  169. data/test/selenium/Form/form4.rsel +17 -0
  170. data/test/selenium/Toggle/toggle1.rsel +2 -0
  171. data/test/selenium/Toggle/toggle2.rsel +18 -0
  172. data/test/sites/zena/columns.yml +3 -0
  173. data/test/sites/zena/versions.yml +7 -0
  174. data/test/unit/cached_page_test.rb +13 -13
  175. data/test/unit/column_test.rb +26 -0
  176. data/test/unit/node_test.rb +16 -1
  177. data/test/unit/project_test.rb +6 -1
  178. data/test/unit/relation_test.rb +1 -1
  179. data/test/unit/role_test.rb +1 -1
  180. data/test/unit/string_hash_test.rb +30 -0
  181. data/test/unit/virtual_class_test.rb +31 -17
  182. data/test/unit/zafu_markup_test.rb +414 -0
  183. data/test/unit/zafu_node_context_test.rb +375 -0
  184. data/test/unit/zafu_ordered_hash_test.rb +69 -0
  185. data/test/unit/zena/acts/enrollable_test.rb +1 -1
  186. data/test/unit/zena/parser/zafu_asset.yml +0 -10
  187. data/test/unit/zena/parser/zazen.yml +1 -1
  188. data/test/unit/zena/parser_test.rb +1 -72
  189. data/test/unit/zena/use/dates_test.rb +1 -1
  190. data/test/unit/zena/use/rendering_test.rb +24 -7
  191. data/test/unit/zena/use/scope_index_test.rb +17 -0
  192. data/test/unit/zena/use/zazen_test.rb +2 -1
  193. data/zena.gemspec +71 -37
  194. metadata +104 -83
  195. data/app/views/nodes/destroy.erb +0 -0
  196. data/bricks/static/lib/bricks/static.rb +0 -151
  197. data/bricks/static/zena/init.rb +0 -1
  198. data/bricks/static/zena/migrate/20110702010330_add_static_to_idx_templates.rb +0 -12
  199. data/bricks/static/zena/test/unit/static_test.rb +0 -33
  200. data/lib/zena/parser/zafu_rules.rb +0 -244
  201. data/lib/zena/parser/zafu_tags.rb +0 -198
  202. data/lib/zena/parser/zena_rules.rb +0 -23
@@ -0,0 +1,42 @@
1
+ module Zafu
2
+ # This is the 'src_helper' used when none is provided. Its main purpose is to provide some information
3
+ # during testing.
4
+ class MockHelper
5
+ def initialize(strings = {})
6
+ @strings = strings
7
+ end
8
+
9
+ def get_template_text(opts)
10
+ src = opts[:src]
11
+ folder = (opts[:base_path] && opts[:base_path] != '') ? opts[:base_path][1..-1].split('/') : []
12
+ src = src[1..-1] if src[0..0] == '/' # just ignore the 'relative' or 'absolute' tricks.
13
+ url = (folder + src.split('/')).join('_')
14
+ if test = @strings[url]
15
+ return [test['src'], url.split('_').join('/')]
16
+ else
17
+ nil
18
+ end
19
+ end
20
+
21
+ def template_url_for_asset(opts)
22
+ "/test_#{opts[:type]}/#{opts[:src]}"
23
+ end
24
+
25
+ def method_missing(sym, *args)
26
+ arguments = args.map do |arg|
27
+ if arg.kind_of?(Hash)
28
+ res = []
29
+ arg.each do |k,v|
30
+ unless v.nil?
31
+ res << "#{k}:#{v.inspect.gsub(/'|"/, "|")}"
32
+ end
33
+ end
34
+ res.sort.join(' ')
35
+ else
36
+ arg.inspect.gsub(/'|"/, "|")
37
+ end
38
+ end
39
+ res = "[#{sym} #{arguments.join(' ')}]"
40
+ end
41
+ end # DummyHelper
42
+ end
@@ -0,0 +1,203 @@
1
+ module Zafu
2
+ class NodeContext
3
+ # The name of the variable halding the current object or list ("@node", "var1")
4
+ attr_reader :name
5
+
6
+ # The previous NodeContext
7
+ attr_reader :up
8
+
9
+ # The type of object contained in the current context (Node, Page, Image)
10
+ attr_reader :klass
11
+
12
+ # The current DOM prefix to use when building DOM ids. This is set by the parser when
13
+ # it has a name or dom id defined ('main', 'related', 'list', etc).
14
+ attr_writer :dom_prefix
15
+
16
+ # This is used to force a given dom_id (in saved templates for example).
17
+ attr_accessor :saved_dom_id
18
+
19
+ # Any kind of information that the compiler might need to use (QueryBuilder query used
20
+ # to fetch the node for example).
21
+ attr_reader :opts
22
+
23
+ def initialize(name, klass, up = nil, opts = {})
24
+ @name, @klass, @up, @opts = name, klass, up, opts
25
+ end
26
+
27
+ def move_to(name, klass, opts={})
28
+ self.class.new(name, klass, self, opts)
29
+ end
30
+
31
+ # Since the idiom to write the node context name is the main purpose of this class, it
32
+ # deserves this shortcut.
33
+ def to_s
34
+ name
35
+ end
36
+
37
+ def single_class
38
+ @single_class ||= Array(klass).flatten.first
39
+ end
40
+
41
+ # Return true if the NodeContext represents an element of the given type. We use 'will_be' because
42
+ # it is equivalent to 'is_a', but for future objects (during rendering).
43
+ def will_be?(type)
44
+ single_class <= type
45
+ end
46
+
47
+ # Return a new node context that corresponds to the current object when rendered alone (in an ajax response or
48
+ # from a direct 'show' in a controller). The returned node context has no parent (up is nil).
49
+ # The convention is to use the class of the current object to build this name.
50
+ # You can also use an 'after_class' parameter to move up in the current object's class hierarchy to get
51
+ # ivar name (see #master_class).
52
+ def as_main(after_class = nil)
53
+ klass = after_class ? master_class(after_class) : single_class
54
+ res = self.class.new("@#{klass.to_s.underscore}", single_class, nil)
55
+ res.propagate_dom_scope! if @dom_scope
56
+ res.dom_prefix = self.dom_prefix
57
+ res
58
+ end
59
+
60
+ # Find the class just afer 'after_class' in the class hierarchy.
61
+ # For example if we have Dog < Mamal < Animal < Creature,
62
+ # master_class(Creature) would return Animal
63
+ def master_class(after_class)
64
+ klass = single_class
65
+ begin
66
+ up = klass.superclass
67
+ return klass if up == after_class
68
+ end while klass = up
69
+ return self.klass
70
+ end
71
+
72
+ # Generate a unique DOM id for this element based on dom_scopes defined in parent contexts.
73
+ # :code option returns ruby
74
+ # :erb option returns either string content or "<%= ... %>"
75
+ # default returns something to insert in interpolated string such as '#{xxx}'
76
+ def dom_id(opts = {})
77
+ dom_prefix = opts[:dom_prefix] || self.dom_prefix
78
+ options = {:list => true, :erb => true}.merge(opts)
79
+
80
+ if options[:erb] || options[:code]
81
+ dom = dom_id(options.merge(:erb => false, :code => false))
82
+
83
+ if dom =~ /^#\{([^\{]+)\}$/
84
+ code = $1
85
+ elsif dom =~ /#\{/
86
+ code = "%Q{#{dom}}"
87
+ else
88
+ str = dom
89
+ code = dom.inspect
90
+ end
91
+
92
+ if options[:code]
93
+ code
94
+ else
95
+ str || "<%= #{code} %>"
96
+ end
97
+ else
98
+ @saved_dom_id || if options[:list]
99
+ scopes = dom_scopes
100
+ scopes = [dom_prefix] if scopes.empty?
101
+ scopes + [make_scope_id]
102
+ else
103
+ scopes = dom_scopes
104
+ scopes + ((@up || scopes.empty?) ? [dom_prefix] : [])
105
+ end.compact.uniq.join('_')
106
+ end
107
+ end
108
+
109
+ # This holds the current context's unique name if it has it's own or one from the hierarchy. If
110
+ # none is found, it builds one.
111
+ def dom_prefix
112
+ @dom_prefix || (@up ? @up.dom_prefix : nil)
113
+ end
114
+
115
+ # Return dom_prefix without looking up.
116
+ def raw_dom_prefix
117
+ @dom_prefix
118
+ end
119
+
120
+ # Mark the current context as being a looping element (each) whose DOM id needs to be propagated to sub-nodes
121
+ # in order to ensure uniqueness of the dom_id (loops in loops problem).
122
+ def propagate_dom_scope!
123
+ @dom_scope = true
124
+ end
125
+
126
+ # Returns the first occurence of the klass up in the hierachy
127
+ # This does not resolve [Node] as [Node].first.
128
+ def get(klass)
129
+ if list_context?
130
+ return @up ? @up.get(klass) : nil
131
+ end
132
+ if self.klass <= klass
133
+ self
134
+ # return self unless list_context?
135
+ #
136
+ # res_class = self.klass
137
+ # method = self.name
138
+ # while res_class.kind_of?(Array)
139
+ # method = "#{method}.first"
140
+ # res_class = res_class.first
141
+ # end
142
+ # move_to(method, res_class)
143
+ elsif @up
144
+ @up.get(klass)
145
+ else
146
+ nil
147
+ end
148
+ end
149
+
150
+ def up(klass = nil)
151
+ klass ? @up.get(klass) : @up
152
+ end
153
+
154
+ # Return true if the current klass is an Array.
155
+ def list_context?
156
+ klass.kind_of?(Array)
157
+ end
158
+
159
+ # Return the name of the current class with underscores like 'sub_page'.
160
+ def underscore
161
+ class_name.to_s.underscore
162
+ end
163
+
164
+ # Return the 'real' class name or the superclass name if the current class is an anonymous class.
165
+ def class_name
166
+ klass = single_class
167
+ while klass.name == ''
168
+ klass = klass.superclass
169
+ end
170
+ if list_context?
171
+ "[#{klass}]"
172
+ else
173
+ klass.name
174
+ end
175
+ end
176
+
177
+ # Return the name to use for input fields
178
+ def form_name
179
+ @form_name ||= master_class(ActiveRecord::Base).name.underscore
180
+ end
181
+
182
+ protected
183
+ # List of scopes defined in ancestry (used to generate dom_id).
184
+ def dom_scopes
185
+ return [@saved_dom_id] if @dom_scope && @saved_dom_id
186
+ if @up
187
+ scopes = @up.dom_scopes
188
+ if @dom_scope
189
+ (scopes.empty? ? [dom_prefix] : scopes) + [make_scope_id]
190
+ else
191
+ scopes
192
+ end
193
+ else
194
+ @dom_scope ? [dom_prefix, make_scope_id] : []
195
+ end
196
+ end
197
+
198
+ private
199
+ def make_scope_id
200
+ "\#{#{@name}.zip}"
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,53 @@
1
+ module Zafu
2
+
3
+ if RUBY_VERSION.split('.')[0..1].join('.').to_f > 1.8
4
+ OrderedHash = Hash
5
+ else
6
+ class OrderedHash < Hash
7
+
8
+ def []=(k, v)
9
+ get_keys << k unless get_keys.include?(k)
10
+ super
11
+ end
12
+
13
+ def merge!(hash)
14
+ hash.keys.each do |k|
15
+ get_keys << k unless get_keys.include?(k)
16
+ end
17
+ super
18
+ end
19
+
20
+ def merge(hash)
21
+ res = dup
22
+ res.merge!(hash)
23
+ res
24
+ end
25
+
26
+ alias o_keys keys
27
+ def get_keys
28
+ @keys ||= o_keys
29
+ end
30
+
31
+ def keys
32
+ get_keys.dup
33
+ end
34
+
35
+ def each
36
+ keys.each do |k|
37
+ yield(k, self[k])
38
+ end
39
+ end
40
+
41
+ def delete(k)
42
+ get_keys.delete(k)
43
+ super
44
+ end
45
+
46
+ def dup
47
+ copy = super
48
+ copy.instance_variable_set(:@keys, keys)
49
+ copy
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,676 @@
1
+ module Zafu
2
+
3
+ def self.parser_with_rules(*modules)
4
+ parser = Class.new(Parser)
5
+ modules.flatten.each do |mod|
6
+ parser.send(:include, mod)
7
+ end
8
+ parser
9
+ end
10
+
11
+ class Parser
12
+ # If you wonder what the difference is between 'after_wrap' and 'after_process' here it is:
13
+ # 'after_wrap' is called by the 'wrap' method from within the method handler, 'after_process' is called
14
+ # at the very end. Example:
15
+ #
16
+ # <% if var = Node.all %> | <---
17
+ # <li>...</li> <--- content for after_wrap | <--- content for after_process
18
+ # <% end %> | <---
19
+ #
20
+
21
+
22
+ TEXT_CALLBACKS = %w{before_parse after_parse before_wrap wrap after_wrap after_process}
23
+ PROCESS_CALLBACKS = %w{before_process expander process_unknown}
24
+ CALLBACKS = TEXT_CALLBACKS + PROCESS_CALLBACKS
25
+
26
+ @@callbacks = {}
27
+ attr_accessor :text, :name, :method, :pass, :options, :blocks, :ids, :defined_ids, :parent, :errors
28
+
29
+ # Method parameters "<r:show attr='name'/>" (params contains {'attr' => 'name'}).
30
+ attr_accessor :params
31
+
32
+ class << self
33
+ def new_with_url(path, opts={})
34
+ helper = opts[:helper] || Zafu::MockHelper.new
35
+ text, fullpath, base_path = self.get_template_text(path, helper)
36
+ return parser_error("template '#{path}' not found", 'include') unless text
37
+ self.new(text, :helper => helper, :base_path => base_path, :included_history => [fullpath], :root => path, :master_template => opts[:master_template])
38
+ end
39
+
40
+ # Retrieve the template text in the current folder or as an absolute path.
41
+ # This method is used when 'including' text
42
+ def get_template_text(path, helper, base_path=nil, opts={})
43
+ cache = (visitor.zafu_cache ||= {})
44
+ unless res = cache[path]
45
+ res = helper.send(:get_template_text, path, base_path, opts)
46
+ res = [parser_error("template '#{path}' not found", 'include'), nil, nil] unless res
47
+ cache[path] = res
48
+ end
49
+ res
50
+ end
51
+
52
+ def parser_error(message, method)
53
+ "<span class='parser_error'><span class='method'>#{erb_safe method}</span> <span class='message'>#{erb_safe message}</span></span>"
54
+ end
55
+
56
+ def erb_safe(text)
57
+ text.gsub('<%', '&lt;%').gsub('%>', '%&gt;')
58
+ end
59
+
60
+ CALLBACKS.each do |clbk|
61
+ eval %Q{
62
+ attr_accessor :#{clbk}_callbacks
63
+
64
+ def #{clbk}_callbacks
65
+ @#{clbk}_callbacks ||= superclass.respond_to?(:#{clbk}_callbacks) ? superclass.#{clbk}_callbacks : []
66
+ end
67
+
68
+ def #{clbk}(*args)
69
+ self.#{clbk}_callbacks += args
70
+ end
71
+ }
72
+ end
73
+ end # class << self
74
+
75
+ PROCESS_CALLBACKS.each do |clbk|
76
+ eval %Q{
77
+ def #{clbk}
78
+ self.class.#{clbk}_callbacks.each do |callback|
79
+ send(callback)
80
+ end
81
+ end
82
+ }
83
+ end
84
+
85
+ def expander
86
+ self.class.expander_callbacks.reverse_each do |callback|
87
+ if res = send(callback)
88
+ if res.kind_of?(String)
89
+ @result << res
90
+ end
91
+ return @result
92
+ end
93
+ end
94
+ nil
95
+ end
96
+
97
+ TEXT_CALLBACKS.each do |clbk|
98
+ eval %Q{
99
+ def #{clbk}(text)
100
+ self.class.#{clbk}_callbacks.each do |callback|
101
+ text = send(callback, text)
102
+ end
103
+ text
104
+ end
105
+ }
106
+ end
107
+
108
+ alias wrap_callbacks wrap
109
+
110
+ def wrap(text)
111
+ after_wrap(
112
+ wrap_callbacks(
113
+ before_wrap(text) + @out_post
114
+ )
115
+ # @text contains unparsed data (white space)
116
+ ) + @text
117
+ end
118
+
119
+ # This method is called at the very beginning of the processing chain and is
120
+ # used to store state to make 'process' reintrant...
121
+ def save_state
122
+ {
123
+ :@context => @context, # <== we need this when rendering twice the same part
124
+ :@result => @result,
125
+ :@out_post => @out_post,
126
+ :@params => @params.dup,
127
+ :@method => @method,
128
+ :@var => @var,
129
+ }
130
+ end
131
+
132
+ # Restore state from a hash
133
+ def restore_state(saved)
134
+ saved.each do |key, value|
135
+ instance_variable_set(key, value)
136
+ end
137
+ end
138
+
139
+ def parser_error(message, method = @method, halt = true)
140
+ if halt
141
+ self.class.parser_error(message, method)
142
+ else
143
+ @errors << self.class.parser_error(message, method)
144
+ nil
145
+ end
146
+ end
147
+
148
+ def parser_continue(message, method = @method)
149
+ parser_error(message, method, false)
150
+ end
151
+
152
+ def process_unknown
153
+ self.class.process_unknown_callbacks.each do |callback|
154
+ if res = send(callback)
155
+ return res
156
+ end
157
+ end
158
+ @errors.empty? ? default_unknown : show_errors
159
+ end
160
+
161
+ def show_errors
162
+ @errors.join(' ')
163
+ end
164
+
165
+ def initialize(text, opts={})
166
+ @stack = []
167
+ @ok = true
168
+ @blocks = []
169
+ @errors = []
170
+
171
+ @options = {:mode=>:void, :method=>'void'}.merge(opts)
172
+ @params = @options.delete(:params) || {}
173
+ @method = @options.delete(:method)
174
+ @ids = @options[:ids] ||= {}
175
+ original_ids = @ids.dup
176
+ @defined_ids = {} # ids defined in this node or this node's sub blocks
177
+ mode = @options.delete(:mode)
178
+ @parent = @options.delete(:parent)
179
+
180
+ if opts[:sub]
181
+ @text = text
182
+ else
183
+ @text = before_parse(text)
184
+ if part = opts[:part]
185
+ # Optimization: try to find part from id
186
+ if @text =~ /^(.*?)<[^<>]+id='#{part}'/m
187
+ eat $1
188
+ end
189
+ end
190
+ end
191
+
192
+ start(mode)
193
+
194
+ # set name
195
+ @name ||= extract_name
196
+ @options[:ids][@name] = self if @name
197
+
198
+ unless opts[:sub]
199
+ @text = after_parse(@text)
200
+ end
201
+
202
+ @ids.keys.each do |k|
203
+ if original_ids[k] != @ids[k]
204
+ @defined_ids[k] = @ids[k]
205
+ end
206
+ end
207
+ @ok
208
+ end
209
+
210
+ def extract_name
211
+ @options[:name] || @params[:id]
212
+ end
213
+
214
+ def to_erb(context)
215
+ context[:helper] ||= @options[:helper]
216
+ process(context)
217
+ end
218
+
219
+ def start(mode)
220
+ enter(mode)
221
+ end
222
+
223
+ # Pass some contextual information to siblings
224
+ def pass(elems = nil)
225
+ return @pass unless elems
226
+ (@pass ||= {}).merge!(elems)
227
+ @pass
228
+ end
229
+
230
+ # Hook called when replacing part of an included template with '<r:with part='main'>...</r:with>'
231
+ # This replaces the current object 'self' which is in the original included template, with the custom version 'obj'.
232
+ def replace_with(obj)
233
+ # keep @method (obj's method is always 'with')
234
+ @blocks = obj.blocks.empty? ? @blocks : obj.blocks
235
+ obj.params.delete(:part)
236
+ @params.merge!(obj.params)
237
+ end
238
+
239
+ # Hook called when including a part "<r:include template='layout' part='title'/>"
240
+ def include_part(obj)
241
+ [obj]
242
+ end
243
+
244
+ def empty?
245
+ @blocks == [] && (@params == {} || @params == {:part => @params[:part]})
246
+ end
247
+
248
+ def process(context={})
249
+ return '' if @method == 'ignore' || @method.blank?
250
+
251
+ saved = save_state
252
+
253
+ if @name
254
+ # we pass the name as 'context' in the children tags
255
+ @context = context.merge(:name => @name)
256
+ else
257
+ @context = context
258
+ end
259
+ # FIXME: replace with array and join (faster)
260
+ @result = ""
261
+ @out_post = ""
262
+ @pass = nil
263
+
264
+ before_process
265
+
266
+ res = wrap(expander || default_expander)
267
+
268
+ res = after_process(res)
269
+
270
+ # restore state
271
+ restore_state(saved)
272
+
273
+ res
274
+ end
275
+
276
+ # Default processing
277
+ def default_expander
278
+ if respond_to?("r_#{@method}".to_sym)
279
+ do_method("r_#{@method}".to_sym)
280
+ else
281
+ do_method(:process_unknown)
282
+ end
283
+ end
284
+
285
+ def do_method(sym)
286
+ res = self.send(sym)
287
+ if res.kind_of?(String)
288
+ @result << res
289
+ elsif @result.blank?
290
+ @result << (@errors.empty? ? '' : show_errors)
291
+ end
292
+ @result
293
+ end
294
+
295
+ def r_void
296
+ expand_with
297
+ end
298
+
299
+ alias to_s r_void
300
+
301
+ def r_inspect
302
+ expand_with(:preflight=>true)
303
+ @blocks = []
304
+ self.inspect
305
+ end
306
+
307
+ # basic rule to display errors
308
+ def default_unknown
309
+ sp = ""
310
+ @params.each do |k,v|
311
+ sp += " #{k}=#{v.inspect.gsub("'","TMPQUOTE").gsub('"',"'").gsub("TMPQUOTE",'"')}"
312
+ end
313
+
314
+ res = "<span class='parser_unknown'>&lt;r:#{@method}#{sp}"
315
+ inner = expand_with
316
+ if inner != ''
317
+ res + "&gt;</span>#{inner}<span class='parser_unknown'>&lt;r:/#{@method}&gt;</span>"
318
+ else
319
+ res + "/&gt;</span>"
320
+ end
321
+ end
322
+
323
+ # Set context with variables (unsafe) from template.
324
+ def r_expand_with
325
+ hash = {}
326
+ @params.each do |k,v|
327
+ hash["exp_#{k}"] = v.inspect
328
+ end
329
+ expand_with(hash)
330
+ end
331
+
332
+ def r_ignore
333
+ ''
334
+ end
335
+
336
+ def include_template
337
+ return parser_error("missing 'template' attribute") unless @params[:template]
338
+ if @options[:part] && @options[:part] == @params[:part]
339
+ # fetching only a part, do not open this element (same as original caller) as it is useless and will make us loop the loop.
340
+ @method = 'ignore'
341
+ enter(:void)
342
+ return
343
+ end
344
+ @method = 'void'
345
+
346
+ # fetch text
347
+ @options[:included_history] ||= []
348
+
349
+ included_text, absolute_url, base_path = self.class.get_template_text(@params[:template], @options[:helper], @options[:base_path])
350
+
351
+ if absolute_url
352
+ absolute_url += "::#{@params[:part].gsub('/','_')}" if @params[:part]
353
+ absolute_url += "??#{@options[:part].gsub('/','_')}" if @options[:part]
354
+ if @options[:included_history].include?(absolute_url)
355
+ included_text = parser_error("infinity loop: #{(@options[:included_history] + [absolute_url]).join(' --&gt; ')}", 'include')
356
+ else
357
+ included_history = @options[:included_history] + [absolute_url]
358
+ end
359
+ else
360
+ # Error: included_text contains the error meessage
361
+ @blocks = [included_text]
362
+ return
363
+ end
364
+
365
+ res = self.class.new(included_text, :helper => @options[:helper], :base_path => base_path, :included_history => included_history, :part => @params[:part], :parent => self) # we set :part to avoid loop failure when doing self inclusion
366
+
367
+ if @params[:part]
368
+ if iblock = res.ids[@params[:part]]
369
+ included_blocks = include_part(iblock)
370
+ # get all ids from inside the included part:
371
+ @ids.merge! iblock.defined_ids
372
+ else
373
+ included_blocks = [parser_error("'#{@params[:part]}' not found in template '#{@params[:template]}'", 'include')]
374
+ end
375
+ else
376
+ included_blocks = res.blocks
377
+ @ids.merge! res.ids
378
+ end
379
+
380
+ enter(:void) # normal scan on content
381
+ # replace 'with'
382
+
383
+ @blocks.each do |b|
384
+ next if b.kind_of?(String) || b.method != 'with'
385
+ if target = res.ids[b.params[:part]]
386
+ if target.kind_of?(String)
387
+ # error
388
+ elsif b.empty?
389
+ target.method = 'ignore'
390
+ else
391
+ target.replace_with(b)
392
+ end
393
+ else
394
+ # part not found
395
+ parser_error("'#{b.params[:part]}' not found in template '#{@params[:template]}'", 'with')
396
+ end
397
+ end
398
+ @blocks = included_blocks
399
+ end
400
+
401
+ # Return a hash of all descendants. Find a specific descendant with descendant['form'] for example.
402
+ def all_descendants
403
+ @all_descendants ||= begin
404
+ d = {}
405
+ @blocks.each do |b|
406
+ next if b.kind_of?(String)
407
+ b.public_descendants.each do |k,v|
408
+ d[k] ||= []
409
+ d[k] += v
410
+ end
411
+ # latest is used first: use direct children before grandchildren.
412
+ d[b.method] ||= []
413
+ d[b.method] << b
414
+ end
415
+ d
416
+ end
417
+ end
418
+
419
+ # Find a direct child with +child[method]+.
420
+ def child
421
+ Hash[*@blocks.map do |b|
422
+ b.kind_of?(String) ? nil : [b.method, b]
423
+ end.compact.flatten]
424
+ end
425
+
426
+ def dynamic_blocks?
427
+ @blocks.detect { |b| !b.kind_of?(String) }
428
+ end
429
+
430
+ def descendants(key)
431
+ all_descendants[key] || []
432
+ end
433
+
434
+ def ancestors
435
+ @ancestors ||= begin
436
+ if parent
437
+ parent.ancestors + [parent]
438
+ else
439
+ []
440
+ end
441
+ end
442
+ end
443
+
444
+ alias public_descendants all_descendants
445
+
446
+ # Return the last defined parent for the given keys.
447
+ def ancestor(keys)
448
+ keys = Array(keys)
449
+ ancestors.reverse_each do |a|
450
+ if keys.include?(a.method)
451
+ return a
452
+ end
453
+ end
454
+ nil
455
+ end
456
+
457
+ # Return the last defined descendant for the given key.
458
+ def descendant(key)
459
+ descendants(key).last
460
+ end
461
+
462
+ # Return the root block (the one opened first).
463
+ def root
464
+ @root ||= parent ? parent.root : self
465
+ end
466
+
467
+ def success?
468
+ return @ok
469
+ end
470
+
471
+ def flush(str=@text)
472
+ return if str == ''
473
+ if @blocks.last.kind_of?(String)
474
+ @blocks[-1] << str
475
+ else
476
+ @blocks << str
477
+ end
478
+ @text = @text[str.length..-1]
479
+ end
480
+
481
+ # Build blocks
482
+ def store(obj)
483
+ if obj.kind_of?(String) && @blocks.last.kind_of?(String)
484
+ @blocks[-1] << obj
485
+ elsif obj != ''
486
+ @blocks << obj
487
+ end
488
+ end
489
+
490
+ # Output ERB code during ast processing.
491
+ def out(str)
492
+ @result << str
493
+ # Avoid double entry when this is the last call in a render method.
494
+ true
495
+ end
496
+
497
+ # Output ERB code that will be inserted after @result.
498
+ def out_post(str)
499
+ @out_post << str
500
+ # Avoid double entry when this is the last call in a render method.
501
+ true
502
+ end
503
+
504
+ # Advance parser.
505
+ def eat(arg)
506
+ if arg.kind_of?(String)
507
+ len = arg.length
508
+ elsif arg.kind_of?(Fixnum)
509
+ len = arg
510
+ else
511
+ raise
512
+ end
513
+ @text = @text[len..-1]
514
+ end
515
+
516
+ def enter(mode)
517
+ @stack << mode
518
+ # puts "ENTER(#{@method},:#{mode}) [#{@text}] #{@zafu_tag_count.inspect}"
519
+ if mode == :void
520
+ sym = :scan
521
+ else
522
+ sym = "scan_#{mode}".to_sym
523
+ end
524
+ while (@text != '' && @stack[-1] == mode)
525
+ # puts "CONTINUE(#{@method},:#{mode}) [#{@text}] #{@zafu_tag_count.inspect}"
526
+ self.send(sym)
527
+ end
528
+ # puts "LEAVE(#{@method},:#{mode}) [#{@text}] #{@zafu_tag_count.inspect}"
529
+ end
530
+
531
+ def make(mode, opts={})
532
+ if opts[:text]
533
+ custom_text = opts.delete(:text)
534
+ end
535
+ text = custom_text || @text
536
+ opts = @options.merge(opts).merge(:sub => true, :mode => mode, :parent => self)
537
+ new_obj = self.class.new(text, opts)
538
+ if new_obj.success?
539
+ @text = new_obj.text unless custom_text
540
+ new_obj.text = ""
541
+ store new_obj
542
+ else
543
+ flush @text[0..(new_obj.text.length - @text.length)] unless custom_text
544
+ end
545
+ # puts "MADE #{new_obj.inspect}"
546
+ # puts "TEXT #{@text.inspect}"
547
+ new_obj
548
+ end
549
+
550
+ def leave(mode=nil)
551
+ if mode.nil?
552
+ @stack = []
553
+ return
554
+ end
555
+ pop = true
556
+ while @stack != [] && pop
557
+ pop = @stack.pop
558
+ break if pop == mode
559
+ end
560
+ end
561
+
562
+ def fail
563
+ @ok = false
564
+ @stack = []
565
+ end
566
+
567
+ def check_params(*args)
568
+ missing = []
569
+ if args[0].kind_of?(Array)
570
+ # or groups
571
+ ok = false
572
+ args.each_index do |i|
573
+ unless args[i].kind_of?(Array)
574
+ missing[i] = [args[i]]
575
+ next
576
+ end
577
+ missing[i] = []
578
+ args[i].each do |arg|
579
+ missing[i] << arg.to_s unless @params[arg]
580
+ end
581
+ if missing[i] == []
582
+ ok = true
583
+ break
584
+ end
585
+ end
586
+ if ok
587
+ return true
588
+ else
589
+ out "[#{@method} parameter(s) missing:#{missing[0].sort.join(', ')}]"
590
+ return false
591
+ end
592
+ else
593
+ args.each do |arg|
594
+ missing << arg.to_s unless @params[arg]
595
+ end
596
+ end
597
+ if missing != []
598
+ out "[#{@method} parameter(s) missing:#{missing.sort.join(', ')}]"
599
+ return false
600
+ end
601
+ true
602
+ end
603
+
604
+ def expand_block(block, new_context={})
605
+ block.process(@context.merge(new_context))
606
+ end
607
+
608
+ def expand_with(acontext={})
609
+
610
+ blocks = acontext.delete(:blocks) || @blocks
611
+ res = ""
612
+
613
+ only = acontext[:only]
614
+ new_context = @context.merge(acontext)
615
+
616
+ if acontext[:ignore]
617
+ new_context[:ignore] = (@context[:ignore] || []) + (acontext[:ignore] || []).uniq
618
+ end
619
+
620
+ if acontext[:no_ignore]
621
+ new_context[:ignore] = (new_context[:ignore] || []) - acontext[:no_ignore]
622
+ end
623
+
624
+ ignore = new_context[:ignore]
625
+
626
+ blocks.each do |b|
627
+ if b.kind_of?(String)
628
+ if (!only || (only.kind_of?(Array) && only.include?(:string))) && (!ignore || !ignore.include?(:string))
629
+ res << b
630
+ end
631
+ elsif (!only || (only.kind_of?(Array) && only.include?(b.method)) || only =~ b.method) && (!ignore || !ignore.include?(b.method))
632
+ res << b.process(new_context.dup)
633
+ if pass = b.pass
634
+ new_context.merge!(pass)
635
+ end
636
+ end
637
+ end
638
+ res
639
+ end
640
+
641
+ def inspect
642
+ attributes = []
643
+ params = []
644
+ (@params || {}).each do |k,v|
645
+ unless v.nil?
646
+ params << "#{k.inspect.gsub('"', "'")}=>'#{v}'"
647
+ end
648
+ end
649
+ attributes << " {= #{params.sort.join(', ')}}" unless params == []
650
+
651
+ context = []
652
+ (@context || {}).each do |k,v|
653
+ unless v.nil?
654
+ context << "#{k.inspect.gsub('"', "'")}=>'#{v}'"
655
+ end
656
+ end
657
+ attributes << " {> #{context.sort.join(', ')}}" unless context == []
658
+
659
+ res = []
660
+ @blocks.each do |b|
661
+ if b.kind_of?(String)
662
+ res << b
663
+ else
664
+ res << b.inspect
665
+ end
666
+ end
667
+ result = "[#{@method}#{attributes.join('')}"
668
+ if res != []
669
+ result += "]#{res}[/#{@method}]"
670
+ else
671
+ result += "/]"
672
+ end
673
+ result + @text
674
+ end
675
+ end # Parser
676
+ end # Zafu