undies 2.2.1 → 3.0.0.rc.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 (54) hide show
  1. data/ARCH.md +116 -0
  2. data/Gemfile +3 -0
  3. data/Gemfile.lock +20 -4
  4. data/LICENSE +22 -0
  5. data/README.md +343 -0
  6. data/Rakefile +25 -17
  7. data/bench/bench_runner.rb +132 -12
  8. data/bench/large.html.erb +9 -13
  9. data/bench/large.html.haml +11 -0
  10. data/bench/large.html.rb +8 -12
  11. data/bench/profiler +1 -1
  12. data/bench/profiler_runner.rb +2 -5
  13. data/bench/small.html.erb +9 -13
  14. data/bench/small.html.haml +11 -0
  15. data/bench/small.html.rb +8 -12
  16. data/bench/verylarge.html.erb +9 -13
  17. data/bench/verylarge.html.haml +11 -0
  18. data/bench/verylarge.html.rb +8 -12
  19. data/lib/undies/api.rb +163 -0
  20. data/lib/undies/element.rb +160 -80
  21. data/lib/undies/element_node.rb +116 -0
  22. data/lib/undies/io.rb +43 -0
  23. data/lib/undies/root_node.rb +62 -0
  24. data/lib/undies/source.rb +78 -2
  25. data/lib/undies/template.rb +17 -131
  26. data/lib/undies/version.rb +1 -1
  27. data/lib/undies.rb +3 -2
  28. data/test/element_closed_test.rb +69 -0
  29. data/test/element_node_test.rb +274 -0
  30. data/test/element_open_test.rb +101 -0
  31. data/test/element_test.rb +23 -196
  32. data/test/fixtures/write_thing.rb +4 -4
  33. data/test/helper.rb +84 -0
  34. data/test/io_test.rb +104 -0
  35. data/test/named_source_test.rb +1 -1
  36. data/test/raw_test.rb +25 -0
  37. data/test/root_node_test.rb +108 -0
  38. data/test/source_stack_test.rb +1 -1
  39. data/test/template_builder_render_test.rb +4 -9
  40. data/test/template_source_render_test.rb +16 -20
  41. data/test/template_test.rb +87 -80
  42. data/test/templates/content.html.rb +1 -1
  43. data/test/templates/test.html.rb +1 -1
  44. data/undies.gemspec +1 -0
  45. metadata +52 -23
  46. data/README.rdoc +0 -203
  47. data/lib/undies/named_source.rb +0 -54
  48. data/lib/undies/node.rb +0 -87
  49. data/lib/undies/node_stack.rb +0 -111
  50. data/lib/undies/output.rb +0 -31
  51. data/lib/undies/source_stack.rb +0 -22
  52. data/test/node_stack_test.rb +0 -109
  53. data/test/node_test.rb +0 -91
  54. data/test/output_test.rb +0 -69
@@ -1,88 +1,48 @@
1
- require 'undies/node'
2
-
3
1
  module Undies
4
- class Element < Node
5
2
 
6
- # have as many methods to the class level as possilbe to keep from
7
- # polluting the public instance methods and to maximize the effectiveness
8
- # of the Element#method_missing logic
3
+
4
+
5
+ module Element
9
6
 
10
7
  def self.hash_attrs(attrs="", ns=nil)
11
8
  return attrs.to_s if !attrs.kind_of?(::Hash)
12
9
 
13
- {}.tap do |a|
14
- attrs.each { |k, v| a[ns ? "#{ns}_#{k}" : k.to_s] = v }
15
- end.sort.inject('') do |html, k_v|
16
- html + if k_v.last.kind_of?(::Hash)
10
+ attrs.collect do |k_v|
11
+ [ns ? "#{ns}_#{k_v.first}" : k_v.first.to_s, k_v.last]
12
+ end.sort.collect do |k_v|
13
+ if k_v.last.kind_of?(::Hash)
17
14
  hash_attrs(k_v.last, k_v.first)
15
+ elsif k_v.last.kind_of?(::Array)
16
+ " #{k_v.first}=\"#{escape_attr_value(k_v.last.join(' '))}\""
18
17
  else
19
18
  " #{k_v.first}=\"#{escape_attr_value(k_v.last)}\""
20
19
  end
21
- end
20
+ end.join
22
21
  end
23
22
 
