undies 1.2.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/undies/source.rb CHANGED
@@ -1,26 +1,72 @@
1
+ require 'undies/named_source'
2
+
1
3
  module Undies
2
4
  class Source
3
5
 
4
- attr_reader :source, :data
6
+ attr_reader :source, :data, :layout
7
+
8
+ def initialize(*args, &block)
9
+ named = args.first.kind_of?(NamedSource) ? args.first : nil
10
+ args << block if block
11
+ self.args = named ? named.args.compact : args
12
+ end
5
13
 
6
- def initialize(source=nil)
7
- raise ArgumentError, "file or block required" if source.nil?
14
+ def file?
15
+ !@source.kind_of?(::Proc)
16
+ end
8
17
 
9
- @source = source
10
- if self.file? && !File.exists?(@source.to_s)
11
- raise ArgumentError, "no template file '#{@source}'"
18
+ def layouts
19
+ if layout
20
+ [self.layout, self.layout.layouts].flatten.compact
21
+ else
22
+ []
12
23
  end
24
+ end
25
+
26
+ def layout_sources
27
+ self.layouts.collect{|l| l.source}
28
+ end
13
29
 
14
- # load source data and prepare (uses binread to avoid encoding issues)
15
- @data = if self.file?
16
- File.send(File.respond_to?(:binread) ? :binread : :read, @source.to_s)
30
+ def ==(other_source)
31
+ self.source == other_source.source &&
32
+ self.layout_sources == other_source.layout_sources
33
+ end
34
+
35
+ def args=(values)
36
+ proc, opts, file = [
37
+ values.last.kind_of?(::Proc) ? values.pop : nil,
38
+ values.last.kind_of?(::Hash) ? values.pop : {},
39
+ values.last.kind_of?(::String) ? values.pop : nil
40
+ ]
41
+
42
+ self.source = file || proc
43
+ self.layout = opts[:layout]
44
+ end
45
+
46
+ def source=(value)
47
+ if value.nil?
48
+ raise ArgumentError, "source name, file, or block required"
49
+ end
50
+ @data = if value.kind_of?(::Proc)
51
+ value
17
52
  else
18
- @source
53
+ raise ArgumentError, "no source file '#{value}'" if !File.exists?(value.to_s)
54
+ File.send(File.respond_to?(:binread) ? :binread : :read, value.to_s)
19
55
  end
56
+ @source = value
20
57
  end
21
58
 
22
- def file?
23
- !@source.kind_of?(::Proc)
59
+ def layout=(value)
60
+ @layout = case value
61
+ when Source, NilClass
62
+ value
63
+ when ::Proc
64
+ Source.new(&value)
65
+ when ::String, NamedSource
66
+ Source.new(value)
67
+ else
68
+ raise ArgumentError, "invalid layout"
69
+ end
24
70
  end
25
71
 
26
72
  end
@@ -0,0 +1,22 @@
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,47 +1,69 @@
1
- require 'undies/source'
2
- require 'undies/node'
3
- require 'undies/element'
1
+ require 'undies/source_stack'
2
+ require 'undies/output'
4
3
 
5
4
  module Undies
6
5
  class Template
7
6
 
8
- # prefixing with a triple underscore to not pollut metaclass locals scope
7
+ # have as many methods to the class level as possilbe to keep from
8
+ # polluting the public instance methods, the instance scope, and to
9
+ # maximize the effectiveness of the Template#method_missing logic
9
10
 
10
- attr_accessor :___nodes
11
+ def self.output(template)
12
+ template.instance_variable_get("@_undies_output")
13
+ end
14
+
15
+ def initialize(source, data, output)
16
+ # setup the source stack and output objects
17
+ raise ArgumentError, "please provide a Source object" if !source.kind_of?(Source)
18
+ @_undies_source_stack = SourceStack.new(source)
19
+ raise ArgumentError, "please provide an Output object" if !output.kind_of?(Output)
20
+ @_undies_output = output
21
+
22
+ # apply data to template scope
23
+ raise ArgumentError if !data.kind_of?(::Hash)
24
+ if (data.keys.map(&:to_s) & self.public_methods.map(&:to_s)).size > 0
25
+ raise ArgumentError, "data conflicts with template public methods."
26
+ end
27
+ metaclass = class << self; self; end
28
+ data.each {|key, value| metaclass.class_eval { define_method(key){value} }}
11
29
 
