summaryrefslogtreecommitdiff
path: root/vendor/oojs/oojs-ui/bin
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/oojs/oojs-ui/bin')
-rw-r--r--vendor/oojs/oojs-ui/bin/doccomparer.rb165
-rw-r--r--vendor/oojs/oojs-ui/bin/docparser.rb243
-rw-r--r--vendor/oojs/oojs-ui/bin/generate-JSPHP-for-karma.php50
-rw-r--r--vendor/oojs/oojs-ui/bin/testsuitegenerator.rb146
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