mullet 0.0.0 → 0.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.
Files changed (37) hide show
  1. data/lib/mullet/container.rb +8 -4
  2. data/lib/mullet/default_model.rb +9 -9
  3. data/lib/mullet/default_nested_model.rb +7 -6
  4. data/lib/mullet/html/attribute_command.rb +8 -4
  5. data/lib/mullet/html/attributes.rb +22 -0
  6. data/lib/mullet/html/command.rb +4 -3
  7. data/lib/mullet/html/command_element_renderer.rb +19 -0
  8. data/lib/mullet/html/element.rb +41 -0
  9. data/lib/mullet/html/element_renderer.rb +261 -0
  10. data/lib/mullet/html/filtered_element_handler.rb +87 -0
  11. data/lib/mullet/html/for_element_renderer.rb +47 -0
  12. data/lib/mullet/html/if_element_renderer.rb +46 -0
  13. data/lib/mullet/html/layout.rb +48 -0
  14. data/lib/mullet/html/message.rb +55 -0
  15. data/lib/mullet/html/message_attribute_command.rb +30 -0
  16. data/lib/mullet/html/model_attribute_command.rb +30 -0
  17. data/lib/mullet/html/page_builder.rb +152 -0
  18. data/lib/mullet/html/parser/attribute.rb +8 -0
  19. data/lib/mullet/html/parser/constants.rb +1061 -0
  20. data/lib/mullet/html/parser/default_handler.rb +27 -0
  21. data/lib/mullet/html/parser/input_stream.rb +711 -0
  22. data/lib/mullet/html/parser/open_element.rb +77 -0
  23. data/lib/mullet/html/parser/simple_parser.rb +128 -0
  24. data/lib/mullet/html/parser/tokenizer.rb +1085 -0
  25. data/lib/mullet/html/remove_mode.rb +30 -0
  26. data/lib/mullet/html/static_text_renderer.rb +20 -0
  27. data/lib/mullet/html/template.rb +44 -0
  28. data/lib/mullet/html/template_builder.rb +208 -63
  29. data/lib/mullet/html/template_loader.rb +77 -39
  30. data/lib/mullet/html/template_parser.rb +48 -0
  31. data/lib/mullet/html/unless_element_renderer.rb +24 -0
  32. data/lib/mullet/model.rb +2 -5
  33. data/lib/mullet/render_context.rb +24 -18
  34. data/lib/mullet/tilt.rb +37 -0
  35. data/lib/mullet/version.rb +2 -1
  36. data/lib/mullet.rb +1 -0
  37. metadata +58 -11
@@ -8,24 +8,28 @@ module Mullet
8
8
  end
9
9
 
10
10
  def add_child(child)
11
- @children << child
11
+ @children.push(child)
12
12
  end
13
13
 
14
14
  def delete_child(child)
15
15
  @children.delete(child);
16
16
  end
17
17
 
18
+ def children()
19
+ return @children
20
+ end
21
+
18
22
  def clear_children()
19
23
  @children.clear()
20
24
  end
21
25
 
22
26
  # Renders children in order they were added.
23
27
  #
24
- # @param [RenderContext] renderContext
28
+ # @param [RenderContext] render_context
25
29
  # render context
26
- def render_children(renderContext)
30
+ def render_children(render_context)
27
31
  @children.each do |child|
28
- child.render(renderContext)
32
+ child.render(render_context)
29
33
  end
30
34
  end
31
35
  end
@@ -22,27 +22,27 @@ module Mullet
22
22
  @data = data
23
23
  end
24
24
 
25
- def fetch_impl(key)
26
- if key == :this
25
+ def fetch_impl(name)
26
+ if name == :this
27
27
  return @data
28
28
  end
29
29
 
30
30
  # Is the variable name a key in a Hash?
31
31
  if @data.respond_to?(:fetch)
32
32
  # Call the block if the key is not found.
33
- return @data.fetch(key) {|k| @data.fetch(k.to_s(), NOT_FOUND) }
33
+ return @data.fetch(name) {|k| @data.fetch(k.to_s(), NOT_FOUND) }
34
34
  end
35
35
 
36
36
  # Does the variable name match a method name in the object?
