mullet 0.0.0 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,30 @@
1
+ require 'mullet/template_error'
2
+
3
+ module Mullet; module HTML
4
+
5
+ # Specifies what to remove.
6
+ class RemoveMode
7
+ # remove tag, and preserve children of element
8
+ TAG = RemoveMode.new()
9
+
10
+ # preserve tag, and remove children of element
11
+ CONTENT = RemoveMode.new()
12
+
13
+ # remove tag and children of element
14
+ ELEMENT = RemoveMode.new()
15
+
16
+ def self.value_of(argument)
17
+ string = argument.downcase()
18
+ if string == 'element'
19
+ return ELEMENT
20
+ elsif string == 'tag'
21
+ return TAG
22
+ elsif string == 'content'
23
+ return CONTENT
24
+ else
25
+ raise TemplateError.new("invalid remove argument '%{argument}'")
26
+ end
27
+ end
28
+ end
29
+
30
+ end; end
@@ -0,0 +1,20 @@
1
+ module Mullet; module HTML
2
+
3
+ # Renders static markup and text. May end with an unclosed start tag under
4
+ # the assumption a subsequent static text fragment closes the tag.
5
+ class StaticTextRenderer
6
+
7
+ # Constructor
8
+ #
9
+ # @param [String] text
10
+ # rendered tags and text
11
+ def initialize(text)
12
+ @text = text
13
+ end
14
+
15
+ def render(render_context)
16
+ render_context << @text
17
+ end
18
+ end
19
+
20
+ end; end
@@ -0,0 +1,44 @@
1
+ require 'mullet/container'
2
+ require 'mullet/render_context'
3
+
4
+ module Mullet; module HTML
5
+
6
+ # Template containing static text and dynamically generated content.
7
+ class Template
8
+ include Container
9
+
10
+ RETURN_EMPTY_STRING = Proc.new { '' }
11
+
12
+ def initialize()
13
+ super
14
+ @on_missing = RETURN_EMPTY_STRING
15
+ @on_nil = RETURN_EMPTY_STRING
16
+ end
17
+
18
+ def on_missing(strategy)
19
+ @on_missing = strategy
20
+ return self
21
+ end
22
+
23
+ def on_nil(strategy)
24
+ @on_nil = strategy
25
+ return self
26
+ end
27
+
28
+ def render(render_context)
29
+ render_children(render_context)
30
+ end
31
+
32
+ # Renders the template.
33
+ #
34
+ # @param [Object] data
35
+ # provides data to render
36
+ # @param [#<<] output
37
+ # where to write rendered output
38
+ def execute(data, output)
39
+ render_context = RenderContext.new(data, @on_missing, @on_nil, output)
40
+ render(render_context)
41
+ end
42
+ end
43
+
44
+ end; end
@@ -1,46 +1,65 @@
1
- require 'nokogiri'
1
+ require 'mullet/html/attributes'
2
2
  require 'mullet/html/command'
3
+ require 'mullet/html/element'
4
+ require 'mullet/html/element_renderer'
5
+ require 'mullet/html/for_element_renderer'
6
+ require 'mullet/html/if_element_renderer'
7
+ require 'mullet/html/parser/default_handler'
8
+ require 'mullet/html/parser/simple_parser'
9
+ require 'mullet/html/remove_mode'
10
+ require 'mullet/html/static_text_renderer'
11
+ require 'mullet/html/template'
12
+ require 'mullet/html/unless_element_renderer'
13
+ require 'mullet/template_error'
3
14
  require 'set'
4
15
 
5
16
  module Mullet; module HTML
6
17
 
7
18
  # Handles SAX events to build a template.
8
- class TemplateBuilder < Nokogiri::XML::SAX::Document
19
+ class TemplateBuilder < Parser::DefaultHandler
9
20
  include Command
10
21
 
11
- XMLNS_ATTRIBUTE_PREFIX = "xmlns:"
12
- COMMANDS = [
13
- ATTRIBUTE,
14
- ATTRIBUTE_MESSAGE,
15
- CONTENT,
22
+ COMMANDS = Set[
23
+ ACTION,
24
+ ALT,
25
+ ALT_MESSAGE,
26
+ ATTR,
27
+ ATTR_MESSAGE,
16
28
  ESCAPE_XML,
17
29
  FOR,
30
+ HREF,
18
31
  IF,
19
32
  INCLUDE,
33
+ REMOVE,
34
+ SRC,
20
35
  TEXT,
21
36
  TEXT_MESSAGE,
22
- UNLESS].to_set
23
- START_CDATA = "<![CDATA["
24
- END_CDATA = "]]>"
37
+ TITLE,
38
+ TITLE_MESSAGE,
39
+ UNLESS,
40
+ VALUE,
41
+ VALUE_MESSAGE]
42
+ START_CDATA = '<![CDATA['
43
+ END_CDATA = ']]>'
25
44
 
