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/History.txt ADDED
@@ -0,0 +1,5 @@
1
+ == 0.1.0 / 2007-05-17
2
+ * Initial Release
3
+ * basic jig support
4
+ * XML, XHMTL support
5
+ * CSS support (experimental)
data/Manifest.txt ADDED
@@ -0,0 +1,12 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ lib/jig.rb
6
+ lib/jig/css.rb
7
+ lib/jig/xhtml.rb
8
+ lib/jig/xml.rb
9
+ test/test_css.rb
10
+ test/test_jig.rb
11
+ test/test_xhtml.rb
12
+ test/test_xml.rb
data/README.txt ADDED
@@ -0,0 +1,114 @@
1
+ Jig
2
+ by Gary R. Wright
3
+ (jig.rubyforge.org)
4
+
5
+ == DESCRIPTION:
6
+
7
+ A jig is a data structure designed to facilitate construction and
8
+ manipulation of strings with an inherent hierarchical syntax. The
9
+ idea is derived from the <bigwig> project (http://www.brics.dk/bigwig/)
10
+ and in particular the XML templating constructs described in the
11
+ paper: A Type System for Dynamic Web Documents
12
+ (http://www.brics.dk/bigwig/publications/dyndoc.pdf). The name is
13
+ derived from woodworking where a jig is a template designed to guide
14
+ other tools.
15
+
16
+ A jig is an ordered sequence of objects (usually strings) and named
17
+ _gaps_. When rendered as a string by Jig#to_s, the objects are
18
+ rendered by calling #to_s on each object in order. The gaps are skipped.
19
+
20
+ A new jig may be constructed from an existing jig by 'plugging' one
21
+ or more of the named gaps. The new jig shares the objects and their
22
+ ordering from the original jig but with the named gap replaced with
23
+ the 'plug'. Gaps may be plugged by any object or sequence of
24
+ objects. When a gap is plugged with another jig, the contents
25
+ (including gaps) are incorporated into the new jig.
26
+
27
+ In addition to strings and gaps, a Jig may contain a proc object
28
+ (or lambda or method). Procs within a jig are not evaluated until
29
+ the jig is rendered as a string by #to_s.
30
+
31
+ Several subclasses (Jig::XML, Jig::XHTML, Jig::CSS) are defined to
32
+ help in the construction of XML, XHTML, and CSS documents.
33
+
34
+ This is a jig with a single gap named <tt>:alpha</tt>
35
+ Jig.new(:alpha) # => <#Jig: [:alpha]>
36
+ This is a jig with two objects, 'before' and 'after' separated by
37
+ a gap named :middle.
38
+ j = Jig.new('before', :middle, 'after) # => #<Jig: ["before", :middle, "after"]>
39
+ The plug operation derives a new jig from the old jig.
40
+ j.plug(:middle, ", during, and") # => #<Jig: ["before", ", during, and ", "after"]>
41
+ This operation doesn't change j. It can be used again:
42
+ j.plug(:middle, " and ") # => #<Jig: ["before", " and ", "after"]>
43
+ There is a destructive version of plug that modifies
44
+ the jig in place:
45
+ j.plug!(:middle, "filled") # => #<Jig: ["before", "filled", "after"]>
46
+ j # => #<Jig: ["before", "filled", "after"]>
47
+ There are a number of ways to construct a Jig and many of
48
+ them insert an implicit gap into the Jig. This gap is
49
+ identified as <tt>:___</tt> and is used as the default gap
50
+ for plug operations when one isn't provided:
51
+
52
+ puts Jig.new("A", :___, "C").plug("B") # => ABC
53
+ puts Jig.new.plug('filled') # => filled
54
+
55
+ The % operator is an alias for Jig#plug
56
+
57
+ name = Jig.new(:last, ", ", :first)
58
+ data = {:first => 'Gary', :last => 'Wright')
59
+ puts name % data # => Wright, Gary
60
+
61
+ In order to make Jig's more useful for XML generation,
62
+ the Jig::XML class supports a variety of convenience methods;
63
+
64
+ X = Jig::XML
65
+ puts b = X.element("body") # => <body></body>
66
+ puts b.plug("text") # => <body>text</body>
67
+
68
+ Method missing makes this even simpler:
69
+
70
+ b = HT.span
71
+ puts b.plug("text") # => <span>text</span>
72
+
73
+ Attributes can be specified with a hash:
74
+
75
+ summary = X.p(:class => "summary")
76
+ puts summary.plug("This is a summary") # => <p class="summary">This is a summary</p>
77
+
78
+ See the Jig::XML for more information about attributes and gaps.
79
+ The Jig::XHTML class has a variety of convenience methods for
80
+ constructing XHTML documents. The Jig::CSS class generates jigs
81
+ that represent CSS rules.
82
+
83
+ == REQUIREMENTS:
84
+
85
+ * Ruby 1.8
86
+
87
+ == INSTALL:
88
+
89
+ * sudo gem install jig
90
+
91
+ == LICENSE:
92
+
93
+ (The MIT License)
94
+
95
+ Copyright (c) 2007 Gary R. Wright
96
+
97
+ Permission is hereby granted, free of charge, to any person obtaining
98
+ a copy of this software and associated documentation files (the
99
+ 'Software'), to deal in the Software without restriction, including
100
+ without limitation the rights to use, copy, modify, merge, publish,
101
+ distribute, sublicense, and/or sell copies of the Software, and to
102
+ permit persons to whom the Software is furnished to do so, subject to
103
+ the following conditions:
104
+
105
+ The above copyright notice and this permission notice shall be
106
+ included in all copies or substantial portions of the Software.
107
+
108
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
109
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
110
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
111
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
112
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
113
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
114
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,39 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require 'spec/rake/spectask'
6
+ require './lib/jig.rb'
7
+
8
+ Hoe.new('jig', Jig::VERSION) do |p|
9
+ p.rubyforge_name = 'jig'
10
+ p.author = ["Gary Wright"]
11
+ p.email = 'gwright@rubyforge.org'
12
+ p.summary = 'a data structure that supports construction and manipulation of strings with hierarchical structure'
13
+ p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
14
+ p.url = "http://jig.rubyforge.org"
15
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
16
+ end
17
+
18
+ task :spec do
19
+ sh "spec -f s #{FileList['test/*spec.rb']}"
20
+ end
21
+
22
+ desc "Run rcov with rSpec"
23
+ Spec::Rake::SpecTask.new('rcov-rspec') do |t|
24
+ t.spec_files = FileList['test/*spec.rb']
25
+ t.rcov = true
26
+ end
27
+
28
+ # RCOV command, run as though from the commandline. Amend as required or perhaps move to config/environment.rb?
29
+ RCOV = "rcov"
30
+
31
+ desc "generate a unit test coverage report in coverage/unit; see coverage/unit/index.html afterwards"
32
+ task :rcov do
33
+ tests = FileList['test/test_*.rb']
34
+ sh "#{RCOV} --xref -Ilib #{tests}"
35
+ end
36
+
37
+
38
+
39
+ # vim: syntax=Ruby
data/lib/jig.rb ADDED
@@ -0,0 +1,903 @@
1
+
2
+ =begin rdoc
3
+ A jig is an ordered sequence of objects and named gaps. During construction,
4
+ a gap is represented by a symbol:
5
+
6
+ Jig.new('left', :middle, 'right') # => <#Jig: ["left", :middle, "right"]>
7
+ Jig.new(1, :two, 3.0, "4") # => <#Jig: [1, :two, 3.0, "4"]>
8
+ Jig.new(lambda { rand(6) }) # => <#Jig: [#<Proc:0x00437ee8>]
9
+
10
+ As a convenience, a block provided to Jig::new is added to the sequence as a proc:
11
+
12
+ Jig.new { rand(6) } # => #<Jig: [#<Proc:0x00026660@-:2>]>
13
+
14
+ Jig#[] also constructs new jig instances:
15
+
16
+ Jig[:header, :body, :footer] # [:header, :body, :footer]
17
+
18
+ At any time, a jig can be converted to a string by Jig#to_s. The string is
19
+ constructed by concatenating string representations of the objects in the jig as follows:
20
+ - strings are concatenated as-is
21
+ - gaps are skipped
22
+ - procs are evaluated, the results converted to a string, and concatenated
23
+ - other objects are converted by calling _to_s_ and the resulting string is concatenated
24
+
25
+ A variety of operations are provided to manipulate jigs but the
26
+ most common is the 'plug' operation:
27
+
28
+ comment = Jig.new("/*", :comment, "*/")
29
+ partial = comment.plug(:comment, "This is a ", :adjective, " comment")
30
+ puts partial.plug(:adjective, 'silly') # => /* This is a silly comment */
31
+ puts partial.plug(:adjective, 'boring') # => /* This is a boring comment */
32
+
33
+ The plug method constructs a new jig that shares all the contents of the
34
+ previous jig but with the named gap replaced by one or more objects.
35
+ If there are more than one gaps with the same name, they are all
36
+ replaced with the same sequence of objects.
37
+
38
+ j = Jig.new("first", :separator, "middle", :separator, "after")
39
+ puts j.plug(:separator, '/') # => "first/middle/last"
40
+ =end
41
+ class Jig
42
+ VERSION = '0.1.0'
43
+ autoload :XML, "jig/xml"
44
+ autoload :XHTML, "jig/xhtml"
45
+ autoload :CSS, "jig/css"
46
+
47
+ # A Gap represents a named position within the ordered sequence of objects
48
+ # stored in a jig. In addition to a name, a gap can also have an associated
49
+ # filter. When a gap is filled by a plug operation, the replacement items are
50
+ # passed to the filter and the return value(s) are used to fill the gap.
51
+ # The default filter simply returns the unchanged list of items.
52
+ class Gap
53
+ ATTRS = :__a
54
+ GAP = :___
55
+
56
+ # the name associated with the gap
57
+ attr :name
58
+
59
+ # the lambda associated with the gap
60
+ attr :filter
61
+
62
+ # Construct a new gap with the specified name. A block, if given, becomes
63
+ # the filter for replacement items.
64
+ def initialize(name=GAP, &filter)
65
+ @name = name.to_sym
66
+ @filter = filter && lambda(&filter)
67
+ end
68
+
69
+ def inspect
70
+ "#<Gap: [#{name}, #{filter.inspect}]>"
71
+ end
72
+
73
+ # Pass the replacement items through the filter.
74
+ def fill(*filling)
75
+ return *(filter && filter[*filling] || filling)
76
+ end
77
+
78
+ # Two gaps are equal if they have the same name and
79
+ # use the same filter.
80
+ def ==(other)
81
+ name == other.name && filter == other.filter
82
+ end
83
+
84
+ # Change the name of the gap.
85
+ def rename(name)
86
+ @name = name.to_sym
87
+ self
88
+ end
89
+
90
+ # Construct a new gap, _name.. This gap will try to re-format lines of
91
+ # text to a maximum of _width_ columns.
92
+ # ten = Jig.new(Jig::Wrap.new(10))
93
+ # puts ten.plug("this is ok") # => "this is ok"
94
+ # puts ten.plug("this will be split") # => "this will\nbe split"
95
+ def self.wrap(width=72, name=GAP)
96
+ new(name) do |plug|
97
+ # From James Edward Gray II's entry to Ruby Quiz #113 [ruby-talk:238693]
98
+ plug.to_s.sub("\n"," ").strip.gsub(/(.{1,#{width}}|\S{#{width+1},})(?: +|$\n?)/, "\\1\n").chomp
99
+ end
100
+ end
101
+
102
+ # Construct a new gap, _name_. This gap will try to re-format lines of
103
+ # text into a single or multi-line comment block with each line of text
104
+ # limited to _width_ columns.
105
+ #
106
+ # If _prefix_ is provided, each line of text will be prefixed accordingly.
107
+ # If _open_ is provided, a single line of text will be wrapped with the _open_
108
+ # and _close_ strings but multiple lines of text will be formatted as a block
109
+ # comment. If _close_ is not provided it is taken to be the <i>open.reverse</i>.
110
+ # Jig[Jig::Gap.comment] # text reformated to 72 columns
111
+ # Jig[Jig::Gap.comment("# ")] # text reformated as Ruby comments
112
+ # Jig[Jig::Gap.comment("// ")] # text reformated as Javascript comments
113
+ # Jig[Jig::Gap.comment(" *", "/* ")] # text reformated as C comments
114
+ # Jig[Jig::Gap.comment(" ", "<-- ", " -->")] # text reformated as XML comments
115
+ #
116
+ # If the default gap name isn't appropriate you must fill in all the arguments:
117
+ # Jig[Jig::Gap.comment("# ", nil, nil, 72, :alternate)] # alternate gap name
118
+ def self.comment(prefix="", open=nil, close=nil, width=72, name=GAP)
119
+ wrap = Jig[Jig::Gap.wrap(width, name)]
120
+ if open
121
+ close ||= open.reverse
122
+ block_line = Jig.new(prefix, " ", GAP, "\n")
123
+ one_line = Jig.new(open, GAP, close, "\n")
124
+ block = Jig.new(open, "\n", GAP, close, "\n")
125
+ else
126
+ one_line = Jig.new(prefix, GAP, "\n")
127
+ block_line = one_line
128
+ block = Jig.new
129
+ end
130
+ new(name) do |plug|
131
+ text = (wrap % plug.to_s).to_s
132
+ if text.index("\n")
133
+ block % (block_line * text.split(/\n/))
134
+ else
135
+ block % (one_line % text)
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ # :section: Construction
142
+
143
+ DEFAULT_GAP = Gap.new
144
+ GAP = DEFAULT_GAP.name
145
+
146
+ # the sequence of objects
147
+ attr_accessor :contents
148
+ protected :contents=
149
+ # the unplugged gaps
150
+ attr_accessor :rawgaps
151
+ protected :rawgaps=
152
+
153
+ class <<self
154
+ alias [] :new
155
+
156
+ # Construct a null jig. A null jig has no contents and no gaps.
157
+ # It can be considered analogous to an empty array or a null string.
158
+ # jigs = (1..3).map { |i| Jig[i, :___, i] }
159
+ # aggregate = jigs.inject(Jig.null) { |s,j| s + j }
160
+ # puts aggregate.plug('x') # 1x12x23x3
161
+ def null
162
+ new(nil)
163
+ end
164
+
165
+ # Convert a string into a jig. The string is scanned for blocks deliminated by %{...}.
166
+ # The blocks are interpreted as follows:
167
+ # %{:identifier:} is converted into a gap named *identifier*
168
+ # %{!code!} is converted to a lambda
169
+ #
170
+ # Code blocks are interpreted when the resulting jig is rendered via Jig#to_s.
171
+ # Each time parse is called, an anonymous module is created to evaluate *all* the
172
+ # code blocks created during that call to parse. Alternatively, the code blocks can
173
+ # be evaluated against an explicit binding passed as the second argument.
174
+ #
175
+ # Jig.parse("abc").to_s # abc
176
+ # Jig.parse("1 %{:x} 3") # Jig[1, :x, 3]
177
+ # Jig.parse("1 %{:x} 3") # Jig[1, :x, 3]
178
+ #
179
+ # a = 5
180
+ # Jig.parse("%{a + 1}", binding).to_s # 6
181
+ # Jig.parse("%{b + 1}").to_s # NameError
182
+ #
183
+ # class A
184
+ # def to_jig
185
+ # Jig.parse("secret: %{secret}", binding)
186
+ # end
187
+ # def secret
188
+ # "xyzzy"
189
+ # end
190
+ # private :secret
191
+ # end
192
+ #
193
+ # A.new.secret # NoMethodError
194
+ # A.new.to_jig.to_s # secret: xyzzy
195
+ def parse(string=nil, context=nil)
196
+ wrapper = context || Module.new.class_eval { binding }
197
+ raw = string.scan(/(.*?)(%\{(.)(.*?)\3\}|\z)/).inject([]) { |list, (before, quoted, delim, stripped)|
198
+ list << before unless before.empty?
199
+ case delim
200
+ when ':'
201
+ list << stripped.to_sym
202
+ when '!'
203
+ list << eval("lambda {#{stripped}}", wrapper)
204
+ when nil,''
205
+ list
206
+ else
207
+ list << parse_other(delim, stripped)
208
+ end
209
+ }
210
+ Jig.new(*raw)
211
+ end
212
+
213
+ def parse_other(delim, stripped)
214
+ raise ArgumentError, "invalid delimiter: \"#{delim}\""
215
+ end
216
+ private :parse_other
217
+ end
218
+
219
+ # Construct a jig from the list of _items_. Symbols in the list are
220
+ # replaced with a gap named by the symbol.
221
+ #
222
+ # j1 = Jig.new('first', :middle, 'last') # => #<Jig: ['first', :middle, 'last']
223
+ # j1.gaps # => [:middle]
224
+ #
225
+ # If a block is provided, it is appended as a proc to the list of items.
226
+ # Procs within a jig are not evaluated until the jig is rendered as a
227
+ # string by to_s.
228
+ #
229
+ # i = 0
230
+ # j = Jig.new("i is ") { i }
231
+ # puts j # => "i is 0"
232
+ # i = 1
233
+ # puts j # => "i is 1"
234
+ #
235
+ # If no arguments are given and no block is given, the jig is constructed
236
+ # with a single default gap named <tt>:___</tt> (also known as Jig::GAP).
237
+ # one_gap = Jig.new
238
+ # one_gap.gaps # => [:___]
239
+ def initialize(*items, &block)
240
+ @contents = [[]]
241
+ @rawgaps = []
242
+ items.push(block) if block
243
+ items.push(DEFAULT_GAP) if items.empty?
244
+ concat(items)
245
+ end
246
+
247
+ # The internal structure of a jig is duplicated on #dup or #clone, but
248
+ # not the objects that make up the contents of the jig. This is analogous
249
+ # to how an array is duplicated.
250
+ def initialize_copy(other)
251
+ super
252
+ @contents = other.contents.dup
253
+ @rawgaps = other.rawgaps.dup
254
+ end
255
+
256
+ # :section: Equality
257
+ # This section describes methods for comparing jigs.
258
+ # [<tt>j1.equal?(j2)</tt>] identity, same objects
259
+ # [<tt>j1.eql?(j2)</tt>] equivalent, same class and structure
260
+ # [<tt>j1 == j2</tt>] similar, jigs with same structure
261
+ # [<tt>j1 =~ j2</tt>] text representations are equal
262
+ #
263
+ # a = Jig.new(:alpha, 1, :beta)
264
+ # b = Jig.new(:alpha, 1, :beta)
265
+ # a.equal?(b) # false
266
+ # a.eql?(b) # true
267
+ #
268
+ # c = Class.new(Jig).new(:alpha, 1, :beta)
269
+ # a.eql?(c) # false
270
+ # a == c # true
271
+ # a =~ c # true
272
+ # a.to_s == c.to_s # true
273
+ #
274
+ # d = Class.new(Jig).new(:beta, 1, :alpha)
275
+ # a.eql?(d) # false
276
+ # a == d # false
277
+ # a =~ d # true
278
+ # a.to_s == d.to_s # true
279
+
280
+ # Returns true if the two jigs are instances of the same class and have
281
+ # equal gap lists and contents (via Array#eql?).
282
+ # Jigs that are not equal may still have the same string representation.
283
+ # Procs are not evaluated by Jig#eql?.
284
+ def eql?(rhs)
285
+ self.class == rhs.class &&
286
+ contents.zip(rawgaps).flatten.eql?(rhs.contents.zip(rhs.rawgaps).flatten)
287
+ end
288
+
289
+ # Returns true if +rhs+ is an instance of Jig or one of Jig's subclasses and
290
+ # the two jigs have equal gap lists and contents (via Array#==).
291
+ # Procs are not evaluated by Jig#==.
292
+ def ==(rhs)
293
+ Jig === rhs &&
294
+ contents.zip(rawgaps).flatten == rhs.contents.zip(rhs.rawgaps).flatten
295
+ end
296
+
297
+ # Returns true if the string representation of the jig matches the
298
+ # +rhs+ using String#=~. Procs are evaluated by Jig#=~.
299
+ # Jig.new("chitchat") =~ Jig.new("chit", "chat") # => true
300
+ # Jig.new("chitchat") =~ Jig.new("chit", :gap, "chat") # => true
301
+ # Jig.new("chitchat") =~ /chit/ # => true
302
+ # Jig.new(1,:a,2) =~ Jig.new(1, 2, :a) # => true
303
+ def =~(rhs)
304
+ if Regexp === rhs
305
+ to_s =~ rhs
306
+ else
307
+ to_s == rhs.to_s
308
+ end
309
+ end
310
+
311
+ # Returns true if the string representation of the jig matches the
312
+ # </tt>rhs.to_str</tt> using String#===. Procs are evaluated by Jig#====.
313
+ def ===(rhs)
314
+ to_s =~ rhs.to_str
315
+ end
316
+
317
+ # :section: Reflection
318
+ # This section describes methods that query the state of a jig.
319
+
320
+ # The inspect string for a jig is an array of objects with gaps
321
+ # represented by symbols. Gaps with associated filters are shown
322
+ # with trailing braces (:gap{}).
323
+ # Jig.new.inspect # => #<Jig: [:___]>
324
+ # Jig.new(1,:a,2).inspect # => #<Jig: [1, :a, 2]>
325
+ # Jig.new(Gap.new(:example) { |x| x.to_s.reverse }) # => #<Jig: [:example{}]>
326
+ def inspect
327
+ info = rawgaps.map {|g| g.filter && "#{g.name}{}".to_sym || g.name }
328
+ "#<Jig: #{contents.zip(info).flatten[0..-2].inspect}>"
329
+ end
330
+
331
+ # Returns true if the jig has no gaps.
332
+ # Jig.new.closed? # false
333
+ # Jig.new('a').closed? # true
334
+ # Jig.new.plug('a').closed? # true
335
+ def closed?
336
+ rawgaps.empty?
337
+ end
338
+
339
+ # Returns true if the jig has any gaps.
340
+ # Jig.new.open? # true
341
+ # Jig.new('a').open? # false
342
+ # Jig.new.plug('a').open? # false
343
+ def open?
344
+ not rawgaps.empty?
345
+ end
346
+
347
+ # Returns true if the jig has no gaps and renders as the empty string.
348
+ # This method will cause proc objects within the jig to be evaluated.
349
+ # Jig.new.null? # false
350
+ # Jig.new(nil).null? # true
351
+ # Jig.new.plug("").null? # true
352
+ def null?
353
+ closed? && to_s.empty?
354
+ end
355
+
356
+ # Returns an array containing the names, in order, of the gaps in
357
+ # the current jig. A name may occur more than once in the list.
358
+ # Jig.new.gaps # => [:___]
359
+ # Jig.new(:a, :b).gaps # => [:a, :b]
360
+ # Jig.new(:a, :b).plug.gaps # => []
361
+ def gaps
362
+ rawgaps.map { |g| g.name }
363
+ end
364
+
365
+ # Returns true if the named gap appears in the jig.
366
+ # Jig.new.has_gap? :___ # => true
367
+ # Jig.new.plug.has_gap? :___ # => false
368
+ def has_gap?(name)
369
+ rawgaps.find {|g| g.name == name }
370
+ end
371
+
372
+ # Returns the position of the first gap with the given name
373
+ # or nil if a gap is not found. See slice for a description
374
+ # of the indexing scheme for jigs.
375
+ # Jig.new.index(:___) # => 1
376
+ # Jig.new.index(:a) # => nil
377
+ # Jig.new(:a,:b).index(:b) # => 3
378
+ def index(name)
379
+ rawgaps.each_with_index {|g,i| return (i*2)+1 if g.name == name }
380
+ nil
381
+ end
382
+
383
+ # Returns self.
384
+ # j = Jig.new
385
+ # j.equal?(j.to_jig) # => true
386
+ def to_jig
387
+ self
388
+ end
389
+
390
+ # :section: Operations
391
+ # This section describes methods that perform operations on jigs,
392
+ # usually resulting in a new jig instance.
393
+
394
+ # call-seq:
395
+ # slice(position) -> jig
396
+ # slice(range) -> jig
397
+ # slice(start, len) -> jig
398
+ #
399
+ # Extracts parts of a jig. The indexing scheme for jigs
400
+ # accounts for contents and gaps as follows:
401
+ #
402
+ # 1 3 <- gaps
403
+ # +----+----+----+
404
+ # | | | |
405
+ # +----+----+----+
406
+ # 0 2 4 <- contents
407
+ #
408
+ # Each indexible element of the contents is itself a list
409
+ # of zero or more objects. A jig with n gaps will always have
410
+ # n + 1 content lists.
411
+ #
412
+ # When called with a single integer (pos), slice returns the
413
+ # indexed item (a gap or a content list) as a jig.
414
+ #
415
+ # j = Jig.new(0, :alpha, 'z')
416
+ # j.slice(0) # => #<Jig: [0]>
417
+ # j.slice(1) # => #<Jig: [:alpha]>
418
+ # j.slice(2) # => #<Jig: ['z']>
419
+ #
420
+ # When called with a range or a start position and length,
421
+ # slice extracts the indexed items and returns them as a new jig.
422
+ #
423
+ # j = Jig.new(0, :alpha, 'z')
424
+ # j.slice(0..1) # => #<Jig: [0, :alpha]>
425
+ # j.slice(1..2) # => #<Jig: [:alpha, 'z']>
426
+ # j.slice(0,1) # => #<Jig: [0]>
427
+ # j.slice(0,2) # => #<Jig: [0, :alpha]>
428
+ #
429
+ # Negative array indices are respected:
430
+ #
431
+ # j = Jig.new(0, :alpha, 'z')
432
+ # j.slice(-1) # => #<Jig: ['z']>
433
+ # j.slice(-2..-1) # => #<Jig: [:alpha, 'z']>
434
+ # j.slice(-2, 1) # => #<Jig: [:alpha]>
435
+ def slice(index, len=1)
436
+ if Range === index
437
+ if index.exclude_end?
438
+ first, last = index.begin, index.end - 1
439
+ else
440
+ first, last = index.begin, index.end
441
+ end
442
+ else
443
+ first, last = index, index + len - 1
444
+ end
445
+
446
+ # Adjust for negative indices.
447
+ first = 2*contents.size + first - 1 if first < 0
448
+ last = 2*contents.size + last - 1 if last < 0
449
+ first_adjust, last_adjust = first % 2, last % 2
450
+
451
+ j = Jig.new
452
+ j.rawgaps = rawgaps[((first - first_adjust)/2)...((last + last_adjust)/2)]
453
+ j.contents = contents[((first + first_adjust)/2)..((last - last_adjust)/2)]
454
+
455
+ j.contents.unshift([]) if first_adjust.nonzero?
456
+ j.contents.push([]) if last_adjust.nonzero?
457
+ if !j.contents or !j.rawgaps
458
+ raise ArgumentError, "index #{index} out of range"
459
+ end
460
+ j
461
+ end
462
+ alias [] :slice
463
+
464
+ # call-seq:
465
+ # jig * count -> a_jig
466
+ # jig * array -> a_jig
467
+ #
468
+ # With an integer argument, a new jig is constructed by concatenating
469
+ # _count_ copies of _self_.
470
+ # three = Jig.new * 3 # => Jig[:___, :___, :___]
471
+ # puts three.plug('3') # => "333"
472
+ #
473
+ # With an array argument, the elements of the array are used to plug
474
+ # the default gap. The resulting jigs are concatenated
475
+ # to form the final result:
476
+ # require 'yaml'
477
+ # item = Jig["- ", :___, "\n"] # => #<Jig: ["- ", :___, "\n"]>
478
+ # list = item * [1,2,3] # => #<Jig: ["- ", 1, "\n", "- ", 2, "\n", "- ", 3, "\n"]>
479
+ # puts list # => "- 1\n- 2\n- 3\n"
480
+ # puts YAML.load(list.to_s) # => [1, 2, 3]
481
+ def mult(rhs)
482
+ case rhs
483
+ when Integer
484
+ raise ArgumentError, "count must be greater than zero" if rhs < 1
485
+ (1...rhs).inject(dup) { |j,i| j.push(self) }
486
+ when Array
487
+ rhs.inject(Jig.null) { |j,x| j.concat( plug(x) ) }
488
+ else
489
+ raise ArgumentError, "rhs operand for * must be Integer or Array, was #{rhs.class})"
490
+ end
491
+ end
492
+ alias * :mult
493
+
494
+ # call-seq:
495
+ # plug -> a_jig
496
+ # plug { |gap| ... } -> a_jig
497
+ # plug(hash) -> a_jig
498
+ # plug(symbol, item, ...) -> a_jig
499
+ # plug(item, ...) -> a_jig
500
+ #
501
+ # Duplicates the current jig, plugs one or more named gaps, and
502
+ # returns the result. Plug silently ignores attempts to fill
503
+ # undefined gaps. In all cases, the replacement items are inserted
504
+ # into the jig as during Jig construction (see Jig#new).
505
+ #
506
+ # If called with no arguments, any remaining gaps are plugged with nil.
507
+ #
508
+ # If called with a block, the name of each gap in the jig is passed to
509
+ # the block and the gap is replaced with the return value of the block.
510
+ #
511
+ # If called with a hash, the keys are used as gap names and
512
+ # the values are used to plug the respective gaps. The gaps
513
+ # are effectively plugged in parallel to avoid any ambiguity
514
+ # when gaps are plugged with jigs that themselves contain
515
+ # additional gaps.
516
+ #
517
+ # If called with a single symbol argument, the default gap is replaced
518
+ # with a new gap named by the symbol.
519
+ #
520
+ # If two or more arguments are provided and the first argument is
521
+ # a symbol, the named gap is replaced with the list of items.
522
+ #
523
+ # In all other cases, the default gap is replaced with the list of
524
+ # items.
525
+ #
526
+ # j = Jig.new # => #<Jig: [:___]>
527
+ # jg = Jig[:gamma, :epsilon] # => #<Jig: [:gamma, :epsilon]>
528
+ #
529
+ # j.plug :alpha # => #<Jig: [:alpha]>
530
+ # j.plug 1 # => #<Jig: [1]>
531
+ # j.plug :alpha, 'a' # => #<Jig: ['a']>
532
+ # jg.plug :gamma, ['a', 'b'] # => #<Jig: ['a', 'b', :epsilon]>
533
+ # jg.plug :gamma => 'a',
534
+ # :epsilon => 'e' # => #<Jig: ['a', 'e']>
535
+ # j.plug [1,2,3] # => #<Jig: [1, 2, 3]>
536
+ # j.plug # => #<Jig: []>
537
+ def plug(*args, &block)
538
+ dup.plug!(*args, &block)
539
+ end
540
+ alias % :plug
541
+
542
+ # call-seq:
543
+ # plugn(n, item) -> a_jig
544
+ # plugn(range, array) -> a_jig
545
+ # plugn(symbol, array) -> a_jig
546
+ # plugn(array) -> a_jig
547
+ # plugn(hash) -> a_jig
548
+ # plugn(item) -> a_jig
549
+ #
550
+ # Similar to #plug but gaps are identified by an integer offset, not by name.
551
+ # Unlike #index, and #slice, #plugn assumes that gaps are indexed
552
+ # consecutively starting with 0.
553
+ #
554
+ # * When the first argument is an integer, +n+, the n-th gap
555
+ # is replaced with the item.
556
+ # * When the first argument is a range, the gaps indexed by +range+
557
+ # are replaced with the items in +array+.
558
+ # * When the only argument is an array, the gaps indexed by
559
+ # +0...array.size+ are replaced with the items in the array.
560
+ # * When the only argument is a hash, the keys of the hash are taken
561
+ # as indices and the respective gaps are replaced with the associated
562
+ # values from the hash.
563
+ # * Any other single argument is taken as the replacement for the first
564
+ # gap.
565
+ #
566
+ # Examples:
567
+ # list = Jig[:item, ',', :item, ',', :item]
568
+ # list.plugn(1, 'second') # => ",second,"
569
+ # list.plugn(1..2, %w{second third}) # => ",second,third"
570
+ # list.plugn('first') # => "first,,"
571
+ # list.plugn(%w{first second}) # => "first,second,"
572
+ # list.plugn(0 => 'first', 2 => 'third') # => "first,,third"
573
+ def plugn(*args, &block)
574
+ dup.plugn!(*args, &block)
575
+ end
576
+
577
+ # call-seq:
578
+ # before(symbol, item, ...) -> a_jig
579
+ # before(item, ...) -> a_jig
580
+ #
581
+ # Returns a new jig constructed by inserting the item *before* the specified gap
582
+ # or the default gap if the first argument is not a symbol.
583
+ # The gap itself remains in the new jig.
584
+ # Jig.new.before(1,2,3) # => #<Jig: [1, 2, 3, :___]>
585
+ # Jig.new.before(:a, 1,2,3) # => #<Jig: [:___]>
586
+ # Jig.new(:a, :b).before(:b, 1) # => #<Jig: [:a, 1, :b]>
587
+ def before(*args)
588
+ dup.before!(*args)
589
+ end
590
+
591
+ # call-seq:
592
+ # after(symbol, item, ...) -> a_jig
593
+ # after(item, ...) -> a_jig
594
+ #
595
+ # A new jig is constructed by inserting the items *after* the specified gap
596
+ # or the default gap if the first argument is not a symbol.
597
+ # The gap itself remains in the new jig.
598
+ # Jig.new.after(1,2,3) # => #<Jig: [:___, 1, 2, 3]>
599
+ # Jig.new.after(:a, 1,2,3) # => #<Jig: [:___]>
600
+ # Jig.new(:a, :b).after(:a, 1) # => #<Jig: [:a, 1, :b]>
601
+ def after(*args)
602
+ dup.after!(*args)
603
+ end
604
+
605
+ # call-seq
606
+ # split(pattern=$;, [limit])
607
+ #
608
+ # With no arguments, the jig is split at the gap positions into an
609
+ # array of strings. If arguments are provided, the entire string is
610
+ # rendered to a string and the result of String#split (with the
611
+ # arguments) is returned.
612
+ def split(*args)
613
+ if args.empty?
614
+ contents.map { |c| c.join }
615
+ else
616
+ to_s.split(*args)
617
+ end
618
+ end
619
+
620
+ # The contents of the jig are joined via Array#join.
621
+ def join(sep=$,)
622
+ contents.join(sep)
623
+ end
624
+
625
+ # Applies Kernel#freeze to the jig and its internal structures. A frozen jig
626
+ # may still be used with non-mutating methods such as #plug but an exception
627
+ # will be raised if a mutating method such as #push or #plug! are called.
628
+ def freeze
629
+ super
630
+ @contents.freeze
631
+ @rawgaps.freeze
632
+ self
633
+ end
634
+
635
+ # :section: Update
636
+ # This section describes methods that modify the current jig.
637
+
638
+ # Pushes the items onto the end of the current jig.
639
+ # The current jig is modified. Use jig.dup.push(*items) if
640
+ # you want a fresh jig. Individual items are handled as follows:
641
+ # - strings: pushed as is
642
+ # - symbols: converted to a gap and pushed
643
+ # - gaps: pushed as is
644
+ # - jigs: each item of the other jig is pushed in order to the current jig, including gaps.
645
+ # - any object that responds to _to_jig_ is converted and the results pushed.
646
+ # - any object that responds to _call_ is pushed as a proc.
647
+ # - all other objects are pushed as is.
648
+ def push(*items)
649
+ items.each do |i|
650
+ case i
651
+ when String then
652
+ contents.last << i
653
+ when Symbol then
654
+ rawgaps << Gap.new(i)
655
+ contents << []
656
+ when Jig then
657
+ push_jig i
658
+ when NilClass, FalseClass then
659
+ next
660
+ when Jig::Gap then
661
+ rawgaps << i
662
+ contents << []
663
+ else
664
+ if respond_to?(p = "push_#{i.class.name.downcase}")
665
+ send(p, i)
666
+ elsif i.respond_to? :to_jig
667
+ push_jig i.to_jig
668
+ elsif i.respond_to? :call
669
+ (class <<i; self; end).class_eval {
670
+ undef inspect
671
+ #:stopdoc:
672
+ alias inspect :to_s
673
+ undef to_s
674
+ def to_s; call.to_s; end
675
+ #:startdoc:
676
+ }
677
+ contents.last << i
678
+ else
679
+ contents.last << i
680
+ end
681
+ end
682
+ #contents.last.concat(add)
683
+ end
684
+ self
685
+ end
686
+
687
+ # The collection is converted to a list of items via <tt>*collection</tt>.
688
+ # Resulting items are pushed onto the end of the current jig.
689
+ #
690
+ # j = Jig.new 1
691
+ # j.concat([2,3])
692
+ # j == Jig[1,2,3] # true
693
+ #
694
+ # j.concat Jig[4,:alpha,5]
695
+ # j == Jig[1,2,3,4,:alpha,5] # true
696
+ #
697
+ def concat(collection)
698
+ push(*collection)
699
+ end
700
+
701
+ # call-seq:
702
+ # plug! -> a_jig
703
+ # plug! { |gap| ... } -> a_jig
704
+ # plug!(hash) -> a_jig
705
+ # plug!(symbol, *items) -> a_jig
706
+ # plug!(*items) -> a_jig
707
+ #
708
+ # Plugs one or more named gaps (see #plug) and returns self. The current jig is
709
+ # modified. To construct a new jig use #plug instead.
710
+ # If the named plug is not defined, the jig is not changed.
711
+ def plug!(*args, &block)
712
+ return fill!(&block) if block or args.empty?
713
+ first, *more = args
714
+ case first
715
+ when Hash
716
+ fill! { |g| first.fetch(g, g) }
717
+ when Symbol
718
+ if more.empty?
719
+ fill! { |g| g == GAP ? first : g }
720
+ else
721
+ fill! { |g| g == first ? (x = *more) : g }
722
+ end
723
+ else
724
+ fill! { |g| g == GAP ? (x = *args) : g }
725
+ end
726
+ end
727
+ alias []= :plug!
728
+ alias << :plug!
729
+
730
+ # Same as #plug but modifies self.
731
+ def plugn!(first=nil, second=nil, &block)
732
+
733
+ return filln!(&block) if block or !first
734
+
735
+ case first
736
+ when Hash
737
+ filln!(*first.keys) { |i| first[i] }
738
+ when Integer
739
+ #filln!(first => second)
740
+ filln!(first) { second }
741
+ when Array
742
+ filln!(*(0...first.size)) { |index| first.fetch(index) }
743
+ when Range
744
+ # pairs = first.inject({}) { |p, i| p[i] = second[i-first.begin]; p }
745
+ filln!(*first) { |index| second && second.fetch(index-first.begin) }
746
+ else
747
+ filln!(0) { first }
748
+ end
749
+ end
750
+
751
+ # call-seq:
752
+ # before!(symbol, item, ...) -> a_jig
753
+ # before!(item, ...) -> a_jig
754
+ #
755
+ # Like #before but modifies the current jig.
756
+ def before!(*args)
757
+ if Symbol === args.first
758
+ gap = args.shift
759
+ else
760
+ gap = GAP
761
+ end
762
+ if current = rawgaps.find {|x| x.name == gap}
763
+ plug!(gap, args.push(current))
764
+ else
765
+ self
766
+ end
767
+ end
768
+
769
+ # call-seq:
770
+ # after!(symbol, item, ...) -> a_jig
771
+ # after!(item, ...) -> a_jig
772
+ #
773
+ # Like #after but modifies the current jig.
774
+ def after!(*args)
775
+ if Symbol === args.first
776
+ gap = args.shift
777
+ else
778
+ gap = GAP
779
+ end
780
+ if current = rawgaps.find {|x| x.name == gap}
781
+ plug!(gap, args.unshift(current))
782
+ else
783
+ self
784
+ end
785
+ end
786
+
787
+ # A string is constructed by concatenating the contents of the jig.
788
+ # Gaps are effectively considered null strings. Any procs in the jig
789
+ # are evaluated, the results converted to a string via to_s. All
790
+ # other objects are converted to strings via to_s.
791
+ def to_s
792
+ contents.flatten.join
793
+ end
794
+ alias to_str :to_s
795
+
796
+
797
+ # Calls the block once for each gap in the jig passing the name of
798
+ # the gap. If the block returns the gapname, the gap remains in the
799
+ # jig, otherwise the gap is replaced with the return value of the block.
800
+ # If called without a block, all the gaps are replaced with the empty
801
+ # string.
802
+ def fill!
803
+ adjust = 0
804
+ gaps.each_with_index do |gap, index|
805
+ match = index + adjust
806
+ items = block_given? && yield(gap)
807
+ fill = rawgaps.at(match).fill(items)
808
+ adjust += plug_gap!(match, fill) - 1
809
+ end
810
+ self
811
+ end
812
+
813
+ # Calls the block once for each index passing the index to the block.
814
+ # The gap is replaced with the return value of the block.
815
+ # If called without a block, the indexed gaps are replaced
816
+ # with the empty string.
817
+ def filln!(*indices)
818
+ # XXX need to handle indices that are too small
819
+ adjust = 0
820
+ normalized = indices.map { |x| (x >= 0) && x || (x+rawgaps.size) }.sort
821
+ normalized.each do |index|
822
+ match = index + adjust
823
+ gap = rawgaps.fetch(match)
824
+ items = block_given? && yield(index)
825
+ fill = gap.fill(items)
826
+ adjust += plug_gap!(match, fill) - 1
827
+ end
828
+ self
829
+ end
830
+
831
+ # :stopdoc:
832
+ # This method alters the current jig by replacing a gap with a
833
+ # (possibly empty) sequence of objects. The contents and rawgap arrays
834
+ # are modified such that the named gap is removed and the sequence of
835
+ # objects are put in the logical position of the former gap.
836
+ #
837
+ # Gaps and contents are maintainted in two separate arrays. Each
838
+ # element in the contents array is a list of objects implemented as
839
+ # an array. The first element of the gap array represents the
840
+ # gap between the the first and second element of the contents array.
841
+ #
842
+ # +----+----+
843
+ # | | | <--- rawgaps array
844
+ # +----+----+
845
+ # +----+----+----+
846
+ # | | | | <--- contents array
847
+ # +----+----+----+
848
+ #
849
+ # The following relation always holds: rawgaps.size == contents.size - 1
850
+ # :startdoc:
851
+ # Replaces the named gap in the current jig with _plug_ and returns the
852
+ # number of gaps that were inserted in its place.
853
+ def plug_gap!(gap, plug)
854
+ case plug
855
+ when String
856
+ contents[gap,2] = [contents[gap] + [plug] + contents[gap+1]]
857
+ rawgaps.delete_at(gap)
858
+ return 0
859
+ when nil, []
860
+ contents[gap,2] = [contents[gap] + contents[gap+1]]
861
+ rawgaps.delete_at(gap)
862
+ return 0
863
+ else
864
+ plug = Jig[*plug] unless Jig === plug
865
+ filling, gaps = plug.contents, plug.rawgaps
866
+ end
867
+
868
+ case filling.size
869
+ when 0
870
+ contents[gap,2] = [contents[gap] + contents[gap+1]]
871
+ when 1
872
+ contents[gap,2] = [contents[gap] + filling.first + contents[gap+1]]
873
+ else
874
+ contents[gap,2] = [contents[gap] + filling.first] + filling[1..-2] + [filling.last + contents[gap+1]]
875
+ end
876
+ rawgaps[gap, 1] = gaps
877
+ gaps.size
878
+ end
879
+
880
+
881
+ # Append a jig onto the end of the current jig.
882
+ def push_jig(other)
883
+ self.contents = contents[0..-2] + [contents[-1] + other.contents[0]] + other.contents[1..-1]
884
+ rawgaps.concat other.rawgaps
885
+ self
886
+ end
887
+
888
+ # call-seq:
889
+ # jig + obj -> a_jig
890
+ #
891
+ # Duplicate the current jig then use concat to add _obj_.
892
+ # j = Jig[1, :alpha]
893
+ # j + 2 # Jig[1, :alpha, 2]
894
+ # j + :beta # Jig[1, :alpha, :beta]
895
+ # j + Jig[:beta] # Jig[1, :alpha, :beta]
896
+ # j + [3,4] # Jig[1, :alpha, 3, 4]
897
+ # j + [Jig.new, Jig.new] # Jig[1, :alpha, :___, :___]
898
+ # j + Jig[:beta] * 2 # Jig[1, :alpha, :beta, :beta]
899
+ def +(obj)
900
+ dup.concat(obj)
901
+ end
902
+ alias append :+
903
+ end