23
+ ESCAPE_ATTRS = {
24
+ "&" => "&amp;",
25
+ "<" => "&lt;",
26
+ '"' => "&quot;"
27
+ }
28
+ ESCAPE_ATTRS_PATTERN = Regexp.union(*ESCAPE_ATTRS.keys)
24
29
  def self.escape_attr_value(value)
25
- value.
26
- to_s.
27
- gsub('&', '&amp;').
28
- gsub('<', '&lt;').
29
- gsub('"', '&quot;')
30
- end
31
-
32
- def self.set_children(element, value)
33
- element.instance_variable_set("@children", value)
34
- end
35
-
36
- def self.children(element)
37
- element.instance_variable_get("@children")
38
- end
39
-
40
- def self.prefix(element, meth, level, indent)
41
- "".tap do |value|
42
- if indent > 0
43
- if meth == 'start_tag'
44
- value << "#{level > 0 ? "\n": ''}#{' '*level*indent}"
45
- elsif meth == 'end_tag'
46
- value << "\n#{' '*level*indent}" if children(element)
47
- end
48
- end
49
- end
30
+ value.to_s.gsub(ESCAPE_ATTRS_PATTERN){|c| ESCAPE_ATTRS[c] }
50
31
  end
51
32
 
52
- def self.set_start_tag(element)
53
- element.instance_variable_set(
54
- "@start_tag",
55
- "<#{node_name(element)}#{hash_attrs(attrs(element))}" + (builds(element).size > 0 ? ">" : " />")
56
- )
33
+ def self.open(*args, &build)
34
+ Open.new(*args, &build)
57
35
  end
58
36
 
59
- def self.set_end_tag(element)
60
- element.instance_variable_set(
61
- "@end_tag",
62
- builds(element).size > 0 ? "</#{node_name(element)}>" : nil
63
- )
37
+ def self.closed(*args, &build)
38
+ Closed.new(*args, &build)
64
39
  end
65
40
 
66
- def initialize(name, attrs={}, &build)
67
- super(nil)
68
- @start_tag = ""
69
- @end_tag = ""
70
- @content = nil
71
- @builds = []
72
- @children = false
41
+ end
73
42
 
74
- if !attrs.kind_of?(::Hash)
75
- raise ArgumentError, "#{name.inspect} attrs must be provided as a Hash."
76
- end
77
43
 
78
- @name = name.to_s
79
- @attrs = attrs
80
- @builds << build if build
81
44
 
82
- # cache in an instance variable for fast access with flush and pop
83
- self.class.set_start_tag(self)
84
- self.class.set_end_tag(self)
85
- end
45
+ module CSSProxy
86
46
 
87
47
  # CSS proxy methods ============================================
88
48
  ID_METH_REGEX = /^([^_].+)!$/
@@ -90,13 +50,11 @@ module Undies
90
50
 
91
51
  def method_missing(meth, *args, &block)
92
52
  if meth.to_s =~ ID_METH_REGEX
93
- proxy($1, args.first || {}, block) do |value|
94
- @attrs.merge!(:id => value)
95
- end
53
+ @attrs[:id] = $1
54
+ proxy(args, block)
96
55
  elsif meth.to_s =~ CLASS_METH_REGEX
97
- proxy($1, args.first || {}, block) do |value|
98
- @attrs[:class] = [@attrs[:class], value].compact.join(' ')
99
- end
56
+ @attrs[:class] = [@attrs[:class], $1].compact.join(' ')
57
+ proxy(args, block)
100
58
  else
101
59
  super
102
60
  end
@@ -111,6 +69,133 @@ module Undies
111
69
  end
112
70
  # ==============================================================
113
71
 
