jig 0.1.0

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.
data/lib/jig/xml.rb ADDED
@@ -0,0 +1,320 @@
1
+ require 'jig'
2
+
3
+ class Jig
4
+ =begin rdoc
5
+ Jig::XML is a subclass of Jig designed to simplify construction of
6
+ strings containing XML text. Several class methods are defined to
7
+ construct standard XML elements. Unknown class method calls are
8
+ interpreted as XML element constructions:
9
+
10
+ j = Jig::XML.xml
11
+ j.inspect # => #<Jig: ["<?xml", " version=\"1.0\"", " ?>\n"]>
12
+ j.to_s # => <?xml version="1.0" ?>\n
13
+
14
+ j = Jig::XML.book(Jig::XMLtitle('War and Peace'))
15
+ j.inspect # => #<Jig: ["<book", nil, ">", "<title", nil, ">\n", "War and Peace", "</title>\n", "</book>\n"]>
16
+ j.to_s # => <book><title>\nWar and Peace</title>\n </book>
17
+
18
+ For most element constructors, arguments to the constructor are inserted as
19
+ the contents of the element. An optional block argument is inserted as a
20
+ proc. If there are no arguments and no block, the element is constructed
21
+ with the default gap, :___, as its contents. XML attributes are specified by
22
+ passing a Hash as the final argument. See the Attributes section below for
23
+ a detailed description of attribute processing.
24
+ Examples:
25
+
26
+ X = Jig::XHTML
27
+ puts X.span('some text') # <span>some text</span>
28
+
29
+ puts X.h3('rendered at: ') do # <h3>rendered at: Mon Apr 16 23:02:13 EDT 2007</h3>
30
+ Time.now
31
+ end
32
+
33
+ j = X.p
34
+ puts j # <p>\n</p>
35
+ puts j.plug('Four score...') # <p>Four score...\n</p>
36
+
37
+ =Attributes
38
+
39
+ A hash passed as a final argument to an XML element constructor is interpreted
40
+ as potential XML attributes for the element. Each key/value pair is considered
41
+ seperately. Pairs are processed as follows:
42
+ * a nil value causes the pair to be silently discarded
43
+ * a symbol value causes an attribute gap to be inserted into the XML element
44
+ * a gap value is inserted as is into the XML element attribute area, the key is discarded in this case (XXX)
45
+ * a proc is inserted as a deferred attribute
46
+ * a jig is inserted as deferred attribute
47
+ * any other value is inserted as an attribute with the value converted via #to_s
48
+
49
+ =Attribute Gaps
50
+
51
+ If an attribute gap remains unfilled it will not appear in the rendered jig. When
52
+ an attribute gap is filled, the result is processed as described for the key/value
53
+ pairs of a hash.
54
+
55
+ =Deferred Attributes
56
+
57
+ A deferred attribute is not finalized until the jig is rendered via #to_s. If a
58
+ deferred proc evaluates to nil, the attribute pair is silently discarded otherwise
59
+ the resulting value is converted to a string and an XML attribute is rendered.
60
+ A deferred jig is rendered and the result used as the XML attribute value.
61
+
62
+ X = Jig::XHTML
63
+ X.div('inner', :class => 'urgent') # => <div class="urgent">inner</div>
64
+ j = X.div('inner', :class => :class) # => <div>inner</div>
65
+ j.plug(:class, 'urgent') # => <div class="urgent">inner</div>
66
+ j.plug(:class, nil) # => <div>inner</div>
67
+
68
+ j = X.input(:type => :type) # => <input/>
69
+ j.plug(:type, "") # => <input type="" />
70
+ j.plug(:type, 'password') # => <input type="password" />
71
+
72
+ css = nil
73
+ j = X.div('inner', :class => proc { css }) # => <div>inner</div>
74
+ css = 'urgent'
75
+ j.to_s # => <div class="header">inner</div>
76
+
77
+ color = Jig.new('color: ', { %w{reb blue green}[rand(3)] }
78
+ j = X.div('inner', :style=> color) # => <div style="color: red">inner</div>
79
+ j.to_s # => <div style="color: green">inner</div>
80
+ =end
81
+ class XML < Jig
82
+ # Converts _hash_ into attribute value pairs and pushes them
83
+ # on the end of the jig.
84
+ def push_hash(hash)
85
+ push(*hash.map { |k,v| self.class.attribute(k, v) })
86
+ end
87
+ protected :push_hash
88
+
89
+ class <<self
90
+ # These elements will have newlines inserted into the default constructions to
91
+ # increase the readability of the generated XML.
92
+ Newlines = [:html, :head, :body, :title, :div, :p, :table, :script, :form]
93
+ Encode = Hash[*%w{& amp " quot > gt < lt}] # :nodoc:
94
+ Entities = Encode.keys.join # :nodoc:
95
+
96
+ # Prepare +aname+ and +value+ for use as an attribute pair in an XML jig:
97
+ # * If +value+ is nil or false, the empty string is returned.
98
+ # * If +value+ is a symbol, an attribute gap is returned.
99
+ # * If +value+ is a gap, the gap is returned.
100
+ # * If +value+ is a proc, method or jig, the construction of the attribute
101
+ # is deferred by wrapping it in a proc inside a jig.
102
+ # * Otherwise, +aname+ and +value+ are converted to strings and rendered as an XML
103
+ # attribute pair.
104
+ # Examples:
105
+ # attribute('value', :firstname) # => Gap.new(:firstname) {...}
106
+ # attribute('type', 'password') # => 'type="password"'
107
+ # attribute('type', nil) # => ''
108
+ # attribute('lastname', 'Einstein') # => 'lastname="Einstein"'
109
+ # a = attribute('lastname', Jig.new('Einstein')) # => #<Jig: [#<Proc:0x00058624>]>
110
+ # a.to_s # => 'lastname="Einstein"'
111
+ # b = attribute('lastname', Jig.new { }) # => #<Jig: [#<Proc:0x000523dc]>
112
+ # b.to_s # => ''
113
+ # c = attribute('lastname', Jig.new {""}) # => #<Jig: [#<Proc:0x00055a3c]>
114
+ # c.to_s # => 'lastname=""'
115
+ def attribute(aname, value)
116
+ case value
117
+ when nil, false
118
+ ""
119
+ when Symbol
120
+ Gap.new(value) { |fill| attribute(aname, fill) }
121
+ when Gap
122
+ value
123
+ when Proc, Method
124
+ Jig.new { attribute(aname, value.call) }
125
+ when Jig
126
+ Jig.new { attribute(aname, value.to_s) }
127
+ else
128
+ " #{aname}=\"#{value}\""
129
+ end
130
+ end
131
+
132
+ # In addition to the parsing done by Jig.parse, Jig::XML.parse recognizes and
133
+ # constructs attribute gaps from text of the form: %{=attribute,gapname=}
134
+ # Jig.parse("<input%{=type,itype} />").plug(:itype, 'password') # <input type="password" />
135
+ def parse(*)
136
+ super
137
+ end
138
+
139
+ # Returns a new string with <, >, and & converted to their HTML entity codes.
140
+ def escape(target)
141
+ new(target.to_s.gsub(/[#{Entities}]/) {|m| "&#{Encode[m]};" })
142
+ end
143
+
144
+ # Extend Jig.parse to recognize attribute gaps as %{=attrname,gapname=}.
145
+ # An attribute gap is returned.
146
+ def parse_other(delim, stripped)
147
+ if delim == '='
148
+ if stripped =~ /\A(.*),(.*)\z/
149
+ new({ $1 => $2.to_sym})
150
+ else
151
+ raise ArgumentError, "invalid gap syntax: #{quoted}"
152
+ end
153
+ else
154
+ super
155
+ end
156
+ end
157
+ private :parse_other
158
+
159
+ ATTRS = Gap::ATTRS # :nodoc:
160
+ ATTRS_GAP = Gap.new(ATTRS) { |h| h && h.map { |k,v| Jig::XML.attribute(k, v) } } # :nodoc:
161
+
162
+ Element_Cache = {} # :nodoc:
163
+ # Construct a generic XML element with two gaps:
164
+ # * +:__a+ filters a hash into an XML attribute list
165
+ # * +:___+ which is a default gap
166
+ # Jig::XML._element('div') # => #<Jig: ["<div", :"__a{}", ">\n", :___, "</div>\n"]>
167
+ def _element(tag)
168
+ Element_Cache[tag] ||= begin
169
+ whitespace = Newlines.include?(tag.to_sym) && "\n" || ""
170
+ new("<#{tag}".freeze, ATTRS_GAP, ">#{whitespace}".freeze, GAP, "</#{tag}>\n".freeze).freeze
171
+ end
172
+ end
173
+
174
+ # Construct a generic XML element with four gaps:
175
+ # * +:__a+ filters a hash into an XML attribute list
176
+ # * +:___+ which is a default gap
177
+ # * +tag+ which acts a placeholder for the element's opening and closing tag
178
+ # Jig::XML._element(:tag) # => #<Jig: ["<", :tag, :"__a{}", ">\n", :___, "</", :tag, ">\n"]>
179
+ def _anonymous(tag) # :nodoc:
180
+ whitespace = Newlines.include?(tag.to_sym) && "\n" || ""
181
+ new("<", tag.to_sym, ATTRS_GAP, ">#{whitespace}", GAP, "</", tag.to_sym, ">\n")
182
+ end
183
+
184
+ Empty_Element_Cache = {} # :nodoc:
185
+ # Construct an XML empty element with one gap:
186
+ # * +:__a+ filters a hash into an XML attribute list
187
+ # * +:___+ which is a default gap
188
+ # Jig::XML._element!('br') # => #<Jig: ["<br", :"__a{}", "/>"]>
189
+ def _element!(tag) # :nodoc:
190
+ Empty_Element_Cache[tag] ||= begin
191
+ new("<#{tag}".freeze, ATTRS_GAP, "/>\n".freeze).freeze
192
+ end
193
+ end
194
+
195
+ # Construct an HTML element using the method name as the element tag.
196
+ # If a method ends with '!', the element is constructed as an empty element.
197
+ # If a method ends with '?', the element is constructed as an anonymous element.
198
+ # If a method ends with '_', it is stripped and the result used as a the tag.
199
+ # If a method contains an '_', it is converted to a ':' to provide XML namespace tags.
200
+ # If a method contains an '__', it is converted to a single '_'.
201
+ #
202
+ # Jig::XML.div.to_s # => "<div></div>"
203
+ # Jig::XML.div_.to_s # => "<div></div>"
204
+ # Jig::XML.br!to_s # => <br />"
205
+ # Jig::XML.heading? # => Jig["<", :heading, ">", :___, "</", :heading, ">"]
206
+ # Jig::XML.xhtml_h1 # => "<xhtml:h1></xhtml:h1>"
207
+ # Jig::XML.xhtml__h1 # => "<xhtml_h1></xhtml_h1>"
208
+ def method_missing(symbol, *args, &block)
209
+ constructor = :element
210
+ text = symbol.to_s
211
+ if text =~ /!\z/
212
+ text.chop!
213
+ constructor = :element!
214
+ elsif text =~ /\?\z/
215
+ text.chop!
216
+ constructor = :anonymous
217
+ end
218
+ if text =~ /_$/ # alternate for clashes with existing methods
219
+ text.chop!
220
+ end
221
+ if text =~ /_/
222
+ # Single _ gets converted to : for XML name spaces
223
+ # Double _ gets converted to single _
224
+ text = text.gsub(/([^_])_([^_])/){|x| "#{$1}:#{$2}"}.gsub(/__/, '_')
225
+ end
226
+ send(constructor, text, *args, &block)
227
+ end
228
+
229
+ # Construct an anonymous XML element. The single argument provides a name for a
230
+ # gap that replaces the XML start and end tags. Use plug to replace the gaps
231
+ # with an actual tag.
232
+ #
233
+ # a = anonymous(:heading) # => #<Jig: ["<", :heading, ">", :___, "</", :heading, ">\n"]>
234
+ # b = a.plug(:heading, 'h1') # => #<Jig: ["<", "h1", ">", :___, "</", "h1", ">\n"]>
235
+ # b.plug('contents') # => #<Jig: ["<", "h1", ">", "contents", "</", "h1", ">\n"]>
236
+ def anonymous(tag='div', *args)
237
+ attrs = args.last.respond_to?(:fetch) && args.pop || nil
238
+ args.push(lambda{|*x| yield(*x) }) if block_given?
239
+ args.push GAP if args.empty?
240
+ _anonymous(tag).plug(ATTRS => attrs, GAP => args)
241
+ end
242
+
243
+ # Construct a standard XML element with +tag+ as the XML tag
244
+ # and a default gap for the contents of the element.
245
+ # Jig::XML.element('h1') # => #<Jig: ["<h1", ">", :___, "</h1>\n"]>
246
+ # Jig::XML.element('p', :class => 'body') # => #<Jig: ["<p", "class=\"body\"", ">", :___, "</h1>\n"]>
247
+ def element(tag='div', *args)
248
+ attrs = args.last.respond_to?(:fetch) && args.pop || nil
249
+ args.push(lambda(&Proc.new)) if block_given?
250
+ args.push GAP if args.empty?
251
+ _element(tag).plug(ATTRS => attrs, GAP => args)
252
+ end
253
+
254
+ # Construct a standard XML empty element with _tag_ as the XML tag.
255
+ #
256
+ # Jig::XHTML.element!('br') # => '<br />'
257
+ #
258
+ # h = { :name => 'year', :maxsize => 4, :type => :type }
259
+ #
260
+ # j = Jig::XHTML.element!('input', h) # => '<input name="year" maxsize="4"/>'
261
+ # j.plug(:type => 'hidden') # => '<input name="year" maxsize="4" type="hidden"/>'
262
+ def element!(tag, *args)
263
+ attrs = args.last.respond_to?(:fetch) && args.pop || nil
264
+ _element!(tag).plug(ATTRS => attrs, GAP => nil)
265
+ end
266
+
267
+ # Construct an XML declaration tag.
268
+ #
269
+ # Jig::XML.xml # => '<?xml version="1.0">'
270
+ # Jig::XML.xml(:lang => 'jp') # => '<?xml version="1.0" lang="jp">'
271
+ def xml(*args)
272
+ attrs = { :version => '1.0' }
273
+ attrs.merge!(args.pop) if args.last.respond_to?(:fetch)
274
+ args.push(lambda{|*x| yield(*x) }) if block_given?
275
+ new("<?xml", attrs, " ?>\n", *args)
276
+ end
277
+
278
+ Cache = {} # :nodoc:
279
+ # Construct a CDATA block
280
+ #
281
+ # Jig::XML.cdata('This data can have < & >')
282
+ #
283
+ # <![CDATA[
284
+ # This data can have < & > ]]>
285
+ def cdata(*args)
286
+ args.push(lambda{|*x| yield(*x) }) if block_given?
287
+ args.push GAP if args.empty?
288
+ jig = (Cache[:cdata] ||= new("<![CDATA[\n".freeze, GAP, " ]]>\n".freeze).freeze)
289
+ jig.plug(GAP, *args)
290
+ end
291
+
292
+ # Construct an XML comment element.
293
+ #
294
+ # Jig::XML.comment("This is a comment")
295
+ #
296
+ # \<!-- This is a comment -->
297
+ def comment(*args)
298
+ args.push(lambda{|*x| yield(*x) }) if block_given?
299
+ args.push GAP if args.empty?
300
+ jig = (Cache[:comment] ||= new("<!-- ".freeze, GAP, " -->\n".freeze).freeze)
301
+ jig.plug(GAP, *args)
302
+ end
303
+
304
+ # Construct a multiline XML comment element.
305
+ #
306
+ # Jig::XML.comment("first line\nsecond line")
307
+ #
308
+ # <!--
309
+ # first line
310
+ # second line
311
+ # -->
312
+ def comments(*args)
313
+ args.push(lambda{|*x| yield(*x) }) if block_given?
314
+ args.push GAP if args.empty?
315
+ args.push "\n"
316
+ comment("\n", *args)
317
+ end
318
+ end # class <<self
319
+ end # class XML
320
+ end # module Jig
data/test/test_css.rb ADDED
@@ -0,0 +1,143 @@
1
+
2
+ require 'jig'
3
+ require 'test/unit'
4
+ require 'test/jig'
5
+
6
+ CSS = Jig::CSS
7
+
8
+ class CSS
9
+ class TestCSS < Test::Unit::TestCase
10
+ include Asserts
11
+ def setup
12
+ @div = CSS.new('div')
13
+ @gaps = [:__s, :__ds, :__de]
14
+ end
15
+ def test_default
16
+ assert_as_string('* {}', CSS.new, 'no selector')
17
+ assert_equal(@gaps, CSS.new.gaps, 'two gaps with new rule')
18
+ end
19
+
20
+ def test_new
21
+ assert_as_string('div {}', CSS.new('div'), 'type selector')
22
+ assert_as_string('div p {}', CSS.new('div p'), 'string as selector')
23
+ assert_equal(@gaps, CSS.new('div').gaps, 'selector and declarations gaps available')
24
+ end
25
+
26
+ def test_declarations
27
+ assert_as_string('div {color: red; }', CSS.new('div', :color => 'red'), 'explicit plist')
28
+ red = CSS.new('div') | {:color => 'red'}
29
+ assert_as_string('div {color: red; }', red, 'added plist')
30
+ assert_equal(@gaps, red.gaps, 'plist leaves gaps')
31
+ background, color = 'background: olive; ', 'color: red; '
32
+ assert_as_string(/div \{(#{color}#{background}|#{background}#{color})\}/,
33
+ @div | {:color => 'red', :background => 'olive'},
34
+ 'added declarations')
35
+ assert_as_string('div {color: red; background: olive; }',
36
+ @div | {:color => 'red'} | {:background => 'olive'},
37
+ 'two declarations')
38
+
39
+ blue = CSS.new('div') | {:color => 'blue'}
40
+ assert_as_string('div {color: blue; }', blue, 'declaration list merge via &')
41
+ end
42
+
43
+ def test_open?
44
+ assert(CSS.div.open?, 'gaps remain')
45
+ assert(CSS.div.plug.closed?, 'no gaps')
46
+ assert(!CSS.div.plug.open?, 'no gaps')
47
+ end
48
+
49
+ def test_method_missing
50
+ assert_as_string('div {}', CSS.div, 'unknown method generates type selector')
51
+ assert_equal(@gaps, CSS.div.gaps, 'unknown method generates new jig')
52
+ end
53
+
54
+ def test_universal_selector
55
+ assert_as_string('* {}', CSS.new, 'universal selector')
56
+ end
57
+
58
+ def test_descendent_combinator
59
+ assert_as_string('h1 li {}', CSS.h1 >> CSS.li , 'descendent combinator')
60
+ end
61
+
62
+ def test_child_combinator
63
+ assert_as_string('div > h1 {}', CSS.div > CSS.h1, 'child combinator')
64
+ end
65
+
66
+ def test_sibling_combinator
67
+ assert_as_string('div + h1 {}', CSS.div + CSS.h1, 'sibling combinator')
68
+ end
69
+
70
+ def test_id_selector
71
+ assert_as_string('div#home {}', CSS.div * 'home', 'id selector')
72
+ end
73
+
74
+ def test_pseudo_selector
75
+ assert_as_string('div:home {}', CSS.div/'home', 'pseudo selector')
76
+ end
77
+
78
+ def test_class_selector
79
+ assert_as_string('h1.urgent {}', CSS.h1.urgent, 'class selector')
80
+ end
81
+ def test_attribute_selector
82
+ assert_as_string('h1[class] {}', CSS.h1['class'], 'attribute selector')
83
+ end
84
+ def test_exact_attribute_selector
85
+ assert_as_string('h1[class="urgent"] {}', CSS.h1['class' => "urgent"], 'exact attribute selector')
86
+ end
87
+ def test_partial_attribute_selector
88
+ assert_as_string('h1[class~="urgent"] {}', CSS.h1['class' => /urgent/], 'partial attribute selector')
89
+ end
90
+ def test_language_attribute_selector
91
+ assert_as_string('h1[lang|="lc"] {}', CSS.h1[:lang => /lc/], 'language attribute selector')
92
+ end
93
+
94
+ def test_selector_list
95
+ assert_as_string('h1, h2 {}', CSS.h1.merge(CSS.h2), 'selector list')
96
+ assert_as_string('h1, h2 {}', CSS.h1 | CSS.h2, 'selector list operator')
97
+ assert_as_string('h1, h2, h3 {}', CSS.h1.merge(CSS.h2).merge(CSS.h3), 'selector list')
98
+ assert_as_string('*, h1 {}', CSS.new | CSS.h1, 'adding to the default rule')
99
+ end
100
+
101
+ def test_units
102
+ units = [:in, :cm, :mm, :pt, :pc, :em, :ex, :px]
103
+ units.each {|u| assert_equal("1#{u}", 1.send(u)) }
104
+ assert_equal("50%", 50.pct)
105
+ assert_equal("50.00%", 0.5.pct)
106
+ assert_equal("99.99%", 0.9999.pct)
107
+ end
108
+
109
+ def test_declarations_merge
110
+ div = CSS.div
111
+ h1 = CSS.h1(:color => 'red')
112
+ result = div * h1
113
+ args = [[:>>, ' ', h1], [:>, ' > ', h1], [:+, ' + ', h1]]
114
+ args.each { |op1, text, arg2|
115
+ assert_as_string("div#{text}h1 {color: red; }", div.send(op1, arg2))
116
+ }
117
+ args = [[:*, '#', 'header'], [:/, ':', 'first-child']]
118
+ args.each { |op1, text, arg2|
119
+ assert_as_string("div#{text}#{arg2} {}", div.send(op1, arg2))
120
+ }
121
+ assert_as_string("div[onclick] {color: red; }", div['onclick'] |{:color => 'red'})
122
+ assert_as_string("div[onclick] {color: red; background: blue; }", div['onclick'].|(:color => 'red')|{:background => 'blue'})
123
+ end
124
+
125
+ def test_extract_selector
126
+ assert_as_string("div", CSS.div.selector)
127
+ assert_as_string("div, h1", (CSS.div | CSS.h1).selector)
128
+ end
129
+
130
+ def test_extract_declarations
131
+ assert_as_string("", CSS.div.declarations)
132
+ assert_as_string("color: red; ", (CSS.div |{'color' => 'red'}).declarations)
133
+ end
134
+
135
+ def test_declartion_with_gaps
136
+ redgap = CSS.div | {'color' => :red }
137
+ assert(redgap.gaps.include?(:red))
138
+ assert_as_string('div {}', redgap , 'gap for property value')
139
+ assert_as_string('div {color: red; }', redgap.plug(:red, 'red'), 'plug gap')
140
+ end
141
+
142
+ end
143
+ end