12
- def initialize(*args, &block)
13
- self.___nodes = NodeList.new
14
- targs = self.___template_args(args.compact, block)
15
- self.___locals, self.___io, self.___layout, self.___markup = targs
16
- self.___stack = ElementStack.new(self, self.___io)
17
- self.___compile { self.___render(self.___markup) if self.___layout }
30
+ # yield to recursivley render the source stack
31
+ self.__yield
32
+
33
+ # flush any remaining output to the stream
34
+ @_undies_output.flush
18
35
  end
19
36
 
20
- def to_s(pp_indent=nil)
21
- self.___nodes.to_s(0, pp_indent)
37
+ # call this to render template source
38
+ # use this method in layouts to insert a layout's content source
39
+ def __yield
40
+ return if @_undies_source_stack.nil? || (source = @_undies_source_stack.pop).nil?
41
+ if source.file?
42
+ instance_eval(source.data, source.source, 1)
43
+ else
44
+ instance_eval(&source.data)
45
+ end
22
46
  end
23
47
 
24
- # Add a text node (data escaped) to the nodes of the current node
25
- def _(data="")
26
- self.__ self.escape_html(data.to_s)
48
+ # call this to render partial source embedded in a template
49
+ # partial source is rendered with its own scope/data but shares
50
+ # its parent template's output object
51
+ def __partial(source, data)
52
+ Undies::Template.new(source, data, @_undies_output)
27
53
  end
28
54
 
55
+ # Add a text node (data escaped) to the nodes of the current node
56
+ def _(data=""); self.__ self.escape_html(data.to_s); end
57
+
29
58
  # Add a text node with the data un-escaped
30
- def __(data="")
31
- node = Node.new(data.to_s)
32
- self.___io << node.to_s if self.___io
33
- self.___add(node)
34
- end
59
+ def __(data=""); @_undies_output.node(data.to_s); end
35
60
 
36
61
  # Add an element to the nodes of the current node
37
- def element(name, attrs={}, &block)
38
- self.___add(Element.new(self.___stack, name, attrs, &block))
39
- end
62
+ def element(*args, &block); @_undies_output.element(*args, &block); end
40
63
  alias_method :tag, :element
41
64
 
42
65
  # Element proxy methods ('_<element>'') ========================
43
66
  ELEM_METH_REGEX = /^_(.+)$/
44
-
45
67
  def method_missing(meth, *args, &block)
46
68
  if meth.to_s =~ ELEM_METH_REGEX
47
69
  element($1, *args, &block)
@@ -49,7 +71,6 @@ module Undies
49
71
  super
50
72
  end
51
73
  end
52
-
53
74
  def respond_to?(*args)
54
75
  if args.first.to_s =~ ELEM_METH_REGEX
55
76
  true
@@ -70,119 +91,11 @@ module Undies
70
91
  "/" => "&#x2F;"
71
92
  }
72
93
  ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
73
-
74
94
  # Escape ampersands, brackets and quotes to their HTML/XML entities.
75
95
  def escape_html(string)
76
96
  string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
77
97
  end
78
98
  # end Rip from Rack v1.3.0 =====================================
79
99
 