26
- @loader = nil
45
+ attr_reader :template
27
46
 
28
- # This is a stack of elements where this handler has seen the start tag and
29
- # not yet seen the end tag.
30
- @openElements = []
31
-
32
- # stack of current containers to add renderers to
33
- @containers = []
34
-
35
- @staticText = ""
36
- @template = nil
37
-
38
47
  # Constructor
39
48
  #
40
49
  # @param [TemplateLoader] loader
41
50
  # template loader to use to load included template files
42
51
  def initialize(loader)
43
52
  @loader = loader
53
+
54
+ # Stack of elements where this handler has seen the start tag and not yet
55
+ # seen the end tag.
56
+ @open_elements = []
57
+
58
+ # stack of current containers to add renderers to
59
+ @containers = []
60
+
61
+ @static_text = ''
62
+ @template = nil
44
63
  end
45
64
 
46
65
  # Adds renderer to current container.
@@ -48,7 +67,7 @@ module Mullet; module HTML
48
67
  # @param [#render] renderer
49
68
  # renderer to add
50
69
  def add_child(renderer)
51
- @containers.last.add_child(renderer)
70
+ @containers.last().add_child(renderer)
52
71
  end
53
72
 
54
73
  # Deletes renderer from current container.
@@ -56,7 +75,7 @@ module Mullet; module HTML
56
75
  # @param [#render] renderer
57
76
  # renderer to delete
58
77
  def delete_child(renderer)
59
- @containers.last.delete_child(renderer)
78
+ @containers.last().delete_child(renderer)
60
79
  end
61
80
 
62
81
  # Partitions the attributes into ordinary and command attributes.
@@ -65,77 +84,203 @@ module Mullet; module HTML
65
84
  # input attributes
66
85
  # @param [Hash] ns
67
86
  # hash of namespace prefix to uri mappings
68
- # @param [#store] ordinaryAttributes
87
+ # @param [#store] ordinary_attributes
69
88
  # hash will receive name to value mappings for ordinary attributes
70
- # @param [#store] commandAttributes
89
+ # @param [#store] command_attributes
71
90
  # hash will receive name to value mappings for command attributes
72
91
  # @return [Boolean] true if any command attribute found
73
- def find_commands(attributes, ns, ordinaryAttributes, commandAttributes)
74
- foundCommand = false
92
+ def find_commands(attributes, ns, ordinary_attributes, command_attributes)
93
+ found_command = false
75
94
  attributes.each do |attr|
76
- if attr.uri == NAMESPACE_URI
77
- commandName = attr.localname
78
- if !COMMANDS.contains(commandName)
79
- raise TemplateException("invalid command '#{commandName}'")
95
+ if attr.localname.start_with?(DATA_PREFIX)
96
+ command_name = attr.localname.slice(DATA_PREFIX.length()..-1)
97
+ if COMMANDS.include?(command_name)
98
+ command_attributes.store(command_name, attr.value)
99
+ found_command = true
100
+ end
101
+ elsif attr.uri == NAMESPACE_URI || attr.prefix == NAMESPACE_PREFIX
102
+ command_name = attr.localname
103
+ if !COMMANDS.include?(command_name)
104
+ raise TemplateError("invalid command '#{command_name}'")
80
105
  end
81
- commandAttributes.store(commandName, attr.value)
82
- foundCommand = true
106
+ command_attributes.store(command_name, attr.value)
107
+ found_command = true
83
108
  else
84
- attributeName = [attr.prefix, attr.localname].compact.join(':')
85
- ordinaryAttributes.store(attributeName, attr.value)
109
+ attribute_name = [attr.prefix, attr.localname].compact().join(':')
110
+ ordinary_attributes.store(attribute_name, attr.value)
86
111
  end
87
112
  end
88
113
 
89
114
  ns.each do |prefix, uri|
90
115
  if uri != NAMESPACE_URI
91
- attributeName = ['xmlns', prefix].compact.join(':')
92
- ordinaryAttributes.store(attributeName, uri)
116
+ attribute_name = ['xmlns', prefix].compact().join(':')
117
+ ordinary_attributes.store(attribute_name, uri)
93
118
  end
94
119
  end
95
120
 
96
- return foundCommand
121
+ return found_command
97
122
  end
98
123
 
