zafu 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ *.gem
2
+ .DS_Store
data/History.txt ADDED
@@ -0,0 +1,9 @@
1
+ == 0.5.0 2010-03-21
2
+
3
+ * 6 major enhancement
4
+ * Initial release.
5
+ * Uses helpers to resolve methods.
6
+ * Compiles 'html' with rubyless declarations.
7
+ * Guesses main ivar from view.
8
+ * Tries to resolve method by adding current node parameter.
9
+ * Added basic conditional execution with 'else' and dummy 'if' clauses.
data/README.rdoc ADDED
@@ -0,0 +1,56 @@
1
+ = zafu
2
+
3
+ * http://zenadmin.org/en/zafu
4
+
5
+ == DESCRIPTION:
6
+
7
+ Provides a powerful templating language based on xhtml for rails.
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * The current implementation of zafu is in zena's repository (http://zenadmin.org).
12
+ The content will be moved here as soon as zafu works as a standalone gem.
13
+
14
+ == SYNOPSIS:
15
+
16
+ <ul do='images where name like "%flower%" in site'>
17
+ <li do='each'>
18
+ <r:img/>
19
+ <r:link/>
20
+ </li>
21
+ </ul>
22
+
23
+ == REQUIREMENTS:
24
+
25
+ * FIX (not very clear yet)
26
+ * yamltest
27
+
28
+ == INSTALL:
29
+
30
+ * !! not ready for deployment, please do not install !!
31
+ * sudo gem install zafu
32
+
33
+ == LICENSE:
34
+
35
+ (The MIT License)
36
+
37
+ Copyright (c) 2007-2009 Gaspard Bucher
38
+
39
+ Permission is hereby granted, free of charge, to any person obtaining
40
+ a copy of this software and associated documentation files (the
41
+ 'Software'), to deal in the Software without restriction, including
42
+ without limitation the rights to use, copy, modify, merge, publish,
43
+ distribute, sublicense, and/or sell copies of the Software, and to
44
+ permit persons to whom the Software is furnished to do so, subject to
45
+ the following conditions:
46
+
47
+ The above copyright notice and this permission notice shall be
48
+ included in all copies or substantial portions of the Software.
49
+
50
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
51
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
52
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
53
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
54
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
55
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
56
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,56 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require(File.join(File.dirname(__FILE__), 'lib/zafu/info'))
4
+
5
+ begin
6
+ require 'jeweler'
7
+ Jeweler::Tasks.new do |gem|
8
+ gem.version = Zafu::VERSION
9
+ gem.name = 'zafu'
10
+ gem.summary = %Q{Provides a powerful templating language based on xhtml for rails}
11
+ gem.description = %Q{Provides a powerful templating language based on xhtml for rails}
12
+ gem.email = "gaspard@teti.ch"
13
+ gem.homepage = "http://zenadmin.org/zafu"
14
+ gem.authors = ["Gaspard Bucher"]
15
+ gem.add_development_dependency "shoulda", ">= 0"
16
+ gem.add_dependency "rubyless", ">= 0.4.0"
17
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
18
+ end
19
+ Jeweler::GemcutterTasks.new
20
+ rescue LoadError
21
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
22
+ end
23
+
24
+ require 'rake/testtask'
25
+ Rake::TestTask.new(:test) do |test|
26
+ test.libs << 'lib' << 'test'
27
+ test.pattern = 'test/**/*_test.rb'
28
+ test.verbose = true
29
+ end
30
+
31
+ begin
32
+ require 'rcov/rcovtask'
33
+ Rcov::RcovTask.new do |test|
34
+ test.libs << 'test'
35
+ test.pattern = 'test/**/*_test.rb'
36
+ test.verbose = true
37
+ end
38
+ rescue LoadError
39
+ task :rcov do
40
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
41
+ end
42
+ end
43
+
44
+ task :test => :check_dependencies
45
+
46
+ task :default => :test
47
+
48
+ require 'rake/rdoctask'
49
+ Rake::RDocTask.new do |rdoc|
50
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
51
+
52
+ rdoc.rdoc_dir = 'rdoc'
53
+ rdoc.title = "zafu #{version}"
54
+ rdoc.rdoc_files.include('README*')
55
+ rdoc.rdoc_files.include('lib/**/*.rb')
56
+ end
data/lib/zafu/all.rb ADDED
@@ -0,0 +1,17 @@
1
+ require 'zafu/parsing_rules'
2
+ # require 'zafu/process/ajax'
3
+ require 'zafu/process/html'
4
+ require 'zafu/process/ruby_less'
5
+ require 'zafu/process/context'
6
+ require 'zafu/process/conditional'
7
+
8
+ module Zafu
9
+ All = [
10
+ Zafu::ParsingRules,
11
+ # Zafu::Process::Ajax,
12
+ Zafu::Process::HTML,
13
+ Zafu::Process::Context,
14
+ Zafu::Process::Conditional,
15
+ Zafu::Process::RubyLess
16
+ ]
17
+ end
@@ -0,0 +1,7 @@
1
+ require 'zafu/parser'
2
+ require 'zafu/node_context'
3
+ require 'zafu/all'
4
+
5
+ module Zafu
6
+ Compiler = Zafu.parser_with_rules(Zafu::All)
7
+ end
@@ -0,0 +1,49 @@
1
+ module Zafu
2
+ module ControllerMethods
3
+ def self.included(base)
4
+ base.helper_method :zafu_context, :get_template_text, :template_url_for_asset
5
+ if RAILS_ENV == 'development'
6
+ base.class_eval do
7
+ def render_for_file_with_rebuild(template_path, status = nil, layout = nil, locals = {}) #:nodoc:
8
+ path = template_path.respond_to?(:path_without_format_and_extension) ? template_path.path_without_format_and_extension : template_path
9
+ logger.info("Rendering #{path}" + (status ? " (#{status})" : '')) if logger
10
+ # if params[:rebuild] == 'true'
11
+ t = self.view_paths.find_template(template_path, 'html')
12
+ t.previously_last_modified = nil
13
+ # end
14
+ render_for_text @template.render(:file => template_path, :locals => locals, :layout => layout), status
15
+ rescue => err
16
+ puts err.backtrace.join("\n")
17
+ end
18
+ alias_method_chain :render_for_file, :rebuild
19
+ end
20
+ end
21
+ end
22
+
23
+ def zafu_node(name, klass)
24
+ zafu_context[:node] = Zafu::NodeContext.new(name, klass)
25
+ end
26
+
27
+ def zafu_context
28
+ @zafu_context ||= {}
29
+ end
30
+
31
+ # This method should return the template for a given 'src' and
32
+ # 'base_path'.
33
+ def get_template_text(path, base_path)
34
+ [path, "#{base_path}/#{path}"].each do |p|
35
+ begin
36
+ t = self.view_paths.find_template(p, 'html') # FIXME: format ?
37
+ rescue ActionView::MissingTemplate
38
+ t = nil
39
+ end
40
+ return [t.source, t.path, t.base_path] if t
41
+ end
42
+ nil
43
+ end
44
+
45
+ def template_url_for_asset(opts)
46
+ opts[:src]
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,57 @@
1
+ require 'zafu/parser'
2
+ require 'zafu/markup'
3
+ require 'zafu/node_context'
4
+ require 'zafu/template'
5
+
6
+ module Zafu
7
+ class Handler < ActionView::TemplateHandler
8
+ include ActionView::TemplateHandlers::Compilable
9
+
10
+ def compile(template)
11
+ @template = template
12
+ helper = Thread.current[:view]
13
+ if !helper.respond_to?(:zafu_context)
14
+ raise Exception.new("Please add \"include Zafu::ControllerMethods\" into your ApplicationController for zafu to work properly.")
15
+ end
16
+ ast = Zafu::Template.new(template, self)
17
+ context = helper.zafu_context.merge(:helper => helper)
18
+ context[:node] ||= get_zafu_node_from_view(helper)
19
+ rb = ast.to_ruby('@output_buffer', context)
20
+ ";@erb = %q{#{ast.to_erb(context)}};#{rb}"
21
+ end
22
+
23
+ def get_template_text(path, base_path)
24
+ if path == @template.path && base_path.nil?
25
+ [@template.source, @template.path, @template.base_path]
26
+ else
27
+ Thread.current[:view].get_template_text(path, base_path)
28
+ end
29
+ end
30
+
31
+ private
32
+ def get_zafu_node_from_view(view)
33
+ controller = view.controller
34
+ if controller.class.to_s =~ /\A([A-Z]\w+?)s?[A-Z]/
35
+ ivar = "@#{$1.downcase}"
36
+ if var = controller.instance_variable_get(ivar.to_sym)
37
+ name = ivar
38
+ klass = var.class
39
+ elsif var = controller.instance_variable_get(ivar + 's')
40
+ name = ivar + 's'
41
+ klass = [var.first.class]
42
+ end
43
+ return Zafu::NodeContext.new(name, klass) if name
44
+ end
45
+ raise Exception.new("Could not guess main instance variable from request parameters, please add something like \"zafu_node('@var_name', Page)\" in your action.")
46
+ end
47
+ end
48
+ end
49
+
50
+ class ActionView::Template
51
+ attr_reader :view
52
+ def render_template_with_zafu(view, local_assigns = {})
53
+ Thread.current[:view] = view
54
+ render_template_without_zafu(view, local_assigns)
55
+ end
56
+ alias_method_chain :render_template, :zafu
57
+ end
data/lib/zafu/info.rb ADDED
@@ -0,0 +1,4 @@
1
+ module Zafu
2
+ VERSION = '0.5.0'
3
+ end
4
+
@@ -0,0 +1,186 @@
1
+ module Zafu
2
+ # A Markup object is used to hold information on the tag used (<li>), it's parameters (.. class='xxx') and
3
+ # indentation.
4
+ class Markup
5
+ EMPTY_TAGS = %w{meta input}
6
+ STEAL_PARAMS = [:class, :id, :style]
7
+
8
+ # Tag used ("li" for example). The tag can be nil (no tag).
9
+ attr_accessor :tag
10
+ # Tag parameters (.. class='xxx' id='yyy')
11
+ attr_accessor :params
12
+ # Dynamic tag parameters that should not be escaped. For example: (.. class='<%= @node.klass %>')
13
+ attr_accessor :dyn_params
14
+ # Ensure wrap is not called more then once unless this attribute has been reset in between
15
+ attr_accessor :done
16
+ # Space to insert before tag
17
+ attr_accessor :space_before
18
+ # Space to insert after tag
19
+ attr_accessor :space_after
20
+
21
+ class << self
22
+
23
+ # Parse parameters into a hash. This parsing supports multiple values for one key by creating additional keys:
24
+ # <tag do='hello' or='goodbye' or='gotohell'> creates the hash {:do=>'hello', :or=>'goodbye', :or1=>'gotohell'}
25
+ def parse_params(text)
26
+ return {} unless text
27
+ return text if text.kind_of?(Hash)
28
+ params = {}
29
+ rest = text.strip
30
+ while (rest != '')
31
+ if rest =~ /(.+?)=/
32
+ key = $1.strip.to_sym
33
+ rest = rest[$&.length..-1].strip
34
+ if rest =~ /('|")(|[^\1]*?[^\\])\1/
35
+ rest = rest[$&.length..-1].strip
36
+ key_counter = 1
37
+ while params[key]
38
+ key = "#{key}#{key_counter}".to_sym
39
+ key_counter += 1
40
+ end
41
+
42
+ if $1 == "'"
43
+ params[key] = $2.gsub("\\'", "'")
44
+ else
45
+ params[key] = $2.gsub('\\"', '"')
46
+ end
47
+ else
48
+ # error, bad format, return found params.
49
+ break
50
+ end
51
+ else
52
+ # error, bad format
53
+ break
54
+ end
55
+ end
56
+ params
57
+ end
58
+ end
59
+
60
+ def initialize(tag)
61
+ @done = false
62
+ @tag = tag
63
+ @params = {}
64
+ @dyn_params = {}
65
+ end
66
+
67
+ # Set params either using a string (like "alt='time passes' class='zen'")
68
+ def params=(p)
69
+ if p.kind_of?(Hash)
70
+ @params = p
71
+ else
72
+ @params = Markup.parse_params(p)
73
+ end
74
+ end
75
+
76
+ # Steal html parameters from an existing hash (the stolen parameters are removed
77
+ # from the argument)
78
+ def steal_html_params_from(p)
79
+ @params ||= {}
80
+ STEAL_PARAMS.each do |k|
81
+ next unless p[k]
82
+ @params[k] = p.delete(k)
83
+ end
84
+ end
85
+
86
+ # Compile dynamic parameters as ERB. A parameter is considered dynamic if it
87
+ # contains the string eval "#{...}"
88
+ def compile_params(helper)
89
+ @params.each do |key, value|
90
+ if value =~ /^(.*)\#\{(.*)\}(.*)$/
91
+ @params.delete(key)
92
+ if $1 == '' && $3 == ''
93
+ append_dyn_param(key, "<%= #{RubyLess.translate($2, helper)} %>")
94
+ else
95
+ append_dyn_param(key, "<%= #{RubyLess.translate_string(value, helper)} %>")
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ # Set dynamic html parameters.
102
+ def set_dyn_params(hash)
103
+ hash.keys.each do |k|
104
+ @params.delete(k)
105
+ end
106
+ @dyn_params.merge!(hash)
107
+ end
108
+
109
+ # Set static html parameters.
110
+ def set_params(hash)
111
+ hash.keys.each do |k|
112
+ @dyn_params.delete(k)
113
+ end
114
+ @params.merge!(hash)
115
+ end
116
+
117
+ def append_param(key, value)
118
+ if prev_value = @dyn_params[key]
119
+ @dyn_params[key] = "#{prev_value} #{value}"
120
+ elsif prev_value = @params[key]
121
+ @params[key] = "#{prev_value} #{value}"
122
+ else
123
+ @params[key] = value
124
+ end
125
+ end
126
+
127
+ def append_dyn_param(key, value)
128
+ if prev_value = @params.delete(key)
129
+ @dyn_params[key] = "#{prev_value} #{value}"
130
+ elsif prev_value = @dyn_params[key]
131
+ @dyn_params[key] = "#{prev_value} #{value}"
132
+ else
133
+ @dyn_params[key] = value
134
+ end
135
+ end
136
+
137
+ # Define the DOM id from a node context
138
+ def set_id(erb_id)
139
+ params[:id] = nil
140
+ dyn_params[:id] = erb_id
141
+ end
142
+
143
+ # Wrap the given text with our tag. If 'append' is not empty, append the text
144
+ # after the tag parameters: <li class='foo'[APPEND HERE]>text</li>.
145
+ def wrap(text, *append)
146
+ return text if @done
147
+ append ||= []
148
+ if @tag
149
+ if text.blank? && EMPTY_TAGS.include?(@tag)
150
+ res = "<#{@tag}#{params_to_html}#{append.join('')}/>"
151
+ else
152
+ res = "<#{@tag}#{params_to_html}#{append.join('')}>#{text}</#{@tag}>"
153
+ end
154
+ else
155
+ res = text
156
+ end
157
+ @done = true
158
+
159
+ (@space_before || '') + res + (@space_after || '')
160
+ end
161
+
162
+ private
163
+ def params_to_html
164
+ para = []
165
+ keys = []
166
+
167
+ @dyn_params.each do |k,v|
168
+ keys << k
169
+ para << " #{k}='#{v}'"
170
+ end
171
+
172
+ @params.each do |k,v|
173
+ next if keys.include?(k)
174
+
175
+ if !v.to_s.include?("'")
176
+ para << " #{k}='#{v}'"
177
+ else
178
+ para << " #{k}=\"#{v.to_s.gsub('"','\"')}\"" # TODO: do this work in all cases ?
179
+ end
180
+ end
181
+
182
+ # we sort so that the output is always the same (needed for testing)
183
+ para.sort.join('')
184
+ end
185
+ end
186
+ end
@@ -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,96 @@
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 type of object contained in the current context (Node, Page, Image)
7
+ attr_reader :klass
8
+
9
+ # The current DOM prefix to use when building DOM ids. This is set by the parser when
10
+ # it has a name or dom id defined ('main', 'related', 'list', etc).
11
+ attr_writer :dom_prefix
12
+
13
+ def initialize(name, klass, up = nil)
14
+ @name, @klass, @up = name, klass, up
15
+ end
16
+
17
+ def move_to(name, klass)
18
+ NodeContext.new(name, klass, self)
19
+ end
20
+
21
+ # Since the idiom to write the node context name is the main purpose of this class, it
22
+ # deserves this shortcut.
23
+ def to_s
24
+ name
25
+ end
26
+
27
+ # Return true if the NodeContext represents an element of the given type. We use 'will_be' because
28
+ # it is equivalent to 'is_a', but for future objects (during rendering).
29
+ def will_be?(type)
30
+ klass.ancestors.include?(type)
31
+ end
32
+
33
+ # Return a new node context that corresponds to the current object when rendered alone (in an ajax response or
34
+ # from a direct 'show' in a controller). The returned node context has no parent (up is nil).
35
+ # The convention is to use the class of the current object to build this name.
36
+ def as_main
37
+ NodeContext.new("@#{klass.to_s.underscore}", klass)
38
+ end
39
+
40
+ # Generate a unique DOM id for this element based on dom_scopes defined in parent contexts.
41
+ def dom_id
42
+ @dom_id ||= begin
43
+ if @up
44
+ [dom_prefix] + @up.dom_scopes + [make_scope_id]
45
+ else
46
+ [dom_prefix] + [make_scope_id]
47
+ end.compact.uniq.join('_')
48
+ end
49
+ end
50
+
51
+ # This holds the current context's unique name if it has it's own or one from the hierarchy. If
52
+ # none is found, it builds one... How ?
53
+ def dom_prefix
54
+ @dom_prefix || (@up ? @up.dom_prefix : nil)
55
+ end
56
+
57
+ # Mark the current context as being a looping element (each) whose DOM id needs to be propagated to sub-nodes
58
+ # in order to ensure uniqueness of the dom_id (loops in loops problem).
59
+ def dom_scope!
60
+ @dom_scope = true
61
+ end
62
+
63
+ def get(klass)
64
+ if list_context?
65
+ if self.klass.first.ancestors.include?(klass)
66
+ NodeContext.new("#{self.name}.first", self.klass.first)
67
+ elsif @up
68
+ @up.get(klass)
69
+ else
70
+ nil
71
+ end
72
+ elsif self.klass.ancestors.include?(klass)
73
+ return self
74
+ elsif @up
75
+ @up.get(klass)
76
+ else
77
+ nil
78
+ end
79
+ end
80
+
81
+ def list_context?
82
+ klass.kind_of?(Array)
83
+ end
84
+
85
+ protected
86
+ # List of scopes defined in ancestry (used to generate dom_id).
87
+ def dom_scopes
88
+ (@up ? @up.dom_scopes : []) + (@dom_scope ? [make_scope_id] : [])
89
+ end
90
+
91
+ private
92
+ def make_scope_id
93
+ "<%= #{@name}.zip %>"
94
+ end
95
+ end
96
+ end