1# Copyright (c) 2006-2007, The RubyCocoa Project.
2# All Rights Reserved.
3#
4# RubyCocoa is free software, covered under either the Ruby's license or the 
5# LGPL. See the COPYRIGHT file for more information.
6
7begin
8  require 'hpricot'
9rescue LoadError
10  require 'rubygems'
11  require 'hpricot'
12end
13require 'osx/cocoa'
14
15lib_path = File.join(File.dirname(File.expand_path(__FILE__)), 'lib/')
16require "#{lib_path}/extras"
17require "#{lib_path}/class_additions"
18require "#{lib_path}/clean_up"
19require "#{lib_path}/hpricot_proxy"
20require "#{lib_path}/class_def"
21require "#{lib_path}/method_def"
22require "#{lib_path}/constant_def"
23require "#{lib_path}/notification_def"
24require "#{lib_path}/function_def"
25require "#{lib_path}/datatype_def"
26
27module CocoaRef
28  class Log
29    def initialize
30      @errors = []
31    end
32    
33    def add(str)
34      @errors.push(str)
35      if $COCOA_REF_DEBUG
36        puts str
37      end
38    end
39    
40    def errors?
41      not @errors.empty?
42    end
43  end
44
45  class Parser
46    attr_reader :class_def
47    
48    def initialize(file, framework = '')
49      @framework = framework.gsub(/Frame\w+/, '')
50      unless framework == 'ApplicationKit' or framework == 'Foundation' or framework.empty?
51        OSX.require_framework @framework
52      end
53      
54      @log = CocoaRef::Log.new
55      
56      # LOAD THE HPRICOT PROXY AND ANY OVERRIDES IF NEEDED
57      @hpricot = CocoaRef::HpricotProxy.new(file)
58      html_parser_overrides_file = File.join(File.dirname(File.expand_path(__FILE__)), @framework, 'HTMLParserOverrides.rb')
59      if File.exist?(html_parser_overrides_file)
60        # If it exists, require it and extend the html parser to use the overrides
61        require html_parser_overrides_file
62        @hpricot.extend HTMLParserOverrides
63      end
64      
65      # START PARSING
66      @class_def = parse_reference(file)
67      @class_def.framework = @framework
68      
69      # Check if there is a overrides file in the override_dir for the given class
70      class_overrides_file = File.join(File.dirname(File.expand_path(__FILE__)), @framework, @class_def.output_filename)
71      if File.exist?(class_overrides_file)
72        # If it exists, require it and extend the methods to use the overrides
73        require class_overrides_file
74        @class_def.method_defs.each do |m|
75          m.instance_eval "self.send :extend, #{@class_def.name}Overrides"
76        end
77        @class_def.delegate_method_defs.each do |dm|
78          dm.instance_eval "self.send :extend, #{@class_def.name}Overrides"
79        end
80        @class_def.function_defs.each do |f|
81          f.instance_eval "self.send :extend, #{@class_def.name}FunctionsOverrides"
82        end
83      end
84    end
85    
86    def empty?
87      @class_def.empty?
88    end
89    
90    def errors?
91      @log.errors? or @hpricot.log.errors? or @class_def.errors?
92    end
93    
94    def parse_reference(file)
95      class_def = ClassDef.new
96      busy_with = ''
97      constant_type = :normal
98      index = 0
99      @hpricot.elements.each do |element|
100        if element.fits_the_description?('h1', 'Class Reference') or element.fits_the_description?('h1', 'Class Objective-C Reference')
101          class_def.type = :class
102          class_def.name = element.inner_html.strip_tags.split(' ').first
103        elsif element.fits_the_description?('h1', 'Additions Reference')
104          class_def.type = :additions
105          class_def.name = element.inner_html.strip_tags.split(' ').first
106        elsif element.fits_the_description?('h1', 'Deprecation Reference')
107          # FIXME: this is a check for NSObjCTypeSerializationCallBack.
108          # It's deprecated therefor it shows up here, but it's a protocol ref actually.
109          # Maybe we need to skip deprecated refs anyway...
110          name = element.inner_html.strip_tags.split(' ').first
111          if name == 'NSObjCTypeSerializationCallBack'
112            class_def.type = :protocols
113            class_def.name = name
114          else
115            class_def.type = :class
116            class_def.name = name
117          end
118        elsif element.fits_the_description?('h1', 'Functions Reference')
119          class_def.type = :functions
120          class_def.name = @framework
121        elsif element.fits_the_description?('h1', 'Data Types Reference')
122          class_def.type = :data_types
123          class_def.name = @framework
124        elsif element.fits_the_description?('h1', 'Constants Reference')
125          class_def.type = :constants
126          class_def.name = @framework
127        elsif element.fits_the_description?('h1', 'Protocol') # FIXME: Had to remove the ref part because of a bug with appkit/protocols/NSIgnoreMisspelledWords
128          class_def.type = :protocols
129          class_def.name = element.inner_html.strip_tags.split(' ').first
130        end
131
132        if element.fits_the_description?('h2', 'Class Description')
133          class_def.description = @hpricot.get_the_text(index + 1).first
134        end
135
136        if element.fits_the_description?('h2', 'Class Methods')
137          busy_with = 'Class Methods'
138        end
139        if busy_with == 'Class Methods' and @hpricot.start_of_method_def?(index)
140          class_def.method_defs.push @hpricot.get_method_def(index, :class_method)
141        end
142        
143        if element.fits_the_description?('h2', 'Instance Methods')
144          busy_with = 'Instance Methods'
145        end
146        if busy_with == 'Instance Methods' and @hpricot.start_of_method_def?(index)
147          class_def.method_defs.push @hpricot.get_method_def(index, :instance_method)
148        end
149        
150        if element.fits_the_description?('h2', 'Delegate Methods')
151          busy_with = 'Delegate Methods'
152        end
153        if busy_with == 'Delegate Methods' and @hpricot.start_of_method_def?(index)
154          class_def.delegate_method_defs.push @hpricot.get_method_def(index, :instance_method)
155        end
156        
157        if class_def.type == :functions and element.fits_the_description?('h2', 'Functions')
158          busy_with = 'Functions'
159        end
160        if busy_with == 'Functions' and @hpricot.start_of_method_def?(index)
161          class_def.function_defs.push @hpricot.get_methodlike_function_def(index)
162        end
163        
164        if class_def.type == :data_types and element.fits_the_description?('h2', 'Data Types')
165          busy_with = 'Data Types'
166        end
167        if busy_with == 'Data Types' and @hpricot.start_of_method_def?(index)
168          datatype = @hpricot.get_methodlike_datatype_def(index)
169          class_def.datatype_defs.push datatype unless datatype.nil?
170        end
171          
172        if element.fits_the_description?('h2', 'Notifications')
173          busy_with = 'Notifications'
174        end
175        if busy_with == 'Notifications' and @hpricot.start_of_method_def?(index)
176          class_def.notification_defs.push @hpricot.get_notification_def(index)
177        end
178
179        if element.fits_the_description?('h2', 'Constants')
180          busy_with = 'Constants'
181        end
182        if busy_with == 'Constants' and class_def.type != :constants and @hpricot.start_of_method_def?(index)
183          constants = @hpricot.get_constant_defs(index)
184          class_def.constant_defs.concat(constants) unless constants.nil?
185        end
186        # There are also constant descriptions which are more like method descriptions
187        if busy_with == 'Constants' and class_def.type != :constants and @hpricot.start_of_method_def?(index)
188          constant = @hpricot.get_methodlike_constant_def(index)
189          if not constant.discussion.empty? and not constant.availability.empty?
190            class_def.constant_defs.push constant
191          end
192        end
193        
194        # For the constants in the misc dir there are several types of constants,
195        # we probably want to use this as different sections.
196        if busy_with == 'Constants' and class_def.type == :constants
197          if element.fits_the_description?('h3', 'Enumerations')
198            constant_type = :enumeration
199          elsif element.fits_the_description?('h3', 'Global Variables')
200            constant_type = :global
201          elsif element.fits_the_description?('h3', 'Errors')
202            constant_type = :error
203          elsif element.fits_the_description?('h3', 'Notifications')
204            constant_type = :notification
205          elsif element.fits_the_description?('h3', 'Exceptions')
206            constant_type = :exception
207          end
208        end
209        # Then there are the constants defined in the misc dir, which are a bit different
210        if busy_with == 'Constants' and class_def.type == :constants and @hpricot.start_of_framework_constant_def?(index)
211          constants = @hpricot.get_constant_defs(index, constant_type)
212          class_def.constant_defs.concat(constants) unless constants.nil?
213        elsif busy_with == 'Constants' and class_def.type == :constants and @hpricot.start_of_method_def?(index)
214          constants = @hpricot.get_constant_defs(index)
215          class_def.constant_defs.concat(constants) unless constants.nil?
216        end
217        
218        index = index.next
219      end
220      
221      # Check if we got a name for the class
222      if class_def.name.empty?
223        error_str = "[ERROR] A empty string was returned as the class name\n"
224        @log.add(error_str)
225      end
226      
227      # Remove duplicate constants 
228      class_def.constant_defs.uniq!
229      
230      class_def.create_syntactic_sugar_methods!
231      
232      return class_def
233    end
234    
235    def to_rb_file(dir)
236      file = File.join(dir, @class_def.output_filename)
237      File.open(file, 'w') {|f| f.write @class_def.to_rdoc}
238    end
239    
240  end
241end
242