dramsay-rubyosa 0.4.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.
@@ -0,0 +1,32 @@
1
+ # For each selected track in iTunes, retrieve the genre from Last.fm and accordingly tag the track.
2
+
3
+ begin require 'rubygems'; rescue LoadError; end
4
+ require 'rbosa'
5
+ require 'net/http'
6
+ require 'cgi'
7
+ require 'rexml/document'
8
+ include REXML
9
+
10
+ itunes = OSA.app('iTunes')
11
+
12
+ selection = itunes.selection.get
13
+ if selection.empty?
14
+ $stderr.puts "Please select some tracks."
15
+ exit 1
16
+ end
17
+
18
+ first = selection.first.artist
19
+ feed = "http://ws.audioscrobbler.com/1.0/artist/#{CGI::escape(first)}/toptags.xml"
20
+ doc = Document.new(Net::HTTP.get(URI(feed)))
21
+
22
+ selection.each do |track|
23
+ if doc.root.attributes['artist'] == track.artist
24
+ genre = doc.root[1][1].text.capitalize
25
+ else
26
+ puts 'Querying Last.fm again...'
27
+ feed = "http://ws.audioscrobbler.com/1.0/artist/#{CGI::escape(track.artist)}/toptags.xml"
28
+ doc = Document.new(Net::HTTP.get(URI(feed)))
29
+ genre = doc.root[1][1].text.capitalize
30
+ end
31
+ track.genre = genre
32
+ end
@@ -0,0 +1,37 @@
1
+ # Print the given application's sdef(5).
2
+
3
+ begin require 'rubygems'; rescue LoadError; end
4
+ require 'rbosa'
5
+ require 'rexml/document'
6
+
7
+ def usage
8
+ STDERR.puts <<-EOS
9
+ Usage: #{$0} [--name | --path | --bundle_id | --signature] ...
10
+ Examples:
11
+ #{$0} --name iTunes
12
+ #{$0} --path /Applications/iTunes.app
13
+ #{$0} --bundle_id com.apple.iTunes
14
+ #{$0} --signature hook
15
+ EOS
16
+ exit 1
17
+ end
18
+
19
+ usage unless ARGV.length == 2
20
+
21
+ key = case ARGV.first
22
+ when '--name'
23
+ :name
24
+ when '--path'
25
+ :path
26
+ when '--bundle_id'
27
+ :bundle_id
28
+ when '--signature'
29
+ :signature
30
+ else
31
+ usage
32
+ end
33
+
34
+ app = OSA.app(key => ARGV.last)
35
+ doc = REXML::Document.new(app.sdef)
36
+ doc.write(STDOUT, 0)
37
+ puts ""
@@ -0,0 +1,1046 @@
1
+ # Copyright (c) 2006-2007, Apple Inc. All rights reserved.
2
+ #
3
+ # Redistribution and use in source and binary forms, with or without
4
+ # modification, are permitted provided that the following conditions
5
+ # are met:
6
+ # 1. Redistributions of source code must retain the above copyright
7
+ # notice, this list of conditions and the following disclaimer.
8
+ # 2. Redistributions in binary form must reproduce the above copyright
9
+ # notice, this list of conditions and the following disclaimer in the
10
+ # documentation and/or other materials provided with the distribution.
11
+ # 3. Neither the name of Apple Inc. ("Apple") nor the names of
12
+ # its contributors may be used to endorse or promote products derived
13
+ # from this software without specific prior written permission.
14
+ #
15
+ # THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND
16
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18
+ # ARE DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR
19
+ # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21
+ # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22
+ # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
23
+ # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
24
+ # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25
+ # POSSIBILITY OF SUCH DAMAGE.
26
+
27
+ $KCODE = 'u' # we will use UTF-8 strings
28
+
29
+ require 'osa'
30
+ require 'date'
31
+ require 'uri'
32
+ require 'iconv'
33
+
34
+ # Try to load RubyGems first, libxml-ruby may have been installed by it.
35
+ begin require 'rubygems'; rescue LoadError; end
36
+
37
+ # If libxml-ruby is not present, switch to REXML.
38
+ USE_LIBXML = begin
39
+ gem 'xml/libxml', '= 0.3.8.4'
40
+
41
+ # libxml-ruby bug workaround.
42
+ class XML::Node
43
+ alias_method :old_cmp, :==
44
+ def ==(x)
45
+ (x != nil and old_cmp(x))
46
+ end
47
+ end
48
+ true
49
+ rescue LoadError
50
+ require 'rexml/document'
51
+
52
+ # REXML -> libxml-ruby compatibility layer.
53
+ class REXML::Element
54
+ alias_method :old_find, :find
55
+ def find(path=nil, &block)
56
+ if path.nil? and block
57
+ old_find { |*x| block.call(*x) }
58
+ else
59
+ list = []
60
+ ::REXML::XPath.each(self, path) { |e| list << e }
61
+ list
62
+ end
63
+ end
64
+ def [](attr)
65
+ attributes[attr]
66
+ end
67
+ def find_first(path)
68
+ ::REXML::XPath.first(self, path)
69
+ end
70
+ end
71
+ false
72
+ end
73
+
74
+ class String
75
+ def to_4cc
76
+ OSA.__four_char_code__(Iconv.iconv('MACROMAN', 'UTF-8', self).to_s)
77
+ end
78
+ end
79
+
80
+ class OSA::Enumerator
81
+ attr_reader :code, :name, :group_code
82
+
83
+ def initialize(const, name, code, group_code)
84
+ @const, @name, @code, @group_code = const, name, code, group_code
85
+ self.class.instances[code] = self
86
+ end
87
+
88
+ def self.enum_for_code(code)
89
+ instances[code]
90
+ end
91
+
92
+ def to_s
93
+ @name
94
+ end
95
+
96
+ def inspect
97
+ "<#{@const}>"
98
+ end
99
+
100
+ #######
101
+ private
102
+ #######
103
+
104
+ def self.instances
105
+ (@@instances rescue @@instances = {})
106
+ end
107
+ end
108
+
109
+ class OSA::Element
110
+ REAL_NAME = CODE = nil
111
+ def to_rbobj
112
+ unless __type__ == 'null'
113
+ val = OSA.convert_to_ruby(self)
114
+ val == 'msng' ? nil : val == nil ? self : val
115
+ end
116
+ end
117
+
118
+ def self.from_rbobj(requested_type, value, enum_group_codes)
119
+ obj = OSA.convert_to_osa(requested_type, value, enum_group_codes)
120
+ obj.is_a?(OSA::Element) ? obj : self.__new__(*obj)
121
+ end
122
+ end
123
+
124
+ class OSA::ElementList
125
+ include Enumerable
126
+ def each
127
+ self.size.times { |i| yield(self[i]) }
128
+ end
129
+ end
130
+
131
+ class OSA::ElementRecord
132
+ def self.from_hash(hash)
133
+ value = {}
134
+ hash.each do |code, val|
135
+ key = OSA.sym_to_code(code)
136
+ if key.nil?
137
+ raise ArgumentError, "invalid key `#{code}'" if code.to_s.length != 4
138
+ key = code
139
+ end
140
+ value[key] = OSA::Element.from_rbobj(nil, val, nil)
141
+ end
142
+ OSA::ElementRecord.__new__(value)
143
+ end
144
+
145
+ def to_hash
146
+ h = {}
147
+ self.to_a.each do |code, val|
148
+ key = (OSA.code_to_sym(code) or code)
149
+ h[key] = val.to_rbobj
150
+ end
151
+ return h
152
+ end
153
+ end
154
+
155
+ module OSA::ObjectSpecifier
156
+ def get
157
+ new_obj = @app.__send_event__('core', 'getd', [['----', self]], true).to_rbobj
158
+ if !new_obj.is_a?(self.class) and new_obj.is_a?(OSA::Element) and self.respond_to?(:properties) and (klass = self.properties[:class])
159
+ klass.__duplicate__(new_obj)
160
+ else
161
+ new_obj
162
+ end
163
+ end
164
+ end
165
+
166
+ class OSA::ObjectSpecifierList
167
+ include Enumerable
168
+
169
+ def initialize(app, desired_class, container)
170
+ @app, @desired_class, @container = app, desired_class, container
171
+ end
172
+
173
+ def length
174
+ @app.__send_event__(
175
+ 'core', 'cnte',
176
+ [['----', @container], ['kocl', OSA::Element.__new__('type', @desired_class::CODE.to_4cc)]],
177
+ true).to_rbobj
178
+ end
179
+ alias_method :size, :length
180
+
181
+ def empty?
182
+ length == 0
183
+ end
184
+
185
+ def [](idx)
186
+ idx += 1 # AE starts counting at 1.
187
+ o = obj_spec_with_key(OSA::Element.__new__('long', [idx].pack('l')))
188
+ o.instance_variable_set(:@app, @app)
189
+ o.extend OSA::ObjectSpecifier
190
+ end
191
+
192
+ def first
193
+ self[0]
194
+ end
195
+
196
+ def last
197
+ self[-1]
198
+ end
199
+
200
+ def each
201
+ self.length.times { |i| yield(self[i]) }
202
+ end
203
+
204
+ def get
205
+ o = obj_spec_with_key(OSA::Element.__new__('abso', 'all '.to_4cc))
206
+ o.instance_variable_set(:@app, @app)
207
+ o.extend OSA::ObjectSpecifier
208
+ o.get
209
+ end
210
+ alias_method :to_a, :get
211
+
212
+ def ==(other)
213
+ other.kind_of?(self.class) \
214
+ and other.length == self.length \
215
+ and (0..other.length).all? { |i| other[i] == self[i] }
216
+ end
217
+
218
+ def inspect
219
+ super.scan(/^([^ ]+)/).to_s << " desired_class=#{@desired_class}>"
220
+ end
221
+
222
+ def every(sym)
223
+ if @desired_class.method_defined?(sym) and code = OSA.sym_to_code(sym)
224
+ o = obj_spec_with_key(OSA::Element.__new__('abso', 'all '.to_4cc))
225
+ pklass = @app.classes[code]
226
+ if pklass.nil? or !OSA.lazy_events
227
+ @app.__send_event__('core', 'getd',
228
+ [['----', OSA::Element.__new_object_specifier__('prop', o,
229
+ 'prop', OSA::Element.__new__('type', code.to_4cc))]],
230
+ true).to_rbobj
231
+ else
232
+ OSA::ObjectSpecifierList.new(@app, pklass, o)
233
+ end
234
+ else
235
+ raise ArgumentError, "desired class `#{@desired_class}' does not have a attribute named `#{sym.to_s}'"
236
+ end
237
+ end
238
+
239
+ #######
240
+ private
241
+ #######
242
+
243
+ def obj_spec_with_key(element)
244
+ @desired_class.__new_object_specifier__(@desired_class::CODE, @container,
245
+ 'indx', element)
246
+ end
247
+ end
248
+
249
+ module OSA::EventDispatcher
250
+
251
+ SCRIPTING_ADDITIONS_DIR = [
252
+ '/System/Library/ScriptingAdditions',
253
+ '/Library/ScriptingAdditions'
254
+ ]
255
+ if home = ENV['HOME']
256
+ SCRIPTING_ADDITIONS_DIR << File.join(home, '/Library/ScriptingAdditions')
257
+ end
258
+
259
+ def merge(args)
260
+ args = { :name => args } if args.is_a?(String)
261
+ by_name = args[:name]
262
+ begin
263
+ name, target, sdef = OSA.__scripting_info__(args)
264
+ rescue RuntimeError => excp
265
+ # If an sdef bundle can't be find by name, let's be clever and look in the ScriptingAdditions locations.
266
+ if by_name
267
+ args = SCRIPTING_ADDITIONS_DIR.each do |dir|
268
+ path = ['.app', '.osax'].map { |e| File.join(dir, by_name + e) }.find { |p| File.exists?(p) }
269
+ if path
270
+ break { :path => path }
271
+ end
272
+ end
273
+ if args.is_a?(Hash)
274
+ by_name = nil
275
+ retry
276
+ end
277
+ end
278
+ raise excp
279
+ end
280
+ app_module_name = self.class.name.scan(/^OSA::(.+)::.+$/).flatten.first
281
+ app_module = OSA.const_get(app_module_name)
282
+ OSA.__load_sdef__(sdef, target, app_module, true, self.class)
283
+ (self.__send_event__('ascr', 'gdut', [], true) rescue nil) # Don't ask me why...
284
+ return self
285
+ end
286
+ end
287
+
288
+ module OSA
289
+ def self.app_with_name(name)
290
+ STDERR.puts "OSA.app_with_name() has been deprecated and its usage is now discouraged. Please use OSA.app('name') instead."
291
+ self.__app__(*OSA.__scripting_info__(:name => name))
292
+ end
293
+
294
+ def self.app_with_path(path)
295
+ STDERR.puts "OSA.app_by_path() has been deprecated and its usage is now discouraged. Please use OSA.app(:path => 'path') instead."
296
+ self.__app__(*OSA.__scripting_info__(:path => path))
297
+ end
298
+
299
+ def self.app_by_bundle_id(bundle_id)
300
+ STDERR.puts "OSA.app_by_bundle_id() has been deprecated and its usage is now discouraged. Please use OSA.app(:bundle_id => 'bundle_id') instead."
301
+ self.__app__(*OSA.__scripting_info__(:bundle_id => bundle_id))
302
+ end
303
+
304
+ def self.app_by_signature(signature)
305
+ STDERR.puts "OSA.app_by_signature() has been deprecated and its usage is now discouraged. Please use OSA.app(:signature => 'signature') instead."
306
+ self.__app__(*OSA.__scripting_info__(:signature => signature))
307
+ end
308
+
309
+ def self.app(*args)
310
+ if args.size == 2
311
+ if args.first.is_a?(String) and args.last.is_a?(Hash)
312
+ hash = args.last
313
+ if hash.has_key?(:name)
314
+ warn "Given Hash argument already has a :name key, ignoring the first String argument `#{args.first}'"
315
+ else
316
+ hash[:name] = args.first
317
+ end
318
+ args = hash
319
+ else
320
+ raise ArgumentError, "when called with 2 arguments, the first is supposed to be a String and the second a Hash"
321
+ end
322
+ else
323
+ if args.size != 1
324
+ raise ArgumentError, "wrong number of arguments (#{args.size} for at least 1)"
325
+ end
326
+ args = args.first
327
+ end
328
+ args = { :name => args } if args.is_a?(String)
329
+ self.__app__(*OSA.__scripting_info__(args))
330
+ end
331
+
332
+ @conversions_to_ruby = {}
333
+ @conversions_to_osa = {}
334
+
335
+ def self.add_conversion(hash, types, block, max_arity, replace=false)
336
+ raise "Conversion block has to accept either #{(1..max_arity).to_a.join(', ')} arguments" unless (1..max_arity) === block.arity
337
+ types.each do |type|
338
+ next if !replace and hash.has_key?(type)
339
+ hash[type] = block
340
+ end
341
+ end
342
+
343
+ def self.replace_conversion_to_ruby(*types, &block)
344
+ add_conversion(@conversions_to_ruby, types, block, 3, true)
345
+ end
346
+
347
+ def self.add_conversion_to_ruby(*types, &block)
348
+ add_conversion(@conversions_to_ruby, types, block, 3)
349
+ end
350
+
351
+ def self.replace_conversion_to_osa(*types, &block)
352
+ add_conversion(@conversions_to_osa, types, block, 2, true)
353
+ end
354
+
355
+ def self.add_conversion_to_osa(*types, &block)
356
+ add_conversion(@conversions_to_osa, types, block, 2)
357
+ end
358
+
359
+ def self.convert_to_ruby(osa_object)
360
+ osa_type = osa_object.__type__
361
+ osa_data = osa_object.__data__(osa_type) if osa_type and osa_type != 'null'
362
+ if conversion = @conversions_to_ruby[osa_type]
363
+ args = [osa_data, osa_type, osa_object]
364
+ conversion.call(*args[0..(conversion.arity - 1)])
365
+ end
366
+ end
367
+
368
+ def self.__convert_to_osa__(requested_type, value, enum_group_codes=nil)
369
+ return value if value.is_a?(OSA::Element)
370
+ if conversion = @conversions_to_osa[requested_type]
371
+ args = [value, requested_type]
372
+ conversion.call(*args[0..(conversion.arity - 1)])
373
+ elsif enum_group_codes and enum_group_codes.include?(requested_type)
374
+ if value.is_a?(Array)
375
+ ary = value.map { |x| OSA::Element.__new__('enum', x.code.to_4cc) }
376
+ ElementList.__new__(ary)
377
+ else
378
+ ['enum', value.code.to_4cc]
379
+ end
380
+ elsif md = /^list_of_(.+)$/.match(requested_type)
381
+ ary = value.to_a.map do |elem|
382
+ obj = convert_to_osa(md[1], elem, enum_group_codes)
383
+ obj.is_a?(OSA::Element) ? obj : OSA::Element.__new__(*obj)
384
+ end
385
+ ElementList.__new__(ary)
386
+ else
387
+ STDERR.puts "unrecognized type #{requested_type}" if $VERBOSE
388
+ ['null', nil]
389
+ end
390
+ end
391
+
392
+ def self.convert_to_osa(requested_type, value, enum_group_codes=nil)
393
+ ary = __convert_to_osa__(requested_type, value, enum_group_codes)
394
+ if ary == ['null', nil]
395
+ new_type = case value
396
+ when String then 'text'
397
+ when Array then 'list'
398
+ when Hash then 'record'
399
+ when Integer then 'integer'
400
+ when Float then 'double'
401
+ end
402
+ if new_type
403
+ ary = __convert_to_osa__(new_type, value, enum_group_codes)
404
+ end
405
+ end
406
+ ary
407
+ end
408
+
409
+ def self.set_params(hash)
410
+ previous_values = {}
411
+ hash.each do |key, val|
412
+ unless OSA.respond_to?(key)
413
+ raise ArgumentError, "Invalid key value (no parameter named #{key} was found)"
414
+ end
415
+ ivar_key = '@' + key.to_s
416
+ previous_val = self.instance_variable_get(ivar_key)
417
+ previous_values[ivar_key] = previous_val;
418
+ self.instance_variable_set(ivar_key, hash[key])
419
+ end
420
+ if block_given?
421
+ yield
422
+ previous_values.each { |key, val| self.instance_variable_set(key, val) }
423
+ end
424
+ nil
425
+ end
426
+
427
+ #######
428
+ private
429
+ #######
430
+
431
+ class DocItem
432
+ attr_reader :name, :description
433
+ def initialize(name, description, optional=false)
434
+ @name = name
435
+ @description = description
436
+ @optional = optional
437
+ end
438
+ def optional?
439
+ @optional
440
+ end
441
+ end
442
+
443
+ class DocMethod < DocItem
444
+ attr_reader :result, :args
445
+ def initialize(name, description, result, args)
446
+ super(name, description)
447
+ @result = result
448
+ @args = args
449
+ end
450
+ def inspect
451
+ "<Method #{name} (#{description})>"
452
+ end
453
+ end
454
+
455
+ def self.__app__(name, target, sdef)
456
+ @apps ||= {}
457
+ app = @apps[target]
458
+ return app if app
459
+
460
+ # Creates a module for this app, we will define the scripting interface within it.
461
+ app_module = Module.new
462
+ self.const_set(rubyfy_constant_string(name), app_module)
463
+
464
+ @apps[target] = __load_sdef__(sdef, target, app_module)
465
+ end
466
+
467
+ def self.__load_sdef__(sdef, target, app_module, merge_only=false, app_class=nil)
468
+ # Load the sdef.
469
+ doc = if USE_LIBXML
470
+ parser = XML::Parser.new
471
+ parser.string = sdef
472
+ parser.parse
473
+ else
474
+ REXML::Document.new(sdef)
475
+ end
476
+
477
+ # Retrieves and creates enumerations.
478
+ enum_group_codes = {}
479
+ doc.find('/dictionary/suite/enumeration').each do |element|
480
+ enum_group_code = element['code']
481
+ enum_module_name = rubyfy_constant_string(element['name'], true)
482
+ enum_module = Module.new
483
+ enum_group_codes[enum_group_code] = enum_module
484
+
485
+ documentation = []
486
+ enum_module.const_set(:DESCRIPTION, documentation)
487
+
488
+ element.find('enumerator').each do |element|
489
+ name = element['name']
490
+ enum_name = rubyfy_constant_string(name, true)
491
+ enum_code = element['code']
492
+ enum_const = app_module.name + '::' + enum_module_name + '::' + enum_name
493
+
494
+ enum = OSA::Enumerator.new(enum_const, name, enum_code, enum_group_code)
495
+ enum_module.const_set(enum_name, enum)
496
+
497
+ documentation << DocItem.new(enum_name, englishify_sentence(element['description']))
498
+ end
499
+
500
+ app_module.const_set(enum_module_name, enum_module) unless app_module.const_defined?(enum_module_name)
501
+ end
502
+
503
+ # Retrieves and creates classes.
504
+ classes = {}
505
+ class_elements = {}
506
+ doc.find('/dictionary/suite/class').each do |element|
507
+ key = (element['id'] or element['name'])
508
+ (class_elements[key] ||= []) << element
509
+ end
510
+ class_elements.values.flatten.each do |element|
511
+ klass = add_class_from_xml_element(element, class_elements, classes, app_module)
512
+ methods_doc = []
513
+ description = englishify_sentence(element['description'])
514
+ if klass.const_defined?(:DESCRIPTION)
515
+ klass.const_set(:DESCRIPTION, description) if klass.const_get(:DESCRIPTION).nil?
516
+ else
517
+ klass.const_set(:DESCRIPTION, description)
518
+ end
519
+ if klass.const_defined?(:METHODS_DESCRIPTION)
520
+ methods_doc = klass.const_get(:METHODS_DESCRIPTION)
521
+ else
522
+ methods_doc = []
523
+ klass.const_set(:METHODS_DESCRIPTION, methods_doc)
524
+ end
525
+
526
+ # Creates properties.
527
+ # Add basic properties that might be missing to the Item class (if any).
528
+ props = {}
529
+ element.find('property').each do |x|
530
+ props[x['name']] = [x['code'], type_of_parameter(x), x['access'], x['description']]
531
+ end
532
+ if klass.name[-6..-1] == '::Item'
533
+ unless props.has_key?('id')
534
+ props['id'] = ['ID ', 'integer', 'r', 'the unique ID of the item']
535
+ end
536
+ end
537
+ props.each do |name, pary|
538
+ code, type, access, description = pary
539
+ setter = (access == nil or access.include?('w'))
540
+
541
+ if type == 'reference'
542
+ pklass = OSA::Element
543
+ else
544
+ pklass = classes[type]
545
+ if pklass.nil?
546
+ pklass_elements = class_elements[type]
547
+ unless pklass_elements.nil?
548
+ pklass = add_class_from_xml_element(pklass_elements.first, class_elements, classes, app_module)
549
+ end
550
+ end
551
+ end
552
+
553
+ # Implicit 'get' if the property class is primitive (not defined in the sdef),
554
+ # otherwise just return an object specifier.
555
+ method_name = rubyfy_method(name, klass, type)
556
+ method_proc = if pklass.nil?
557
+ proc do
558
+ @app.__send_event__('core', 'getd',
559
+ [['----', Element.__new_object_specifier__('prop', @app == self ? Element.__new__('null', nil) : self,
560
+ 'prop', Element.__new__('type', code.to_4cc))]],
561
+ true).to_rbobj
562
+ end
563
+ else
564
+ proc do
565
+ o = pklass.__new_object_specifier__('prop', @app == self ? Element.__new__('null', nil) : self,
566
+ 'prop', Element.__new__('type', code.to_4cc))
567
+ unless OSA.lazy_events?
568
+ @app.__send_event__('core', 'getd', [['----', o]], true).to_rbobj
569
+ else
570
+ o.instance_variable_set(:@app, @app)
571
+ o.extend(OSA::ObjectSpecifier)
572
+ end
573
+ end
574
+ end
575
+
576
+ klass.class_eval { define_method(method_name, method_proc) }
577
+ ptypedoc = if pklass.nil?
578
+ type_doc(type, enum_group_codes, app_module)
579
+ else
580
+ "a #{pklass} object"
581
+ end
582
+ if description
583
+ description[0] = description[0].chr.downcase
584
+ description = '-- ' << description
585
+ end
586
+ methods_doc << DocMethod.new(method_name, englishify_sentence("Gets the #{name} property #{description}"), DocItem.new('result', englishify_sentence("the property value, as #{ptypedoc}")), nil)
587
+
588
+ # For the setter, always send an event.
589
+ if setter
590
+ method_name = rubyfy_method(name, klass, type, true)
591
+ method_proc = proc do |val|
592
+ @app.__send_event__('core', 'setd',
593
+ [['----', Element.__new_object_specifier__('prop', @app == self ? Element.__new__('null', nil) : self,
594
+ 'prop', Element.__new__('type', code.to_4cc))],
595
+ ['data', val.is_a?(OSA::Element) ? val : Element.from_rbobj(type, val, enum_group_codes.keys)]],
596
+ true)
597
+ return nil
598
+ end
599
+ klass.class_eval { define_method(method_name, method_proc) }
600
+ methods_doc << DocMethod.new(method_name, englishify_sentence("Sets the #{name} property #{description}"), nil, [DocItem.new('val', englishify_sentence("the value to be set, as #{ptypedoc}"))])
601
+ end
602
+
603
+ OSA.add_property(name.intern, code)
604
+ end
605
+
606
+ # Creates elements.
607
+ element.find('element').each do |eelement|
608
+ type = eelement['type']
609
+
610
+ eklass = classes[type]
611
+ if eklass.nil?
612
+ eklass_elements = class_elements[type]
613
+ unless eklass_elements.nil?
614
+ eklass = add_class_from_xml_element(eklass_elements.first, class_elements, classes, app_module)
615
+ end
616
+ end
617
+
618
+ if eklass.nil?
619
+ STDERR.puts "Cannot find class '#{type}', skipping element '#{eelement}'" if $DEBUG
620
+ next
621
+ end
622
+
623
+ method_name = rubyfy_method(eklass::PLURAL, klass)
624
+ method_proc = proc do
625
+ unless OSA.lazy_events?
626
+ @app.__send_event__('core', 'getd',
627
+ [['----', Element.__new_object_specifier__(
628
+ eklass::CODE.to_4cc, @app == self ? Element.__new__('null', nil) : self,
629
+ 'indx', Element.__new__('abso', 'all '.to_4cc))]],
630
+ true).to_rbobj
631
+ else
632
+ ObjectSpecifierList.new(@app, eklass, @app == self ? Element.__new__('null', nil) : self)
633
+ end
634
+ end
635
+ klass.class_eval { define_method(method_name, method_proc) }
636
+ methods_doc << DocMethod.new(method_name, englishify_sentence("Gets the #{eklass::PLURAL} associated with this object"), DocItem.new('result', englishify_sentence("an Array of #{eklass} objects")), nil)
637
+ end
638
+ end
639
+
640
+ unless merge_only
641
+ # Having an 'application' class is required.
642
+ app_class = classes['application']
643
+ raise "No application class defined." if app_class.nil?
644
+ all_classes_but_app = classes.values.reject { |x| x.ancestors.include?(OSA::EventDispatcher) }
645
+ else
646
+ all_classes_but_app = classes.values
647
+ end
648
+
649
+ # Maps commands to the right classes.
650
+ doc.find('/dictionary/suite/command').each do |element|
651
+ name = element['name']
652
+ next if /NOT AVAILABLE/.match(name) # Finder's sdef (Tiger) names some commands with this 'tag'.
653
+ description = element['description']
654
+ direct_parameter = element.find_first('direct-parameter')
655
+ result = element.find_first('result')
656
+ has_result = result != nil
657
+
658
+ code = element['code']
659
+ begin
660
+ code = Iconv.iconv('MACROMAN', 'UTF-8', code).to_s
661
+ rescue Iconv::IllegalSequence
662
+ # We can't do more...
663
+ STDERR.puts "unrecognized command code encoding '#{code}', skipping..." if $DEBUG
664
+ next
665
+ end
666
+
667
+ classes_to_define = []
668
+ forget_direct_parameter = true
669
+ direct_parameter_optional = false
670
+
671
+ if direct_parameter.nil?
672
+ # No direct parameter, this is for the application class.
673
+ classes_to_define << app_class
674
+ else
675
+ # We have a direct parameter:
676
+ # - map it to the right class if it's a class defined in our scripting dictionary
677
+ # - map it to all classes if it's a 'reference' and to the application class if it's optional
678
+ # - otherwise, just map it to the application class.
679
+ type = type_of_parameter(direct_parameter)
680
+ direct_parameter_optional = parameter_optional?(direct_parameter)
681
+
682
+ if type == 'reference'
683
+ classes_to_define = all_classes_but_app
684
+ classes_to_define << app_class if direct_parameter_optional
685
+ else
686
+ klass = classes[type]
687
+ if klass.nil?
688
+ forget_direct_parameter = false
689
+ classes_to_define << app_class
690
+ else
691
+ classes_to_define << klass
692
+ end
693
+ end
694
+ end
695
+
696
+ # Reject classes which are already represented by an ancestor.
697
+ if classes_to_define.length > 1
698
+ classes_to_define.uniq!
699
+ classes_to_define.reject! do |x|
700
+ classes_to_define.any? { |y| x != y and x.ancestors.include?(y) }
701
+ end
702
+ end
703
+
704
+ params = []
705
+ params_doc = []
706
+ unless direct_parameter.nil?
707
+ pdesc = direct_parameter['description']
708
+ params << [
709
+ 'direct',
710
+ '----',
711
+ direct_parameter_optional,
712
+ type_of_parameter(direct_parameter)
713
+ ]
714
+ unless forget_direct_parameter
715
+ params_doc << DocItem.new('direct', englishify_sentence(pdesc))
716
+ end
717
+ end
718
+
719
+ element.find('parameter').to_a.each do |element|
720
+ poptional = parameter_optional?(element)
721
+ params << [
722
+ rubyfy_string(element['name']),
723
+ element['code'],
724
+ poptional,
725
+ type_of_parameter(element)
726
+ ]
727
+ params_doc << DocItem.new(rubyfy_string(element['name'], true), englishify_sentence(element['description']), poptional)
728
+ end
729
+
730
+ method_proc = proc do |*args_ary|
731
+ args = []
732
+ min_argc = i = 0
733
+ already_has_optional_args = false # Once an argument is optional, all following arguments should be optional.
734
+ optional_hash = nil
735
+ params.each do |pname, pcode, optional, ptype|
736
+ self_direct = (pcode == '----' and forget_direct_parameter)
737
+ if already_has_optional_args or (optional and !self_direct)
738
+ already_has_optional_args = true
739
+ else
740
+ if args_ary.size < i
741
+ raise ArgumentError, "wrong number of arguments (#{args_ary.size} for #{i})"
742
+ end
743
+ end
744
+ val = if self_direct
745
+ self.is_a?(OSA::EventDispatcher) ? [] : ['----', self]
746
+ else
747
+ arg = args_ary[i]
748
+ min_argc += 1 unless already_has_optional_args
749
+ i += 1
750
+ if arg.is_a?(Hash) and already_has_optional_args and i >= args_ary.size and min_argc + 1 == i
751
+ optional_hash = arg
752
+ end
753
+ if optional_hash
754
+ arg = optional_hash.delete(pname.intern)
755
+ end
756
+ if arg.nil?
757
+ if already_has_optional_args
758
+ []
759
+ end
760
+ else
761
+ [pcode, arg.is_a?(OSA::Element) ? arg : OSA::Element.from_rbobj(ptype, arg, enum_group_codes.keys)]
762
+ end
763
+ end
764
+ args << val
765
+ end
766
+ if args_ary.size > params.size or args_ary.size < min_argc
767
+ raise ArgumentError, "wrong number of arguments (#{args_ary.size} for #{min_argc})"
768
+ end
769
+ if optional_hash and !optional_hash.empty?
770
+ raise ArgumentError, "inappropriate optional argument(s): #{optional_hash.keys.join(', ')}"
771
+ end
772
+ wait_reply = (OSA.wait_reply != nil ? OSA.wait_reply : (has_result or @app.remote?))
773
+ ret = @app.__send_event__(code[0..3], code[4..-1], args, wait_reply)
774
+ wait_reply ? ret.to_rbobj : ret
775
+ end
776
+
777
+ unless has_result
778
+ result_type = result_doc = nil
779
+ else
780
+ result_type = type_of_parameter(result)
781
+ result_klass = classes[result_type]
782
+ result_doc = DocItem.new('result', englishify_sentence(result['description']))
783
+ end
784
+
785
+ classes_to_define.each do |klass|
786
+ method_name = rubyfy_method(name, klass, result_type)
787
+ klass.class_eval { define_method(method_name, method_proc) }
788
+ methods_doc = klass.const_get(:METHODS_DESCRIPTION)
789
+ methods_doc << DocMethod.new(method_name, englishify_sentence(description), result_doc, params_doc)
790
+ end
791
+
792
+ # Merge some additional commands, if necessary.
793
+ unless app_class.method_defined?(:activate)
794
+ app_class.class_eval do
795
+ define_method(:activate) do
796
+ __send_event__('misc', 'actv', [], true)
797
+ nil
798
+ end
799
+ end
800
+ methods_doc = app_class.const_get(:METHODS_DESCRIPTION)
801
+ methods_doc << DocMethod.new('activate', 'Activate the application.', nil, [])
802
+ end
803
+ end
804
+
805
+ unless merge_only
806
+ # Returns an application instance, that's all folks!
807
+ hash = {}
808
+ classes.each_value { |klass| hash[klass::CODE] = klass }
809
+ app_class.class_eval do
810
+ attr_reader :sdef, :classes
811
+ define_method(:remote?) { @is_remote == true }
812
+ end
813
+ is_remote = target.length > 4
814
+ app = is_remote ? app_class.__new__('aprl', target) : app_class.__new__('sign', target.to_4cc)
815
+ app.instance_variable_set(:@is_remote, is_remote)
816
+ app.instance_variable_set(:@sdef, sdef)
817
+ app.instance_variable_set(:@classes, hash)
818
+ app.extend OSA::EventDispatcher
819
+ end
820
+ end
821
+
822
+ def self.parameter_optional?(element)
823
+ element['optional'] == 'yes'
824
+ end
825
+
826
+ def self.add_class_from_xml_element(element, class_elements, repository, app_module)
827
+ real_name = element['name']
828
+ key = (element['id'] or real_name)
829
+ klass = repository[key]
830
+ if klass.nil?
831
+ code = element['code']
832
+ inherits = element['inherits']
833
+ plural = element['plural']
834
+
835
+ if real_name == inherits
836
+ # Inheriting from itself is a common idiom when adding methods
837
+ # to a class that has already been defined, probably to avoid
838
+ # mentioning the subclass name more than once.
839
+ inherits = nil
840
+ end
841
+
842
+ if inherits.nil?
843
+ klass = Class.new(OSA::Element)
844
+ else
845
+ super_elements = class_elements[inherits]
846
+ super_class = if super_elements.nil?
847
+ STDERR.puts "sdef bug: class '#{real_name}' inherits from '#{inherits}' which is not defined - fall back inheriting from OSA::Element" if $DEBUG
848
+ OSA::Element
849
+ else
850
+ add_class_from_xml_element(super_elements.first, class_elements, repository, app_module)
851
+ end
852
+ klass = Class.new(super_class)
853
+ end
854
+
855
+ klass.class_eval { include OSA::EventDispatcher } if real_name == 'application'
856
+
857
+ klass.const_set(:REAL_NAME, real_name) unless klass.const_defined?(:REAL_NAME)
858
+ klass.const_set(:PLURAL, plural == nil ? real_name + 's' : plural) unless klass.const_defined?(:PLURAL)
859
+ klass.const_set(:CODE, code) unless klass.const_defined?(:CODE)
860
+
861
+ app_module.const_set(rubyfy_constant_string(real_name), klass)
862
+
863
+ repository[key] = klass
864
+ end
865
+
866
+ return klass
867
+ end
868
+
869
+ def self.type_doc(type, enum_group_codes, app_module)
870
+ if mod = enum_group_codes[type]
871
+ mod.to_s
872
+ elsif md = /^list_of_(.+)$/.match(type)
873
+ "list of #{type_doc(md[1], enum_group_codes, app_module)}"
874
+ else
875
+ up_type = type.upcase
876
+ begin
877
+ app_module.const_get(up_type).to_s
878
+ rescue
879
+ type
880
+ end
881
+ end
882
+ end
883
+
884
+ def self.type_of_parameter(element)
885
+ type = element['type']
886
+ if type.nil?
887
+ etype = element.find_first('type')
888
+ if etype
889
+ type = etype['type']
890
+ if type.nil? and (etype2 = etype.find_first('type')) != nil
891
+ type = etype2['type']
892
+ end
893
+ type = "list_of_#{type}" if etype['list'] == 'yes'
894
+ end
895
+ end
896
+ raise "Parameter #{element} has no type." if type.nil?
897
+ return type
898
+ end
899
+
900
+ def self.escape_string(string)
901
+ string.gsub(/[\$\=\s\-\.\/]/, '_').gsub(/&/, 'and')
902
+ end
903
+
904
+ def self.rubyfy_constant_string(string, upcase=false)
905
+ string = string.gsub(/[^\w\s]/, '')
906
+ first = string[0]
907
+ if (?a..?z).include?(first)
908
+ string[0] = first.chr.upcase
909
+ elsif !(?A..?Z).include?(first)
910
+ string.insert(0, 'C')
911
+ end
912
+ escape_string(upcase ? string.upcase : string.gsub(/\s(.)/) { |s| s[1].chr.upcase })
913
+ end
914
+
915
+ RUBY_RESERVED_KEYWORDS = ['for', 'in', 'class']
916
+ def self.rubyfy_string(string, handle_ruby_reserved_keywords=false)
917
+ # Prefix with '_' parameter names to avoid possible collisions with reserved Ruby keywords (for, etc...).
918
+ if handle_ruby_reserved_keywords and RUBY_RESERVED_KEYWORDS.include?(string)
919
+ '_' + string
920
+ else
921
+ escape_string(string).downcase
922
+ end
923
+ end
924
+
925
+ def self.rubyfy_method(string, klass, return_type=nil, setter=false)
926
+ base = rubyfy_string(string)
927
+ s, i = base.dup, 1
928
+ loop do
929
+ if setter
930
+ # Suffix setters with '='.
931
+ s << '='
932
+ elsif return_type == 'boolean'
933
+ # Suffix predicates with '?'.
934
+ s << '?'
935
+ end
936
+ break unless klass.method_defined?(s)
937
+ # Suffix with an integer if the class already has a method with such a name.
938
+ i += 1
939
+ s = base + i.to_s
940
+ end
941
+ return s
942
+ end
943
+
944
+ def self.englishify_sentence(string)
945
+ return '' if string.nil? or string.empty?
946
+ string[0] = string[0].chr.upcase
947
+ string.strip!
948
+ last = string[-1].chr
949
+ string << '.' if last != '.' and last != '?' and last != '!'
950
+ return string
951
+ end
952
+ end
953
+
954
+ # String, for unicode stuff force utf8 type if specified.
955
+ OSA.add_conversion_to_ruby('TEXT') { |value, type, object| object.__data__('TEXT') }
956
+ OSA.add_conversion_to_ruby('utxt', 'utf8') { |value, type, object| object.__data__(OSA.utf8_strings ? 'utf8' : 'TEXT') }
957
+ OSA.add_conversion_to_osa('string', 'text') { |value| ['TEXT', value.to_s] }
958
+ OSA.add_conversion_to_osa('Unicode text') { |value| [OSA.utf8_strings ? 'utf8' : 'TEXT', value.to_s] }
959
+
960
+ # Signed/unsigned integer.
961
+ OSA.add_conversion_to_ruby('shor', 'long') { |value| value.unpack('l').first }
962
+ OSA.add_conversion_to_ruby('comp') { |value| value.unpack('q').first }
963
+ OSA.add_conversion_to_osa('integer', 'double integer') { |value| ['magn', [value].pack('l')] }
964
+
965
+ # Float
966
+ OSA.add_conversion_to_ruby('sing') { |value| value.unpack('f').first }
967
+ OSA.add_conversion_to_ruby('magn', 'doub') { |value| value.unpack('d').first }
968
+ OSA.add_conversion_to_osa('double') { |value| ['doub', [value].pack('d')] }
969
+
970
+ # Boolean.
971
+ OSA.add_conversion_to_ruby('bool') { |value| value.unpack('c').first != 0 }
972
+ OSA.add_conversion_to_osa('boolean') { |value| [(value ? 'true'.to_4cc : 'fals'.to_4cc), nil] }
973
+ OSA.add_conversion_to_ruby('true') { |value| true }
974
+ OSA.add_conversion_to_ruby('fals') { |value| false }
975
+
976
+ # Date.
977
+ OSA.add_conversion_to_ruby('ldt ') { |value|
978
+ Date.new(1904, 1, 1) + Date.time_to_day_fraction(0, 0, value.unpack('q').first)
979
+ }
980
+
981
+ # Array.
982
+ OSA.add_conversion_to_osa('list') do |value|
983
+ # The `list_of_XXX' types are not handled here.
984
+ if value.is_a?(Array)
985
+ elements = value.map { |x| OSA::Element.from_rbobj(nil, x, nil) }
986
+ OSA::ElementList.__new__(elements)
987
+ else
988
+ value
989
+ end
990
+ end
991
+ OSA.add_conversion_to_ruby('list') { |value, type, object|
992
+ object.is_a?(OSA::ElementList) ? object.to_a.map { |x| x.to_rbobj } : object
993
+ }
994
+
995
+ # File name.
996
+ # Let's use the 'furl' type here instead of 'alis', as we don't have a way to produce an alias for a file that does not exist yet.
997
+ OSA.add_conversion_to_osa('alias', 'file') { |value| ['furl', value.to_s] }
998
+ OSA.add_conversion_to_ruby('alis') do |value, type, object|
999
+ URI.unescape(URI.parse(object.__data__('furl')).path)
1000
+ end
1001
+
1002
+ # Hash.
1003
+ OSA.add_conversion_to_ruby('reco') { |value, type, object| object.is_a?(OSA::ElementRecord) ? object.to_hash : value }
1004
+ OSA.add_conversion_to_osa('record') do |value|
1005
+ if value.is_a?(Hash)
1006
+ OSA::ElementRecord.from_hash(value)
1007
+ else
1008
+ value
1009
+ end
1010
+ end
1011
+
1012
+ # Enumerator.
1013
+ OSA.add_conversion_to_ruby('enum') { |value, type, object| OSA::Enumerator.enum_for_code(object.__data__('TEXT')) or object }
1014
+
1015
+ # Class.
1016
+ OSA.add_conversion_to_osa('type class', 'type') { |value| value.is_a?(Class) and value.ancestors.include?(OSA::Element) ? ['type', value::CODE.to_4cc] : value }
1017
+ OSA.add_conversion_to_ruby('type') do |value, type, object|
1018
+ if value == 'msng'
1019
+ # Missing values.
1020
+ value
1021
+ else
1022
+ hash = object.instance_variable_get(:@app).instance_variable_get(:@classes)
1023
+ hash[value] or value
1024
+ end
1025
+ end
1026
+
1027
+ # QuickDraw Rectangle, aka "bounding rectangle".
1028
+ OSA.add_conversion_to_ruby('qdrt') { |value| value.unpack('S4') }
1029
+ OSA.add_conversion_to_osa('bounding rectangle') { |value| ['qdrt', value.pack('S4')] }
1030
+
1031
+ # Pictures (just return the raw data).
1032
+ OSA.add_conversion_to_ruby('PICT') { |value, type, object| value[222..-1] } # Removing trailing garbage.
1033
+ OSA.add_conversion_to_osa('picture') { |value| ['PICT', value.to_s] }
1034
+ OSA.add_conversion_to_ruby('imaA') { |value, type, object| value }
1035
+ OSA.add_conversion_to_ruby('TIFF') { |value, type, object| value }
1036
+ OSA.add_conversion_to_osa('Image') { |value| ['imaA', value.to_s] }
1037
+ OSA.add_conversion_to_osa('TIFF picture') { |value| ['TIFF', value.to_s] }
1038
+
1039
+ # RGB color.
1040
+ OSA.add_conversion_to_ruby('cRGB') { |value| value.unpack('S3') }
1041
+ OSA.add_conversion_to_osa('color') do |values|
1042
+ ary = values.map { |i| OSA::Element.__new__('long', [i].pack('l')) }
1043
+ OSA::ElementList.__new__(ary)
1044
+ end
1045
+
1046
+ require 'rbosa_properties'