graffle 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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