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
data/README.rdoc DELETED
@@ -1,203 +0,0 @@
1
- = Undies
2
- A pure-Ruby DSL for streaming templated HTML, XML, or plain text. Named for its gratuitous use of the underscore.
3
-
4
- == Installation
5
- $ gem install undies
6
-
7
- == DSL
8
-
9
- Undies uses an underscore-based DSL for rendering content. It can render plain text, html-escaped text, xml, and html.
10
-
11
- === Plain text
12
-
13
- Escaped (xml / html) text:
14
-
15
- _ "this will be escaped & streamed"
16
- # => "this will be escaped & streamed"
17
-
18
- Raw (un-escaped) text:
19
- __ "this will <em>not</em> be escaped"
20
- # => "this will <em>not</em> be escaped"
21
-
22
- === XML
23
-
24
- Empty node:
25
- _thing # => "<thing />"
26
-
27
- Node with content:
28
- _thing {
29
- _ "Some Content"
30
- _ "More Content "
31
- } # => "<thing>Some ContentMore Content</thing>"
32
-
33
- Node with attributes:
34
- _thing(:one => 1, 'a' => "Aye")
35
- # => "<thing one=\"1\" a=\"Aye\" />"
36
-
37
- Nested nodes:
38
- _thing {
39
- _Something {
40
- _ "Some Content"
41
- }
42
- } # => "<thing><Something>Some Content</Something></thing>"
43
-
44
- Namespaces / Doctype stuff:
45
- __ "<?xml version="1.0" encoding="UTF-8"?>"
46
-
47
- _thing('xmlns:ss="urn:some-cool-namespace"') {
48
- send("_ss:Value", {'ss:some_attr' => "some attr value"}) {
49
- _ "Some Content"
50
- }
51
- } # => "<thing xmlns:ss=\"urn:some-cool-namespace\"><ss:Value ss:some_attr=\"some attr value\">Some Content</ss:Value>"
52
-
53
- Comments:
54
- __ "<!-- some comment -->"
55
- # => "<!-- some comment -->"
56
-
57
- === HTML
58
-
59
- Same stuff applies:
60
- __ "<!DOCTYPE html>"
61
-
62
- _div(:style => "border-top: 1px") {
63
- _h1 { _ "Hello World" }
64
- _p { _ "blah blah" }
65
- } # => "<div style=\"border-top: 1px\"><h1>Hello World</h1><p>blah blah</p></div>"
66
-
67
- id attribute helper:
68
- _h1.header!
69
- # => "<h1 id=\"header\" />"
70
-
71
- _h1.header!.title!
72
- # => "<h1 id=\"title\" />"
73
-
74
- class attributes helper:
75
- _h1.header.awesome
76
- # => "<h1 class=\"header awesome\" />"
77
-
78
- use in combo:
79
- _h1.header!.awesome
80
- # => "<h1 class=\"awesome\" id=\"header\" />
81
-
82
- == Streamed Output
83
-
84
- Undies works by streaming content defined by using its DSL to a given output stream. Any call to Template#_, Template#__, or Tempate#_<node> will stream its output (or you can write helpers you mix in that use these base api methods). This has a number of advantages:
85
-
86
- * content is written out immediately
87
- * maintain a relatively low memory profile while rendering
88
- * can process large templates with linear performance impact
89
-
90
- However, because content is streamed then forgotten as it is being rendered, Undies templates cannot be self-referrential. No one element may refer to other previously rendered elements.
91
-
92
- == Rendering
93
-
94
- To render using Undies, create a Template instance, providing the template source, data, and output information.
95
-
96
- source = Undies::Source.new("/path/to/sourcefile")
97
- data = { :two_plus_two => 4 }
98
- output = Undies::Output.new(@some_io_stream)
99
-
100
- Undies::Template.new(source, {}, output)
101
-
102
- === Source
103
-
104
- You specify Undies source using the Undies::Source object. You can create source either form a block or a file. Source content (either block or file) will be evaluated in context of the template.
105
-
106
- === Data
107
-
108
- Undies renders source content in isolated scope (the context of the template). This means that content has access to only the data it is given or the Undies API itself. When you define a template for rendering, you provide not only the template source, but any data that source should be rendered with. Data is given in the form of a Hash. The string form of the hash keys are exposed as local methods that return their corresponding values.
109
-
110
- === Output
111
-
112
- As said before, Undies streams to a given output stream. You specify a Template's output by creating an Undies::Output object. These objects take an io stream and a hash of options:
113
-
114
- * :pp (pretty-print) : set to a Fixnum to tab-space indent pretty print the output.
115
-
116
- === Examples
117
-
118
- # file source, no local data, no pretty printing
119
- source = Undies::Source.new("/path/to/source")
120
- Undies::Template.new(source, {}, Undies::Output.new(@io))
121
-
122
-
123
- # proc source, simple local data, no pretty printing
124
- source = Undies::Source.new(Proc.new do
125
- _div {
126
- _ content.to_s
127
- }
128
- end)
129
- Undies::Template.new(source, {:content => "Some Content!!" }, Undies::Output.new(@io))
130
-
131
-
132
- # pretty printing (4 space tab indentation)
133
- source = Undies::Source.new("/path/to/source")
134
- Undies::Template.new(source, {}, Undies::Output.new(@io, :pp => 4))
135
-
136
- === Builder approach
137
-
138
- The above examples use the "source rendering" approach. This works great when you know your source content before render time and create a source object from it (ie rendering a view template). However, in some cases, you may not know the source until render time and/or want to use a more declarative style to specify render output. Undies content can be specified programmatically using the "builder rendering" approach.
139
-
140
- To render using this approach, create a Template instance passing it data and output info as above. However, don't pass in any source info, only pass in any local data if you like, and save off the created template:
141
-
142
- # choosing not to use any local data in this example
143
- template = Undies::Template.new(Undies::Output.new(@io))
144
-
145
- Now just interact with the Undies API directly.
146
-
147
- # notice that it becomes less important to bind any local data to the Template using this approach
148
- something = "Some Thing!"
149
- template._div.something! {
150
- template._ something.to_s
151
- }
152
-
153
- *Note:* there is one extra caveat to be aware of using this approach. You need to be sure and flush the template when content processing is complete. Just pass the template to the Undies::Template#flush method:
154
-
155
- # ensures all content is streamed to the template's output stream
156
- # this is necessary when not using the source approach above
157
- Undies::Template.flush(template)
158
-
159
- === Manual approach
160
-
161
- There is another method you can use to render output: the Manual approach. Like the Builder approach, this method is ideal when you don't know the source until render time. The key difference is that blocks are not used to imply nesting relationships. Using this approach, you manually 'push' and 'pop' to move up and down nesting relationship contexts. So a push on an element would move the template context to the element pushed. A pop would move back to the current context's parent element. As you would expect, pop'ing on the root of a template has no effect on the context and pushing a non-element node has no effect on the context.
162
-
163
- To render using this approach, create a Template as you would with the Builder approach. Interact with the Undies API directly. Use the Template#push and Template#pop class methods to change the template scope.
164
-
165
- # this is the equivalent to the Builder approach example above
166
-
167
- template = Undies::Template.new(Undies::Output.new(@io))
168
- something = "Some Thing!"
169
- current = template._div.something!
170
-
171
- template.__push(current)
172
- template._ something.to_s
173
- template.__pop
174
-
175
- # alternate method for flushing a template
176
- template.__flush
177
-
178
- *Note:* as with the Builder approach, you must flush the template when content processing is complete.
179
-
180
- == License
181
-
182
- Copyright (c) 2011-Present Kelly Redding
183
-
184
- Permission is hereby granted, free of charge, to any person
185
- obtaining a copy of this software and associated documentation
186
- files (the "Software"), to deal in the Software without
187
- restriction, including without limitation the rights to use,
188
- copy, modify, merge, publish, distribute, sublicense, and/or sell
189
- copies of the Software, and to permit persons to whom the
190
- Software is furnished to do so, subject to the following
191
- conditions:
192
-
193
- The above copyright notice and this permission notice shall be
194
- included in all copies or substantial portions of the Software.
195
-
196
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
197
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
198
- OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
199
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
200
- HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
201
- WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
202
- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
203
- OTHER DEALINGS IN THE SOFTWARE.
@@ -1,54 +0,0 @@
1
- module Undies
2
-
3
- class Source; end
4
-
5
- class NamedSource
6
-
7
- attr_accessor :file, :opts, :proc
8
-
9
- def initialize(*args, &block)
10
- args << block if block
11
- self.args = args
12
- end
13
-
14
- def ==(other_named_source)
15
- self.file == other_named_source.file &&
16
- self.opts == other_named_source.opts &&
17
- self.proc == other_named_source.proc
18
- end
19
-
20
- def args=(values)
21
- self.proc, self.opts, self.file = [
22
- values.last.kind_of?(::Proc) ? values.pop : nil,
23
- values.last.kind_of?(::Hash) ? values.pop : {},
24
- values.last.kind_of?(::String) ? values.pop : nil
25
- ]
26
- end
27
-
28
- def args
29
- [self.file, self.opts, self.proc]
30
- end
31
-
32
- end
33
-
34
- # singleton accessors for named sources
35
-
36
- def self.named_sources
37
- @@sources ||= {}
38
- end
39
-
40
- def self.named_source(name, *args, &block)
41
- if args.empty? && block.nil?
42
- self.named_sources[name]
43
- else
44
- self.named_sources[name] = Undies::NamedSource.new(*args, &block)
45
- end
46
- end
47
-
48
- def self.source(name)
49
- if ns = self.named_source(name)
50
- Undies::Source.new(ns)
51
- end
52
- end
53
-
54
- end
data/lib/undies/node.rb DELETED
@@ -1,87 +0,0 @@
1
- module Undies
2
-
3
- class Node
4
-
5
- # have as many methods to the class level as possible to keep from
6
- # polluting the public instance methods and to maximize the effectiveness
7
- # of the Element#method_missing logic
8
-
9
- def self.start_tag(node)
10
- node.instance_variable_get("@start_tag") || ""
11
- end
12
-
13
- def self.end_tag(node)
14
- node.instance_variable_get("@end_tag") || ""
15
- end
16
-
17
- def self.set_start_tag(node); end
18
- def self.set_end_tag(node); end
19
-
20
- def self.builds(node)
21
- node.instance_variable_get("@builds") || []
22
- end
23
-
24
- def self.add_build(node, build_block)
25
- node.instance_variable_set("@builds", builds(node) + [build_block])
26
- end
27
-
28
- def self.children(node)
29
- node.instance_variable_get("@children")
30
- end
31
-
32
- def self.set_children(node, value)
33
- node.instance_variable_set("@children", value)
34
- end
35
-
36
- def self.attrs(element)
37
- element.instance_variable_get("@attrs")
38
- end
39
-
40
- def self.merge_attrs(element, value={})
41
- attrs(element).merge(value).tap do |a|
42
- element.instance_variable_set("@attrs", a)
43
- end
44
- end
45
-
46
- def self.node_name(node)
47
- node.instance_variable_get("@name") || ""
48
- end
49
-
50
- def self.content(node)
51
- node.instance_variable_get("@content") || ""
52
- end
53
-
54
- def self.mode(node)
55
- node.instance_variable_get("@mode") || :inline
56
- end
57
-
58
- def self.prefix(node, meth, level, indent)
59
- "".tap do |value|
60
- if mode(node) != :inline && indent > 0
61
- if meth == 'start_tag'
62
- value << "#{level > 0 ? "\n": ''}#{' '*level*indent}"
63
- elsif meth == 'end_tag'
64
- value << "\n#{' '*(level > 0 ? level-1 : level)*indent}"
65
- end
66
- end
67
- end
68
- end
69
-
70
- def initialize(content, mode=:inline)
71
- @start_tag = nil
72
- @end_tag = nil
73
- @content = content
74
- @builds = []
75
- @attrs = {}
76
- @children = false
77
- @mode = mode
78
- end
79
-
80
- def ==(other_node)
81
- self.class.content(self) == other_node.class.content(other_node) &&
82
- self.instance_variable_get("@start_tag") == other_node.instance_variable_get("@start_tag") &&
83
- self.instance_variable_get("@end_tag") == other_node.instance_variable_get("@end_tag")
84
- end
85
-
86
- end
87
- end
@@ -1,111 +0,0 @@
1
- module Undies
2
- class NodeStack
3
-
4
- # the node stack handles caching nodes for processing, running node
5
- # builds, and buffering node output for writing. I want to treat this
6
- # as a stack of nodes for the template API to reference. I need to push
7
- # a node onto the stack, reference it using the 'current' method,
8
- # and pop it off the stack when I'm done.
9
-
10
- # the stack first caches new nodes before pushing them onto the stack
11
- # and node output is buffered as new nodes are pushed
12
-
13
- def self.create(output)
14
- output.kind_of?(NodeStack) ? output : NodeStack.new(output)
15
- end
16
-
17
- attr_reader :stack, :output, :buffer
18
- attr_accessor :cached_node
19
-
20
- def initialize(output)
21
- @stack = []
22
- @cached_node = nil
23
- @output = output
24
- @written_level = 0
25
- end
26
-
27
- def empty?; @stack.empty?; end
28
- def size; @stack.size; end
29
- def first; @stack.first; end
30
- def last; @stack.last; end
31
-
32
- alias_method :current, :last
33
- alias_method :level, :size
34
-
35
- def push(node)
36
- if current
37
- current.class.set_children(current, node.kind_of?(Element))
38
- end
39
- if @written_level < level
40
- open(current)
41
- end
42
- @stack.push(node)
43
- end
44
-
45
- def pop(*args)
46
- if !empty?
47
- node = if @written_level < level
48
- @stack.pop.tap { |node| write(node) }
49
- else
50
- @stack.pop.tap { |node| close(node) }
51
- end
52
- end
53
- end
54
-
55
- def node(node)
56
- clear_cached
57
- self.cached_node = node
58
- end
59
-
60
- def flush
61
- clear_cached
62
- end
63
-
64
- def clear_cached
65
- node_to_push, self.cached_node = self.cached_node, nil
66
- if node_to_push
67
- push(node_to_push)
68
- node_to_push.class.builds(node_to_push).each { |build| build.call }
69
- clear_cached
70
- pop
71
- end
72
- end
73
-
74
- private
75
-
76
- def open(node)
77
- start_tag(node, @written_level)
78
- @written_level += 1
79
- content(node, @written_level)
80
- end
81
-
82
- def write(node)
83
- start_tag(node, @written_level)
84
- content(node, @written_level+1)
85
- end_tag(node, @written_level)
86
- end
87
-
88
- def close(node)
89
- @written_level -= 1
90
- end_tag(node, @written_level)
91
- end
92
-
93
- # TODO: Fill up an output write buffer with arg arrays
94
- # then empty it
95
-
96
- def start_tag(node, level)
97
- @output.write(node, 'start_tag', level)
98
- end
99
-
100
- def content(node, level)
101
- @output.write(node, 'content', level)
102
- end
103
-
104
- def end_tag(node, level)
105
- @output.write(node, 'end_tag', level)
106
- end
107
-
108
- end
109
-
110
-
111
- end
data/lib/undies/output.rb DELETED
@@ -1,31 +0,0 @@
1
- module Undies
2
- class Output
3
-
4
- # the output class wraps an IO stream, gathers pretty printing options,
5
- # and handles writing out buffered node stack items
6
-
7
- attr_reader :io, :pp
8
- attr_accessor :pp_level
9
-
10
- def initialize(io, opts={})
11
- @io = io
12
- self.options = opts
13
- end
14
-
15
- def options=(opts)
16
- if !opts.kind_of?(::Hash)
17
- raise ArgumentError, "please provide a hash to set options with"
18
- end
19
-
20
- # setup any pretty printing
21
- @pp = opts[:pp] || 0
22
- @pp_level = opts[:pp_level] || 0
23
- end
24
-
25
- def write(node, meth, level)
26
- @io << node.class.prefix(node, meth, level+@pp_level, @pp)
27
- @io << node.class.send(meth, node)
28
- end
29
-
30
- end
31
- end
@@ -1,22 +0,0 @@
1
- require "undies/source"
2
-
3
- module Undies
4
- class SourceStack < ::Array
5
-
6
- # a source stack is used to manage which sources and any deeply nested
7
- # layouts they are in. initialize this object with a content source obj
8
- # and get a stack where the the top source is the outer most layout and
9
- # the bottom source is the source used to initialize the stack (the content
10
- # source). naturally any sources in between are the intermediate layouts
11
- # for the content source
12
-
13
- def initialize(source)
14
- super([source, source.layouts].flatten.compact)
15
- end
16
-
17
- def pop
18
- super
19
- end
20
-
21
- end
22
- end
@@ -1,109 +0,0 @@
1
- require 'assert'
2
-
3
- require 'undies/node'
4
- require 'undies/element'
5
- require 'test/fixtures/write_thing'
6
-
7
- require 'undies/node_stack'
8
-
9
- module Undies
10
-
11
- class NodeStackTests < Assert::Context
12
- desc "a NodeStack"
13
- before do
14
- @hello = Node.new "hello"
15
- @something = Node.new "something else"
16
- @hi = Node.new("hi")
17
- @br = Element.new :br
18
- @div = Element.new(:div) {}
19
-
20
- @outstream = StringIO.new(@out = "")
21
- @stream_test_output = Output.new(@outstream)
22
- @ns = NodeStack.new @stream_test_output
23
- end
24
- subject { @ns }
25
-
26
- should have_class_method :create
27
- should have_readers :stack, :cached_node, :output
28
- should have_instance_methods :current, :size, :level, :empty?, :first, :last
29
- should have_instance_methods :push, :pop, :node, :flush
30
- should have_instance_methods :clear_cached
31
-
32
- should "be empty by default" do
33
- assert_empty subject
34
- end
35
-
36
- should "have an empty cache by default" do
37
- assert_nil subject.cached_node
38
- end
39
-
40
- end
41
-
42
-
43
- class StackTests < NodeStackTests
44
-
45
- should "push nodes onto the stack" do
46
- assert_nothing_raised do
47
- subject.push(@hello)
48
- subject.push(@something)
49
- end
50
-
51
- assert_equal 2, subject.size
52
- end
53
-
54
- should "know its level (should be one less than the array's size)" do
55
- assert_equal 0, subject.level
56
- subject.push(@hello)
57
- assert_equal 1, subject.level
58
- end
59
-
60
- should "fetch the last item in the array with the current method" do
61
- subject.push(@hello)
62
- subject.push(@something)
63
-
64
- assert_same @something, subject.current
65
- end
66
-
67
- should "remove the last item in the array with the pop method" do
68
- subject.push(@hello)
69
- subject.push(@something)
70
-
71
- assert_equal 2, subject.size
72
-
73
- node = subject.pop
74
- assert_same @something, node
75
- assert_equal 1, subject.size
76
- end
77
-
78
- end
79
-
80
-
81
- class CacheTests < NodeStackTests
82
-
83
- should "add nodes to its cache using the #node method" do
84
- subject.node(@hi)
85
- assert_equal @hi, subject.cached_node
86
- end
87
-
88
- should "push the current cached node onto the stack when caching a new node" do
89
- subject.node(@br)
90
- assert_equal @br, subject.cached_node
91
- assert_equal 0, subject.size
92
-
93
- subject.node(@div)
94
- assert_equal @div, subject.cached_node
95
- end
96
-
97
- should "call the node's builds when flushed from cache" do
98
- build = "build"
99
- subject.node(Element.new(:div) { build += " called" })
100
- subject.flush
101
-
102
- assert_equal "build called", build
103
- end
104
-
105
- end
106
-
107
-
108
-
109
- end