]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blobdiff - vendor/oojs/oojs-ui/bin/docparser.rb
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / vendor / oojs / oojs-ui / bin / docparser.rb
diff --git a/vendor/oojs/oojs-ui/bin/docparser.rb b/vendor/oojs/oojs-ui/bin/docparser.rb
new file mode 100644 (file)
index 0000000..e995ec8
--- /dev/null
@@ -0,0 +1,295 @@
+require 'pp'
+require 'json'
+
+$bad_input = false
+def bad_input file, text
+       $bad_input = true
+       $stderr.puts "#{file}: unrecognized input: #{text}"
+end
+
+def parse_dir dirname
+       Dir.entries(dirname).map{|filename|
+               if filename == '.' || filename == '..'
+                       nil
+               else
+                       parse_any_path "#{dirname}/#{filename}"
+               end
+       }.compact.inject(:+)
+end
+
+def cleanup_class_name class_name
+       class_name.sub(/OO\.ui\./, '').sub(/mixin\./, '')
+end
+
+def extract_default_from_description item
+       m = item[:description].match(/\(default: (.+?)\)\s*?$/i)
+       return if !m
+       # modify `item` in-place
+       item[:default] = m[1]
+       item[:description] = m.pre_match
+end
+
+def parse_file filename
+       if filename !~ /\.(php|js)$/
+               return nil
+       end
+       filetype = filename[/\.(php|js)$/, 1].to_sym
+
+       text = File.read filename, encoding: 'utf-8'
+
+       # ewwww
+       # some docblocks are missing and we really need them
+       text = text.sub(/(?<!\*\/\n)^(class|trait)/, "/**\n*/\n\\1")
+
+       # find all documentation blocks, together with the following line (unless it contains another docblock)
+       docblocks = text.scan(/\/\*\*[\s\S]+?\*\/\n[ \t]*(?:(?=\/\*\*)|.*)/)
+
+       current_class = nil
+       output = []
+       previous_item = {} # dummy
+
+       docblocks.each{|d|
+               kind = nil
+               previous_item = data = {
+                       name: nil,
+                       description: '',
+                       parent: nil,
+                       mixins: [],
+                       methods: [],
+                       properties: [],
+                       events: [],
+                       params: [],
+                       config: [],
+                       visibility: :public,
+                       type: nil,
+               }
+               valid_for_all = %w[name description].map(&:to_sym)
+               valid_per_kind = {
+                       class:    valid_for_all + %w[parent mixins methods properties events abstract mixin].map(&:to_sym),
+                       method:   valid_for_all + %w[params config return visibility static].map(&:to_sym),
+                       property: valid_for_all + %w[type static].map(&:to_sym),
+                       event:    valid_for_all + %w[params].map(&:to_sym),
+               }
+
+               js_class_constructor = false
+               js_class_constructor_desc = ''
+               php_trait_constructor = false
+               ignore = false
+
+               comment, code_line = d.split '*/'
+               comment.split("\n").each{|comment_line|
+                       next if comment_line.strip == '/**'
+                       comment_line.sub!(/^[ \t]*\*[ \t]?/, '') # strip leading '*' and whitespace
+
+                       m = comment_line.match(/^@(\w+)[ \t]*(.*)/)
+                       if !m
+                               # this is a continuation of previous item's description
+                               previous_item[:description] << comment_line + "\n"
+                               if filetype == :php
+                                       extract_default_from_description(previous_item)
+                               end
+                               next
+                       end
+
+                       keyword, content = m.captures
+
+                       # handle JS class/constructor conundrum
+                       if keyword == 'class' || keyword == 'constructor'
+                               js_class_constructor = true
+                       end
+
+                       case keyword
+                       when 'constructor'
+                               # handle JS class/constructor conundrum
+                               js_class_constructor_desc = data[:description]
+                               data[:description] = ''
+                               kind = :method
+                       when 'class'
+                               kind = :class
+                               data[:name] = cleanup_class_name(content.strip) if content && !content.strip.empty?
+                       when 'method'
+                               kind = :method
+                       when 'property', 'var'
+                               kind = :property
+                               m = content.match(/^\{?(.+?)\}?( .+)?$/)
+                               if !m
+                                       bad_input filename, comment_line
+                                       next
+                               end
+                               type, description = m.captures
+                               data[:type] = type
+                               data[:description] = description if description
+                       when 'event'
+                               kind = :event
+                               data[:name] = content.strip
+                       when 'extends'
+                               data[:parent] = cleanup_class_name(content.strip)
+                       when 'mixins'
+                               data[:mixins] << cleanup_class_name(content.strip)
+                       when 'param'
+                               case filetype
+                               when :js
+                                       type, name, default, description = content.match(/^\{(.+?)\} \[?([\w.$]+?)(?:=(.+?))?\]?( .+)?$/).captures
+                                       next if type == 'Object' && name == 'config'
+                                       data[:params] << {name: name, type: cleanup_class_name(type), description: description || '', default: default}
+                                       previous_item = data[:params][-1]
+                               when :php
+                                       type, name, config, description = content.match(/^(\S+) \&?\$(\w+)(?:\['(\w+)'\])?( .+)?$/).captures
+                                       next if type == 'array' && name == 'config' && !config
+                                       if config && name == 'config'
+                                               data[:config] << {name: config, type: cleanup_class_name(type), description: description || ''}
+                                               previous_item = data[:config][-1]
+                                       else
+                                               data[:params] << {name: name, type: cleanup_class_name(type), description: description || ''}
+                                               previous_item = data[:params][-1]
+                                       end
+                                       if filetype == :php
+                                               extract_default_from_description(previous_item)
+                                       end
+                               end
+                       when 'cfg' # JS only
+                               m = content.match(/^\{(.+?)\} \[?([\w.$]+?)(?:=(.+?))?\]?( .+)?$/)
+                               if !m
+                                       bad_input filename, comment_line
+                                       next
+                               end
+                               type, name, default, description = m.captures
+                               data[:config] << {name: name, type: cleanup_class_name(type), description: description || '', default: default}
+                               previous_item = data[:config][-1]
+                       when 'return'
+                               case filetype
+                               when :js
+                                       m = content.match(/^\{(.+?)\}( .+)?$/)
+                               when :php
+                                       m = content.match(/^(\S+)( .+)?$/)
+                               end
+                               if !m
+                                       bad_input filename, comment_line
+                                       next
+                               end
+                               type, description = m.captures
+                               data[:return] = {type: cleanup_class_name(type), description: description || ''}
+                               previous_item = data[:return]
+                       when 'private'
+                               data[:visibility] = :private
+                       when 'protected'
+                               data[:visibility] = :protected
+                       when 'static'
+                               data[:static] = true
+                       when 'abstract'
+                               data[:abstract] = true
+                       when 'ignore', 'inheritdoc'
+                               ignore = true
+                       when 'inheritable', 'deprecated', 'singleton', 'throws',
+                                'chainable', 'fires', 'localdoc', 'member',
+                                'see', 'uses'
+                               # skip
+                       else
+                               bad_input filename, comment_line
+                               next
+                       end
+               }
+
+               next if ignore
+
+               if code_line && code_line.strip != ''
+                       case filetype
+                       when :js
+                               m = code_line.match(/(?:(static|prototype|mixin)\.)?(\w+) =/)
+                               if !m
+                                       bad_input filename, code_line.strip
+                                       next
+                               end
+                               kind_, name = m.captures
+                               data[:static] = true if kind_ == 'static'
+                               kind = {'static' => :property, 'prototype' => :method}[ kind_.strip ] if kind_ && !kind
+                               data[:mixin] = true if kind_ == 'mixin'
+                               data[:name] ||= cleanup_class_name(name)
+                       when :php
+                               m = code_line.match(/
+                                       \s*
+                                       (?:(public|protected|private)\s)?
+                                       (?:(static)\s)?(function\s|class\s|trait\s|\$)
+                                       (\w+)
+                                       (?:\sextends\s(\w+))?
+                               /x)
+                               if !m
+                                       bad_input filename, code_line.strip
+                                       next
+                               end
+                               visibility, static, kind_, name, parent = m.captures
+                               kind = {'$' => :property, 'function' => :method, 'class' => :class, 'trait' => :class}[ kind_.strip ]
+                               data[:visibility] = {'private' => :private, 'protected' => :protected, 'public' => :public}[ visibility ] || :public
+                               data[:mixin] = true if kind_.strip == 'trait'
+                               data[:static] = true if static
+                               data[:parent] = cleanup_class_name(parent) if parent
+                               data[:name] ||= cleanup_class_name(name)
+                               php_trait_constructor = true if kind == :method && data[:name] == 'initialize' + current_class[:name]
+                       end
+               end
+
+               # handle JS class/constructor conundrum
+               if kind == :class || js_class_constructor
+                       if current_class
+                               output << current_class
+                       end
+                       current_class = data.select{|k, _v| valid_per_kind[:class].include? k }
+                       current_class[:description] = js_class_constructor_desc if js_class_constructor_desc != ''
+                       previous_item = current_class
+               end
+
+               # standardize
+               # (also handle fake constructors for traits)
+               if data[:name] == '__construct' || js_class_constructor || php_trait_constructor
+                       data[:name] = '#constructor'
+               end
+
+               # put into the current class
+               if kind && kind != :class
+                       keys = {
+                               method: :methods,
+                               property: :properties,
+                               event: :events,
+                       }
+                       if current_class
+                               current_class[keys[kind]] << data.select{|k, _v| valid_per_kind[kind].include? k }
+                               previous_item = current_class[keys[kind]]
+                       end
+               end
+       }
+
+       # this is evil, assumes we only have one class in a file, but we'd need a proper parser to do it better
+       if current_class
+               current_class[:mixins] +=
+                       text.scan(/^[ \t]*use (\w+)(?: ?\{|;)/).flatten.map(&method(:cleanup_class_name))
+       end
+
+       output << current_class if current_class
+       output
+end
+
+def parse_any_path path
+       if File.directory? path
+               result = parse_dir path
+       else
+               result = parse_file path
+       end
+       if $bad_input
+               $stderr.puts 'Unrecognized inputs encountered, stopping.'
+               exit 1
+       end
+       result
+end
+
+if __FILE__ == $PROGRAM_NAME
+       if ARGV.empty? || ARGV == ['-h'] || ARGV == ['--help']
+               $stderr.puts "usage: ruby #{$PROGRAM_NAME} <files...>"
+               $stderr.puts "       ruby #{$PROGRAM_NAME} src > docs-js.json"
+               $stderr.puts "       ruby #{$PROGRAM_NAME} php > docs-php.json"
+       else
+               out = JSON.pretty_generate ARGV.map{|a| parse_any_path a }.inject(:+)
+               # ew
+               out = out.gsub(/\{\s+\}/, '{}').gsub(/\[\s+\]/, '[]')
+               puts out
+       end
+end