undies 2.2.1 → 3.0.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
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