Jdlrobson has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/182956

Change subject: Hygiene: Move JavaScript from Mantle to MobileFrontend
......................................................................

Hygiene: Move JavaScript from Mantle to MobileFrontend

Move all the files in Mantle here.
Rename mw.mantle to mw.mobileFrontend

Change-Id: Iab38441a9b961571d375e1d6b4456b4026385bce
---
M Gruntfile.js
M includes/Resources.php
A javascripts/Class.js
A javascripts/View.js
A javascripts/eventemitter.js
A javascripts/externals/hogan.js
A javascripts/hogan.js
M javascripts/modes.js
A javascripts/modules.js
D javascripts/template.js
A tests/qunit/test_Class.js
A tests/qunit/test_View.js
A tests/qunit/test_eventemitter.js
13 files changed, 1,318 insertions(+), 15 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/MobileFrontend 
refs/changes/56/182956/1

diff --git a/Gruntfile.js b/Gruntfile.js
index 703dbe9..c98b10b 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -26,8 +26,7 @@
                files: {
                        js: 'javascripts/**/*.js',
                        jsTests: 'tests/qunit/**/*.js',
-                       jsExternals: 'javascripts/externals/**/*.js',
-                       mantleJs: MW_INSTALL_PATH + 
'/extensions/Mantle/javascripts/**/*.js'
+                       jsExternals: 'javascripts/externals/**/*.js'
                },
                jshint: {
                        options: {
@@ -97,7 +96,7 @@
                },
                jsduck: {
                        main: {
-                               src: [ '<%= files.mantleJs %>', '<%= files.js 
%>', '!<%= files.jsExternals %>' ],
+                               src: [ '<%= files.js %>', '!<%= 
files.jsExternals %>' ],
                                dest: 'docs/js',
                                options: {
                                        'builtin-classes': true,
diff --git a/includes/Resources.php b/includes/Resources.php
index 6ecaed9..264b407 100644
--- a/includes/Resources.php
+++ b/includes/Resources.php
@@ -150,10 +150,11 @@
 $wgResourceModules = array_merge( $wgResourceModules, array(
        'mobile.templates' => $wgMFResourceFileModuleBoilerplate + array(
                'dependencies' => array(
-                       'ext.mantle.hogan',
+                       'mediawiki.template',
                ),
                'scripts' => array(
-                       'javascripts/template.js',
+                       'javascripts/externals/hogan.js',
+                       'javascripts/hogan.js',
                ),
        ),
 
@@ -216,13 +217,13 @@
                        'mobile.user',
                        'mediawiki.api',
                        'mobile.redlinks',
-                       'ext.mantle.views',
                ),
                'templates' => array(
                        'icon.hogan' => 'templates/icon.hogan',
                        'Section.hogan' => 'templates/Section.hogan',
                ),
                'scripts' => array(
+                       'javascripts/View.js',
                        'javascripts/Router.js',
                        'javascripts/OverlayManager.js',
                        // FIXME: Remove api code to mobile.ajax
@@ -1404,10 +1405,12 @@
                        'mediawiki.language',
                        'mediawiki.jqueryMsg',
                        'mobile.templates',
-                       'ext.mantle.modules',
-                       'ext.mantle.oo',
+                       'oojs',
                ),
                'scripts' => array(
+                       'javascripts/modules.js',
+                       'javascripts/Class.js',
+                       'javascripts/eventemitter.js',
                        'javascripts/modes.js',
                        'javascripts/browser.js',
                        'javascripts/mainmenu.js',
diff --git a/javascripts/Class.js b/javascripts/Class.js
new file mode 100644
index 0000000..84a6b60
--- /dev/null
+++ b/javascripts/Class.js
@@ -0,0 +1,45 @@
+/**
+ * @class Class
+ */
+( function( M ) {
+
+       /**
+        * Extends a class with new methods and member properties.
+        *
+        * @param {Object} prototype Prototype that should be incorporated into 
the new Class.
+        * @method
+        * @return {Class}
+        */
+       function extend( prototype ) {
+               var Parent = this, key;
+               function Child() {
+                       return Parent.apply( this, arguments );
+               }
+               OO.inheritClass( Child, Parent );
+               for ( key in prototype ) {
+                       Child.prototype[key] = prototype[key];
+               }
+               Child.extend = extend;
+               // FIXME: Use OOJS super here instead.
+               Child.prototype._parent = Parent.prototype;
+               return Child;
+       }
+
+       /**
+        * An extensible program-code-template for creating objects
+        *
+        * @class Class
+        */
+       function Class() {
+               this.initialize.apply( this, arguments );
+       }
+       /**
+        * Constructor, if you override it, use _super().
+        * @method
+        */
+       Class.prototype.initialize = function() {};
+       Class.extend = extend;
+
+       M.define( 'Class', Class );
+
+}( mw.mobileFrontend ) );
diff --git a/javascripts/View.js b/javascripts/View.js
new file mode 100644
index 0000000..9d39085
--- /dev/null
+++ b/javascripts/View.js
@@ -0,0 +1,311 @@
+( function( M, $ ) {
+
+       var EventEmitter = M.require( 'eventemitter' ),
+               View,
+               // Cached regex to split keys for `delegate`.
+               delegateEventSplitter = /^(\S+)\s*(.*)$/,
+               idCounter = 0;
+
+       /**
+        * Generate a unique integer id (unique within the entire client 
session).
+        * Useful for temporary DOM ids.
+        * @ignore
+        * @param {String} prefix Prefix to be used when generating the id.
+        * @returns {String}
+        */
+       function uniqueId( prefix ) {
+               var id = ++idCounter + '';
+               return prefix ? prefix + id : id;
+       }
+
+       /**
+        * Should be extended using extend().
+        *
+        * When options contains el property, this.$el in the constructed object
+        * will be set to the corresponding jQuery object. Otherwise, this.$el
+        * will be an empty div.
+        *
+        * When extended using extend(), if the extended prototype contains
+        * template property, this.$el will be filled with rendered template 
(with
+        * options parameter used as template data).
+        *
+        * template property can be a string which will be passed to 
mw.template.compile()
+        * or an object that has a render() function which accepts an object 
with
+        * template data as its argument (similarly to an object created by
+        * mw.template.compile()).
+        *
+        * You can also define a defaults property which should be an object
+        * containing default values for the template (if they're not present in
+        * the options parameter).
+        *
+        * If this.$el is not a jQuery object bound to existing DOM element, the
+        * view can be attached to an element using appendTo(), prependTo(),
+        * insertBefore(), insertAfter() proxy functions.
+        *
+        * append(), prepend(), before(), after() can be used to modify $el. 
on()
+        * can be used to bind events.
+        *
+        * You can also use declarative DOM events binding by specifying an 
`events`
+        * map on the class. The keys will be 'event selector' and the value 
can be
+        * either the name of a method to call, or a function. All methods and
+        * functions will be executed on the context of the View.
+        *
+        * Inspired from Backbone.js
+        * https://github.com/jashkenas/backbone/blob/master/backbone.js#L1128
+        *
+        *     @example
+        *     <code>
+        *     var MyComponent = View.extend( {
+        *       events: {
+        *             'mousedown .title': 'edit',
+        *             'click .button': 'save',
+        *             'click .open': function(e) { ... }
+        *       },
+        *       edit: function ( ev ) {
+        *         //...
+        *       },
+        *       save: function ( ev ) {
+        *         //...
+        *       }
+        *     } );
+        *     </code>
+        *
+        * @class View
+        * @extends EventEmitter
+        * @param {Object} options Options for the view, containing the el or
+        * template data or any other information you want to use in the view.
+        * Example:
+        *     @example
+        *     <pre>
+        *     var View, Section, section;
+        *     View = M.require( 'View' );
+        *     Section = View.extend( {
+        *       template: mw.template.compile( 
"&lt;h2&gt;{{title}}&lt;/h2&gt;" ),
+        *     } );
+        *     section = new Section( { title: 'Test', text: 'Test section 
body' } );
+        *     section.appendTo( 'body' );
+        *     </pre>
+        */
+       View = EventEmitter.extend( {
+               /**
+                * Name of tag that contains the rendered template
+                * @type String
+                */
+               tagName: 'div',
+
+               /**
+                * @type {Mixed}
+                * Specifies the template used in render(). 
Object|String|HoganTemplate
+                */
+               template: undefined,
+
+               /**
+                * Specifies partials (sub-templates) for the main template. 
Example:
+                *
+                *     @example
+                *     // example content for the "some" template (sub-template 
will be
+                *     // inserted where {{>content}} is):
+                *     // <h1>Heading</h1>
+                *     // {{>content}}
+                *
+                *     var SomeView = View.extend( {
+                *     template: M.template.get( 'some.hogan' ),
+                *     templatePartials: { content: M.template.get( 'sub.hogan' 
) }
+                *     }
+                *
+                * @type {Object}
+                */
+               templatePartials: {},
+
+               /**
+                * A set of default options that are merged with options passed 
into the initialize function.
+                *
+                * @type {Object}
+                */
+               defaults: {},
+
+               /**
+                * Default events map
+                */
+               events: null,
+
+               /**
+                * Run once during construction to set up the View
+                * @method
+                * @param {Object} options Object passed to the constructor.
+                */
+               initialize: function( options ) {
+                       EventEmitter.prototype.initialize.apply( this, 
arguments );
+                       this.defaults = $.extend( {}, this._parent.defaults, 
this.defaults );
+                       this.templatePartials = $.extend( {}, 
this._parent.templatePartials, this.templatePartials );
+                       options = $.extend( {}, this.defaults, options );
+                       if ( options.el ) {
+                               this.$el = $( options.el );
+                       } else {
+                               this.$el = $( '<' + this.tagName + '>' );
+                       }
+                       this.$el.addClass( this.className );
+
+                       // TODO: if template compilation is too slow, don't 
compile them on a
+                       // per object basis, but don't worry about it now 
(maybe add cache to
+                       // M.template.compile())
+                       if ( typeof this.template === 'string' ) {
+                               this.template = mw.template.compile( 
this.template );
+                       }
+
+                       this.options = options;
+                       this.render( options );
+
+                       // Assign a unique id for dom events binding/unbinding
+                       this.cid = uniqueId( 'view' );
+                       this.delegateEvents();
+               },
+
+               /**
+                * Function called before the view is rendered. Can be 
redefined in
+                * objects that extend View.
+                *
+                * @method
+                * @param {Object} options Object passed to the constructor.
+                */
+               preRender: function() {},
+
+               /**
+                * Function called after the view is rendered. Can be redefined 
in
+                * objects that extend View.
+                *
+                * @method
+                * @param {Object} options Object passed to the constructor.
+                */
+               postRender: function() {},
+
+               /**
+                * Fill this.$el with template rendered using data if template 
is set.
+                *
+                * @method
+                * @param {Object} data Template data.
+                */
+               render: function( data ) {
+                       data = $.extend( true, {}, this.options, data );
+                       this.preRender( data );
+                       if ( this.template ) {
+                               this.$el.html( this.template.render( data, 
this.templatePartials ) );
+                       }
+                       this.postRender( data );
+
+                       return this;
+               },
+
+               /**
+                * Wraps this.$el.find, so that you can search for elements in 
the view's
+                * ($el's) scope.
+                *
+                * @method
+                * @param {string} query A jQuery CSS selector.
+                * @return {jQuery.Object} jQuery object containing results of 
the search.
+                */
+               $: function(query) {
+                       return this.$el.find(query);
+               },
+
+               /**
+                * Set callbacks, where `this.events` is a hash of
+                *
+                * {"event selector": "callback"}
+                *
+                * {
+                *      'mousedown .title': 'edit',
+                *      'click .button': 'save',
+                *      'click .open': function(e) { ... }
+                * }
+                *
+                * pairs. Callbacks will be bound to the view, with `this` set 
properly.
+                * Uses event delegation for efficiency.
+                * Omitting the selector binds the event to `this.el`.
+                *
+                * @param {Object} events Optionally set this events instead of 
the ones on this.
+                * @returns {Object} this
+                */
+               delegateEvents: function ( events ) {
+                       var match, key, method;
+                       // Take either the events parameter or the this.events 
to process
+                       events = events || this.events;
+                       if ( events ) {
+                               // Remove current events before re-binding them
+                               this.undelegateEvents();
+                               for ( key in events ) {
+                                       method = events[ key ];
+                                       // If the method is a string name of 
this.method, get it
+                                       if ( !$.isFunction( method ) ) {
+                                               method = this[ events[ key ] ];
+                                       }
+                                       if ( method ) {
+                                               // Extract event and selector 
from the key
+                                               match = key.match( 
delegateEventSplitter );
+                                               this.delegate( match[ 1 ], 
match[ 2 ], $.proxy( method, this ) );
+                                       }
+                               }
+                       }
+               },
+
+               /**
+                * Add a single event listener to the view's element (or a 
child element
+                * using `selector`). This only works for delegate-able events: 
not `focus`,
+                * `blur`, and not `change`, `submit`, and `reset` in Internet 
Explorer.
+                *
+                * @param {String} eventName
+                * @param {String} selector
+                * @param {Function} listener
+                */
+               delegate: function ( eventName, selector, listener ) {
+                       this.$el.on( eventName + '.delegateEvents' + this.cid, 
selector,
+                               listener );
+               },
+
+               /**
+                * Clears all callbacks previously bound to the view by 
`delegateEvents`.
+                * You usually don't need to use this, but may wish to if you 
have multiple
+                * views attached to the same DOM element.
+                */
+               undelegateEvents: function () {
+                       if ( this.$el ) {
+                               this.$el.off( '.delegateEvents' + this.cid );
+                       }
+               },
+
+               /**
+                * A finer-grained `undelegateEvents` for removing a single 
delegated event.
+                * `selector` and `listener` are both optional.
+                *
+                * @param {String} eventName
+                * @param {String} selector
+                * @param {Function} listener
+                */
+               undelegate: function ( eventName, selector, listener ) {
+                       this.$el.off( eventName + '.delegateEvents' + this.cid, 
selector,
+                               listener );
+               }
+
+       } );
+
+       $.each( [
+               'append',
+               'prepend',
+               'appendTo',
+               'prependTo',
+               'after',
+               'before',
+               'insertAfter',
+               'insertBefore',
+               'remove',
+               'detach'
+       ], function( i, prop ) {
+               View.prototype[prop] = function() {
+                       this.$el[prop].apply( this.$el, arguments );
+                       return this;
+               };
+       } );
+
+       M.define( 'View', View );
+
+}( mw.mobileFrontend, jQuery ) );
diff --git a/javascripts/eventemitter.js b/javascripts/eventemitter.js
new file mode 100644
index 0000000..3b6acd8
--- /dev/null
+++ b/javascripts/eventemitter.js
@@ -0,0 +1,24 @@
+( function( M, $, OO ) {
+
+       var Class = M.require( 'Class' ), EventEmitter;
+
+       // HACK: wrap around oojs's EventEmitter
+       // This needs some hackery to make oojs's
+       // and MobileFrontend's different OO models get along,
+       // and we need to alias one() to once().
+       /**
+        * A base class with support for event emitting.
+        * @class EventEmitter
+        * @extends Class
+        * @uses OO.EventEmitter
+       **/
+       EventEmitter = Class.extend( $.extend( {
+               initialize: OO.EventEmitter
+       }, OO.EventEmitter.prototype ) );
+
+       M.define( 'eventemitter', EventEmitter );
+       // FIXME: if we want more of M's functionality in loaded in <head>,
+       // move this to a separate file
+       $.extend( mw.mobileFrontend, new EventEmitter() );
+
+}( mw.mobileFrontend, jQuery, OO ) );
diff --git a/javascripts/externals/hogan.js b/javascripts/externals/hogan.js
new file mode 100644
index 0000000..60e56e2
--- /dev/null
+++ b/javascripts/externals/hogan.js
@@ -0,0 +1,575 @@
+/*
+ *  Copyright 2011 Twitter, Inc.
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+
+
+var Hogan = {};
+
+(function (Hogan, useArrayBuffer) {
+  Hogan.Template = function (renderFunc, text, compiler, options) {
+    this.r = renderFunc || this.r;
+    this.c = compiler;
+    this.options = options;
+    this.text = text || '';
+    this.buf = (useArrayBuffer) ? [] : '';
+  }
+
+  Hogan.Template.prototype = {
+    // render: replaced by generated code.
+    r: function (context, partials, indent) { return ''; },
+
+    // variable escaping
+    v: hoganEscape,
+
+    // triple stache
+    t: coerceToString,
+
+    render: function render(context, partials, indent) {
+      return this.ri([context], partials || {}, indent);
+    },
+
+    // render internal -- a hook for overrides that catches partials too
+    ri: function (context, partials, indent) {
+      return this.r(context, partials, indent);
+    },
+
+    // tries to find a partial in the curent scope and render it
+    rp: function(name, context, partials, indent) {
+      var partial = partials[name];
+
+      if (!partial) {
+        return '';
+      }
+
+      if (this.c && typeof partial == 'string') {
+        partial = this.c.compile(partial, this.options);
+      }
+
+      return partial.ri(context, partials, indent);
+    },
+
+    // render a section
+    rs: function(context, partials, section) {
+      var tail = context[context.length - 1];
+
+      if (!isArray(tail)) {
+        section(context, partials, this);
+        return;
+      }
+
+      for (var i = 0; i < tail.length; i++) {
+        context.push(tail[i]);
+        section(context, partials, this);
+        context.pop();
+      }
+    },
+
+    // maybe start a section
+    s: function(val, ctx, partials, inverted, start, end, tags) {
+      var pass;
+
+      if (isArray(val) && val.length === 0) {
+        return false;
+      }
+
+      if (typeof val == 'function') {
+        val = this.ls(val, ctx, partials, inverted, start, end, tags);
+      }
+
+      pass = (val === '') || !!val;
+
+      if (!inverted && pass && ctx) {
+        ctx.push((typeof val == 'object') ? val : ctx[ctx.length - 1]);
+      }
+
+      return pass;
+    },
+
+    // find values with dotted names
+    d: function(key, ctx, partials, returnFound) {
+      var names = key.split('.'),
+          val = this.f(names[0], ctx, partials, returnFound),
+          cx = null;
+
+      if (key === '.' && isArray(ctx[ctx.length - 2])) {
+        return ctx[ctx.length - 1];
+      }
+
+      for (var i = 1; i < names.length; i++) {
+        if (val && typeof val == 'object' && names[i] in val) {
+          cx = val;
+          val = val[names[i]];
+        } else {
+          val = '';
+        }
+      }
+
+      if (returnFound && !val) {
+        return false;
+      }
+
+      if (!returnFound && typeof val == 'function') {
+        ctx.push(cx);
+        val = this.lv(val, ctx, partials);
+        ctx.pop();
+      }
+
+      return val;
+    },
+
+    // find values with normal names
+    f: function(key, ctx, partials, returnFound) {
+      var val = false,
+          v = null,
+          found = false;
+
+      for (var i = ctx.length - 1; i >= 0; i--) {
+        v = ctx[i];
+        if (v && typeof v == 'object' && key in v) {
+          val = v[key];
+          found = true;
+          break;
+        }
+      }
+
+      if (!found) {
+        return (returnFound) ? false : "";
+      }
+
+      if (!returnFound && typeof val == 'function') {
+        val = this.lv(val, ctx, partials);
+      }
+
+      return val;
+    },
+
+    // higher order templates
+    ho: function(val, cx, partials, text, tags) {
+      var compiler = this.c;
+      var options = this.options;
+      options.delimiters = tags;
+      var text = val.call(cx, text);
+      text = (text == null) ? String(text) : text.toString();
+      this.b(compiler.compile(text, options).render(cx, partials));
+      return false;
+    },
+
+    // template result buffering
+    b: (useArrayBuffer) ? function(s) { this.buf.push(s); } :
+                          function(s) { this.buf += s; },
+    fl: (useArrayBuffer) ? function() { var r = this.buf.join(''); this.buf = 
[]; return r; } :
+                           function() { var r = this.buf; this.buf = ''; 
return r; },
+
+    // lambda replace section
+    ls: function(val, ctx, partials, inverted, start, end, tags) {
+      var cx = ctx[ctx.length - 1],
+          t = null;
+
+      if (!inverted && this.c && val.length > 0) {
+        return this.ho(val, cx, partials, this.text.substring(start, end), 
tags);
+      }
+
+      t = val.call(cx);
+
+      if (typeof t == 'function') {
+        if (inverted) {
+          return true;
+        } else if (this.c) {
+          return this.ho(t, cx, partials, this.text.substring(start, end), 
tags);
+        }
+      }
+
+      return t;
+    },
+
+    // lambda replace variable
+    lv: function(val, ctx, partials) {
+      var cx = ctx[ctx.length - 1];
+      var result = val.call(cx);
+
+      if (typeof result == 'function') {
+        result = coerceToString(result.call(cx));
+        if (this.c && ~result.indexOf("{\u007B")) {
+          return this.c.compile(result, this.options).render(cx, partials);
+        }
+      }
+
+      return coerceToString(result);
+    }
+
+  };
+
+  var rAmp = /&/g,
+      rLt = /</g,
+      rGt = />/g,
+      rApos =/\'/g,
+      rQuot = /\"/g,
+      hChars =/[&<>\"\']/;
+
+
+  function coerceToString(val) {
+    return String((val === null || val === undefined) ? '' : val);
+  }
+
+  function hoganEscape(str) {
+    str = coerceToString(str);
+    return hChars.test(str) ?
+      str
+        .replace(rAmp,'&amp;')
+        .replace(rLt,'&lt;')
+        .replace(rGt,'&gt;')
+        .replace(rApos,'&#39;')
+        .replace(rQuot, '&quot;') :
+      str;
+  }
+
+  var isArray = Array.isArray || function(a) {
+    return Object.prototype.toString.call(a) === '[object Array]';
+  };
+
+})(typeof exports !== 'undefined' ? exports : Hogan);
+
+
+
+
+(function (Hogan) {
+  // Setup regex  assignments
+  // remove whitespace according to Mustache spec
+  var rIsWhitespace = /\S/,
+      rQuot = /\"/g,
+      rNewline =  /\n/g,
+      rCr = /\r/g,
+      rSlash = /\\/g,
+      tagTypes = {
+        '#': 1, '^': 2, '/': 3,  '!': 4, '>': 5,
+        '<': 6, '=': 7, '_v': 8, '{': 9, '&': 10
+      };
+
+  Hogan.scan = function scan(text, delimiters) {
+    var len = text.length,
+        IN_TEXT = 0,
+        IN_TAG_TYPE = 1,
+        IN_TAG = 2,
+        state = IN_TEXT,
+        tagType = null,
+        tag = null,
+        buf = '',
+        tokens = [],
+        seenTag = false,
+        i = 0,
+        lineStart = 0,
+        otag = '{{',
+        ctag = '}}';
+
+    function addBuf() {
+      if (buf.length > 0) {
+        tokens.push(new String(buf));
+        buf = '';
+      }
+    }
+
+    function lineIsWhitespace() {
+      var isAllWhitespace = true;
+      for (var j = lineStart; j < tokens.length; j++) {
+        isAllWhitespace =
+          (tokens[j].tag && tagTypes[tokens[j].tag] < tagTypes['_v']) ||
+          (!tokens[j].tag && tokens[j].match(rIsWhitespace) === null);
+        if (!isAllWhitespace) {
+          return false;
+        }
+      }
+
+      return isAllWhitespace;
+    }
+
+    function filterLine(haveSeenTag, noNewLine) {
+      addBuf();
+
+      if (haveSeenTag && lineIsWhitespace()) {
+        for (var j = lineStart, next; j < tokens.length; j++) {
+          if (!tokens[j].tag) {
+            if ((next = tokens[j+1]) && next.tag == '>') {
+              // set indent to token value
+              next.indent = tokens[j].toString()
+            }
+            tokens.splice(j, 1);
+          }
+        }
+      } else if (!noNewLine) {
+        tokens.push({tag:'\n'});
+      }
+
+      seenTag = false;
+      lineStart = tokens.length;
+    }
+
+    function changeDelimiters(text, index) {
+      var close = '=' + ctag,
+          closeIndex = text.indexOf(close, index),
+          delimiters = trim(
+            text.substring(text.indexOf('=', index) + 1, closeIndex)
+          ).split(' ');
+
+      otag = delimiters[0];
+      ctag = delimiters[1];
+
+      return closeIndex + close.length - 1;
+    }
+
+    if (delimiters) {
+      delimiters = delimiters.split(' ');
+      otag = delimiters[0];
+      ctag = delimiters[1];
+    }
+
+    for (i = 0; i < len; i++) {
+      if (state == IN_TEXT) {
+        if (tagChange(otag, text, i)) {
+          --i;
+          addBuf();
+          state = IN_TAG_TYPE;
+        } else {
+          if (text.charAt(i) == '\n') {
+            filterLine(seenTag);
+          } else {
+            buf += text.charAt(i);
+          }
+        }
+      } else if (state == IN_TAG_TYPE) {
+        i += otag.length - 1;
+        tag = tagTypes[text.charAt(i + 1)];
+        tagType = tag ? text.charAt(i + 1) : '_v';
+        if (tagType == '=') {
+          i = changeDelimiters(text, i);
+          state = IN_TEXT;
+        } else {
+          if (tag) {
+            i++;
+          }
+          state = IN_TAG;
+        }
+        seenTag = i;
+      } else {
+        if (tagChange(ctag, text, i)) {
+          tokens.push({tag: tagType, n: trim(buf), otag: otag, ctag: ctag,
+                       i: (tagType == '/') ? seenTag - ctag.length : i + 
otag.length});
+          buf = '';
+          i += ctag.length - 1;
+          state = IN_TEXT;
+          if (tagType == '{') {
+            if (ctag == '}}') {
+              i++;
+            } else {
+              cleanTripleStache(tokens[tokens.length - 1]);
+            }
+          }
+        } else {
+          buf += text.charAt(i);
+        }
+      }
+    }
+
+    filterLine(seenTag, true);
+
+    return tokens;
+  }
+
+  function cleanTripleStache(token) {
+    if (token.n.substr(token.n.length - 1) === '}') {
+      token.n = token.n.substring(0, token.n.length - 1);
+    }
+  }
+
+  function trim(s) {
+    if (s.trim) {
+      return s.trim();
+    }
+
+    return s.replace(/^\s*|\s*$/g, '');
+  }
+
+  function tagChange(tag, text, index) {
+    if (text.charAt(index) != tag.charAt(0)) {
+      return false;
+    }
+
+    for (var i = 1, l = tag.length; i < l; i++) {
+      if (text.charAt(index + i) != tag.charAt(i)) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  function buildTree(tokens, kind, stack, customTags) {
+    var instructions = [],
+        opener = null,
+        token = null;
+
+    while (tokens.length > 0) {
+      token = tokens.shift();
+      if (token.tag == '#' || token.tag == '^' || isOpener(token, customTags)) 
{
+        stack.push(token);
+        token.nodes = buildTree(tokens, token.tag, stack, customTags);
+        instructions.push(token);
+      } else if (token.tag == '/') {
+        if (stack.length === 0) {
+          throw new Error('Closing tag without opener: /' + token.n);
+        }
+        opener = stack.pop();
+        if (token.n != opener.n && !isCloser(token.n, opener.n, customTags)) {
+          throw new Error('Nesting error: ' + opener.n + ' vs. ' + token.n);
+        }
+        opener.end = token.i;
+        return instructions;
+      } else {
+        instructions.push(token);
+      }
+    }
+
+    if (stack.length > 0) {
+      throw new Error('missing closing tag: ' + stack.pop().n);
+    }
+
+    return instructions;
+  }
+
+  function isOpener(token, tags) {
+    for (var i = 0, l = tags.length; i < l; i++) {
+      if (tags[i].o == token.n) {
+        token.tag = '#';
+        return true;
+      }
+    }
+  }
+
+  function isCloser(close, open, tags) {
+    for (var i = 0, l = tags.length; i < l; i++) {
+      if (tags[i].c == close && tags[i].o == open) {
+        return true;
+      }
+    }
+  }
+
+  Hogan.generate = function (tree, text, options) {
+    var code = 'var _=this;_.b(i=i||"");' + walk(tree) + 'return _.fl();';
+    if (options.asString) {
+      return 'function(c,p,i){' + code + ';}';
+    }
+
+    return new Hogan.Template(new Function('c', 'p', 'i', code), text, Hogan, 
options);
+  }
+
+  function esc(s) {
+    return s.replace(rSlash, '\\\\')
+            .replace(rQuot, '\\\"')
+            .replace(rNewline, '\\n')
+            .replace(rCr, '\\r');
+  }
+
+  function chooseMethod(s) {
+    return (~s.indexOf('.')) ? 'd' : 'f';
+  }
+
+  function walk(tree) {
+    var code = '';
+    for (var i = 0, l = tree.length; i < l; i++) {
+      var tag = tree[i].tag;
+      if (tag == '#') {
+        code += section(tree[i].nodes, tree[i].n, chooseMethod(tree[i].n),
+                        tree[i].i, tree[i].end, tree[i].otag + " " + 
tree[i].ctag);
+      } else if (tag == '^') {
+        code += invertedSection(tree[i].nodes, tree[i].n,
+                                chooseMethod(tree[i].n));
+      } else if (tag == '<' || tag == '>') {
+        code += partial(tree[i]);
+      } else if (tag == '{' || tag == '&') {
+        code += tripleStache(tree[i].n, chooseMethod(tree[i].n));
+      } else if (tag == '\n') {
+        code += text('"\\n"' + (tree.length-1 == i ? '' : ' + i'));
+      } else if (tag == '_v') {
+        code += variable(tree[i].n, chooseMethod(tree[i].n));
+      } else if (tag === undefined) {
+        code += text('"' + esc(tree[i]) + '"');
+      }
+    }
+    return code;
+  }
+
+  function section(nodes, id, method, start, end, tags) {
+    return 'if(_.s(_.' + method + '("' + esc(id) + '",c,p,1),' +
+           'c,p,0,' + start + ',' + end + ',"' + tags + '")){' +
+           '_.rs(c,p,' +
+           'function(c,p,_){' +
+           walk(nodes) +
+           '});c.pop();}';
+  }
+
+  function invertedSection(nodes, id, method) {
+    return 'if(!_.s(_.' + method + '("' + esc(id) + '",c,p,1),c,p,1,0,0,"")){' 
+
+           walk(nodes) +
+           '};';
+  }
+
+  function partial(tok) {
+    return '_.b(_.rp("' +  esc(tok.n) + '",c,p,"' + (tok.indent || '') + 
'"));';
+  }
+
+  function tripleStache(id, method) {
+    return '_.b(_.t(_.' + method + '("' + esc(id) + '",c,p,0)));';
+  }
+
+  function variable(id, method) {
+    return '_.b(_.v(_.' + method + '("' + esc(id) + '",c,p,0)));';
+  }
+
+  function text(id) {
+    return '_.b(' + id + ');';
+  }
+
+  Hogan.parse = function(tokens, text, options) {
+    options = options || {};
+    return buildTree(tokens, '', [], options.sectionTags || []);
+  },
+
+  Hogan.cache = {};
+
+  Hogan.compile = function(text, options) {
+    // options
+    //
+    // asString: false (default)
+    //
+    // sectionTags: [{o: '_foo', c: 'foo'}]
+    // An array of object with o and c fields that indicate names for custom
+    // section tags. The example above allows parsing of {{_foo}}{{/foo}}.
+    //
+    // delimiters: A string that overrides the default delimiters.
+    // Example: "<% %>"
+    //
+    options = options || {};
+
+    var key = text + '||' + !!options.asString;
+
+    var t = this.cache[key];
+
+    if (t) {
+      return t;
+    }
+
+    t = this.generate(this.parse(this.scan(text, options.delimiters), text, 
options), text, options);
+    return this.cache[key] = t;
+  };
+})(typeof exports !== 'undefined' ? exports : Hogan);
diff --git a/javascripts/hogan.js b/javascripts/hogan.js
new file mode 100644
index 0000000..74a9f2a
--- /dev/null
+++ b/javascripts/hogan.js
@@ -0,0 +1,29 @@
+// Register the Hogan compiler with MediaWiki.
+( function() {
+       /*
+        * Hogan template compiler
+        */
+       var hogan = {
+               /*
+                * Registers a partial internally in the compiler.
+                * Not used in Hogan compiler
+                *
+                * @method
+                * @param {String} name Name of the template
+                * @param {HandleBars.Template} partial
+                */
+               registerPartial: function( /* name, partial */ ) {},
+               /*
+                * Compiler source code into a template object
+                *
+                * @method
+                * @param {String} src the source of a template
+                * @return {Hogan.Template} template object
+                */
+               compile: function( src ) {
+                       return Hogan.compile( src );
+               }
+       };
+       // register hogan compiler with core
+       mw.template.registerCompiler( 'hogan', hogan );
+}() );
diff --git a/javascripts/modes.js b/javascripts/modes.js
index 42f81b2..8c272ba 100644
--- a/javascripts/modes.js
+++ b/javascripts/modes.js
@@ -46,6 +46,6 @@
                                throw new Error( 'Attempt to run module outside 
declared environment mode ' + mode );
                        }
                }
-       }, mw.mantle );
+       }, mw.mobileFrontend );
 
 }( jQuery ) );
