WMDE-leszek has submitted this change and it was merged.

Change subject: Initial commit.
......................................................................


Initial commit.

This turns the prototype code of the Revision Slider into a MediaWiki
extension.
With this extension enabled the slider should appear on diff pages
already but it won't actually show diffs when adjusting the slider
pointers.

Things that changed so far in the prototype JS code:
- pulled out the rainbow function and made it use Math.floor instead of
 the ~~ binary operator
- pulled out the API request that fetches the revisions into its own
 module
- cleaned up and documented some parts of the code

The code is still very much WIP and JSCS still has a couple of
complaints.

Bug: T132576
Change-Id: I2e22365f3b93a76d5b8d3997242b5fed996c6d78
---
A .gitignore
A .jscsrc
A .jshintignore
A .jshintrc
A Gruntfile.js
A RevisionSlider.hooks.php
A RevisionSlider.php
A composer.json
A extension.json
A i18n/en.json
A i18n/qqq.json
A modules/ext.RevisionSlider.css
A modules/ext.RevisionSlider.fetchRevisions.js
A modules/ext.RevisionSlider.init.js
A modules/ext.RevisionSlider.rainbow.js
A package.json
A phpcs.xml
A tests/RevisionSlider.test.js
A tests/RevisionSlider.test.php
19 files changed, 702 insertions(+), 0 deletions(-)

Approvals:
  WMDE-Fisch: Verified; Looks good to me, but someone else must approve
  WMDE-leszek: Verified; Looks good to me, approved
  jenkins-bot: Verified



diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fbc2c3f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+.idea/
+composer.lock
+node_modules/
+vendor/
diff --git a/.jscsrc b/.jscsrc
new file mode 100755
index 0000000..a031ea4
--- /dev/null
+++ b/.jscsrc
@@ -0,0 +1,3 @@
+{
+       "preset": "wikimedia"
+}
\ No newline at end of file
diff --git a/.jshintignore b/.jshintignore
new file mode 100755
index 0000000..b512c09
--- /dev/null
+++ b/.jshintignore
@@ -0,0 +1 @@
+node_modules
\ No newline at end of file
diff --git a/.jshintrc b/.jshintrc
new file mode 100755
index 0000000..69882aa
--- /dev/null
+++ b/.jshintrc
@@ -0,0 +1,22 @@
+{
+       // Enforcing
+       "bitwise": true,
+       "eqeqeq": true,
+       "es3": true,
+       "latedef": true,
+       "noarg": true,
+       "nonew": true,
+       "undef": true,
+       "unused": true,
+       "strict": false,
+
+       // Environment
+       "browser": true,
+
+       "globals": {
+               "mw": false,
+               "$": false,
+               "mediaWiki": false,
+               "jQuery": false
+       }
+}
diff --git a/Gruntfile.js b/Gruntfile.js
new file mode 100755
index 0000000..56ff218
--- /dev/null
+++ b/Gruntfile.js
@@ -0,0 +1,35 @@
+/*jshint node:true */
+module.exports = function ( grunt ) {
+       grunt.loadNpmTasks( 'grunt-contrib-jshint' );
+       grunt.loadNpmTasks( 'grunt-jsonlint' );
+       grunt.loadNpmTasks( 'grunt-banana-checker' );
+       grunt.loadNpmTasks( 'grunt-jscs' );
+
+       grunt.initConfig( {
+               jshint: {
+                       options: {
+                               jshintrc: true
+                       },
+                       all: [
+                               '*.js',
+                               'modules/**/*.js'
+                       ]
+               },
+               jscs: {
+                       src: '<%= jshint.all %>'
+               },
+               banana: {
+                       all: 'i18n/'
+               },
+               jsonlint: {
+                       all: [
+                               '*.json',
+                               '**/*.json',
+                               '!node_modules/**'
+                       ]
+               }
+       } );
+
+       grunt.registerTask( 'test', [ 'jshint', 'jscs', 'jsonlint', 'banana' ] 
);
+       grunt.registerTask( 'default', 'test' );
+};
diff --git a/RevisionSlider.hooks.php b/RevisionSlider.hooks.php
new file mode 100755
index 0000000..0efe5a3
--- /dev/null
+++ b/RevisionSlider.hooks.php
@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * RevisionSlider extension hooks
+ *
+ * @file
+ * @ingroup Extensions
+ * @license GPL-2.0+
+ */
+class RevisionSliderHooks {
+       public static function onBeforePageDisplay( OutputPage $out, Skin $skin 
) {
+               if ( self::isRevisionPage( $skin->getContext()->getRequest() ) 
) {
+                       $out->addModules( 'ext.RevisionSlider.init' );
+               }
+       }
+
+       private static function isRevisionPage( WebRequest $request ) {
+               return $request->getVal( 'action' ) === 'history' || 
$request->getVal( 'type' ) === 'revision';
+       }
+}
diff --git a/RevisionSlider.php b/RevisionSlider.php
new file mode 100644
index 0000000..6df2e0a
--- /dev/null
+++ b/RevisionSlider.php
@@ -0,0 +1,36 @@
+<?php
+/**
+ * Revision Slider MediaWiki Extension
+ */
+
+$wgAutoloadClasses['RevisionSliderHooks'] = __DIR__ . 
'/RevisionSlider.hooks.php';
+$wgHooks['BeforePageDisplay'][] = 'RevisionSliderHooks::onBeforePageDisplay';
+
+$wgResourceModules['ext.RevisionSlider.init'] = [
+       'scripts' => [
+               'modules/ext.RevisionSlider.init.js',
+       ],
+       'styles' => [
+               'modules/ext.RevisionSlider.css',
+       ],
+       'dependencies' => [
+               'ext.RevisionSlider.rainbow',
+               'ext.RevisionSlider.fetchRevisions',
+       ],
+
+       'localBasePath' => __DIR__,
+];
+
+$wgResourceModules['ext.RevisionSlider.rainbow'] = [
+       'scripts' => [
+               'modules/ext.RevisionSlider.rainbow.js',
+       ],
+       'localBasePath' => __DIR__,
+];
+
+$wgResourceModules['ext.RevisionSlider.fetchRevisions'] = [
+       'scripts' => [
+               'modules/ext.RevisionSlider.fetchRevisions.js',
+       ],
+       'localBasePath' => __DIR__,
+];
diff --git a/composer.json b/composer.json
new file mode 100755
index 0000000..a35cc39
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,13 @@
+{
+       "require-dev": {
+               "jakub-onderka/php-parallel-lint": "0.9",
+               "mediawiki/mediawiki-codesniffer": "0.4.0"
+       },
+       "scripts": {
+               "test": [
+                       "parallel-lint . --exclude vendor",
+                       "phpcs -sp"
+               ],
+               "fix": "phpcbf"
+       }
+}
diff --git a/extension.json b/extension.json
new file mode 100755
index 0000000..8fb3887
--- /dev/null
+++ b/extension.json
@@ -0,0 +1,54 @@
+{
+       "name": "RevisionSlider",
+       "version": "0.0.0",
+       "author": [
+               "WMDE"
+       ],
+       "url": "https://phabricator.wikimedia.org/project/profile/1639/";,
+       "namemsg": "revisionSlider",
+       "descriptionmsg": "revisionSlider-desc",
+       "license-name": "GPL-2.0+",
+       "type": "other",
+       "manifest_version": 1,
+       "MessageDirs": {
+               "RevisionSlider": [
+                       "i18n"
+               ]
+       },
+       "AutoloadClasses": {
+               "RevisionSliderHooks": "RevisionSlider.hooks.php"
+       },
+       "ResourceModules": {
+               "ext.RevisionSlider.init": {
+                       "scripts": [
+                               "modules/ext.RevisionSlider.init.js"
+                       ],
+                       "styles": [
+                               "modules/ext.RevisionSlider.css"
+                       ],
+                       "dependencies": [
+                               "ext.RevisionSlider.rainbow",
+                               "ext.RevisionSlider.fetchRevisions"
+                       ],
+                       "position": "top"
+               },
+               "ext.RevisionSlider.rainbow": {
+                       "scripts": [
+                               "modules/ext.RevisionSlider.rainbow.js"
+                       ]
+               },
+               "ext.RevisionSlider.fetchRevisions": {
+                       "scripts": [
+                               "modules/ext.RevisionSlider.fetchRevisions.js"
+                       ]
+               }
+       },
+       "ResourceFileModulePaths": {
+               "localBasePath": ""
+       },
+       "Hooks": {
+               "BeforePageDisplay": [
+                       "RevisionSliderHooks::onBeforePageDisplay"
+               ]
+       }
+}
diff --git a/i18n/en.json b/i18n/en.json
new file mode 100755
index 0000000..173a79f
--- /dev/null
+++ b/i18n/en.json
@@ -0,0 +1,9 @@
+{
+    "@metadata": {
+        "authors": [
+            "WMDE"
+        ]
+    },
+    "revisionSlider": "RevisionSlider",
+    "revisionSlider-desc": ""
+}
\ No newline at end of file
diff --git a/i18n/qqq.json b/i18n/qqq.json
new file mode 100755
index 0000000..7e056b5
--- /dev/null
+++ b/i18n/qqq.json
@@ -0,0 +1,9 @@
+{
+    "@metadata": {
+        "authors": [
+            "WMDE"
+        ]
+    },
+    "revisionSlider": "Revision Slider",
+    "revisionSlider-desc": "{{desc|name=RevisionSlider|url=}}"
+}
diff --git a/modules/ext.RevisionSlider.css b/modules/ext.RevisionSlider.css
new file mode 100755
index 0000000..3aeaa12
--- /dev/null
+++ b/modules/ext.RevisionSlider.css
@@ -0,0 +1,98 @@
+.ui-slider-tick-mark {
+    display:   inline-block;
+    border:    1px solid black;
+    height:            9px;
+    position:  absolute;
+    top:               -3px;
+    opacity:   0.35;
+    z-index:   10;
+}
+
+.ui-slider-tick-mark:hover {
+    opacity: 0.65!important;
+    -webkit-transition: all ease 0.2s;
+    -moz-transition: all ease 0.2s;
+    -o-transition: all ease 0.2s;
+    transition: all ease 0.2s;
+}
+
+.rvslider-legend-box {
+    opacity: 0.35;
+    font-size: 1.4em;
+}
+
+.revisions {
+    height: 150px;
+}
+.revisions-container {
+    width: 90%;
+    position: relative;
+    overflow: hidden;
+}
+.revisions-container, .arrow {
+    float: left;
+}
+.arrow {
+    width: 5%;
+    margin-top: 63px;
+}
+.left-arrow {
+    width: 0;
+    height: 0;
+    margin-right: 20px;
+    border-style: solid;
+    border-width: 12px 18px 12px 0;
+    border-color: transparent #424242 transparent transparent;
+}
+.right-arrow {
+    margin-left: 20px;
+    width: 0;
+    height: 0;
+    border-style: solid;
+    border-width: 12px 0 12px 18px;
+    border-color: transparent transparent transparent #424242;
+}
+
+.revision {
+    width: 1%;
+    margin-top: 75px;
+}
+
+.stopper {
+    position: absolute;
+    top: 50%;
+    margin-left: -10px;
+    height: 50px;
+    width: 0;
+}
+
+.slider {
+    position: relative;
+}
+.revision-slider {
+    position: relative;
+    width: 100%;
+    margin: 0 auto;
+}
+
+.pointer-container {
+    position: absolute;
+    clear: both;
+    width: 100%;
+    top: 77px;
+    z-index: 10;
+}
+.left-pointer {
+    border-color: transparent transparent #00f transparent;
+}
+.right-pointer {
+    border-color: transparent transparent #f00 transparent;
+}
+.pointer {
+    position: absolute !important;
+    z-index: 11;
+    width: 0;
+    height: 0;
+    border-style: solid;
+    border-width: 0 10px 10px 10px;
+}
diff --git a/modules/ext.RevisionSlider.fetchRevisions.js 
b/modules/ext.RevisionSlider.fetchRevisions.js
new file mode 100644
index 0000000..933cf23
--- /dev/null
+++ b/modules/ext.RevisionSlider.fetchRevisions.js
@@ -0,0 +1,26 @@
+( function ( mw, $ ) {
+       mw.libs.revisionSlider = mw.libs.revisionSlider || {};
+
+       /**
+        * Fetches up to 500 revisions at a time
+        *
+        * @param {{}} options - Options containing success callback, pageName 
and startId
+        */
+       mw.libs.revisionSlider.fetchRevisions = function ( options ) {
+               $.ajax( {
+                       url: mw.util.wikiScript( 'api' ),
+                       data: {
+                               action: 'query',
+                               prop: 'revisions',
+                               format: 'json',
+                               rvprop: 
'ids|timestamp|user|comment|parsedcomment|size|flags',
+                               titles: options.pageName,
+                               formatversion: 2,
+                               rvstartid: options.startId,
+                               'continue': '',
+                               rvlimit: 500
+                       },
+                       success: options.success
+               } );
+       };
+}( mediaWiki, jQuery ) );
diff --git a/modules/ext.RevisionSlider.init.js 
b/modules/ext.RevisionSlider.init.js
new file mode 100755
index 0000000..bc13c64
--- /dev/null
+++ b/modules/ext.RevisionSlider.init.js
@@ -0,0 +1,281 @@
+( function ( mw, $ ) {
+       var revisionWidth = 10,
+               $container = null,
+               $revisionSlider = null,
+               revs = [],
+               pointerPosL = -1,
+               pointerPosR = -1;
+
+       // Function called when a tick on the slider is clicked
+       // Params: v1 - Left revision ID; v2 - Right revision ID
+       // function refresh( v1, v2 ) {
+       //      if( v1 === -1 || v2 === -1 ) return;
+       //
+       //      var $url = gServer + gScript + '?title=' + gPageName + '&diff=' 
+ v2 + '&oldid=' + v1;
+       //      location.href = $url;
+       // }
+       
+       // Formating date in JS
+       function formatDate( rawDate ) {
+               var months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'June', 
'July', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec' ],
+                       f = new Date( rawDate ),
+                       fDate = f.getUTCDate(),
+                       fMonth = f.getUTCMonth(),
+                       fYear = f.getUTCFullYear(),
+                       fHours = ( '0' + f.getUTCHours()).slice( -2 ),
+                       fMinutes = ( '0' + f.getUTCMinutes()).slice( -2 );
+
+               return ( fHours + ':' + fMinutes + ', ' + fDate + ' ' + months[ 
fMonth ] + ' ' + fYear ).toString();
+       }
+
+       function getSection( text ) {
+               text = text.match(
+                       new RegExp( '(/\\* [^\\*]* \\*/)', 'gi' )
+               );
+               if ( !text ) {
+                       return '';
+               }
+               return text[ 0 ].replace(
+                       new RegExp( ' \\*/|/\\* ', 'ig' ),
+                       ''
+               );
+       }
+
+       function getComposedRevData( revs ) {
+               var max = 0,
+                       changeSize = 0,
+                       section,
+                       sectionMap = {},
+                       result;
+
+               for ( var i = 1; i < revs.length; i++ ) {
+                       changeSize = Math.abs( revs[ i ].size - revs[ i - 1 
].size );
+                       section = getSection( revs[ i ].comment );
+                       if ( changeSize > max ) {
+                               max = changeSize;
+                       }
+                       if ( section.length > 0 && !(section in sectionMap) ) {
+                               sectionMap[ section ] = '';
+                       }
+               }
+
+               i = 0;
+               for ( var sectionName in sectionMap ) {
+                       sectionMap[ sectionName ] = 
mw.libs.revisionSlider.rainbow( Object.keys( sectionMap ).length, i );
+                       i++;
+               }
+
+               result = {
+                       maxChangeSize: max,
+                       sectionMap: sectionMap
+               };
+
+               return result;
+       }
+
+       // Setting the tick marks on the slider
+       // Params: element - jQuery slider; revs - revisions data from API
+       function setSliderTicks( element, revs ) {
+               var $slider = $( element ),
+                       revData = getComposedRevData( revs ),
+                       maxChangeSizeLogged = Math.log( revData.maxChangeSize );
+
+               for ( var i = 1; i < revs.length; i++ ) {
+                       var diffSize = revs[ i ].size - revs[ i - 1 ].size,
+                               relativeChangeSize = Math.ceil( 65.0 * 
Math.log( Math.abs( diffSize ) ) / maxChangeSizeLogged ) + 5,
+                               section = getSection( revs[ i ].comment ),
+                               html = '<b>' + formatDate( revs[ i ].timestamp 
) + '</b><br>';
+
+                       html += mw.html.escape( revs[ i ].user ) + '<br>';
+                       if ( revs[ i ].comment !== '' ) {
+                               html += '<br><i>' + mw.html.escape( revs[ i 
].parsedcomment ) + '</i>';
+                       }
+                       //html += '<br> ' + revs[i].minor ? '<b>K</b> ' : '';
+                       html += '<br>' + diffSize + ' byte';
+                       $( '<div class="ui-slider-tick-mark revision" 
title="<center>' + html + '</center>"/>' )
+                               .css( {
+                                       'left': i + '%',
+                                       'height': relativeChangeSize + 'px',
+                                       'top': diffSize > 0 ? '-' + 
relativeChangeSize + 'px' : 0,
+                                       'background': revData.sectionMap[ 
section ] || 'black'
+                                       //'opacity' : revs[i].minor ? 0.15 : 
0.35
+                                       //'background' : i%2 == 1 ? 'white' : 
'black'
+                               } )
+                               .tipsy( {
+                                       gravity: 's',
+                                       html: true,
+                                       fade: true
+                               } )
+                               .appendTo( $slider );
+                       $( '<div class="stopper"/>' )
+                               .css( 'left', (i - 1) + '.5%' )
+                               .appendTo( $slider );
+               }
+       }
+
+       /**
+        * Checks whether pointerPos is between start and end
+        *
+        * @param {int} pointerPos
+        * @param {int} start
+        * @param {int} end
+        * @returns {boolean}
+        */
+       function isPointerInRange( pointerPos, start, end ) {
+               return pointerPos >= start &&
+                       pointerPos <= Math.min( revs.length, end );
+       }
+
+       /**
+        * Slowly slides/scrolls $container one viewport in a given direction
+        *
+        * @param {jQuery} $container
+        * @param {int} direction
+        */
+       function slide( $container, direction ) {
+               $container.animate( {
+                       scrollLeft: $container.scrollLeft() + 
($container.width() * direction)
+               } );
+       }
+
+       /**
+        * Determines the revision from a position in the revisionsContainer
+        *
+        * @param {int} pos
+        * @returns {number}
+        */
+       function revisionOfPosition( pos ) {
+               return Math.floor( pos / revisionWidth );
+       }
+
+       /**
+        * Slides a $pointer to a position
+        *
+        * @param {jQuery} $pointer
+        * @param {int} pos
+        */
+       function slideToPosition( $pointer, pos ) {
+               var containerOffset = $container.offset().left - 
$revisionSlider.offset().left,
+                       left = (pos % 100) * revisionWidth;
+
+               $pointer.animate( { left: left + containerOffset } );
+       }
+
+       /**
+        * Slides a $pointer to the side of the slider
+        *
+        * @param {jQuery} $pointer
+        * @param {int} pointerPos
+        * @param {int} direction
+        */
+       function slideToSide( $pointer, pointerPos, direction ) {
+               var containerOffset = $revisionSlider.find( '.arrow' 
).outerWidth() + 20, // 20 == margin right
+                       isLeft = pointerPos < revisionOfPosition( 
$container.scrollLeft() ) + direction * revisionOfPosition( $container.width() 
),
+                       sideFactor = isLeft ? -1 : 1,
+                       sideOffset = 3 * revisionWidth * sideFactor / 2,
+                       offsetRight = $pointer.hasClass( 'left-pointer' ) ? 
-revisionWidth : 0,
+                       xPos = isLeft ? containerOffset : $container.width() + 
containerOffset;
+
+               $pointer.animate( { left: xPos + offsetRight + sideOffset } );
+       }
+
+       function getSectionLegend( revs ) {
+               var revData = getComposedRevData( revs ),
+                       html = '';
+               for ( var sectionName in revData.sectionMap ) {
+                       html += '<span class="rvslider-legend-box" 
style="color:' + revData.sectionMap[ sectionName ] + ';"> ■</span>' + 
sectionName + '';
+               }
+               return html;
+       }
+
+       function addSlider( revs ) {
+               var $revisions = $( '<div class="revisions"></div>' ).css( 
'width', revs.length * revisionWidth ),
+                       $leftPointer = $( '<div class="pointer left-pointer" 
/>' ),
+                       $rightPointer = $( '<div class="pointer right-pointer" 
/>' );
+
+               $revisionSlider = $( '<div class="revision-slider" />' )
+                       .append( $( '<a class="arrow left-arrow" 
data-dir="-1"></a>' ) )
+                       .append( $( '<div class="revisions-container" />' 
).append( $revisions ) )
+                       .append( $( '<a class="arrow right-arrow" 
data-dir="1"></a>' ) )
+                       .append( $( '<div style="clear: both" />' ) )
+                       .append(
+                               $( '<div class="pointer-container" />' )
+                                       .append( $leftPointer )
+                                       .append( $rightPointer )
+                       );
+
+               $container = $revisionSlider.find( '.revisions-container' );
+
+               $revisionSlider.find( '.arrow' ).click( function () {
+                       var direction = $( this ).data( 'dir' ),
+                               newStart = revisionOfPosition(
+                                               Math.min( $container.find( 
'.revisions' ).width() - $container.width(), Math.max( 0, 
$container.scrollLeft() ) )
+                                       ) + ( direction * revisionOfPosition( 
$container.width() ) ),
+                               newEnd = newStart + revisionOfPosition( 
$container.width() );
+
+                       if ( isPointerInRange( pointerPosL, newStart, newEnd ) 
) {
+                               slideToPosition( $leftPointer, pointerPosL );
+                       } else {
+                               slideToSide( $leftPointer, pointerPosL, 
direction );
+                       }
+
+                       if ( isPointerInRange( pointerPosR, newStart, newEnd ) 
) {
+                               slideToPosition( $rightPointer, pointerPosR );
+                       } else {
+                               slideToSide( $rightPointer, pointerPosR, 
direction );
+                       }
+
+                       slide( $container, direction );
+               } );
+
+               setSliderTicks( $revisions, revs );
+
+               $revisionSlider.find( '.pointer' ).draggable( {
+                       axis: 'x',
+                       snap: '.stopper',
+                       containment: '.revisions-container',
+                       stop: function () {
+                               var posLeft = parseInt( $( this ).css( 'left' 
), 10 ),
+                                       offset = $revisionSlider.find( '.arrow' 
).outerWidth() + 20,
+                                       pos = Math.round( (posLeft + 
$container.scrollLeft() - offset) / revisionWidth );
+
+                               if ( $( this ).hasClass( 'left-pointer' ) ) {
+                                       pointerPosL = pos;
+                               } else {
+                                       pointerPosR = pos;
+                               }
+
+                               // refresh( pointerPosL, pointerPosR );
+                       }
+               } );
+
+               var $html = $( '<td colspan="4" style="text-align:center;" 
class="slider"></td>' ).append( $revisionSlider );
+               var $html2 = $( '<tr>' ).append( $html );
+               var $legendHtml = $( '<td colspan="4" style="text-align:center; 
font-size: 0.75em; padding: 1em 0 0.5em;"></td>' ).append( getSectionLegend( 
revs ) );
+               $( '.diff > tbody > tr' ).eq( 0 ).after( $legendHtml ).after( 
$html2 );
+
+               revisionWidth = $( '.slider' ).width() * 90 / 10000;
+               slideToSide( $leftPointer, -1, 1 );
+               slideToSide( $rightPointer, -1, 1 );
+       }
+
+       mw.loader.using( [ 'jquery.ui.draggable', 'jquery.ui.tooltip', 
'jquery.tipsy' ], function () {
+               $( function () {
+                       mw.libs.revisionSlider.fetchRevisions( {
+                               pageName: mw.config.get( 'wgPageName' ),
+                               startId: mw.config.get( 'wgCurRevisionId' ),
+
+                               success: function ( data ) {
+                                       revs = data.query.pages[ 0 ].revisions;
+                                       if ( !revs ) {
+                                               return;
+                                       }
+                                       revs.reverse();
+
+                                       addSlider( revs );
+                               }
+                       } );
+               } );
+       } );
+
+}( mediaWiki, jQuery ) );
diff --git a/modules/ext.RevisionSlider.rainbow.js 
b/modules/ext.RevisionSlider.rainbow.js
new file mode 100644
index 0000000..f71304e
--- /dev/null
+++ b/modules/ext.RevisionSlider.rainbow.js
@@ -0,0 +1,52 @@
+( function ( mw ) {
+       mw.libs.revisionSlider = mw.libs.revisionSlider || {};
+
+       // see http://stackoverflow.com/a/7419630/4782503
+       mw.libs.revisionSlider.rainbow = function ( numOfSteps, step ) {
+               // This function generates vibrant, "evenly spaced" colours 
(i.e. no clustering). This is ideal for creating easily distinguishable vibrant 
markers in Google Maps and other apps.
+               // Adam Cole, 2011-Sept-14
+               // HSV to RBG adapted from: 
http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
+               var r, g, b,
+                       c,
+                       h = step / numOfSteps,
+                       i = Math.floor( h * 6 ),
+                       f = h * 6 - i,
+                       q = 1 - f;
+
+               switch ( i % 6 ) {
+                       case 0:
+                               r = 1;
+                               g = f;
+                               b = 0;
+                               break;
+                       case 1:
+                               r = q;
+                               g = 1;
+                               b = 0;
+                               break;
+                       case 2:
+                               r = 0;
+                               g = 1;
+                               b = f;
+                               break;
+                       case 3:
+                               r = 0;
+                               g = q;
+                               b = 1;
+                               break;
+                       case 4:
+                               r = f;
+                               g = 0;
+                               b = 1;
+                               break;
+                       case 5:
+                               r = 1;
+                               g = 0;
+                               b = q;
+                               break;
+               }
+               c = '#' + ( '00' + Math.floor( r * 255 ).toString( 16 ) 
).slice( -2 ) + ( '00' + Math.floor( g * 255 ).toString( 16 ) ).slice( -2 ) + ( 
'00' + Math.floor( b * 255 ).toString( 16 ) ).slice( -2 );
+
+               return c;
+       };
+}( mediaWiki ) );
diff --git a/package.json b/package.json
new file mode 100755
index 0000000..d64fe9b
--- /dev/null
+++ b/package.json
@@ -0,0 +1,12 @@
+{
+  "name": "RevisionSlider",
+  "version": "0.0.0",
+  "devDependencies": {
+    "grunt": "0.4.5",
+    "grunt-cli": "0.1.13",
+    "grunt-contrib-jshint": "0.11.3",
+    "grunt-banana-checker": "0.4.0",
+    "grunt-jscs": "2.1.0",
+    "grunt-jsonlint": "1.0.4"
+  }
+}
\ No newline at end of file
diff --git a/phpcs.xml b/phpcs.xml
new file mode 100644
index 0000000..e12d0ec
--- /dev/null
+++ b/phpcs.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<ruleset>
+    <rule ref="vendor/mediawiki/mediawiki-codesniffer/MediaWiki"/>
+    <file>.</file>
+    <arg name="extensions" value="php,php5,inc"/>
+    <arg name="encoding" value="utf8"/>
+    <exclude-pattern>vendor</exclude-pattern>
+</ruleset>
diff --git a/tests/RevisionSlider.test.js b/tests/RevisionSlider.test.js
new file mode 100755
index 0000000..34dcf99
--- /dev/null
+++ b/tests/RevisionSlider.test.js
@@ -0,0 +1,12 @@
+( function ( mw, $ ) {
+       QUnit.module( 'ext.revisionSlider' );
+
+       /**
+        * Write your QUnit tests here. For more information on
+        * how to write proper JavaScript QUnit tests for
+        * MediaWiki extension development, please read
+        * the manual:
+        * 
https://www.mediawiki.org/wiki/Manual:JavaScript_unit_testing#Write_a_unit_test
+        */
+
+} )( mediaWiki, jQuery );
diff --git a/tests/RevisionSlider.test.php b/tests/RevisionSlider.test.php
new file mode 100755
index 0000000..b7a9013
--- /dev/null
+++ b/tests/RevisionSlider.test.php
@@ -0,0 +1,7 @@
+<?php
+
+/**
+ * For more information on how to create PHPUnit tests
+ * for your extension, visit the documentation page:
+ * https://www.mediawiki.org/wiki/Manual:PHP_unit_testing/Writing_unit_tests
+ */

-- 
To view, visit https://gerrit.wikimedia.org/r/284173
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I2e22365f3b93a76d5b8d3997242b5fed996c6d78
Gerrit-PatchSet: 5
Gerrit-Project: mediawiki/extensions/RevisionSlider
Gerrit-Branch: master
Gerrit-Owner: Jakob <jakob.warkot...@wikimedia.de>
Gerrit-Reviewer: Addshore <addshorew...@gmail.com>
Gerrit-Reviewer: Legoktm <legoktm.wikipe...@gmail.com>
Gerrit-Reviewer: Siebrand <siebr...@kitano.nl>
Gerrit-Reviewer: Tobias Gritschacher <tobias.gritschac...@wikimedia.de>
Gerrit-Reviewer: WMDE-Fisch <christoph.fisc...@wikimedia.de>
Gerrit-Reviewer: WMDE-leszek <leszek.mani...@wikimedia.de>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to