37
- if @data.respond_to?(key)
38
- method = @data.method(key)
37
+ if @data.respond_to?(name)
38
+ method = @data.method(name)
39
39
  if method.arity == 0
40
40
  return method.call()
41
41
  end
42
42
  end
43
43
 
44
44
  # Does the variable name match an instance variable name in the object?
45
- variable = :"@#{key}"
45
+ variable = :"@#{name}"
46
46
  if @data.instance_variable_defined?(variable)
47
47
  return @data.instance_variable_get(variable)
48
48
  end
@@ -52,11 +52,11 @@ module Mullet
52
52
 
53
53
  # Resolves variable name to value.
54
54
  #
55
- # @param [Symbol] key
55
+ # @param [Symbol] name
56
56
  # variable name
57
57
  # @return variable value
58
- def fetch(key)
59
- value = fetch_impl(key)
58
+ def get_variable_value(name)
59
+ value = fetch_impl(name)
60
60
  if value.is_a?(Proc)
61
61
  value = value.call()
62
62
  end
@@ -18,18 +18,18 @@ module Mullet
18
18
 
19
19
  # Resolves variable name to value.
20
20
  #
21
- # @param [Symbol] key
21
+ # @param [Symbol] name
22
22
  # variable name
23
23
  # @return variable value
24
- def fetch(key)
24
+ def get_variable_value(name)
25
25
  @scopes.reverse_each do |scope|
26
- value = scope.fetch(key)
27
- if value != Model::NOT_FOUND
26
+ value = scope.get_variable_value(name)
27
+ if value != NOT_FOUND
28
28
  return value
29
29
  end
30
30
  end
31
31
 
32
- return Model::NOT_FOUND
32
+ return NOT_FOUND
33
33
  end
34
34
 
35
35
  # Adds a nested scope to search in subsequent lookups.
@@ -37,7 +37,8 @@ module Mullet
37
37
  # @param data
38
38
  # data object
39
39
  def push_scope(data)
40
- @scopes.push(data.is_a?(Model) ? data : DefaultModel.new(data))
40
+ @scopes.push(
41
+ data.respond_to?(:get_variable_value) ? data : DefaultModel.new(data))
41
42
  end
42
43
 
43
44
  # Removes innermost nested scope.
@@ -1,6 +1,10 @@
1
- module Mullet
1
+ require 'mullet/model'
2
2
 
3
- # Operation to set attribute value.
3
+ module Mullet; module HTML
4
+
5
+ # Operation to set attribute value. Classes including this module are
6
+ # expected to respond to the get_value method returning the attribute value
7
+ # to set.
4
8
  module AttributeCommand
5
9
 
6
10
  # Constructor
@@ -11,7 +15,7 @@ module Mullet
11
15
  @attribute_name = attribute_name
12
16
  end
13
17
 
14
- # Sets attribute in the _attributecommand.
18
+ # Sets attribute in the _attributes_ collection.
15
19
  #
16
20
  # @param [RenderContext] render_context
17
21
  # render context
@@ -29,4 +33,4 @@ module Mullet
29
33
  end
30
34
  end
31
35
 