80
- protected
81
-
82
- # prefixing non-public methods with a triple underscore to not pollute
83
- # metaclass locals scope
84
-
85
- def ___compile
86
- self.___render(self.___layout || self.___markup)
87
- end
88
-
89
- def ___render(source)
90
- if source.file?
91
- instance_eval(source.data, source.source, 1)
92
- else
93
- instance_eval(&source.data)
94
- end
95
- end
96
-
97
- def ___locals=(data)
98
- if !data.kind_of?(::Hash)
99
- raise ArgumentError
100
- end
101
- if invalid_locals?(data.keys)
102
- raise ArgumentError, "locals conflict with template's public methods."
103
- end
104
- data.each do |key, value|
105
- self.___metaclass do
106
- define_method(key) { value }
107
- end
108
- end
109
- end
110
-
111
- def ___add(node)
112
- self.___stack.last.___nodes.append(node)
113
- end
114
-
115
- def ___stack
116
- @stack
117
- end
118
-
119
- def ___stack=(value)
120
- raise ArgumentError if !value.respond_to?(:push) || !value.respond_to?(:pop)
121
- @stack = value
122
- end
123
-
124
- def ___io
125
- @io
126
- end
127
-
128
- def ___io=(value)
129
- raise ArgumentError if value && !self.___is_a_stream?(value)
130
- @io = value
131
- end
132
-
133
- def ___layout
134
- @layout
135
- end
136
-
137
- def ___layout=(value)
138
- if value && !(value.kind_of?(Source) && value.file?)
139
- raise ArgumentError, "layout must be a file source"
140
- end
141
- @layout = value
142
- end
143
-
144
- def ___markup
145
- @markup
146
- end
147
-
148
- def ___markup=(value)
149
- raise ArgumentError if value && !value.kind_of?(Source)
150
- @markup = value
151
- end
152
-
153
- def ___template_args(args, block)
154
- [ args.last.kind_of?(::Hash) ? args.pop : {},
155
- self.___is_a_stream?(args.last) ? args.pop : nil,
156
- self.___layout_arg?(args, block) ? Source.new(args.pop) : nil,
157
- Source.new(args.first || block)
158
- ]
159
- end
160
-
161
- def ___metaclass(&block)
162
- metaclass = class << self; self; end
163
- metaclass.class_eval(&block)
164
- end
165
-
166
- def ___is_a_stream?(thing)
167
- !thing.kind_of?(::String) && thing.respond_to?(:<<)
168
- end
169
-
170
- def ___layout_arg?(args, block)
171
- if args.size >= 2
172
- true
173
- elsif args.size <= 0
174
- false
175
- else # args.size == 1
176
- !block.nil?
177
- end
178
- end
179
-
180
- private
181
-
182
- # you can't define locals that conflict with the template's public methods
183
- def invalid_locals?(keys)
184
- (keys.collect(&:to_s) & self.public_methods.collect(&:to_s)).size > 0
185
- end
186
-
187
100
  end
188
101
  end
@@ -1,3 +1,3 @@
1
1
  module Undies
2
- VERSION = "1.2.0"
2
+ VERSION = "2.0.0"
3
3
  end
data/lib/undies.rb CHANGED
@@ -1,13 +1,3 @@
1
1
  require 'undies/template'
2
- require 'undies/partial'
3
2
 
4
- module Undies
5
-
6
- # TODO: maybe implement this pattern??
7
- # class << self
8
- # def new(&block)
9
- # Template.new(&block)
10
- # end
11
- # end
12
-
13
- end
3
+ module Undies; end
data/test/element_test.rb CHANGED
@@ -1,116 +1,61 @@
1
- require "test_belt"
1
+ require "assert"
2
2
 
3
3
  require "undies/element"
4
- require "undies/element_stack"
5
4
  require "undies/template"
6
5
 
7
6
  class Undies::Element
8
7
 
9
-
10
-
11
- class BasicTest < Test::Unit::TestCase
12
- include TestBelt
13
-
14
- context 'an element'
15
- before { @e = Undies::Element.new(Undies::ElementStack.new, :div) }
8
+ class BasicTests < Assert::Context
9
+ desc 'an element'
10
+ before do
11
+ @e = Undies::Element.new(:div)
12
+ end
16
13
  subject { @e }