diff --git a/javascripts/modules.js b/javascripts/modules.js
new file mode 100644
index 0000000..28aab86
--- /dev/null
+++ b/javascripts/modules.js
@@ -0,0 +1,35 @@
+/**
+ *
+ * @class mw.mobileFrontend
+ * @singleton
+ */
+( function( $ ) {
+       mw.mobileFrontend = {
+               _modules: {},
+               /**
+                * Require (import) a module previously defined using define().
+                *
+                * @param {string} id Required module id.
+                * @return {Object} Required module, can be any JavaScript 
object.
+                */
+               require: function( id ) {
+                       if ( !this._modules.hasOwnProperty( id ) ) {
+                               throw new Error( 'Module not found: ' + id );
+                       }
+                       return this._modules[ id ];
+               },
+
+               /**
+                * Define a module which can be later required (imported) using 
require().
+                *
+                * @param {string} id Defined module id.
+                * @param {Object} obj Defined module body, can be any 
JavaScript object.
+                */
+               define: function( id, obj ) {
+                       if ( this._modules.hasOwnProperty( id ) ) {
+                               throw new Error( 'Module already exists: ' + id 
);
+                       }
+                       this._modules[ id ] = obj;
+               }
+       };
+}( jQuery ) );
diff --git a/javascripts/template.js b/javascripts/template.js
deleted file mode 100644
index 3666229..0000000
--- a/javascripts/template.js
+++ /dev/null
@@ -1,6 +0,0 @@
-// Convenience wrapper for Mantle
-( function ( $ ) {
-       $.extend( mw.mobileFrontend, {
-               template: mw.mantle.template
-       } );
-}( jQuery ) );
diff --git a/tests/qunit/test_Class.js b/tests/qunit/test_Class.js
new file mode 100644
index 0000000..8cfbd7d
--- /dev/null
+++ b/tests/qunit/test_Class.js
@@ -0,0 +1,57 @@
+( function( M ) {
+       var Class = M.require( 'Class' );
+
+       QUnit.module( 'MobileFrontend Class' );
+
+       QUnit.test( '.extend', 6, function( assert ) {
+               var Parent, Child, child;
+
+               Parent = Class.extend( {
+                       prop: 'parent',
+                       parent: function() {
+                               return 'parent';
+                       },
+                       override: function() {
+                               return 'override';
+                       },
+                       callSuper: function() {
+                               return 'super';
+                       }
+               } );
+
+               Child = Parent.extend( {
+                       prop: 'child',
+                       override: function() {
+                               return 'overriden';
+                       },
+                       child: function() {
+                               return 'child';
+                       },
+                       callSuper: function() {
+                               var _super = Parent.prototype.callSuper;
+                               return _super.apply( this ) + ' duper';
+                       }
+               } );
+
+               child = new Child();
+               assert.strictEqual( child.parent(), 'parent', 'inherit parent 
properties' );
+               assert.strictEqual( child.override(), 'overriden', 'override 
parent properties' );
+               assert.strictEqual( child.child(), 'child', 'add new 
properties' );
+               assert.strictEqual( child.callSuper(), 'super duper', "call 
parent's functions" );
+               assert.strictEqual( child._parent.prop, 'parent', "access 
parent's prototype through _parent" );
+               assert.strictEqual( Child.extend, Class.extend, 'make Child 
extendeable' );
+       } );
+
+       QUnit.test( '#initialize', 1, function( assert ) {
+               var Thing, spy = this.sandbox.spy();
+
+               Thing = Class.extend( {
+                       initialize: spy
+               } );
+
+               new Thing( 'abc', 123 );
+
+               assert.ok( spy.calledWith( 'abc', 123 ), 'call #initialize when 
creating new instance' );
+       } );
+
+}( mw.mobileFrontend ) );
diff --git a/tests/qunit/test_View.js b/tests/qunit/test_View.js
new file mode 100644
index 0000000..da45110
--- /dev/null
+++ b/tests/qunit/test_View.js
@@ -0,0 +1,208 @@
+( function( M, $ ) {
+
+var View = M.require( 'View' );
+
+QUnit.module( 'MobileFrontend view', {
+       setup: function() {
+               var compiler = {
+                       compile: function() {
+                               return {
+                                       render: function( data, partials ) {
+                                               if ( partials && 
partials.content ) {
+                                                       return '<h1>' + 
data.title + '</h1><p>' + partials.content.render( data ) + '</p>';
+                                               } else if ( data.content ) {
+                                                       return '<h1>' + 
data.title + '</h1><p>' + data.content + '</p>';
+                                               } else {
+                                                       return '<p>' + 
data.text + '</p>';
+                                               }
+                                       }
+                               };
+                       }
+               };
+               // Register template compiler
+               mw.template.registerCompiler( 'xyz', compiler );
+       }
+} );
+
+QUnit.test( 'View', 2, function( assert ) {
+       var view = new View( { el: 'body' } );
+       assert.ok( view.$el instanceof $, 'assign jQuery object to $el' );
+       assert.strictEqual( view.$el[0].tagName.toUpperCase(), 'BODY', 'assign 
proper jQuery object to $el' );
+} );
+
+QUnit.test( 'View, jQuery proxy functions', 10, function( assert ) {
+       var self = this, view = new View( { el: 'body' } );
+       [
+               'append',
+               'prepend',
+               'appendTo',
+               'prependTo',
+               'after',
+               'before',
+               'insertAfter',
+               'insertBefore',
+               'remove',
+               'detach'
+       ].forEach( function( prop ) {
+               var stub = self.sandbox.stub( view.$el, prop );
+               view[prop]( 'test', 1 );
+               assert.ok( stub.calledWith( 'test', 1 ) );
+               stub.restore();
+       } );
+} );
+
+QUnit.test( 'View.extend, with el property', 1, function( assert ) {
+       var ChildView, $testEl, view;
+       ChildView = View.extend( {
+               firstHeading: function() {
+                       return this.$( 'h1' ).text();
+               }
+       } );
+       $testEl = $( '<div id="testView"><h1>Test</h1></div>' ).appendTo( 
'body' );
+
+       view = new ChildView( { el: '#testView' } );
+       assert.strictEqual( view.firstHeading(), 'Test', 'register additional 
functions' );
+       $testEl.remove();
+} );
+
+QUnit.test( 'View.extend, with defined template', 4, function( assert ) {
+       var ChildView, view;
+       ChildView = View.extend( {
+               className: 'my-class',
+               template: mw.template.compile( 
'<h1>{{title}}</h1><p>{{content}}</p>', 'xyz' ),
+               title: function() {
+                       return this.$( 'h1' ).text();
+               },
+               content: function() {
+                       return this.$( 'p' ).text();
+               }
+       } );
+
+       view = new ChildView( { title: 'Test', content: 'Some content' } );
+       assert.strictEqual( view.$el[0].tagName.toUpperCase(), 'DIV', 'wrap 
template in <div>' );
+       assert.strictEqual( view.$el.attr( 'class' ), 'my-class', 'set class 
for $el' );
+       assert.strictEqual( view.title(), 'Test', 'fill template with data from 
options' );
+       assert.strictEqual( view.content(), 'Some content', 'fill template with 
data from options' );
+} );
+
+QUnit.test( 'View.extend, with partials', 2, function( assert ) {
+       var ParentView, ChildView, view;
+
+       ParentView = View.extend( {
+               template: mw.template.compile( 
'<h1>{{title}}</h1>{{>content}}', 'xyz' )
+       } );
+
+       ChildView = ParentView.extend( {
+               templatePartials: {
+                       content: mw.template.compile( '<p>{{text}}</p>', 'xyz' )
+               }
+       } );
+
+       view = new ChildView( { title: 'Test', text: 'Some content' } );
+       assert.strictEqual( view.$( 'h1' ).text(), 'Test', 'fill template with 
data from options' );
+       assert.strictEqual( view.$( 'p' ).text(), 'Some content', 'fill partial 
with data from options' );
+} );
+
+QUnit.test( 'View.extend, extending partials', 1, function( assert ) {
+       var ParentView, ChildView, view;
+
+       ParentView = View.extend( {
+               templatePartials: {
+                       a: 1,
+                       b: 2
+               }
+       } );
+
+       ChildView = ParentView.extend( {
+               templatePartials: {
+                       b: 3,
+                       c: 4
+               }
+       } );
+
+       view = new ChildView();
+       assert.deepEqual( view.templatePartials, { a: 1, b: 3, c: 4 } );
+} );
+
+QUnit.test( 'View.extend, extending defaults', 1, function( assert ) {
+       var ParentView, ChildView, view;
+
+       ParentView = View.extend( {
+               defaults: {
+                       a: 1,
+                       b: 2
+               }
+       } );
+
+       ChildView = ParentView.extend( {
+               defaults: {
+                       b: 3,
+                       c: 4
+               }
+       } );
+
+       view = new ChildView( { c: 5 } );
+       assert.deepEqual( view.options, { a: 1, b: 3, c: 5 } );
+} );
+
+QUnit.test( 'View#preRender', 1, function( assert ) {
+       var ChildView, view;
+       ChildView = View.extend( {
+               template: mw.template.compile( '<p>{{text}}</p>', 'xyz' ),
+               preRender: function( options ) {
+                       options.text = 'hello';
+               }
+       } );
+
+       view = new ChildView();
+       assert.strictEqual( view.$el.html(), '<p>hello</p>', 'manipulate 
template data' );
+} );
+
+QUnit.test( 'View#postRender', 1, function( assert ) {
+       var ChildView, view, spy = this.sandbox.spy();
+       ChildView = View.extend( {
+               postRender: function() {
+                       spy();
+               }
+       } );
+
+       view = new ChildView();
+       assert.ok( spy.calledOnce, 'invoke postRender' );
+} );
+
+QUnit.test( 'View#delegateEvents', 3, function ( assert ) {
+
+       var view, EventsView = View.extend( {
+               template: mw.template.compile( '<p><span>test</span></p>', 
'xyz' ),
+               events: {
+                       'click p span': function ( ev ) {
+                               ev.preventDefault();
+                               assert.ok( true, 'Span was clicked and handled' 
);
+                       },
+                       'click p': 'onParagraphClick',
+                       'click': 'onClick'
+               },
+               onParagraphClick: function ( ev ) {
+                       ev.preventDefault();
+                       assert.ok( true, 'Paragraph was clicked and handled' );
+               },
+               onClick: function ( ev ) {
+                       ev.preventDefault();
+                       assert.ok( true, 'View was clicked and handled' );
+               }
+       } );
+
+       view = new EventsView();
+       view.appendTo( 'body' );
+       // Check if events are set and handlers called
+       view.$el.find( 'span' ).trigger( 'click' );
+       view.$el.find( 'p' ).trigger( 'click' );
+       view.$el.trigger( 'click' );
+       // Check if events can be unset and handlers are not called
+       view.undelegateEvents();
+       view.$el.find( 'span' ).trigger( 'click' );
+       view.$el.find( 'p' ).trigger( 'click' );
+       view.$el.trigger( 'click' );
+} );
+
+}( mw.mobileFrontend, jQuery) );
diff --git a/tests/qunit/test_eventemitter.js b/tests/qunit/test_eventemitter.js
new file mode 100644
index 0000000..f61024c
--- /dev/null
+++ b/tests/qunit/test_eventemitter.js
@@ -0,0 +1,23 @@
+( function( M ) {
+
+       var EventEmitter = M.require( 'eventemitter' );
+
+       QUnit.module( 'MobileFrontend EventEmitter' );
+
+       QUnit.test( '#on', 1, function( assert ) {
+               var e = new EventEmitter(), spy = this.sandbox.spy();
+               e.on( 'testEvent', spy );
+               e.emit( 'testEvent', 'first', 2 );
+               assert.ok( spy.calledWith( 'first', 2 ), 'run callback when 
event runs' );
+       } );
+
+       QUnit.test( '#one', 2, function( assert ) {
+               var e = new EventEmitter(), spy = this.sandbox.spy();
+               e.once( 'testEvent', spy );
+               e.emit( 'testEvent', 'first', 2 );
+               e.emit( 'testEvent', 'second', 2 );
+               assert.ok( spy.calledWith( 'first', 2 ), 'run callback when 
event runs' );
+               assert.ok( spy.calledOnce, 'run callback once' );
+       } );
+
+}( mw.mobileFrontend ) );

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: Iab38441a9b961571d375e1d6b4456b4026385bce
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/MobileFrontend
Gerrit-Branch: master
Gerrit-Owner: Jdlrobson <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to