User:Enterprisey/afch-master.js/core.js

/* Uploaded from https://github.com/WPAFC/afch-rewrite, commit: baba0a9536a1a5591cdfc081eba9878d1ace470e (master) */

/*

* 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 = /

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,'&')

.replace(rLt,'<')

.replace(rGt,'>')

.replace(rApos,''')

.replace(rQuot, '"') :

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);

;//

( function ( AFCH, $, mw ) {

$.extend( AFCH, {

/**

* Log anything to the console

* @param {anything} thing(s)

*/

log: function () {

var args = Array.prototype.slice.call( arguments );

if ( AFCH.consts.beta && console && console.log ) {

args.unshift( 'AFCH:' );

console.log.apply( console, args );

}

},

/**

* @internal Functions called when AFCH.destroy() is run

* @type {Array}

*/

_destroyFunctions: [],

/**

* Add a function to run when AFCH.destroy() is run

* @param {Function} fn

*/

addDestroyFunction: function ( fn ) {

AFCH._destroyFunctions.push( fn );

},

/**

* Destroys all AFCH-y things. Subscripts can add custom

* destroy functions by running AFCH.addDestroyFunction( fn )

*/

destroy: function () {

$.each( AFCH._destroyFunctions, function ( _, fn ) {

fn();

} );

window.AFCH = false;

},

/**

* Prepares the AFCH gadget by setting constants and checking environment

* @return {bool} Whether or not all setup functions executed successfully

*/

setup: function () {

// Check requirements

if ( 'ajax' in $.support && !$.support.ajax ) {

AFCH.error = 'AFCH requires AJAX';

return false;

}

if ( AFCH.consts.baseurl.indexOf( 'MediaWiki:' + 'Gadget-afch.js' ) === -1 ) {

AFCH.consts.beta = true;

}

AFCH.api = new mw.Api();

// Set up the preferences interface

AFCH.preferences = new AFCH.Preferences();

AFCH.prefs = AFCH.preferences.prefStore;

// Add more constants -- don't overwrite those already set, though

AFCH.consts = $.extend( {}, {

// If true, the script will NOT modify actual wiki content and

// will instead mock all such API requests (success assumed)

mockItUp: false,

// Full page name, "Wikipedia talk:Articles for creation/sandbox"

pagename: mw.config.get( 'wgPageName' ).replace( /_/g, ' ' ),

// Link to the current page, "/wiki/Wikipedia talk:Articles for creation/sandbox"

pagelink: mw.util.getUrl(),

// Used when status is disabled

nullstatus: { update: function () { return; } },

// Current user

user: mw.user.getName(),

// Edit summary ad

summaryAd: ' (AFCH ' + AFCH.consts.version + ')',

// Require users to be on whitelist to use the script

// Testwiki users don't need to be on it

whitelistRequired: mw.config.get( 'wgDBname' ) !== 'testwiki',

// Name of the whitelist page for reviewers

whitelistTitle: 'Wikipedia:WikiProject Articles for creation/Participants'

}, AFCH.consts );

// Check whitelist if necessary, but don't delay loading of the

// script for users who ARE allowed; rather, just destroy the

// script instance when and if it finds the user is not listed

if ( AFCH.consts.whitelistRequired ) {

AFCH.checkWhitelist();

}

return true;

},

/**

* Check if the current user is allowed to use the helper script;

* if not, display an error and destroy AFCH

*/