17
- should have_readers :___name, :___attrs
18
- should have_accessor :___nodes
14
+
15
+ should have_class_methods :html_attrs, :content, :flush
16
+ should have_instance_method :to_str
19
17
 
20
18
  should "be a Node" do
21
19
  assert_kind_of Undies::Node, subject
22
20
  end
23
21
 
24
22
  should "store it's name as a string" do
25
- assert_equal "div", subject.___name
23
+ assert_equal "div", subject.instance_variable_get("@name")
26
24
  end
27
25
 
28
- should "have a NodeList as its nodes" do
29
- assert_kind_of Undies::NodeList, subject.___nodes
30
- end
31
-
32
- should "have its nodes be its content" do
33
- assert_equal subject.___nodes.object_id, subject.___content.object_id
34
- end
35
-
36
- should "have an element stack as its stack" do
37
- assert_kind_of Undies::ElementStack, subject.send(:instance_variable_get, "@stack")
38
- end
39
-
40
- should "complain is not created with an ElementStack" do
41
- assert_raises ArgumentError do
42
- Undies::Element.new([], :div)
43
- end
44
- end
45
-
46
- end
47
-
48
-
49
-
50
- class EmptyTest < Test::Unit::TestCase
51
- include TestBelt
52
- context 'an empty element'
53
- before { @e = Undies::Element.new(Undies::ElementStack.new, :br) }
54
- subject { @e }
55
-
56
- should "have no nodes" do
57
- assert_equal([], subject.___nodes)
26
+ should "have no content itself" do
27
+ assert_nil subject.class.content(subject)
58
28
  end
59
29
 
60
30
  end
61
31
 
62
-
63
-
64
- class HtmlAttrsTest < BasicTest
65
- context "html_attrs util"
32
+ class HtmlAttrsTest < BasicTests
33
+ desc "the element class html_attrs util"
66
34
 
67
35
  should "convert an empty hash to html attrs" do
68
- assert_equal('', subject.send(:html_attrs, {}))
36
+ assert_equal '', Undies::Element.html_attrs({})
69
37
  end
70
38
 
71
39
  should "convert a basic hash to html attrs" do
72
- attrs = subject.send(:html_attrs, :class => "test", :id => "test_1")
40
+ attrs = Undies::Element.html_attrs(:class => "test", :id => "test_1")
73
41
  assert_match /^\s{1}/, attrs
74
42
  assert attrs.include?('class="test"')
75
43
  assert attrs.include?('id="test_1"')
76
44
  end
77
45
 
78
46
  should "convert a nested hash to html attrs" do
79
- attrs = subject.send(:html_attrs, {
47
+ attrs = Undies::Element.html_attrs({
80
48
  :class => "testing", :id => "test_2",
81
49
  :nested => {:something => 'is_awesome'}
82
50
  })
83
51
  assert_match /^\s{1}/, attrs
84
- assert attrs.include?('class="testing"')
85
- assert attrs.include?('id="test_2"')
86
- assert attrs.include?('nested="somethingis_awesome"')
52
+ assert_included 'class="testing"', attrs
53
+ assert_included 'id="test_2"', attrs
54
+ assert_included 'nested_something="is_awesome"', attrs
87
55
  end
88
56
  end
89
57
 
90
-
91
-
92
- class SerializeTest < BasicTest
93
- should "serialize with no child elements" do
94
- element = Undies::Element.new(Undies::ElementStack.new, :br)
95
- assert_equal "<br />", element.to_s
96
- end
97
-
98
- should "serialize with attrs" do
99
- element = Undies::Element.new(Undies::ElementStack.new, :br, {:class => 'big'})
100
- assert_equal '<br class="big" />', element.to_s
101
- end
102
-
103
- should "serialize with attrs and content" do
104
- templ = Undies::Template.new do
105
- element(:strong, {:class => 'big'}) { __ "Loud Noises!" }
106
- end
107
- assert_equal '<strong class="big">Loud Noises!</strong>', templ.to_s
108
- end
109
- end
110
-
111
-
112
-
113
- class CSSProxyTest < BasicTest
58
+ class CSSProxyTests < BasicTests
114
59
 
115
60
  should "respond to any method ending in '!' as an id proxy" do
116
61
  assert subject.respond_to?(:asdgasdg!)
@@ -119,32 +64,25 @@ class Undies::Element
119
64
  should "proxy id attr with methods ending in '!'" do
120
65
  assert_equal({
121
66
  :id => 'thing1'
122
- }, subject.thing1!.___attrs)
123
- end
124
-
125
- should "nest elements from proxy id call" do
126
- templ = Undies::Template.new do
127
- element(:div).thing1! { _ "stuff" }
128
- end
129
- assert_equal "<div id=\"thing1\">stuff</div>", templ.to_s
67
+ }, subject.thing1!.instance_variable_get("@attrs"))
130
68
  end
