diff options
Diffstat (limited to 'vendor/oojs/oojs-ui/bin')
-rw-r--r-- | vendor/oojs/oojs-ui/bin/doccomparer.rb | 165 | ||||
-rw-r--r-- | vendor/oojs/oojs-ui/bin/docparser.rb | 243 | ||||
-rw-r--r-- | vendor/oojs/oojs-ui/bin/generate-JSPHP-for-karma.php | 50 | ||||
-rw-r--r-- | vendor/oojs/oojs-ui/bin/testsuitegenerator.rb | 146 |
4 files changed, 604 insertions, 0 deletions
diff --git a/vendor/oojs/oojs-ui/bin/doccomparer.rb b/vendor/oojs/oojs-ui/bin/doccomparer.rb new file mode 100644 index 00000000..cd3623df --- /dev/null +++ b/vendor/oojs/oojs-ui/bin/doccomparer.rb @@ -0,0 +1,165 @@ +require 'pp' +require_relative 'docparser' + +# convert [ {name: 'foo'}, … ] to { foo: {name: 'foo'}, … } +def reindex arg + if arg.is_a?(Array) && arg.all?{|v| v.is_a? Hash } + Hash[ arg.map{|v| [ v[:name], reindex(v) ] } ] + elsif arg.is_a? Hash + arg.each_pair{|k, v| arg[k] = reindex(v) } + else + arg + end +end + +def indent text, tabs + text == '' ? text : text.gsub(/^/, ' ' * tabs) +end + +# whitespace-insensitive strings +def canonicalize value + if value.is_a? String + value.strip.gsub(/\s+/, ' ') + elsif value.is_a? Array + value.map{|v| canonicalize v } + elsif value.is_a? Hash + value.each_pair{|k, v| value[k] = canonicalize v } + else + value + end +end + +def sanitize_description text + cleanup_class_name(text) + .gsub('null and undefined', 'null') + .gsub('undefined and null', 'null') + .gsub('array()', '[]') + .gsub('jQuery|string|Function', 'string') + .gsub('jQuery', 'Tag') + .gsub('string|Function', 'string') + .gsub('object', 'array') + .gsub(/#(\w+)/, '\1()') + .gsub(/Object\.<.+?>/, 'array') +end + +def smart_compare_process val, type + val[:description] = sanitize_description val[:description] + + case type + when :class + val = val.dup + val[:mixins].delete 'OO.EventEmitter' # JS only + val[:mixins].delete 'PendingElement' # JS only + val.delete :parent if val[:parent] == 'ElementMixin' # PHP only + val.delete :methods + val.delete :properties + val.delete :events + + when :method + if val[:name] == '#constructor' + val[:params].delete 'element' # PHP only + end + val[:config].each_pair{|_k, v| + default = v.delete :default + v[:description] << " (default: #{default})" if default + v[:description] = sanitize_description v[:description] + v[:type] = sanitize_description v[:type] + } + val[:params].each_pair{|_k, v| + default = v.delete :default + v[:description] << " (default: #{default})" if default + v[:description] = sanitize_description v[:description] + v[:type] = sanitize_description v[:type] + } + if val[:return] + val[:return][:description] = sanitize_description val[:return][:description] + val[:return][:type] = sanitize_description val[:return][:type] + end + + when :property + val[:description] = sanitize_description val[:description] + val[:type] = sanitize_description val[:type] + + end + val +end + +def smart_compare a, b, a_name, b_name, type + a = smart_compare_process a, type + b = smart_compare_process b, type + compare_hash a, b, a_name, b_name +end + +def smart_compare_methods a, b, a_name, b_name + smart_compare a, b, a_name, b_name, :method +end + +def smart_compare_properties a, b, a_name, b_name + smart_compare a, b, a_name, b_name, :property +end + +def compare_hash a, b, a_name, b_name, nested=:compare_hash + keys = (a ? a.keys : []) + (b ? b.keys : []) + out = keys.to_a.sort.uniq.map do |key| + a_val = a ? canonicalize(a[key]) : nil + b_val = b ? canonicalize(b[key]) : nil + + if [a_val, b_val] == [{}, []] || [a_val, b_val] == [[], {}] + a_val, b_val = {}, {} + end + + if a_val.is_a?(Hash) && b_val.is_a?(Hash) + comparison_result = indent method(nested).call(a_val, b_val, a_name, b_name), 2 + if comparison_result.strip == '' + "#{key}: match" if $VERBOSE + else + "#{key}: MISMATCH\n#{comparison_result}" + end + else + if a_val == b_val + "#{key}: match" if $VERBOSE + elsif a_val.nil? + "#{key}: #{a_name} missing" + elsif b_val.nil? + "#{key}: #{b_name} missing" + else + "#{key}: MISMATCH\n #{a_name}: #{a_val.inspect}\n #{b_name}: #{b_val.inspect}" + end + end + end + out.compact.join "\n" +end + +if ARGV.empty? || ARGV == ['-h'] || ARGV == ['--help'] + $stderr.puts "usage: ruby [-v] #{$PROGRAM_NAME} <dirA> <dirB> <nameA> <nameB>" + $stderr.puts " ruby #{$PROGRAM_NAME} src php JS PHP > compare.txt" +else + dir_a, dir_b, name_a, name_b = ARGV + + js = parse_any_path dir_a + php = parse_any_path dir_b + + js = reindex js + php = reindex php + + (js.keys + php.keys).sort.uniq.each do |class_name| + where = [js.key?(class_name) ? name_a : nil, php.key?(class_name) ? name_b : nil].compact + puts "\n#{class_name}: #{where.join '/'}" if $VERBOSE || where.length == 2 + + if where.length == 2 + data = { + 'Basic:' => + smart_compare(js[class_name], php[class_name], name_a, name_b, :class), + 'Methods:' => + compare_hash(js[class_name][:methods], php[class_name][:methods], name_a, name_b, :smart_compare_methods), + 'Properties:' => + compare_hash(js[class_name][:properties], php[class_name][:properties], name_a, name_b, :smart_compare_properties), + } + data = data + .select{|_k, v| v != ''} + .map{|k, v| "#{k}\n#{indent v, 2}" } + .join("\n") + puts indent data, 2 + end + end +end diff --git a/vendor/oojs/oojs-ui/bin/docparser.rb b/vendor/oojs/oojs-ui/bin/docparser.rb new file mode 100644 index 00000000..9f58549b --- /dev/null +++ b/vendor/oojs/oojs-ui/bin/docparser.rb @@ -0,0 +1,243 @@ +require 'pp' +require 'json' + +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\./, '') +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/, "/**\n*/\nclass") + # text = text.sub('public static $targetPropertyName', "/**\n*/\npublic static $targetPropertyName") + + # 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].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 = '' + ignore = false + + comment, code_line = d.split '*/' + comment.split("\n").each{|c| + next if c.strip == '/**' + c.sub!(/^[ \t]*\*[ \t]?/, '') # strip leading * + + m = c.match(/^@(\w+)[ \t]*(.*)/) + unless m + previous_item[:description] << c + "\n" + 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 + when 'method' + kind = :method + when 'property', 'var' + kind = :property + m = content.match(/^\{?(.+?)\}?( .+)?$/) + if m.captures + type, description = m.captures + data[:type] = type + data[:description] = description if description + end + 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 + end + when 'cfg' # JS only + type, name, default, description = content.match(/^\{(.+?)\} \[?([\w.$]+?)(?:=(.+?))?\]?( .+)?$/).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 + type, description = content.match(/^\{(.+?)\}( .+)?$/).captures + data[:return] = {type: cleanup_class_name(type), description: description || ''} + previous_item = data[:return] + when :php + type, description = content.match(/^(\S+)( .+)?$/).captures + data[:return] = {type: cleanup_class_name(type), description: description || ''} + previous_item = data[:return] + end + when 'private' + data[:visibility] = :private + when 'protected' + data[:visibility] = :protected + when 'static' + data[:static] = true + when 'abstract' + data[:abstract] = true + when 'ignore' + ignore = true + when 'inheritable', 'deprecated', 'singleton', 'throws', + 'chainable', 'fires', 'localdoc', 'inheritdoc', 'member', + 'see' + # skip + else + fail "unrecognized keyword: #{keyword}" + end + } + + next if ignore + + if code_line && code_line.strip != '' + case filetype + when :js + m = code_line.match(/(?:(static|prototype)\.)?(\w+) =/) + kind_, name = m.captures + data[:static] = true if kind_ == 'static' + kind = {'static' => :property, 'prototype' => :method}[ kind_.strip ] if kind_ && !kind + data[:name] = cleanup_class_name(name) + when :php + m = code_line.match(/ + \s* + (?:(public|protected|private)\s)? + (?:(static)\s)?(function\s|class\s|\$) + (\w+) + (?:\sextends\s(\w+))? + /x) + visibility, static, kind_, name, parent = m.captures + kind = {'$' => :property, 'function' => :method, 'class' => :class}[ kind_.strip ] + data[:visibility] = {'private' => :private, 'protected' => :protected, 'public' => :public}[ visibility ] || :public + data[:static] = true if static + data[:parent] = cleanup_class_name(parent) if parent + data[:name] = cleanup_class_name(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 + if data[:name] == '__construct' || js_class_constructor + data[:name] = '#constructor' + end + + # put into the current class + if kind && kind != :class + keys = { + method: :methods, + property: :properties, + event: :events, + } + current_class[keys[kind]] << data.select{|k, _v| valid_per_kind[kind].include? k } + previous_item = current_class[keys[kind]] + 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(/\$this->mixin\( .*?new (\w+)\( \$this/).flatten.map(&method(:cleanup_class_name)) + end + + output << current_class if current_class + output +end + +def parse_any_path path + if File.directory? path + parse_dir path + else + parse_file path + end +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 diff --git a/vendor/oojs/oojs-ui/bin/generate-JSPHP-for-karma.php b/vendor/oojs/oojs-ui/bin/generate-JSPHP-for-karma.php new file mode 100644 index 00000000..445da65c --- /dev/null +++ b/vendor/oojs/oojs-ui/bin/generate-JSPHP-for-karma.php @@ -0,0 +1,50 @@ +<?php + +// Quick and dirty autoloader to make it possible to run without Composer. +spl_autoload_register( function ( $class ) { + $class = preg_replace( '/^OOUI\\\\/', '', $class ); + foreach ( array( 'elements', 'layouts', 'themes', 'widgets', '.' ) as $dir ) { + $path = "../php/$dir/$class.php"; + if ( file_exists( $path ) ) { + require_once $path; + return; + } + } +} ); + +$testSuiteJSON = file_get_contents( 'JSPHP-suite.json' ); +$testSuite = json_decode( $testSuiteJSON, true ); +$testSuiteOutput = array(); + +function new_OOUI( $class, $config = array() ) { + $class = "OOUI\\" . $class; + return new $class( $config ); +} +function unstub( &$value ) { + if ( is_string( $value ) && substr( $value, 0, 13 ) === '_placeholder_' ) { + $value = json_decode( substr( $value, 13 ), true ); + array_walk_recursive( $value['config'], 'unstub' ); + $value = new_OOUI( $value['class'], $value['config'] ); + } +} +// Keep synchronized with tests/index.php +$themes = array( 'ApexTheme', 'MediaWikiTheme' ); +foreach ( $themes as $theme ) { + OOUI\Theme::setSingleton( new_OOUI( $theme ) ); + foreach ( $testSuite as $className => $tests ) { + foreach ( $tests as $test ) { + // Unstub placeholders + $config = $test['config']; + array_walk_recursive( $config, 'unstub' ); + $config['infusable'] = true; + $instance = new_OOUI( $test['class'], $config ); + $testSuiteOutput[$theme][$className][] = "$instance"; + } + } +} + +$testSuiteOutputJSON = json_encode( $testSuiteOutput, JSON_PRETTY_PRINT ); + +echo "var testSuiteConfigs = $testSuiteJSON;\n\n"; +echo "var testSuitePHPOutput = $testSuiteOutputJSON;\n\n"; +echo file_get_contents( 'JSPHP.test.karma.js' ); diff --git a/vendor/oojs/oojs-ui/bin/testsuitegenerator.rb b/vendor/oojs/oojs-ui/bin/testsuitegenerator.rb new file mode 100644 index 00000000..28ab1a85 --- /dev/null +++ b/vendor/oojs/oojs-ui/bin/testsuitegenerator.rb @@ -0,0 +1,146 @@ +require 'pp' +require_relative 'docparser' + +if ARGV.empty? || ARGV == ['-h'] || ARGV == ['--help'] + $stderr.puts "usage: ruby #{$PROGRAM_NAME} <dirA> <dirB>" + $stderr.puts " ruby #{$PROGRAM_NAME} src php > tests/JSPHP-suite.json" +else + dir_a, dir_b = ARGV + js = parse_any_path dir_a + php = parse_any_path dir_b + + class_names = (js + php).map{|c| c[:name] }.sort.uniq + + tests = [] + classes = php.select{|c| class_names.include? c[:name] } + + testable_classes = classes + .reject{|c| c[:abstract] } # can't test abstract classes + .reject{|c| !c[:parent] || c[:parent] == 'ElementMixin' || c[:parent] == 'Theme' } # can't test abstract + .reject{|c| %w[Element Widget Layout Theme].include? c[:name] } # no toplevel + .reject{|c| c[:name] == 'DropdownInputWidget' } # different PHP and JS implementations + + # values to test for each type + expandos = { + 'null' => [nil], + 'number' => [0, -1, 300], + 'boolean' => [true, false], + 'string' => ['Foo bar', '<b>HTML?</b>'], + } + + # values to test for names + sensible_values = { + 'href' => ['http://example.com/'], + ['TextInputWidget', 'type'] => %w[text password], + ['ButtonInputWidget', 'type'] => %w[button input], + ['FieldLayout', 'help'] => true, # different PHP and JS implementations + ['FieldsetLayout', 'help'] => true, # different PHP and JS implementations + 'type' => %w[text button], + 'method' => %w[GET POST], + 'action' => [], + 'enctype' => true, + 'target' => ['_blank'], + 'accessKey' => ['k'], + 'name' => true, + 'autofocus' => true, # usually makes no sense in JS + 'tabIndex' => [-1, 0, 100], + 'icon' => ['picture'], + 'indicator' => ['down'], + 'flags' => %w[constructive], + 'label' => expandos['string'] + ['', ' '], + # these are defined by Element and would bloat the tests + 'classes' => true, + 'id' => true, + 'content' => true, + 'text' => true, + } + + find_class = lambda do |klass| + return classes.find{|c| c[:name] == klass } + end + + expand_types_to_values = lambda do |types| + return types.map{|t| + as_array = true if t.sub! '[]', '' + t = 'ButtonWidget' if t == 'Widget' # Widget is not "testable", use a subclass + if expandos[t] + # Primitive. Run tests with the provided values. + vals = expandos[t] + elsif testable_classes.find{|c| c[:name] == t } + # OOUI object. Test suite will instantiate one and run the test with it. + params = find_class.call(t)[:methods][0][:params] || [] + config = params.map{|config_option| + types = config_option[:type].split '|' + values = expand_types_to_values.call(types) + { config_option[:name] => values[0] } + } + vals = [ '_placeholder_' + { + class: t, + config: config.inject({}, :merge) + }.to_json ] + else + # We don't know how to test this. The empty value will result in no + # tests being generated for this combination of config values. + vals = [] + end + as_array ? vals.map{|v| [v] } : vals + }.inject(:+) + end + + find_config_sources = lambda do |klass_name| + return [] unless klass_name + klass_names = [klass_name] + while klass_name + klass = find_class.call(klass_name) + break unless klass + klass_names += + find_config_sources.call(klass[:parent]) + + klass[:mixins].map(&find_config_sources).flatten + klass_name = klass[:parent] + end + return klass_names.uniq + end + + testable_classes.each do |klass| + config_sources = find_config_sources.call(klass[:name]) + .map{|c| find_class.call(c)[:methods][0] } + config = config_sources.map{|c| c[:config] }.compact.inject(:+) + required_config = klass[:methods][0][:params] || [] + + # generate every possible configuration of configuration option sets + maxlength = [config.length, 2].min + config_combinations = (0..maxlength).map{|l| config.combination(l).to_a }.inject(:+) + # for each set, generate all possible values to use based on option's type + config_combinations = config_combinations.map{|config_comb| + config_comb += required_config + expanded = config_comb.map{|config_option| + types = config_option[:type].split '|' + sensible = sensible_values[ [ klass[:name], config_option[:name] ] ] || + sensible_values[ config_option[:name] ] + if sensible == true + [] # the empty value will result in no tests being generated + else + values = sensible || expand_types_to_values.call(types) + values.map{|v| config_option.dup.merge(value: v) } + [nil] + end + } + expanded.length > 0 ? expanded[0].product(*expanded[1..-1]) : [] + }.inject(:concat).map(&:compact).uniq + + # really require the required ones + config_combinations = config_combinations.select{|config_comb| + required_config.all?{|r| config_comb.find{|c| c[:name] == r[:name] } } + } + + config_combinations.each do |config_comb| + tests << { + class: klass[:name], + config: Hash[ config_comb.map{|c| [ c[:name], c[:value] ] } ] + } + end + end + + tests = tests.group_by{|t| t[:class] } + + puts JSON.pretty_generate tests +end |