72
+ end
73
+
74
+
75
+
76
+ module MergeAttrs
77
+
78
+ def __attrs(attrs_hash=nil)
79
+ return @attrs if attrs_hash.nil?
80
+ @attrs.merge!(attrs_hash)
81
+ end
82
+
83
+ end
84
+
85
+
86
+
87
+ class Raw < ::String
88
+
89
+ # A Raw string is one that is impervious to String#gsub and returns itself
90
+ # when `to_s` is called.
91
+
92
+ def gsub(*args, &block)
93
+ self
94
+ end
95
+
96
+ def gsub!(*args, &block)
97
+ nil
98
+ end
99
+
100
+ def to_s
101
+ self
102
+ end
103
+
104
+ end
105
+
106
+
107
+
108
+ class Element::Open
109
+ include CSSProxy
110
+ include MergeAttrs
111
+
112
+ def initialize(name, *args, &build)
113
+ @name = name.to_s
114
+ @attrs = {}
115
+ @content = []
116
+ @build = nil
117
+
118
+ proxy(args, build)
119
+ end
120
+
121
+ def __start_tag
122
+ "<#{@name}#{Element.hash_attrs(@attrs)}>"
123
+ end
124
+
125
+ def __content
126
+ @content.collect{ |c| Template.escape_html(c) }.join
127
+ end
128
+
129
+ def __build
130
+ @build.call if @build
131
+ end
132
+
133
+ def __end_tag
134
+ "</#{@name}>"
135
+ end
136
+
137
+ def to_s
138
+ "#{__start_tag}#{__content}#{__end_tag}"
139
+ end
140
+
141
+ def ==(other)
142
+ other.instance_variable_get("@name") == @name &&
143
+ other.instance_variable_get("@attrs") == @attrs &&
144
+ other.instance_variable_get("@content") == @content
145
+ end
146
+
147
+ # overriding this because the base Node class defines a 'to_s' method that
148
+ # needs to be honored
149
+ def to_str(*args)
150
+ "Undies::Element::Open:#{self.object_id} " +
151
+ "@name=#{@name.inspect}, @attrs=#{@attrs.inspect}, @content=#{@content.inspect}"
152
+ end
153
+ alias_method :inspect, :to_str
154
+
155
+ private
156
+
157
+ def proxy(args, build)
158
+ if args.last.kind_of?(Hash)
159
+ @attrs.merge!(args.pop)
160
+ end
161
+
162
+ @content.push *args
163
+ @build = build
164
+
165
+ self
166
+ end
167
+
168
+ end
169
+
170
+
171
+
172
+ class Element::Closed
173
+ include CSSProxy
174
+ include MergeAttrs
175
+
176
+ def initialize(name, attrs={})
177
+ @name = name.to_s
178
+ @attrs = {}
179
+ proxy([attrs])
180
+ end
181
+
182
+ def __start_tag
183
+ "<#{@name}#{Element.hash_attrs(@attrs)} />"
184
+ end
185
+
186
+ # closed elements have no content
187
+ def __content; ''; end
188
+
189
+ # closed elements should have no build so do nothing
190
+ def __build; end
191
+
192
+ # closed elements have no end tag
193
+ def __end_tag; ''; end
194
+
195
+ def to_s
196
+ "#{__start_tag}"
197
+ end
198
+
114
199
  def ==(other)
115
200
  other.instance_variable_get("@name") == @name &&
116
201
  other.instance_variable_get("@attrs") == @attrs
@@ -119,25 +204,20 @@ module Undies
119
204
  # overriding this because the base Node class defines a 'to_s' method that
120
205
  # needs to be honored
121
206
  def to_str(*args)
122
- "Undies::Element:#{self.object_id} " +
207
+ "Undies::Element::Closed:#{self.object_id} " +
123
208
  "@name=#{@name.inspect}, @attrs=#{@attrs.inspect}"
124
209
  end
125
210
  alias_method :inspect, :to_str
126
211
 
127
212
  private
128
213
 
129
- def proxy(value, attrs, build)
130
- yield value if block_given?
131
- @attrs.merge!(attrs)
132
- @builds << build if build
133
-
134
- # cache in an instance variable for fast access with flush and pop
135
- self.class.set_start_tag(self)
136
- self.class.set_end_tag(self)
137
-
138
- # return self so you can chain proxy method calls
214
+ def proxy(args, build=nil)
215
+ @attrs.merge!(args.last || {})
139
216
  self
140
217
  end
141
218
 
142
219
  end
220
+
221
+
222
+
143
223
  end