99
- def render_start_tag(name, attributes, prefix, uri, namespaceDecls)
100
- tag = "<"
101
- if prefix
102
- tag << prefix << ":"
124
+ def append_static_text(data)
125
+ @static_text << data
126
+ end
127
+
128
+ def end_static_text()
129
+ if !@static_text.empty?()
130
+ add_child(StaticTextRenderer.new(@static_text))
131
+ @static_text = ''
132
+ end
133
+ end
134
+
135
+ # Marks the most deeply nested open element as having content.
136
+ def set_has_content()
137
+ if !@open_elements.empty?()
138
+ @open_elements.last().has_content = true
139
+ end
140
+ end
141
+
142
+ def start_document()
143
+ @template = Template.new()
144
+ @containers.push(@template)
145
+ end
146
+
147
+ def end_document()
148
+ end_static_text()
149
+ end
150
+
151
+ def doctype(name, public_id, system_id)
152
+ text = "<!DOCTYPE #{name}"
153
+
154
+ if public_id
155
+ text << %( PUBLIC "#{public_id}")
156
+ end
157
+
158
+ if system_id
159
+ text << %( "#{system_id}")
160
+ end
161
+
162
+ text << '>'
163
+ append_static_text(text)
164
+ end
165
+
166
+ def create_element_renderer(element, command_attributes)
167
+ renderer = nil
168
+
169
+ variable_name = command_attributes.fetch(IF, nil)
170
+ if variable_name != nil
171
+ renderer = IfElementRenderer.new(element, variable_name)
103
172
  end
104
- tag << name
105
173
 
106
- attributes.each do |attribute|
107
- tag << " "
108
- if attribute.prefix
109
- tag << attribute.prefix << ":"
174
+ if renderer == nil
175
+ variable_name = command_attributes.fetch(UNLESS, nil)
176
+ if variable_name != nil
177
+ renderer = UnlessElementRenderer.new(element, variable_name)
110
178
  end
111
- tag = attribute.localname << '="' << attribute.value << '"'
112
179
  end
113
180
 
114
- namespaceDecls.each do |namespaceDecl|
115
- uri = namespaceDecl[1]
116
- if uri != TEMPLATE_NAMESPACE_URI
117
- tag << " xmlns:" << namespaceDecl[0] << '="' << uri << '"'
181
+ if renderer == nil
182
+ variable_name = command_attributes.fetch(FOR, nil)
183
+ if variable_name != nil
184
+ renderer = ForElementRenderer.new(element, variable_name)
118
185
  end
119
186
  end
120
187
 
121
- tag << ">"
122
- return tag
188
+ if renderer == nil
189
+ renderer = ElementRenderer.new(element)
190
+ end
191
+
192
+ renderer.configure_commands(command_attributes, @loader)
193
+ return renderer
123
194
  end
124
195
 
125
196
  def start_element_namespace(name, attributes, prefix, uri, ns)
126
- puts "start element #{name} #{attributes} #{prefix} #{uri} #{ns}"
127
- templateAttributes = attributes.select do |attribute|
128
- attribute.uri == TEMPLATE_NAMESPACE_URI
129
- end
130
- if templateAttributes.empty?
131
- puts render_start_tag(name, attributes, prefix, uri, ns)
197
+ set_has_content()
198
+
199
+ ordinary_attributes = Attributes.new()
200
+ command_attributes = Attributes.new()
201
+ found_command = find_commands(
202
+ attributes, ns, ordinary_attributes, command_attributes)
203
+
204
+ qualified_name = [prefix, name].compact().join(':')
205
+ element = Element.new(qualified_name, ordinary_attributes)
206
+ @open_elements.push(element)
207
+
208
+ if found_command
209
+ end_static_text()
210
+
211
+ element.has_command = true
212
+ renderer = create_element_renderer(element, command_attributes)
213
+ add_child(renderer)
214
+
215
+ @containers.push(renderer)
132
216
  else
133
- puts "attributes #{templateAttributes}"
217
+ append_static_text(element.render_start_tag(element.attributes))
134
218
  end
135
219
  end
136
220
 