131
69
 
132
70
  should "proxy id attr with last method call ending in '!'" do
133
71
  assert_equal({
134
72
  :id => 'thing2'
135
- }, subject.thing1!.thing2!.___attrs)
73
+ }, subject.thing1!.thing2!.instance_variable_get("@attrs"))
136
74
  end
137
75
 
138
76
  should "set id attr to explicit if called last " do
139
77
  assert_equal({
140
78
  :id => 'thing3'
141
- }, subject.thing1!.thing2!(:id => 'thing3').___attrs)
79
+ }, subject.thing1!.thing2!(:id => 'thing3').instance_variable_get("@attrs"))
142
80
  end
143
81
 
144
82
  should "set id attr to proxy if called last" do
145
83
  assert_equal({
146
84
  :id => 'thing1'
147
- }, subject.thing2!(:id => 'thing3').thing1!.___attrs)
85
+ }, subject.thing2!(:id => 'thing3').thing1!.instance_variable_get("@attrs"))
148
86
  end
149
87
 
150
88
  should "respond to any other method as a class proxy" do
@@ -154,32 +92,25 @@ class Undies::Element
154
92
  should "proxy single html class attr" do
155
93
  assert_equal({
156
94
  :class => 'thing'
157
- }, subject.thing.___attrs)
158
- end
159
-
160
- should "nest elements from proxy class call" do
161
- templ = Undies::Template.new do
162
- element(:div).thing { _ "stuff" }
163
- end
164
- assert_equal "<div class=\"thing\">stuff</div>", templ.to_s
95
+ }, subject.thing.instance_variable_get("@attrs"))
165
96
  end
166
97
 
167
98
  should "proxy multi html class attrs" do
168
99
  assert_equal({
169
100
  :class => 'list thing awesome'
170
- }, subject.list.thing.awesome.___attrs)
101
+ }, subject.list.thing.awesome.instance_variable_get("@attrs"))
171
102
  end
172
103
 
173
104
  should "set class attr with explicit if called last " do
174
105
  assert_equal({
175
106
  :class => 'list'
176
- }, subject.thing.awesome(:class => "list").___attrs)
107
+ }, subject.thing.awesome(:class => "list").instance_variable_get("@attrs"))
177
108
  end
178
109
 
179
110
  should "update class attr with proxy if called last" do
180
111
  assert_equal({
181
112
  :class => 'list is good'
182
- }, subject.thing.awesome(:class => "list is").good.___attrs)
113
+ }, subject.thing.awesome(:class => "list is").good.instance_variable_get("@attrs"))
183
114
  end
184
115
 
185
116
  should "proxy mixed class and id selector attrs" do
@@ -189,11 +120,87 @@ class Undies::Element
189
120
  }, subject.thing1!.awesome({
190
121
  :class => "list is",
191
122
  :id => "thing2"
192
- }).good.thing3!.___attrs)
123
+ }).good.thing3!.instance_variable_get("@attrs"))
124
+ end
125
+
126
+ should "not proxy if private methods are called" do
127
+ assert_equal "<div />", subject.send(:start_tag)
128
+ assert_equal nil, subject.send(:end_tag)
193
129
  end
