s0nspark-rubyosa 0.5.2

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