@@ -0,0 +1,116 @@
1
+ module Undies
2
+
3
+ class ElementAPIError < RuntimeError; end
4
+
5
+ class ElementNode
6
+
7
+ # Used internally to implement the markup tree nodes. Each node caches and
8
+ # processes nested markup and elements. At each node level in the markup
9
+ # tree, nodes/markup are cached until the next sibling node or raw markup
10
+ # is defined, or until the node is flushed. This keeps nodes from bloating
11
+ # memory on large documents and allows for output streaming.
12
+
13
+ # ElementNode is specifically used to handle nested element markup.
14
+
15
+ attr_reader :io, :element, :cached
16
+
17
+ def initialize(io, element)
18
+ @io = io
19
+ @cached = nil
20
+ @element = element
21
+
22
+ @start_tag_written = false
23
+ end
24
+
25
+ def attrs(*args, &block)
26
+ @element.__attrs(*args, &block)
27
+ end
28
+
29
+ def text(raw)
30
+ raise ElementAPIError, "can't insert text markup in an element build block - pass in as a content argument instead"
31
+ end
32
+
33
+ def element_node(element_node)
34
+ if !@start_tag_written
35
+ # with newline
36
+ # -1 level offset b/c we are operating on the element build one deep
37
+ write_start_tag(@io.newline, -1)
38
+ end
39
+ write_cached
40
+ @cached = element_node
41
+ end
42
+
43
+ def partial(partial)
44
+ element_node(partial)
45
+ end
46
+
47
+ def flush
48
+ write_cached
49
+ @cached = nil
50
+ self
51
+ end
52
+
53
+ def push
54
+ @io.push(@cached)
55
+ @cached = nil
56
+ end
57
+
58
+ def pop
59
+ flush
60
+ @io.pop
61
+ write_end_tag
62
+ end
63
+
64
+ def to_s
65
+ @io.push(self)
66
+
67
+ @element.__build
68
+ flush
69
+
70
+ @io.pop
71
+ write_end_tag
72
+
73
+ # needed so the `write_cached` calls on ElementNode and RootNode won't add
74
+ # anything else to the IO
75
+ return ""
76
+ end
77
+
78
+ def ==(other)
79
+ other.instance_variable_get("@io") == @io &&
80
+ other.instance_variable_get("@element") == @element
81
+ end
82
+
83
+ # overriding this because the base Node class defines a 'to_s' method that
84
+ # needs to be honored
85
+ def to_str(*args)
86
+ "Undies::ElementNode:#{self.object_id} @element=#{@element.inspect}"
87
+ end
88
+ alias_method :inspect, :to_str
89
+
90
+ private
91
+
92
+ def write_cached
93
+ @io << @cached.to_s
94
+ end
95
+
96
+ def write_start_tag(newline='', level_offset=0)
97
+ @io << "#{@io.line_indent(level_offset)}#{@element.__start_tag}#{newline}"
98
+ @start_tag_written = true
99
+ end
100
+
101
+ def write_content
102
+ @io << @element.__content
103
+ end
104
+
105
+ def write_end_tag(level_offset=0)
106
+ if !@start_tag_written
107
+ write_start_tag('', level_offset)
108
+ write_content
109
+ @io << "#{@element.__end_tag}#{@io.newline}"
110
+ else
111
+ @io << "#{@io.line_indent(level_offset)}#{@element.__end_tag}#{@io.newline}"
112
+ end
113
+ end
114
+
115
+ end
116
+ end
data/lib/undies/io.rb ADDED
@@ -0,0 +1,43 @@
1
+ module Undies
2
+ class IO
3
+
4
+ # the IO class wraps a stream (anything that responds to '<<' and
5
+ # gathers streaming options options. handles writing markup to the
6
+ # stream.
7
+
8
+ attr_reader :stream, :indent, :newline, :node_stack
9
+ attr_accessor :level
10
+
11
+ def initialize(stream, opts={})
12
+ @stream = stream
13
+ @node_stack = []
14
+ self.options = opts
15
+ end
16
+
17
+ def options=(opts)
18
+ if !opts.kind_of?(::Hash)
19
+ raise ArgumentError, "please provide a hash to set IO options"
20
+ end
21
+
22
+ @indent = opts[:pp] || 0
23
+ @newline = opts[:pp].nil? ? "" : "\n"
24
+ @level = opts[:level] || 0
25
+ end
26
+
27
+ def line_indent(relative_level=0)
28
+ "#{' '*(@level+relative_level)*@indent}"
29
+ end
30
+
31
+ # TODO: threaded/forked writing for performance improvement
32
+ def <<(markup)
33
+ @stream << markup
34
+ end
35
+
36
+ def push(scope); @level += 1; push!(scope); end
37
+ def push!(scope); @node_stack.push(scope); end
38
+ def pop; @level -= 1; @node_stack.pop; end
39
+ def current; @node_stack.last; end
40
+ def empty?; @node_stack.empty? end
41
+
42
+ end
43
+ end
@@ -0,0 +1,62 @@
1
+ module Undies
2
+
3
+ class RootAPIError < RuntimeError; end
4
+
5
+ class RootNode
6
+
7
+ # Used internally to implement the markup tree nodes. Each node caches and
8
+ # processes nested markup and elements. At each node level in the markup
9
+ # tree, nodes/markup are cached until the next sibling node or raw markup
10
+ # is defined, or until the node is flushed. This keeps nodes from bloating
11
+ # memory on large documents and allows for output streaming.
12
+
13
+ # RootNode is specifically used to handle root document markup.
14
+
15
+ attr_reader :io, :cached
16
+
17
+ def initialize(io)
18
+ @io = io
19
+ @cached = nil
20
+ end
21
+
22
+ def attrs(*args, &block)
23
+ raise RootAPIError, "can't call '__attrs' at the root node level"
24
+ end
25
+
26
+ def text(raw)
27
+ write_cached
28
+ @cached = "#{@io.line_indent}#{raw.to_s}#{@io.newline}"
29
+ end
30
+
31
+ def element_node(element_node)
32
+ write_cached
33
+ @cached = element_node
34
+ end
35
+
36
+ def partial(partial)
37
+ text(partial)
38
+ end
39
+
40
+ def flush
41
+ write_cached
42
+ @cached = nil
43
+ self
44
+ end
45
+
46
+ def push
47
+ @io.push(@cached)
48
+ @cached = nil
49
+ end
50
+
51
+ def pop
52
+ flush
53
+ end
54
+
55
+ private
56
+
57
+ def write_cached
58
+ @io << @cached.to_s
59
+ end
60
+
61
+ end
62
+ end
data/lib/undies/source.rb CHANGED
@@ -1,6 +1,7 @@
1
- require 'undies/named_source'
2
-
3
1
  module Undies
