graffle 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.
Files changed (54) hide show
  1. data/History.txt +2 -0
  2. data/LICENSE.txt +34 -0
  3. data/Manifest.txt +53 -0
  4. data/README.txt +19 -0
  5. data/Rakefile +32 -0
  6. data/Rakefile.hoe +23 -0
  7. data/bin/bin-skeleton +23 -0
  8. data/graffle.tmproj +335 -0
  9. data/lib/graffle.rb +43 -0
  10. data/lib/graffle/.document +4 -0
  11. data/lib/graffle/lib-skeleton +3 -0
  12. data/lib/graffle/nodoc/hacks.rb +69 -0
  13. data/lib/graffle/point.rb +42 -0
  14. data/lib/graffle/stereotypes.rb +446 -0
  15. data/lib/graffle/styled-text-reader.rb +52 -0
  16. data/lib/graffle/third-party/s4t-utils.rb +21 -0
  17. data/lib/graffle/third-party/s4t-utils/capturing-globals.rb +78 -0
  18. data/lib/graffle/third-party/s4t-utils/claims.rb +14 -0
  19. data/lib/graffle/third-party/s4t-utils/command-line.rb +15 -0
  20. data/lib/graffle/third-party/s4t-utils/error-handling.rb +20 -0
  21. data/lib/graffle/third-party/s4t-utils/friendly-format.rb +27 -0
  22. data/lib/graffle/third-party/s4t-utils/hacks.rb +32 -0
  23. data/lib/graffle/third-party/s4t-utils/load-path-auto-adjuster.rb +120 -0
  24. data/lib/graffle/third-party/s4t-utils/more-assertions.rb +29 -0
  25. data/lib/graffle/third-party/s4t-utils/os.rb +28 -0
  26. data/lib/graffle/third-party/s4t-utils/rake-task-helpers.rb +75 -0
  27. data/lib/graffle/third-party/s4t-utils/rakefile-common.rb +106 -0
  28. data/lib/graffle/third-party/s4t-utils/svn-file-movement.rb +101 -0
  29. data/lib/graffle/third-party/s4t-utils/test-util.rb +19 -0
  30. data/lib/graffle/third-party/s4t-utils/version.rb +3 -0
  31. data/lib/graffle/version.rb +8 -0
  32. data/setup.rb +1585 -0
  33. data/test/abstract-graphic-tests.rb +56 -0
  34. data/test/array-and-hash-stereotyping-tests.rb +49 -0
  35. data/test/document-tests.rb +117 -0
  36. data/test/graffle-file-types/as-a-package.graffle/data.plist +953 -0
  37. data/test/graffle-file-types/as-a-package.graffle/image1.png +0 -0
  38. data/test/graffle-file-types/as-a-package.graffle/image2.png +0 -0
  39. data/test/graffle-file-types/as-a-package.graffle/image3.png +0 -0
  40. data/test/graffle-file-types/multiple-canvases.graffle +6821 -0
  41. data/test/graffle-file-types/opening-tests.rb +45 -0
  42. data/test/graffle-file-types/two-boxes-and-a-line.graffle +347 -0
  43. data/test/group-tests.rb +109 -0
  44. data/test/hacks-tests.rb +58 -0
  45. data/test/line-graphic-tests.rb +155 -0
  46. data/test/point-tests.rb +43 -0
  47. data/test/set-standalone-test-paths.rb +5 -0
  48. data/test/shaped-graphic-tests.rb +93 -0
  49. data/test/sheet-tests.rb +124 -0
  50. data/test/styled-text-reader-tests.rb +89 -0
  51. data/test/test-skeleton +19 -0
  52. data/test/text-tests.rb +55 -0
  53. data/test/util.rb +15 -0
  54. metadata +139 -0
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created by Brian Marick on 2007-07-06.
4
+ # Copyright (c) 2007. All rights reserved.
5
+
6
+
7
+
8
+ require 's4t-utils'
9
+ require 'ostruct'
10
+ require 'plist'
11
+
12
+ require 'graffle/version'
13
+ require 'graffle/nodoc/hacks'
14
+ require 'graffle/styled-text-reader'
15
+ require 'graffle/stereotypes'
16
+ require 'graffle/point'
17
+
18
+ # See README.txt[link:files/README_txt.html]
19
+ module Graffle
20
+
21
+ # Parse the given data and stereotype the result. Returns an object
22
+ # stereotyped as Graffle::Document.
23
+ def self.parse(filename_or_xml)
24
+ if File.directory?(filename_or_xml)
25
+ filename_or_xml = File.join(filename_or_xml, "data.plist")
26
+ end
27
+
28
+ prog1(Plist.parse_xml(filename_or_xml)) do | doc |
29
+ Document.takes_on(doc)
30
+ end
31
+ end
32
+
33
+ def self.stereotype(o) # :nodoc:
34
+ return true if ShapedGraphic.takes_on(o)
35
+ return true if LineGraphic.takes_on(o)
36
+ return true if Group.takes_on(o)
37
+ # Sheet has no distinguishing marks.
38
+ false
39
+ end
40
+
41
+
42
+ end
43
+
@@ -0,0 +1,4 @@
1
+ point.rb
2
+ stereotypes.rb
3
+ styled-text-reader.rb
4
+ version.rb
@@ -0,0 +1,3 @@
1
+ module Graffle
2
+ # Your code here
3
+ end
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created by Brian Marick on 2007-07-06.
4
+ # Copyright (c) 2007. All rights reserved.
5
+
6
+ require 'pp'
7
+
8
+ class Object
9
+
10
+ def behaves_like?(mod)
11
+ kind_of?(mod)
12
+ end
13
+
14
+ def behave_like(mod)
15
+ @_stereotyped_as_module__bemhack = mod
16
+ def self.stereotype; @_stereotyped_as_module__bemhack; end
17
+
18
+ _cause_printing_to_include_stereotype__bemhack
19
+
20
+ extend(self.stereotype)
21
+ self
22
+ end
23
+ alias and_behave_like behave_like
24
+
25
+
26
+ private
27
+ def _cause_printing_to_include_stereotype__bemhack
28
+ @_stereotype_print_tag__bemhack = "acts as #{stereotype.basename}: "
29
+
30
+ @_original_inspect__bemhack = self.method(:inspect)
31
+ def self.inspect
32
+ @_stereotype_print_tag__bemhack + @_original_inspect__bemhack.call
33
+ end
34
+
35
+ @_original_to_s__bemhack = self.method(:to_s)
36
+ def self.to_s
37
+ @_stereotype_print_tag__bemhack + @_original_to_s__bemhack.call
38
+ end
39
+
40
+ @_original_pretty_print__bemhack = self.method(:pretty_print)
41
+ def self.pretty_print(printer)
42
+ printer.text(@_stereotype_print_tag__bemhack)
43
+ @_original_pretty_print__bemhack.call(printer)
44
+ end
45
+ end
46
+ end
47
+
48
+
49
+
50
+ class Hash
51
+ def << (hash)
52
+ self.merge!(hash)
53
+ self
54
+ end
55
+
56
+ # Default Hash to_s is lame. Note that to_s doesn't just call
57
+ # inspect because inspect might be overridden. We want what
58
+ # inspect does by default, not whatever inspect does at the moment.
59
+ alias_method :_unchanging_inspect__bemhack, :inspect
60
+ def to_s; _unchanging_inspect__bemhack; end
61
+ end
62
+
63
+ class Module
64
+ def basename
65
+ name.split("::").last
66
+ end
67
+ end
68
+
69
+
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created by Brian Marick on 2007-07-06.
4
+ # Copyright (c) 2007. All rights reserved.
5
+
6
+
7
+ module Graffle
8
+
9
+ # An {x,y} coordinate relative to the origin of a Sheet.
10
+ class Point
11
+
12
+ attr_reader :x, :y
13
+
14
+ # If two arguments, they are the x and y coordinates of the
15
+ # Point. Otherwise, the single argument is a string to be
16
+ # parsed to find the coordinates.
17
+ def initialize(*args)
18
+ if args.length == 2
19
+ @x = args[0]; @y = args[1]
20
+ elsif args[0] =~ /\{\{(.*),\s(.*)\}, \{.*,\s.*\}\}/
21
+ initialize(Float($1), Float($2))
22
+ elsif args[0] =~ /\{(.*),\s(.*)\}/
23
+ initialize(Float($1), Float($2))
24
+ else
25
+ # TODO: this should be user_is_bewildered(msg = "how could this point be reached?")
26
+ user_disputes("this point can be reached") { "unreachable?"}
27
+ end
28
+ end
29
+
30
+ include Comparable
31
+
32
+ # One Point is "less than" another if it starts higher
33
+ # than it on the page. If they start at the same height, the
34
+ # one furthest to the left is less.
35
+ def <=>(other)
36
+ return self.x <=> other.x if self.y == other.y
37
+ return self.y <=> other.y
38
+ end
39
+ end
40
+
41
+
42
+ end
@@ -0,0 +1,446 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created by Brian Marick on 2007-07-06.
4
+ # Copyright (c) 2007. All rights reserved.
5
+
6
+ module Graffle
7
+ module Builders # :nodoc:
8
+ def self.raw(mod, basic_structure = {}, &block)
9
+ g = basic_structure.dup
10
+ g.behave_like(mod)
11
+ g.instance_eval(&block) if block
12
+ g
13
+ end
14
+
15
+ def self.classed(mod, basic_structure = {}, &block)
16
+ g = raw(mod, basic_structure, &block)
17
+ g['Class'] = mod.basename
18
+ g
19
+ end
20
+
21
+ def abstract_graphic(&block)
22
+ Builders.raw(Graffle::AbstractGraphic, &block)
23
+ end
24
+
25
+ def shaped_graphic(&block)
26
+ Builders.classed(Graffle::ShapedGraphic, &block)
27
+ end
28
+
29
+ def line_label(&block)
30
+ prog1(shaped_graphic) do | g |
31
+ g.act_as_line_label
32
+ g.instance_eval(&block) if block
33
+ end
34
+ end
35
+
36
+ def line_graphic(&block)
37
+ Builders.classed(Graffle::LineGraphic, &block)
38
+ end
39
+
40
+ def sheet(&block)
41
+ Builders.raw(Graffle::Sheet, {'GraphicsList' => []}, &block)
42
+ end
43
+
44
+ def group(&block)
45
+ Builders.classed(Graffle::Group, {'Graphics' => []}, &block)
46
+ end
47
+
48
+ def document(&block)
49
+ prog1(Builders.raw(Graffle::Document, &block)) { | doc |
50
+ doc['Creator'] = 'graffle.rb'
51
+ }
52
+ end
53
+
54
+ def text(string, &block)
55
+ prog1(Builders.raw(Graffle::Text, &block)) { | text |
56
+ text['Text'] = string
57
+ }
58
+ end
59
+
60
+ end
61
+
62
+
63
+ # TODO: rename AbstractGraphic Nestable
64
+
65
+ # Behavior that's common to all visible objects, be they lines,
66
+ # rectangles, text objects, etc.
67
+ module AbstractGraphic
68
+ include Comparable
69
+
70
+ # TODO: I'm not wild about the way this both checks if something's
71
+ # possible and also does it. Separate in caller?
72
+ def self.takes_on(o, mod) # :nodoc:
73
+ if graffle_class_matches?(o, mod)
74
+ o.behave_like(mod)
75
+ return true
76
+ end
77
+ end
78
+
79
+ def self.graffle_class_matches?(o, mod) # :nodoc:
80
+ o.is_a?(Hash) && o['Class'] == mod.basename
81
+ end
82
+
83
+ # The Group or Sheet containing the object. Mostly for internal use.
84
+ attr_accessor :container
85
+
86
+ # Every visible graffle object has an integer ID.
87
+ # Mostly for internal use.
88
+ def graffle_id; self['ID']; end
89
+ def graffle_id_is(id) # :nodoc:
90
+ self['ID'] = id
91
+ end
92
+
93
+ # One AbstractGraphic is before another if its origin starts higher
94
+ # than the other's. If their origins are at the same height, the
95
+ # one furthest to the left comes before.
96
+ def before?(other)
97
+ self.origin < other.origin
98
+ end
99
+
100
+ # A Point. Must be defined in the whatever includes this module.
101
+ def origin; includer_responsibility; end
102
+
103
+ end
104
+
105
+
106
+ # An entire Graffle file, parsed. The only thing of interest is
107
+ # an array of objects stereotyped as Sheet. A Sheet is the internal
108
+ # name for what the OmniGraffle Pro GUI calls "canvases."
109
+ #
110
+ # OmniGraffle non-Pro can only produce one-canvas documents.
111
+ #
112
+ # Some one-canvas OmniGraffle documents don't actually have Sheets. So
113
+ # that you don't have to worry about that, a one-element Sheet array
114
+ # is inserted if needed.
115
+
116
+ module Document
117
+ include Builders
118
+
119
+ def sheets; self['Sheets']; end
120
+ def first_sheet; sheets[0]; end
121
+
122
+
123
+ def with(*objects) # :nodoc:
124
+ self['Sheets'] = [] unless self.has_key?('Sheets')
125
+ objects.each do | o |
126
+ self['Sheets'] << o
127
+ end
128
+ end
129
+
130
+ def self.takes_on(hash) # :nodoc:
131
+ doc = hash.behave_like(self)
132
+ doc.make_sure_has_sheets
133
+ doc.sheets.each do | child |
134
+ Sheet.takes_on(child)
135
+ end
136
+ true
137
+ end
138
+
139
+
140
+ # If this is a graffle document with an implicit single
141
+ # sheet, make it explicit.
142
+ # TODO: Check that OmniGraffle non-pro can read such a doc. Probably
143
+ # more needs to go along with the GraphicsList.
144
+ def make_sure_has_sheets # :nodoc:
145
+ splice_in_single_sheet unless self.has_key?('Sheets')
146
+ self
147
+ end
148
+
149
+ private
150
+
151
+ def splice_in_single_sheet
152
+ graphics = self.delete("GraphicsList")
153
+ self['Sheets'] = [ { 'GraphicsList' => graphics }]
154
+ end
155
+ end
156
+
157
+ # Visible graphics that aren't lines. (Those are stereotyped
158
+ # LineGraphic.)
159
+ module ShapedGraphic
160
+ include AbstractGraphic
161
+ include Builders
162
+
163
+ def self.takes_on(o) # :nodoc:
164
+ if AbstractGraphic.takes_on(o, self)
165
+ o.content.behave_like(Text) if o.has_content?
166
+ o.act_as_line_label if o.is_line_label?
167
+ true
168
+ end
169
+ end
170
+
171
+ # Does this object contain Text? (The kind you put in when you
172
+ # double-click on the object.)
173
+ def has_content?; self.has_key?('Text'); end
174
+
175
+ # Is this object the label for some LineGraphic?
176
+ def is_line_label?; self.has_key?('Line'); end
177
+
178
+ def act_as_line_label # :nodoc:
179
+
180
+ def self.for_line(graffle_id) # :nodoc:
181
+ self['Line'] = { 'ID' => graffle_id }
182
+ end
183
+
184
+ # The integer ID of the LineGraphic this ShapedGraphic labels.
185
+ # This method only exists if the object really is a label.
186
+ #
187
+ # BUG: A line can have more than one label.
188
+ def self.line_id; self['Line']['ID']; end
189
+
190
+ # The LineGraphic this ShapedGraphic labels.
191
+ # This method only exists if the object really is a label, so
192
+ # use is_line_label? first.
193
+ def self.line
194
+ container.find_by_id(line_id)
195
+ end
196
+ end
197
+
198
+
199
+ # The x coordinate of this object's bounding box.
200
+ def x; bounds.x; end
201
+ # The y coordinate of this object's bounding box.
202
+ def y; bounds.y; end
203
+ # The width of this object's bounding box.
204
+ def width; bounds.width; end
205
+ # The height of this object's bounding box.
206
+ def height; bounds.height; end
207
+
208
+ # This object's bounding box, an OpenStruct with methods x, y,
209
+ # width, and height.
210
+ def bounds
211
+ self['Bounds'] =~ /\{\{(.*),\s(.*)\}, \{(.*),\s(.*)\}\}/
212
+ OpenStruct.new(:x => Float($1), :y => Float($2),
213
+ :width => Float($3), :height => Float($4))
214
+ end
215
+
216
+ def origin # :nodoc:
217
+ Point.new(self['Bounds'])
218
+ end
219
+
220
+ def bounded_by(x, y, width, height) # :nodoc:
221
+ self << {"Bounds" => "{{#{x}, #{y}}, {#{width}, #{height}}}" }
222
+ end
223
+
224
+
225
+
226
+ # The Text within the object (or nil if there isn't any).
227
+ def content; self['Text']; end
228
+ alias_method :contents, :content
229
+
230
+ def with_text(string) # :nodoc:
231
+ self['Text'] = text(string)
232
+ end
233
+ end
234
+
235
+ # A line, be it straight, curved, or jagged. Lines can be connected to
236
+ # other objects. Even if there's no arrowhead on the line, it still has
237
+ # a notion of a _head_ and _tail_. (Alternate notation: it goes from one,
238
+ # to the other). The head or tail can be nil.
239
+ module LineGraphic
240
+ include AbstractGraphic
241
+ include Builders
242
+
243
+ def self.takes_on(o) # :nodoc:
244
+ AbstractGraphic.takes_on(o, self)
245
+ end
246
+
247
+
248
+ # The AbstractGraphic the line comes from.
249
+ def from; _follow__bemhack('Tail'); end
250
+ alias_method :tail, :from
251
+
252
+ # The AbstractGraphic the line goes to.
253
+ def to; _follow__bemhack('Head'); end
254
+ alias_method :head, :to
255
+
256
+ # The label attached to a line. In the document, the label is a
257
+ # ShapedGraphic in the same Container as its line. The label points
258
+ # to the line, not the reverse. This method, though, pretends the
259
+ # line points to the label and returns it (or nil if there is no
260
+ # label).
261
+ def label
262
+ container.graphics.find do |g|
263
+ g.respond_to?(:line_id) && g.line_id == self.graffle_id
264
+ end
265
+ end
266
+
267
+ # Almost certainly, what you care about is the Text of the label, which
268
+ # you could get like this:
269
+ # line.label.content
270
+ # This method is shorthand for that.
271
+ def label_rtf
272
+ label.content
273
+ end
274
+
275
+ # An array of Point objects that make up the line. Not editable (yet).
276
+ def points
277
+ self['Points'].collect do |p|
278
+ p =~ /\{(.*),\s(.*)\}/
279
+ Point.new(Float($1), Float($2))
280
+ end
281
+ end
282
+
283
+ # The origin of the LineGraphic is the first Point in the points array.
284
+ # That's the place the mouse pointer was when some person began to
285
+ # create the LineGraphic. It is _not_ necessarily the Point in the line
286
+ # closest to {0,0}.
287
+ def origin
288
+ Point.new(self['Points'][0])
289
+ end
290
+
291
+ private
292
+ def points_at(*points)
293
+ self['Points'] = points.collect { |p| "{#{p[0]}, #{p[1]}}" }
294
+ end
295
+
296
+ def go_from(thing); _link__bemhack('Tail', thing); end
297
+ def go_to(thing); _link__bemhack('Head', thing); end
298
+
299
+ def _follow__bemhack(which)
300
+ container.find_by_id(self[which]['ID'])
301
+ end
302
+
303
+ # TODO: note this is no good for updating, since fields other than
304
+ # ID may be destroyed.
305
+ def _link__bemhack(type, thing)
306
+ id = thing.respond_to?(:graffle_id) ? thing.graffle_id : thing
307
+ self << { type => { 'ID' => id } }
308
+ end
309
+
310
+ end
311
+
312
+ # Behavior common to Sheet and Group, both of which contain an array
313
+ # of AbstractGraphic.
314
+ # :stopdoc:
315
+ # TODO: perhaps some cleverness with Enumerator would be in order?
316
+ # :startdoc:
317
+ module Container
318
+
319
+ def with(*objects) # :nodoc:
320
+ objects.each do | o |
321
+ self.graphics << o
322
+ o.container = self # works because instance_evaled.
323
+ end
324
+ end
325
+
326
+ # Find the AbstractGraphic matching the integer id. Returns nil
327
+ # if not found.
328
+ #
329
+ # Mostly for internal use.
330
+ def find_by_id(id)
331
+ result = graphics.find { | elt | elt.graffle_id == id }
332
+ unless result
333
+ return nil unless respond_to?(:container) # at the top of the tree.
334
+ return nil unless container # A null container should only happen
335
+ # in tests. Consider a test that
336
+ # uses find_by_id on a group, but the
337
+ # group is not contained in a sheet.
338
+ result = container.find_by_id(id)
339
+ end
340
+ result
341
+ end
342
+
343
+ def self.stereotype_children_of(parent) # :nodoc:
344
+ parent.graphics.each do | child |
345
+ if Graffle.stereotype(child)
346
+ child.container = parent
347
+ else
348
+ puts "TODO: can not yet stereotype child with id #{child['ID'].inspect} and class #{child['Class'].inspect}."
349
+ pp child.keys
350
+ end
351
+ end
352
+ true
353
+ end
354
+
355
+ end
356
+
357
+ # A Sheet is what the UI calls a "canvas." It's the drawing surface.
358
+ # OmniGraffle Pro lets you have multiple canvases.
359
+ module Sheet
360
+ include Builders
361
+ include Container
362
+
363
+ # All the visible graphics within the Sheet: objects stereotyped
364
+ # as ShapedGraphic, LineGraphic, or Group.
365
+ def graphics
366
+ self['GraphicsList']
367
+ end
368
+
369
+ # Only the LineGraphic objects within the Sheet.
370
+ def all_lines
371
+ graphics.find_all do | g |
372
+ g.behaves_like?(Graffle::LineGraphic)
373
+ end
374
+ end
375
+
376
+ # The first LineGraphic within the Sheet.
377
+ # Warning: currently, that does _not_ mean the one highest on
378
+ # the page, so this is really useful only for finding the only
379
+ # line.
380
+ def first_line; all_lines[0]; end
381
+
382
+ def self.takes_on(hash) # :nodoc:
383
+ sheet = hash.behave_like(self)
384
+ Container.stereotype_children_of(sheet)
385
+ true
386
+ end
387
+
388
+ end
389
+
390
+ # What you get when you group graphics in the UI.
391
+ module Group
392
+ include Builders
393
+ include Container
394
+ include AbstractGraphic
395
+
396
+ # The visible objects that were grouped.
397
+ def graphics
398
+ self['Graphics']
399
+ end
400
+
401
+ def self.takes_on(hash) # :nodoc:
402
+ if AbstractGraphic.takes_on(hash, self)
403
+ Container.stereotype_children_of(hash)
404
+ true
405
+ end
406
+ end
407
+
408
+ # TODO: What should the origin be for groups?
409
+
410
+ # TODO: When a group gets a collective bounding-box, how
411
+ # should that incorporate lines within the group?
412
+
413
+ end
414
+
415
+ # OmniGraffle text is RTF. This gives access to an object's text.
416
+ module Text
417
+ # Return the text as the original RTF string.
418
+ def as_rtf
419
+ self['Text']
420
+ end
421
+
422
+ # Strip all the styling from the RTF string and return it as
423
+ # humble ASCII.
424
+ def as_plain_text
425
+ StyledTextReader.new(self.as_rtf).as_lines.join("\n")
426
+ end
427
+
428
+ # Return an array of arrays. Each of the inner arrays represents
429
+ # a line in the original. At the moment, the inner array is what
430
+ # you get when you split the line at double-quote boundaries. For
431
+ # example, given this string:
432
+ #
433
+ # He visits the "login" page.
434
+ # His login is "peter", his password is "paul"
435
+ #
436
+ # the method returns:
437
+ #
438
+ # [ ['He visits the', 'login', 'page.']
439
+ # ['His login is', 'peter', 'his password is', 'paul']]
440
+ #
441
+ # There's more to come here.
442
+ def as_tokens_within_lines
443
+ StyledTextReader.new(self.as_rtf).as_tokens_within_lines
444
+ end
445
+ end
446
+ end