137
- def end_document
138
- puts "the document has ended"
221
+ def configure_remove_command(element, renderer)
222
+ case renderer.remove_mode
223
+ when RemoveMode::TAG
224
+ if !renderer.has_command && !renderer.has_dynamic_content
225
+ # Discard tag, but preserve the children.
226
+ delete_child(renderer)
227
+ renderer.children.each do |child|
228
+ add_child(child)
229
+ end
230
+ end
231
+ when RemoveMode::CONTENT
232
+ if !renderer.has_command
233
+ # Discard children. Statically render the tag.
234
+ delete_child(renderer)
235
+
236
+ append_static_text(element.render_start_tag(element.attributes))
237
+ append_static_text(element.render_end_tag())
238
+ end
239
+ when RemoveMode::ELEMENT
240
+ # Discard element and all its content.
241
+ delete_child(renderer)
242
+ end
243
+ end
244
+
245
+ def end_element_namespace(name, prefix, uri)
246
+ element = @open_elements.pop()
247
+ if element.has_command
248
+ end_static_text()
249
+
250
+ renderer = @containers.pop()
251
+ if renderer.has_dynamic_content
252
+ # Discard children because the content will be replaced at render
253
+ # time.
254
+ renderer.clear_children()
255
+ end
256
+
257
+ configure_remove_command(element, renderer)
258
+ elsif uri != Parser::SimpleParser::IMPLICIT_END_TAG_NS_URI
259
+ append_static_text(element.render_end_tag())
260
+ end
261
+ end
262
+
263
+ def characters(data)
264
+ set_has_content()
265
+ append_static_text(data)
266
+ end
267
+
268
+ def cdata_block(data)
269
+ append_static_text(START_CDATA)
270
+ characters(data)
271
+ append_static_text(END_CDATA)
272
+ end
273
+
274
+ def comment(data)
275
+ append_static_text('<!--')
276
+ characters(data)
277
+ append_static_text('-->')
278
+ end
279
+
280
+ def processing_instruction(data)
281
+ append_static_text('<?')
282
+ characters(data)
283
+ append_static_text('?>')
139
284
  end
140
285
  end
141
286
 
@@ -1,55 +1,93 @@
1
- require 'nokogiri'
1
+ require 'mullet/html/template'
2
+ require 'mullet/html/template_parser'
2
3
 
3
- module Mullet
4
+ module Mullet; module HTML
4
5
 
5
- class TemplateDocument < Nokogiri::XML::SAX::Document
6
- TEMPLATE_NAMESPACE_URI = "http://pukkaone.github.com/mullet/1"
6
+ # Loads templates from files, and caches them for fast retrieval of already
7
+ # loaded templates.
8
+ #
9
+ # By default, templates render an empty string when a variable is not found
10
+ # or its value is null. Call the `on_missing` and `on_nil` methods to
11
+ # configure how templates loaded by this loader should handle missing and nil
12
+ # values respectively.
13
+ class TemplateLoader
14
+
15
+ attr_accessor :template_path
7
16
 
8
- def render_start_tag(name, attributes, prefix, uri, namespaceDecls)
9
- tag = "<"
10
- if prefix
11
- tag << prefix << ":"
17
+ # Constructor
18
+ #
19
+ # @param [String] template_path
20
+ # name of directory to load templates from
21
+ def initialize(template_path)
22
+ @template_path = template_path
23
+ @template_cache = Hash.new()
24
+ @parser = TemplateParser.new(self)
25
+ @on_missing = Template::RETURN_EMPTY_STRING
26
+ @on_nil = Template::RETURN_EMPTY_STRING
27
+ end
28
+
29
+ # Sets block to execute on attempt to render a variable that was not found.
30
+ #
31
+ # @param [Proc] strategy
32
+ # The value returned from block will be rendered.
33
+ # @return [TemplateLoader] this object to allow method call chaining
34
+ def on_missing(strategy)
35
+ @on_missing = strategy
36
+ return self
12
37
  end
13
- tag << name
14
38
 
15
- attributes.each do |attribute|
16
- tag << " "
17
- if attribute.prefix
18
- tag << attribute.prefix << ":"
19
- end
20
- tag = attribute.localname << '="' << attribute.value << '"'
39
+ # Sets block to execute on attempt to render a nil value.
40
+ #
41
+ # @param [Proc] strategy
42
+ # The value returned from block will be rendered.
43
+ # @return [TemplateLoader] this object to allow method call chaining
44
+ def on_nil(strategy)
45
+ @on_nil = strategy
46
+ return self
21
47
  end
22
48
 
23
- namespaceDecls.each do |namespaceDecl|
24
- uri = namespaceDecl[1]
25
- if uri != TEMPLATE_NAMESPACE_URI
26
- tag << " xmlns:" << namespaceDecl[0] << '="' << uri << '"'
49
+ # Loads named template.
50
+ #
51
+ # @param [String] uri
52
+ # file name optionally followed by `#`_id_
53
+ def load(uri)
54
+ id = nil
55
+ hash_index = uri.index('#')
56
+ if hash_index
57
+ id = uri[(hash_index + 1)..-1]
58
+ uri = uri[0...hash_index]
27
59
  end
60
+
61
+ return load_file(uri, id)
28
62
  end
29
63
 