2
+
3
+
4
+
4
5
  class Source
5
6
 
6
7
  attr_reader :source, :data, :layout
@@ -70,4 +71,79 @@ module Undies
70
71
  end
71
72
 
72
73
  end
74
+
75
+
76
+
77
+ class NamedSource
78
+
79
+ attr_accessor :file, :opts, :proc
80
+
81
+ def initialize(*args, &block)
82
+ args << block if block
83
+ self.args = args
84
+ end
85
+
86
+ def ==(other_named_source)
87
+ self.file == other_named_source.file &&
88
+ self.opts == other_named_source.opts &&
89
+ self.proc == other_named_source.proc
90
+ end
91
+
92
+ def args=(values)
93
+ self.proc, self.opts, self.file = [
94
+ values.last.kind_of?(::Proc) ? values.pop : nil,
95
+ values.last.kind_of?(::Hash) ? values.pop : {},
96
+ values.last.kind_of?(::String) ? values.pop : nil
97
+ ]
98
+ end
99
+
100
+ def args
101
+ [self.file, self.opts, self.proc]
102
+ end
103
+
104
+ end
105
+
106
+ # singleton accessors for named sources
107
+
108
+ def self.named_sources
109
+ @@sources ||= {}
110
+ end
111
+
112
+ def self.named_source(name, *args, &block)
113
+ if args.empty? && block.nil?
114
+ self.named_sources[name]
115
+ else
116
+ self.named_sources[name] = Undies::NamedSource.new(*args, &block)
117
+ end
118
+ end
119
+
120
+ def self.source(name)
121
+ if ns = self.named_source(name)
122
+ Undies::Source.new(ns)
123
+ end
124
+ end
125
+
126
+
127
+
128
+ class SourceStack < ::Array
129
+
130
+ # a source stack is used to manage which sources and any deeply nested
131
+ # layouts they are in. initialize this object with a content source obj
132
+ # and get a stack where the the top source is the outer most layout and
133
+ # the bottom source is the source used to initialize the stack (the content
134
+ # source). naturally any sources in between are the intermediate layouts
135
+ # for the content source
136
+
137
+ def initialize(source)
138
+ super([source, source.layouts].flatten.compact)
139
+ end
140
+
141
+ def pop
142
+ super
143
+ end
144
+
145
+ end
146
+
147
+
148
+
73
149
  end