checkWhitelist: function () {

var user = AFCH.consts.user,

whitelist = new AFCH.Page( AFCH.consts.whitelistTitle );

whitelist.getText().done( function ( text ) {

// sanitizedUser is user, but escaped for use in the regex.

// Otherwise a user named ... would always be able to use

// the script, so long as there was a user whose name was

// three characters long on the list!

var $howToDisable,

sanitizedUser = user.replace( /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&' ),

userAllowed = ( new RegExp( '\\|\\s*' + sanitizedUser + '\\s*}' ) ).test( text );

if ( !userAllowed ) {

// If we can detect that the gadget is currently enabled, offer a one-click "disable" link

if ( mw.user.options.get( 'gadget-afchelper' ) === '1' ) {

$howToDisable = $( '' )

.append( 'If you wish to disable the helper script, ' )

.append( $( '' )

.text( 'click here' )

.click( function () {

// Submit the API request to disable the gadget.

// Note: We don't use `AFCH.api` here, because AFCH has already been

// destroyed due to the user not being on the whitelist!

( new mw.Api() ).postWithToken( 'options', {

action: 'options',

change: 'gadget-afchelper=0'

} ).done( function ( data ) {

mw.notify( 'AFCH has been disabled successfully. If you wish to re-enable it in the ' +

'future, you can do so via your Preferences by checking "Yet Another AFC Helper Script".' );

} );

} )

)

.append( '. ' );

// Otherwise, AFCH is probably installed via common.js/skin.js -- offer links for easy access.

} else {

$howToDisable = $( '' )

.append( 'If you wish to disable the helper script, you will need to manually ' +

'remove it from your ' )

.append( AFCH.makeLinkElementToPage( 'Special:MyPage/common.js', 'common.js' ) )

.append( ' or your ' )

.append( AFCH.makeLinkElementToPage( 'Special:MyPage/skin.js', 'skin.js' ) )

.append( 'page. ' );

}

// Finally, make and push the notification, then explode AFCH

mw.notify(

$( '

' )

.append( 'AFCH could not be loaded because "' + user + '" is not listed on ' )

.append( AFCH.makeLinkElementToPage( whitelist.rawTitle ) )

.append( '. You can request access to the AfC helper script there. ' )

.append( $howToDisable )

.append( 'If you have any questions or concerns, please ' )

.append( AFCH.makeLinkElementToPage( 'WT:AFCH', 'get in touch' ) )

.append( '!' ),

{

title: 'AFCH error: user not listed',

autoHide: false

}

);

AFCH.destroy();

}

} );

},

/**

* Loads the subscript and dependencies

* @param {string} type Which type of script to load:

* 'redirects' or 'ffu' or 'submissions'

*/

load: function ( type ) {

if ( !AFCH.setup() ) {

return false;

}

if ( AFCH.consts.beta ) {

// Load minified css

mw.loader.load( AFCH.consts.scriptpath + '?action=raw&ctype=text/css&title=User:Enterprisey/afch-master.css', 'text/css' );

// Load dependencies

mw.loader.load( [

// jquery resources

'jquery.chosen',

'jquery.spinner',

'jquery.ui.dialog',

// mediawiki.api

'mediawiki.api',

'mediawiki.api.titleblacklist',

// mediawiki plugins

'mediawiki.feedback'

] );

}

// And finally load the subscript

$.getScript( AFCH.consts.baseurl + '/' + type + '.js' );

return true;

},

/**

* Appends a feedback link to the given element

* @param {string|jQuery} $element The jQuery element or selector to which the link should be appended

* @param {string} type (optional) The part of AFCH that feedback is being given for, e.g. "files for upload"

* @param {string} linkText (optional) Text to display in the link; by default "Give feedback!"

*/

initFeedback: function ( $element, type, linkText ) {

var feedback = new mw.Feedback( {

title: new mw.Title( 'Wikipedia talk:WikiProject Articles for creation/Helper script' ),

bugsLink: 'https://en.wikipedia.org/w/index.php?title=Wikipedia_talk:WikiProject_Articles_for_creation/Helper_script&action=edit§ion=new',

bugsListLink: 'https://en.wikipedia.org/w/index.php?title=Wikipedia_talk:WikiProject_Articles_for_creation/Helper_script'

} );

$( '' )

.text( linkText || 'Give feedback!' )

.addClass( 'feedback-link link' )

.click( function () {

feedback.launch( {

subject: '[' + AFCH.consts.version + '] ' + ( type ? 'Feedback about ' + type : 'AFCH feedback' )

} );

} )

.appendTo( $element );

},

/**

* Represents a page, mainly a wrapper for various actions

*/

Page: function ( name ) {

var pg = this;

this.title = new mw.Title( name );

this.rawTitle = this.title.getPrefixedText();

this.additionalData = {};

this.hasAdditionalData = false;

this.toString = function () {

return this.rawTitle;

};

this.edit = function ( options ) {

var deferred = $.Deferred();

AFCH.actions.editPage( this.rawTitle, options )

.done( function ( data ) {

deferred.resolve( data );

} );

return deferred;

};

/**

* Makes an API request to get a variety of details about the current

* revision of the page, which it then sets.

* @param {bool} usecache if true, will resolve immediately if function has

* run successfully before

* @return {$.Deferred} resolves when data set successfully

*/

this._revisionApiRequest = function ( usecache ) {

var deferred = $.Deferred();

if ( usecache && pg.hasAdditionalData ) {

return deferred.resolve();

}

AFCH.actions.getPageText( this.rawTitle, {

hide: true,

moreProps: 'timestamp|user|ids',

moreParameters: { rvgeneratexml: true }

} ).done( function ( pagetext, data ) {

// Set internal data

pg.pageText = pagetext;

pg.additionalData.lastModified = new Date( data.timestamp );

pg.additionalData.lastEditor = data.user;

pg.additionalData.rawTemplateModel = data.parsetree;

pg.additionalData.revId = data.revid;

pg.hasAdditionalData = true;

// Resolve; it's now safe to request this data

deferred.resolve();

} );

return deferred;

};

/**

* Gets the page text

* @param {bool} usecache use cache if possible

* @return {string}

*/

this.getText = function ( usecache ) {

var deferred = $.Deferred();

this._revisionApiRequest( usecache ).done( function () {

deferred.resolve( pg.pageText );

} );

return deferred;

};

/**

* Gets templates on the page

* @return {array} array of objects, each representing a template like

* {

* target: 'templateName',

* params: { 1: 'foo', test: 'go to the {{bar}}' }

* }

*/

this.getTemplates = function () {

var $templateDom, templates = [],

deferred = $.Deferred();

this._revisionApiRequest( true ).done( function () {

$templateDom = $( $.parseXML( pg.additionalData.rawTemplateModel ) ).find( 'root' );

// We only want top level templates

$templateDom.children( 'template' ).each( function () {

var $el = $( this ),

data = {

target: $el.children( 'title' ).text(),

params: {}

};

/**

* Essentially, this function takes a template value DOM object, $v,

* and removes all signs of XML-ishness. It does this by manipulating

* the raw text and doing a few choice string replacements to change

* the templates to use wikicode syntax instead. Rather than messing

* with recursion and all that mess, /g is our friend...which is pefectly

* satisfactory for our purposes.

*/

function parseValue( $v ) {

var text = AFCH.jQueryToHtml( $v );

// Convert templates to look more template-y

text = text.replace( /