194
130
 
195
131
  end
196
132
 
133
+ class SerializeTests < BasicTests
134
+ before do
135
+ @output = Undies::Output.new(StringIO.new(@out = ""))
136
+ end
137
+
138
+ should "serialize with no child elements" do
139
+ src = Undies::Source.new do
140
+ element(:br)
141
+ end
142
+ templ = Undies::Template.new(src, {}, @output)
143
+ assert_equal "<br />", @out
144
+ end
145
+
146
+ should "serialize with attrs" do
147
+ src = Undies::Source.new do
148
+ element(:br, :class => 'big')
149
+ end
150
+ templ = Undies::Template.new(src, {}, @output)
151
+ assert_equal '<br class="big" />', @out
152
+ end
197
153
 
154
+ should "serialize with attrs and content" do
155
+ src = Undies::Source.new do
156
+ element(:strong, {:class => 'big'}) { __ "Loud Noises!" }
157
+ end
158
+ templ = Undies::Template.new(src, {}, @output)
159
+ assert_equal '<strong class="big">Loud Noises!</strong>', @out
160
+ end
161
+
162
+ should "serialize element proxy id call" do
163
+ src = Undies::Source.new do
164
+ element(:div).thing1! { _ "stuff" }
165
+ end
166
+ templ = Undies::Template.new(src, {}, @output)
167
+ assert_equal "<div id=\"thing1\">stuff</div>", @out
168
+ end
169
+
170
+ should "serialize element proxy class call" do
171
+ src = Undies::Source.new do
172
+ element(:div).thing { _ "stuff" }
173
+ end
174
+ templ = Undies::Template.new(src, {}, @output)
175
+ assert_equal "<div class=\"thing\">stuff</div>", @out
176
+ end
177
+
178
+ should "serialize content from separate content blocks" do
179
+ src = Undies::Source.new do
180
+ element(:div){ _ "stuff" }.thing1!{ _ " and more stuff" }
181
+ end
182
+ templ = Undies::Template.new(src, {}, @output)
183
+ assert_equal "<div id=\"thing1\">stuff and more stuff</div>", @out
184
+ end
185
+
186
+ should "serialize nested elements with pp" do
187
+ output = Undies::Output.new(StringIO.new(@out = ""), :pp => 4)
188
+ src = Undies::Source.new do
189
+ element(:div) {
190
+ element(:span) { _ "Content!" }
191
+ __ "Raw"
192
+ element(:span) { _ "More content" }
193
+ }
194
+ end
195
+ templ = Undies::Template.new(src, {}, output)
196
+ assert_equal "
197
+ <div>
198
+ <span>Content!</span>
199
+ Raw
200
+ <span>More content</span>
201
+ </div>", @out
202
+ end
203
+
204
+ end
198
205
 
199
206
  end
data/test/helper.rb CHANGED
@@ -1,2 +1,4 @@
1
- # this file is automatically required in when you require 'test_belt'
2
- # put test helpers here
1
+ # this file is automatically required in when you require 'assert' in your tests
2
+
3
+ # add root dir to the load path
4
+ $LOAD_PATH.unshift(File.expand_path("../..", __FILE__))
data/test/irb.rb CHANGED
@@ -1,10 +1,9 @@
1
- require 'test_belt'
1
+ require 'assert/setup'
2
2
 
3
3
  # this file is required in when the 'irb' rake test is run.
4
- # b/c 'test_belt' is required above, lib and test dirs will
5
- # be added to the LOAD_PATH and the test helper will be
4
+ # b/c 'assert' is required above, the test helper will be
6
5
  # required in.
7
6
 
8
7
  # put any IRB setup code here
9
8
 
10
- require 'undies'
9
+ require 'undies'