30
- tag << ">"
31
- return tag
32
- end
64
+ private
33
65
 
34
- def start_element_namespace(name, attributes, prefix, uri, ns)
35
- puts "start element #{name} #{attributes} #{prefix} #{uri} #{ns}"
36
- templateAttributes = attributes.select do |attribute|
37
- attribute.uri == TEMPLATE_NAMESPACE_URI
66
+ def get_cache_key(file_name, id)
67
+ cache_key = File.join(@template_path, file_name)
68
+ if id != nil
69
+ cache_key << '#' << id
70
+ end
71
+ return cache_key
38
72
  end
39
- if templateAttributes.empty?
40
- puts render_start_tag(name, attributes, prefix, uri, ns)
41
- else
42
- puts "attributes #{templateAttributes}"
73
+
74
+ def parse_file(file_name, id)
75
+ template_file = File.join(@template_path, file_name)
76
+ template = @parser.parse_file(template_file, id)
77
+
78
+ template.on_missing(@on_missing).on_nil(@on_nil)
79
+ return template
43
80
  end
44
- end
45
81
 
46
- def end_document
47
- puts "the document has ended"
82
+ def load_file(file_name, id)
83
+ cache_key = get_cache_key(file_name, id)
84
+ template = @template_cache.fetch(cache_key, nil)
85
+ if template == nil
86
+ template = parse_file(file_name, id)
87
+ @template_cache.store(cache_key, template)
88
+ end
89
+ return template
90
+ end
48
91
  end
49
- end
50
92
 
51
- parser = Nokogiri::HTML::SAX::Parser.new(TemplateDocument.new)
52
- f = File.open("login.html")
53
- parser.parse(f)
54
- f.close
55
- end
93
+ end; end
@@ -0,0 +1,48 @@
1
+ require 'mullet/html/filtered_element_handler'
2
+ require 'mullet/html/parser/simple_parser'
3
+ require 'mullet/html/template_builder'
4
+
5
+ module Mullet; module HTML
6
+
7
+ class TemplateParser
8
+
9
+ def initialize(loader)
10
+ @loader = loader
11
+ end
12
+
13
+ # Parses template from string.
14
+ #
15
+ # @param [String] source
16
+ # string to parse
17
+ # @return [Template] template
18
+ def parse(source)
19
+ template_builder = TemplateBuilder.new(@loader)
20
+ parser = Parser::SimpleParser.new(template_builder)
21
+ parser.parse(source)
22
+ return template_builder.template
23
+ end
24
+
25
+ # Parses template from file.
26
+ #
27
+ # @param [String] file_name
28
+ # name of file containing template
29
+ # @param [String] id
30
+ # If `nil`, then the template is the entire file, otherwise the
31
+ # template is the content of the element having an `id` attribute
32
+ # value equal to this argument.
33
+ # @return [Template] template
34
+ def parse_file(file_name, id)
35
+ template_builder = TemplateBuilder.new(@loader)
36
+ handler = (id == nil) ?
37
+ template_builder : FilteredElementHandler.new(template_builder, id)
38
+
39
+ parser = Parser::SimpleParser.new(handler)
40
+ File.open(file_name) do |file|
41
+ parser.parse(file)
42
+ end
43
+
44
+ return template_builder.template
45
+ end
46
+ end
47
+
48
+ end; end
@@ -0,0 +1,24 @@
1
+ require 'mullet/html/if_element_renderer'
2
+
3
+ module Mullet; module HTML
4
+
5
+ # Renders an element if variable is false.
6
+ class UnlessElementRenderer < IfElementRenderer
7
+
8
+ # Constructor
9
+ #
10
+ # @param [Element] element
11
+ # element to render
12
+ # @param [String] variable_name
13
+ # name of variable containing condition
14
+ def initialize(element, variable_name)
15
+ super(element, variable_name)
16
+ @variable_name = variable_name.to_sym()
17
+ end
18
+
19
+ def should_render_element(render_context)
20
+ return !super
21
+ end
22
+ end
23
+
24
+ end; end
data/lib/mullet/model.rb CHANGED
@@ -1,10 +1,7 @@
1
1
  module Mullet
2
2
 
3
- # A model responds to the method `fetch` taking a variable name argument and
4
- # returning the variable value. If the variable name is not found, it
5
- # returns the value `NOT_FOUND` instead of raising an exception. A model
6
- # class must include this module because the implementation calls
7
- # `is_a?(Model)` to determine if an object satisfies the concept of a model.
3
+ # A model responds to the method `get_variable_value` taking a variable name
4
+ # argument and returning the variable value.
8
5
  module Model
9
6
 
10
7
  # special value indicating variable name was not found