32
- end
36
+ end; end
@@ -0,0 +1,22 @@
1
+ module Mullet; module HTML
2
+
3
+ # Maps attribute names to values. Also renders attributes to HTML.
4
+ class Attributes < Hash
5
+
6
+ def escape_quote(value)
7
+ return value.include?('"') ? value.gsub(/"/, '&#34;') : value
8
+ end
9
+
10
+ # Renders attributes to HTML syntax.
11
+ #
12
+ # @return rendered HTML
13
+ def render()
14
+ output = ''
15
+ each do |key, value|
16
+ output << %( #{key}="#{escape_quote(value)}")
17
+ end
18
+ return output
19
+ end
20
+ end
21
+
22
+ end; end
@@ -1,8 +1,9 @@
1
- module Mullet
1
+ module Mullet; module HTML
2
2
 
3
3
  # Recognized template commands
4
4
  module Command
5
- PREFIX = 'mullet:'
5
+ DATA_PREFIX = 'data-'
6
+ NAMESPACE_PREFIX = 'mullet'
6
7
  NAMESPACE_URI = 'http://pukkaone.github.com/mullet/1'
7
8
 
8
9
  ACTION = 'action'
@@ -26,4 +27,4 @@ module Mullet
26
27
  VALUE_MESSAGE = 'value-message'
27
28
  end
28
29
 
29
- end
30
+ end; end
@@ -0,0 +1,19 @@
1
+ module Mullet; module HTML
2
+
3
+ # Answers true when asked if it has any command.
4
+ class CommandElementRenderer < ElementRenderer
5
+
6
+ # Constructor
7
+ #
8
+ # @param [Element] element
9
+ # element to render
10
+ def initialize(element)
11
+ super(element)
12
+ end
13
+
14
+ def has_command()
15
+ return true
16
+ end
17
+ end
18
+
19
+ end; end
@@ -0,0 +1,41 @@
1
+ module Mullet; module HTML
2
+
3
+ # HTML element
4
+ class Element
5
+
6
+ # attributes from template
7
+ attr_reader :attributes
8
+
9
+ # true if element has any child element or text content
10
+ attr_accessor :has_content
11
+
12
+ # true if element has any template command
13
+ attr_accessor :has_command
14
+
15
+ # Constructor
16
+ #
17
+ # @param [String] qualified_name
18
+ # tag name with namespace prefix
19
+ # @param [Attributes] attributes
20
+ # attributes from template
21
+ def initialize(qualified_name, attributes)
22
+ @name = qualified_name
23
+ @attributes = attributes
24
+ @has_content = false
25
+ @has_command = false
26
+ end
27
+
28
+ # Renders start tag
29
+ #
30
+ # @param [Attributes] attributes
31
+ # attributes to render
32
+ def render_start_tag(attributes)
33
+ return "<#{@name}#{attributes.render()}>"
34
+ end
35
+
36
+ def render_end_tag()
37
+ return "</#{@name}>"
38
+ end
39
+ end
40
+
41
+ end; end
@@ -0,0 +1,261 @@
1
+ require 'mullet/container'
2
+ require 'mullet/template_error'
3
+ require 'mullet/html/command'
4
+ require 'mullet/html/message'
5
+ require 'mullet/html/message_attribute_command'
6
+ require 'mullet/html/model_attribute_command'
7
+
8
+ module Mullet; module HTML
9
+
10
+ # Renders an HTML element.
11
+ class ElementRenderer
12
+ include Container
13
+
14
+ ATTRIBUTE_SEPARATOR = ';'
15
+ ATTRIBUTE_NAME_SEPARATOR = '='
16
+ ATTRIBUTE_SYNTAX_ERROR = "expected '%s' in '%s'"
17
+ ATTRIBUTE_NAME_MISSING_ERROR = "attribute name missing in '%s'"
18
+ CONVENIENT_ATTRIBUTE_COMMANDS = [
19
+ Command::ACTION,
20
+ Command::ALT,
21
+ Command::HREF,
22
+ Command::SRC,
23
+ Command::TITLE,
24
+ Command::VALUE ]
25
+
26
+ attr_reader :remove_mode
27
+
28
+ # Constructor
29
+ #
30
+ # @param [Element] element
31
+ # element to render
32
+ def initialize(element)
33
+ super()
34
+
35
+ @element = element
36
+ @attribute_commands = []
37
+ @remove_mode = nil
38
+ @text_variable_name = nil
39
+ @text_message = nil
40
+ @template = nil
41
+ @escape_xml = nil
42
+ end
43
+
44
+ def add_attribute_command(attribute_name, variable_name)
45
+ @attribute_commands <<
46
+ ModelAttributeCommand.new(attribute_name, variable_name)
47
+ end
48
+
49
+ def add_attribute_commands(attribute_variable_pairs)
50
+ attribute_variable_pairs.split(ATTRIBUTE_SEPARATOR).each do |command_text|
51
+ command_parts = command_text.split(ATTRIBUTE_NAME_SEPARATOR, 2)
52
+ if command_parts.length() < 2
53
+ raise TemplateError.new(
54
+ ATTRIBUTE_SYNTAX_ERROR % [ATTRIBUTE_NAME_SEPARATOR, command_text])
55
+ end
56
+
57
+ attribute_name = command_parts[0].strip()
58
+ if attribute_name.empty?()
59
+ raise TemplateError.new(ATTRIBUTE_NAME_MISSING_ERROR % command_text)
60
+ end
61
+
62
+ variable_name = command_parts[1].strip()
63
+ if variable_name.empty?()
64
+ raise TemplateError.new("variable name missing in '#{command_text}'")
65
+ end
66
+
67
+ add_attribute_command(attribute_name, variable_name)
68
+ end
69
+ end
70
+
71
+ def configure_attribute_commands(command_attributes)
72
+ value = command_attributes.fetch(Command::ATTR, nil)
73
+ if value != nil
74
+ add_attribute_commands(value)
75
+ end
76
+
77
+ CONVENIENT_ATTRIBUTE_COMMANDS.each do |attribute_name|
78
+ value = command_attributes.fetch(attribute_name, nil)
79
+ if value != nil
80
+ add_attribute_command(attribute_name, value)
81
+ end
82
+ end
83
+
84
+ value = command_attributes.fetch(Command::REMOVE, nil)
85
+ if value != nil
86
+ value.strip!();
87
+ value.downcase!()
88
+ @remove_mode = RemoveMode.value_of(value)
89
+ if @remove_mode == nil
90
+ raise TemplateError.new("invalid remove argument '#{value}'")
91
+ end
92
+ end
93
+ end
94
+
95
+ def add_attribute_message_command(attribute_name, message_arguments)
96
+ @attribute_commands << MessageAttributeCommand.new(
97
+ attribute_name, Message.new(message_arguments))
98
+ end
99
+
100
+ def add_attribute_message_commands(attribute_message_pairs)
101
+ attribute_message_pairs.split(ATTRIBUTE_SEPARATOR).each do |command_text|
102
+ command_parts = command_text.split(ATTRIBUTE_NAME_SEPARATOR, 2)
103
+ if command_parts.length() < 2
104
+ raise TemplateError.new(
105
+ ATTRIBUTE_SYNTAX_ERROR % [ATTRIBUTE_NAME_SEPARATOR, command_text])
106
+ end
107
+
108
+ attribute_name = command_parts[0].strip()
109
+ if attribute_name.empty?()
110
+ raise TemplateError.new(ATTRIBUTE_NAME_MISSING_ERROR % command_text)
111
+ end
112
+
113
+ message_arguments = command_parts[1].strip()
114
+ if message_arguments.empty?()
115
+ raise TemplateError.new(
116
+ "message arguments missing in '#{command_text}'")
117
+ end
118
+
119
+ add_attribute_message_command(attribute_name, message_arguments)
120
+ end
121
+ end
122
+
123
+ def configure_attribute_message_commands(command_attributes)
124
+ value = command_attributes.fetch(Command::ALT_MESSAGE, nil)
125
+ if value != nil
126
+ add_attribute_message_command(Command::ALT, value)
127
+ end
128
+
129
+ value = command_attributes.fetch(Command::ATTR_MESSAGE, nil)
130
+ if value != nil
131
+ add_attribute_message_commands(value)
132
+ end
133
+
134
+ value = command_attributes.fetch(Command::TITLE_MESSAGE, nil)
135
+ if value != nil
136
+ add_attribute_message_command(Command::TITLE, value)
137
+ end
138
+
139
+ value = command_attributes.fetch(Command::VALUE_MESSAGE, nil)
140
+ if value != nil
141
+ add_attribute_message_command(Command::VALUE, value)
142
+ end
143
+ end
144
+
145
+ def configure_content(command_attributes, template_loader)
146
+ @text_variable_name = command_attributes.fetch(Command::TEXT, nil)
147
+ if @text_variable_name != nil
148
+ @text_variable_name = @text_variable_name.to_sym()
149
+ return
150
+ end
151
+
152
+ text_message_arguments =
153
+ command_attributes.fetch(Command::TEXT_MESSAGE, nil)
154
+ if text_message_arguments != nil
155
+ @text_message = Message.new(text_message_arguments)
156
+ return
157
+ end
158
+
159
+ uri = command_attributes.fetch(Command::INCLUDE, nil)
160
+ if uri != nil
161
+ @template = template_loader.load(uri)
162
+ end
163
+ end
164
+
165
+ def configure_commands(command_attributes, template_loader)
166
+ configure_attribute_commands(command_attributes)
167
+ configure_attribute_message_commands(command_attributes)
168
+
169
+ configure_content(command_attributes, template_loader)
170
+
171
+ escape_xml_value = command_attributes.fetch(Command::ESCAPE_XML, nil)
172
+ if escape_xml_value != nil
173
+ @escape_xml = escape_xml_value != 'false'
174
+ end
175
+ end
176
+
177
+ # Checks if this renderer has a command. If it has a command, then the
178
+ # template builder should not discard it.
179
+ def has_command()
180
+ return !@attribute_commands.empty?()
181
+ end
182
+
183
+ # Checks if the element content will be rendered by a command.
184
+ def has_dynamic_content()
185
+ return @text_variable_name != nil ||
186
+ @text_message != nil ||
187
+ @template != nil
188
+ end
189
+
190
+ def execute_attribute_commands(render_context)
191
+ if @attribute_commands.empty?()
192
+ return @element.attributes()
193
+ end
194
+
195
+ # Copy the original attributes. The commands modify the copy to
196
+ # produce the attributes to render.
197
+ render_attributes = @element.attributes().dup()
198
+ @attribute_commands.each do |command|
199
+ command.execute(render_context, render_attributes)
200
+ end
201
+ return render_attributes
202
+ end
203
+
204
+ def should_render_tag()
205
+ return @remove_mode == nil || @remove_mode == RemoveMode::CONTENT
206
+ end
207
+
208
+ def render_start_tag(render_context)
209
+ if should_render_tag()
210
+ attributes = execute_attribute_commands(render_context)
211
+ render_context << @element.render_start_tag(attributes)
212
+ end
213
+ end
214
+
215
+ def render_end_tag(render_context)
216
+ if should_render_tag()
217
+ render_context << @element.render_end_tag()
218
+ end
219
+ end
220
+
221
+ def should_render_content()
222
+ return @remove_mode != RemoveMode::CONTENT
223
+ end
224
+
225
+ def render_content(render_context)
226
+ if should_render_content()
227
+ if @text_variable_name != nil
228
+ value = render_context.get_display_value(@text_variable_name)
229
+ text = render_context.escape_xml(value.to_s())
230
+ render_context << text
231
+ elsif @text_message != nil
232
+ text = @text_message.translate(render_context)
233
+ text = render_context.escape_xml(text)
234
+ render_context << text
235
+ elsif @template != nil
236
+ @template.render(render_context)
237
+ else
238
+ render_children(render_context)
239
+ end
240
+ end
241
+ end
242
+
243
+ def render(render_context)
244
+ # Process the command to change the escaping mode.
245
+ original_escape_xml_enabled = render_context.escape_xml_enabled()
246
+ if @escape_xml != nil
247
+ render_context.escape_xml_enabled = @escape_xml
248
+ end
249
+
250
+ render_start_tag(render_context)
251
+ render_content(render_context)
252
+ render_end_tag(render_context)
253
+
254
+ # Restore the original escaping mode.
255
+ if @escape_xml != nil
256
+ render_context.escape_xml_enabled = original_escape_xml_enabled
257
+ end
258
+ end
259
+ end
260
+
261
+ end; end
@@ -0,0 +1,87 @@
1
+ require 'mullet/html/command'
2
+ require 'mullet/template_error'
3
+ require 'nokogiri'
4
+ require 'set'
5
+
6
+ module Mullet; module HTML
7
+
8
+ # Proxy for a SAX event handler that forwards events only while the
9
+ # parser is within an element identified by an `id` attribute.
10
+ class FilteredElementHandler < Nokogiri::XML::SAX::Document
11
+ ID = 'id'
12
+
13
+ # Constructor
14
+ #
15
+ # @param [Document] handler
16
+ # event handler to forward events to
17
+ # @param [String] id
18
+ # id attribute value
19
+ def initialize(handler, id)
20
+ @handler = handler
21
+ @id = id
22
+ @depth = 0
23
+ @found_id = false
24
+ end
25
+
26
+ def should_forward()
27
+ return @depth > 0
28
+ end
29
+
30
+ def start_document()
31
+ @handler.start_document()
32
+ end
33
+
34
+ def end_document()
35
+ @handler.end_document()
36
+
37
+ if !@found_id
38
+ raise TemplateError.new("element with attribute id='#{@id}' not found")
39
+ end
40
+ end
41
+
42
+ def start_element_namespace(name, attributes, prefix, uri, namespaces)
43
+ if should_forward()
44
+ @depth += 1
45
+ return @handler.start_element_namespace(
46
+ name, attributes, prefix, uri, namespaces)
47
+ end
48
+
49
+ value = attributes.each do |attr|
50
+ qualified_name = [attr.prefix, attr.localname].compact.join(':')
51
+ if qualified_name == ID && attr.value == @id
52
+ # Enable event forwarding.
53
+ @depth = 1
54
+ @found_id = true
55
+ end
56
+ end
57
+ end
58
+
59
+ def end_element_namespace(name, prefix, uri)
60
+ if should_forward()
61
+ @depth -= 1
62
+ if @depth > 0
63
+ @handler.end_element_namespace(name, prefix, uri)
64
+ end
65
+ end
66
+ end
67
+
68
+ def characters(data)
69
+ if should_forward()
70
+ @handler.characters(data)
71
+ end
72
+ end
73
+
74
+ def cdata_block(data)
75
+ if should_forward()
76
+ @handler.cdata_block(data)
77
+ end
78
+ end
79
+
80
+ def comment(data)
81
+ if should_forward()
82
+ @handler.comment(data)
83
+ end
84
+ end
85
+ end
86
+
87
+ end; end
@@ -0,0 +1,47 @@
1
+ require 'mullet/html/command_element_renderer'
2
+ require 'mullet/model'
3
+
4
+ module Mullet; module HTML
5
+
6
+ # Renders an element for each item in a collection.
7
+ class ForElementRenderer < CommandElementRenderer
8
+
9
+ # Constructor
10
+ #
11
+ # @param [Element] element
12
+ # element to render
13
+ # @param [String] variable_name
14
+ # name of variable containing collection
15
+ def initialize(element, variable_name)
16
+ super(element)
17
+ @variable_name = variable_name.to_sym()
18
+ end
19
+
20
+ alias :super_render :render
21
+
22
+ def render_nested_model(data, render_context)
23
+ render_context.push_scope(data)
24
+ super_render(render_context)
25
+ render_context.pop_scope()
26
+ end
27
+
28
+ def render(render_context)
29
+ value = render_context.get_variable_value(@variable_name)
30
+ if value == Model::NOT_FOUND || value == nil || value == false
31
+ return
32
+ end
33
+
34
+ if value.respond_to?(:empty?) && value.empty?()
35
+ return
36
+ end
37
+
38
+ if value.respond_to?(:each)
39
+ value.each {|item| render_nested_model(item, render_context) }
40
+ return
41
+ end
42
+
43
+ render_nested_model(value, render_context)
44
+ end
45
+ end
46
+
47
+ end; end
@@ -0,0 +1,46 @@
1
+ require 'mullet/html/command_element_renderer'
2
+ require 'mullet/model'
3
+
4
+ module Mullet; module HTML
5
+
6
+ # Renders an element if variable is true.
7
+ class IfElementRenderer < CommandElementRenderer
8
+
9
+ # Constructor
10
+ #
11
+ # @param [Element] element
12
+ # element to render
13
+ # @param [String] variable_name
14
+ # name of variable containing condition
15
+ def initialize(element, variable_name)
16
+ super(element)
17
+ @variable_name = variable_name.to_sym()
18
+ end
19
+
20
+ def should_render_element(render_context)
21
+ value = render_context.get_variable_value(@variable_name)
22
+ if value == Model::NOT_FOUND || value == nil
23
+ return false
24
+ end
25
+
26
+ if value.is_a?(FalseClass) || value.is_a?(TrueClass)
27
+ return value
28
+ end
29
+
30
+ if value.respond_to?(:empty?)
31
+ return !value.empty?()
32
+ end
33
+
34
+ return true
35
+ end
36
+
37
+ alias :super_render :render
38
+
39
+ def render(render_context)
40
+ if should_render_element(render_context)
41
+ super_render(render_context)
42
+ end
43
+ end
44
+ end
45
+
46
+ end; end