rubyosa19 0.5.0

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