"میڈیاویکی:Gadget-afchelper.js/core.js" کے نسخوں کے درمیان فرق
dfghjk>Admin م (۱ نسخه واردشده) |
Admin (تبادلۂ خیال | شراکتیں) م (1 نسخہ درآمد کیا گیا) |
حالیہ نسخہ بمطابق 18:12، 2 اگست 2023ء
/* Uploaded from https://github.com/WPAFC/afch-rewrite, commit: fd9c10c62701c4a5f5e9c21d94c5aed3d5650537 (master) */
// <nowiki>
/*
* 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,'&')
.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('بستن برچسب بدون گشاینده: /' + token.n);
}
opener = stack.pop();
if (token.n != opener.n && !isCloser(token.n, opener.n, customTags)) {
throw new Error('خطای تودرتویی: ' + opener.n + ' در مقابل ' + token.n);
}
opener.end = token.i;
return instructions;
} else {
instructions.push(token);
}
}
if (stack.length > 0) {
throw new Error('برچسب بستن ناموجود: ' + 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);
;//<nowiki>
( 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;
}
AFCH.api = new mw.Api();
// Set up the preferences interface
AFCH.preferences = new AFCH.Preferences();
AFCH.prefs = AFCH.preferences.prefStore;
// Must be defined above the larger $.extend block
// because AFCH.consts.summaryAd depends on it
AFCH.consts.version = '0.9.1';
// Add more constants -- don't overwrite those already set, though
AFCH.consts = $.extend( AFCH.consts, {
versionName: 'Imperial Ibex',
// If true, the script will NOT modify actual wiki content and
// will instead mock all such API requests (success assumed)
mockItUp: AFCH.consts.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: ' ([[WP:AFCH|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: 'ویکیپدیا:ویکیپروژه مقالهها برای ایجاد/مشارکتکنندگان'
}, 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 = $( '<span>' )
.append( 'اگر تمایل دارید که ابزار کمکی را غیرفعال کنید، ' )
.append( $( '<a>' )
.text( 'اینجا کلیک کنید' )
.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() ).postWithEditToken( {
action: 'options',
change: 'gadget-afchelper=0'
} ).done( function ( data ) {
mw.notify( 'ابزار مبا با موفقیت غیر فعال شد. اگر در آینده تمایل به فعالیسازی مجدد آن داشتید، ' +
' میتوانید از طریق صفححهٔ ترجیحات و فعالسازی گزینهٔ «ابزار کمکی ویکیپروژه وپ:مبا آن را فعال کنید».' );
} );
} )
)
.append( '. ' );
// Otherwise, AFCH is probably installed via common.js/skin.js:
// offer links for easy access.
} else {
$howToDisable = $( '<span>' )
.append( 'اگر تمایل دارید که ابزار کمکی را غیرفعال کنید، لازم است که بهصورت دستی ' +
'آن را از صفحهٔ ' )
.append( AFCH.makeLinkElementToPage( 'ویژه:صفحهٔ من/common.js', 'common.js' ) )
.append( ' یا ' )
.append( AFCH.makeLinkElementToPage( 'ویژه:صفحهٔ من/skin.js', 'skin.js' ) )
.append( 'خود حذف کنید. ' );
}
// Finally, make and push the notification, then explode AFCH
mw.notify(
$( '<div>' )
.append( 'ابزار مبا قادر به بارگیریشدن نبود؛ زیرا نام «' + user + '» در صفحهٔ ' )
.append( AFCH.makeLinkElementToPage( whitelist.rawTitle ) )
.append( ' فهرست نشدهسات. در آن صفحه میتوانید برای کسب دسترسی به این ابزار درخواست دهید. ' )
.append( $howToDisable )
.append( 'اگر هرگونه پرسش یا دغدغهای دارید، لطفاً ' )
.append( AFCH.makeLinkElementToPage( 'بوپ:کمبا', 'با ما در تماس باشید' ) )
.append( '!' ),
{
title: 'خطای ابزار مبا: کاربر فهرست نشدهاست',
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;
}
var promise = $.when();
if ( AFCH.consts.beta ) {
// Load minified css
mw.loader.load( AFCH.consts.scriptpath + '?action=raw&ctype=text/css&title=Mediawiki:Gadget-afchelper.css', 'text/css' );
promise = mw.loader.using( [
'jquery.chosen',
'jquery.spinner',
'jquery.ui',
'mediawiki.api',
'mediawiki.util',
'mediawiki.user'
] );
}
// And finally load the subscript
promise.then( function () {
$.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( 'بحث ویکیپدیا:ویکیپروژه مقالهها برای ایجاد/ابزار کمکی' ),
bugsLink: 'https://fa.wikipedia.org/w/index.php?title=بحث_ویکیپدیا:ویکیپروژه_مقالهها_برای_ایجاد/ابزار_کمکی&action=edit§ion=new',
bugsListLink: 'https://fa.wikipedia.org/w/index.php?title=بحث_ویکیپدیا:ویکیپروژه_مقالهها_برای_ایجاد/ابزار_کمکی'
} );
$( '<span>' )
.text( linkText || 'بازخورد دهید!' )
.addClass( 'feedback-link link' )
.click( function () {
feedback.launch( {
subject: '[' + AFCH.consts.version + '] ' + ( type ? 'بازخورد دربارهٔ ' + type : 'بازخورد ابزار مبا' )
} );
} )
.appendTo( $element );
},
/**
* Represents a page, mainly a wrapper for various actions
*
* @param name
*/
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 {jQuery.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.
*
* @param $v
*/
function parseValue( $v ) {
var text = AFCH.jQueryToHtml( $v );
// Convert templates to look more template-y
text = text.replace( /<template>/g, '{{' );
text = text.replace( /<\/template>/g, '}}' );
text = text.replace( /<part>/g, '|' );
// Expand embedded tags (like <nowiki>)
text = text.replace( new RegExp( '<ext><name>(.*?)<\\/name>(?:<attr>.*?<\\/attr>)*' +
'<inner>(.*?)<\\/inner><close>(.*?)<\\/close><\\/ext>', 'g' ), '<$1>$2$3' );
// Now convert it back to text, removing all the rest of the XML tags
return $( text ).text();
}
$el.children( 'part' ).each( function () {
var $part = $( this ),
$name = $part.children( 'name' ),
// Use the name if set, or fall back to index if implicitly numbered
name = $.trim( $name.text() || $name.attr( 'index' ) ),
value = $.trim( parseValue( $part.children( 'value' ) ) );
data.params[ name ] = value;
} );
templates.push( data );
} );
deferred.resolve( templates );
} );
return deferred;
};
/**
* Gets the categories from the page
*
* @param {bool} useApi If true, use the api to get categories, instead of parsing the page. This is
* necessary if you need info about transcluded categories.
* @param {bool} includeCategoryLinks If true, will also include links to categories (e.g. [[:Category:Foo]]).
* Note that if useApi is true, includeCategoryLinks must be false.
* @return {Array}
*/
this.getCategories = function ( useApi, includeCategoryLinks ) {
var deferred = $.Deferred(),
text = this.pageText;
if ( useApi ) {
AFCH.api.getCategories( this.title ).done( function ( categories ) {
// The api returns mw.Title objects, so we convert them to simple
// strings before resolving the deferred.
deferred.resolve( categories ? $.map( categories, function ( cat ) {
return cat.getPrefixedText();
} ) : [] );
} );
return deferred;
}
this._revisionApiRequest( true ).done( function () {
var catRegex = new RegExp( '\\[\\[' + ( includeCategoryLinks ? ':?' : '' ) + 'رده:(.*?)\\s*\\]\\]', 'gi' ),
match = catRegex.exec( text ),
categories = [];
while ( match ) {
// Name of each category, with first letter capitalized
categories.push( match[ 1 ].charAt( 0 ).toUpperCase() + match[ 1 ].substring( 1 ) );
match = catRegex.exec( text );
}
deferred.resolve( categories );
} );
return deferred;
};
this.getShortDescription = function () {
return AFCH.api.get( {
action: 'query',
prop: 'description',
titles: this.rawTitle,
formatversion: 2
} ).then( function ( json ) {
return json.query.pages[ 0 ].description || '';
} );
};
this.getLastModifiedDate = function () {
var deferred = $.Deferred();
this._revisionApiRequest( true ).done( function () {
deferred.resolve( pg.additionalData.lastModified );
} );
return deferred;
};
this.getLastEditor = function () {
var deferred = $.Deferred();
this._revisionApiRequest( true ).done( function () {
deferred.resolve( pg.additionalData.lastEditor );
} );
return deferred;
};
this.getCreator = function () {
var request, deferred = $.Deferred();
if ( this.additionalData.creator ) {
deferred.resolve( this.additionalData.creator );
return deferred;
}
request = {
action: 'query',
prop: 'revisions',
rvprop: 'user',
rvdir: 'newer',
rvlimit: 1,
indexpageids: true,
titles: this.rawTitle
};
// FIXME: Handle failure more gracefully
AFCH.api.get( request )
.done( function ( data ) {
var rev, id = data.query.pageids[ 0 ];
if ( id && data.query.pages[ id ] ) {
rev = data.query.pages[ id ].revisions[ 0 ];
pg.additionalData.creator = rev.user;
deferred.resolve( rev.user );
} else {
deferred.reject( data );
}
} );
return deferred;
};
this.exists = function () {
var deferred = $.Deferred();
AFCH.api.get( {
action: 'query',
prop: 'info',
titles: this.rawTitle
} ).done( function ( data ) {
// A nonexistent page will be indexed as '-1'
if ( data.query.pages.hasOwnProperty( '-1' ) ) {
deferred.resolve( false );
} else {
deferred.resolve( true );
}
} );
return deferred;
};
/**
* Gets the associated talk page
*
* @param textOnly
* @return {AFCH.Page}
*/
this.getTalkPage = function ( textOnly ) {
var title, ns = this.title.getNamespaceId();
// Odd-numbered namespaces are already talk namespaces
if ( ns % 2 !== 0 ) {
return this;
}
title = new mw.Title( this.title.getMainText(), ns + 1 );
return new AFCH.Page( title.getPrefixedText() );
};
},
/**
* Perform a specific action
*/
actions: {
/**
* Gets the full wikicode content of a page
*
* @param {string} pagename The page to get the contents of, namespace included
* @param {Object} options Object with properties:
* hide: {bool} set to true to hide the API request in the status log
* moreProps: {string} additional properties to request, separated by `|`,
* moreParameters: {object} additioanl query parameters
* @return {jQuery.Deferred} Resolves with pagetext and full data available as parameters
*/
getPageText: function ( pagename, options ) {
var status, request, rvprop = 'content',
deferred = $.Deferred();
if ( !options.hide ) {
status = new AFCH.status.Element( 'در حال دریافت $1...',
{ $1: AFCH.makeLinkElementToPage( pagename ) } );
} else {
status = AFCH.consts.nullstatus;
}
if ( options.moreProps ) {
rvprop += '|' + options.moreProps;
}
request = {
action: 'query',
prop: 'revisions',
rvprop: rvprop,
format: 'json',
indexpageids: true,
titles: pagename
};
$.extend( request, options.moreParameters || {} );
AFCH.api.get( request )
.done( function ( data ) {
var rev, id = data.query.pageids[ 0 ];
if ( id && data.query.pages ) {
// The page might not exist; resolve with an empty string
if ( id === '-1' ) {
deferred.resolve( '', {} );
return;
}
rev = data.query.pages[ id ].revisions[ 0 ];
deferred.resolve( rev[ '*' ], rev );
status.update( '$1 دریافت شد' );
} else {
deferred.reject( data );
// FIXME: get detailed error info from API result
status.update( 'خطا در دریافت $1: ' + JSON.stringify( data ) );
}
} )
.fail( function ( err ) {
deferred.reject( err );
status.update( 'خطا در دریافت $1: ' + JSON.stringify( err ) );
} );
return deferred;
},
/**
* Modifies a page's content
* TODO the property name "contents" is quite silly, because people used to the MediaWiki API are gonna write "text"
*
* @param {string} pagename The page to be modified, namespace included
* @param {Object} options Object with properties:
* contents: {string} the text to add to/replace the page,
* summary: {string} edit summary, will have the edit summary ad at the end,
* createonly: {bool} set to true to only edit the page if it doesn't exist,
* mode: {string} 'appendtext' or 'prependtext'; default: (replace everything)
* hide: {bool} Set to true to supress logging in statusWindow
* statusText: {string} message to show in status; default: "Editing"
* @return {jQuery.Deferred} Resolves if saved with all data
*/
editPage: function ( pagename, options ) {
var status, request, deferred = $.Deferred();
if ( !options ) {
options = {};
}
if ( !options.hide ) {
status = new AFCH.status.Element( ( options.statusText || 'در حال ویرایش' ) + ' $1...',
{ $1: AFCH.makeLinkElementToPage( pagename ) } );
} else {
status = AFCH.consts.nullstatus;
}
request = {
action: 'edit',
text: options.contents,
title: pagename,
summary: options.summary + AFCH.consts.summaryAd
};
if ( pagename.indexOf( 'پیشنویس:' ) === 0 ) {
request.nocreate = 'true';
}
if ( options.minor ) {
request.minor = 'true';
}
// Depending on mode, set appendtext=text or prependtext=text,
// which overrides the default text option
if ( options.mode ) {
request[ options.mode ] = options.contents;
}
if ( AFCH.consts.mockItUp ) {
AFCH.log( 'ویرایش در «' + pagename + '»', request );
deferred.resolve();
return deferred;
}
AFCH.api.postWithEditToken( request )
.done( function ( data ) {
var $diffLink;
if ( data && data.edit && data.edit.result && data.edit.result === 'Success' ) {
deferred.resolve( data );
if ( data.edit.hasOwnProperty( 'nochange' ) ) {
status.update( 'هیچ تغییر در $1 اعمال نشد' );
return;
}
// Create a link to the diff of the edit
$diffLink = AFCH.makeLinkElementToPage(
'ویژه:تفاوت/' + data.edit.oldrevid + '/' + data.edit.newrevid, '(تفاوت)'
).addClass( 'text-smaller' );
status.update( '$1 ذخیره شد ' + AFCH.jQueryToHtml( $diffLink ) );
} else {
deferred.reject( data );
// FIXME: get detailed error info from API result??
status.update( 'خطا در زمان ذخیرهسازی $1: ' + JSON.stringify( data ) );
}
} )
.fail( function ( err ) {
deferred.reject( err );
status.update( 'خطا در زمان ذخیرهسازی $1: ' + JSON.stringify( err ) );
} );
return deferred;
},
/**
* Deletes a page
*
* @param {string} pagename Page to delete
* @param {string} reason Reason for deletion; shown in deletion log
* @return {jQuery.Deferred} Resolves with success/failure
*/
deletePage: function ( pagename, reason ) {
// FIXME: implement
return false;
},
/**
* Moves a page
*
* @param {string} oldTitle Page to move
* @param {string} newTitle Move target
* @param {string} reason Reason for moving; shown in move log
* @param {Object} additionalParameters https://www.mediawiki.org/wiki/API:Move#Parameters
* @param {bool} hide Don't show the move in the status display
* @return {jQuery.Deferred} Resolves with success/failure
*/
movePage: function ( oldTitle, newTitle, reason, additionalParameters, hide ) {
var status, request, deferred = $.Deferred();
if ( !hide ) {
status = new AFCH.status.Element( 'در حال انتقال $1 به $2...', {
$1: AFCH.makeLinkElementToPage( oldTitle ),
$2: AFCH.makeLinkElementToPage( newTitle )
} );
} else {
status = AFCH.consts.nullstatus;
}
request = $.extend( {
action: 'move',
from: oldTitle,
to: newTitle,
reason: reason + AFCH.consts.summaryAd
}, additionalParameters );
if ( AFCH.consts.mockItUp ) {
AFCH.log( request );
deferred.resolve( { to: newTitle } );
return deferred;
}
AFCH.api.postWithEditToken( request ) // Move token === edit token
.done( function ( data ) {
if ( data && data.move ) {
status.update( '$1 به $2 منتقل شد' );
deferred.resolve( data.move );
} else {
// FIXME: get detailed error info from API result??
status.update( 'خطا در انتقال $1 به $2: ' + JSON.stringify( data.error ) );
deferred.reject( data.error );
}
} )
.fail( function ( err ) {
status.update( 'خطا در انتقال $1 به $2: ' + JSON.stringify( err ) );
deferred.reject( err );
} );
return deferred;
},
/**
* Notifies a user. Follows redirects and appends a message
* to the bottom of the user's talk page.
*
* @param {string} user
* @param {Object} data object with properties
* - message: {string}
* - summary: {string}
* - hide: {bool}, default false
* @param options
* @return {jQuery.Deferred} Resolves with success/failure
*/
notifyUser: function ( user, options ) {
var deferred = $.Deferred(),
userTalkPage = new AFCH.Page( new mw.Title( user, 3 ).getPrefixedText() ); // 3 = user talk namespace
userTalkPage.exists().done( function ( exists ) {
userTalkPage.edit( {
contents: ( exists ? '' : '{{صفحه بحث}}' ) + '\n\n' + options.message,
summary: options.summary || 'آگاهسازی کاربر',
mode: 'appendtext',
statusText: 'در حال آگاهسازی',
hide: options.hide
} )
.done( function () {
deferred.resolve();
} )
.fail( function () {
deferred.reject();
} );
} );
return deferred;
},
/**
* Logs a CSD nomination
*
* @param {Object} options
* - title {string}
* - reason {string}
* - usersNotified {array} optional
* @return {jQuery.Deferred} resolves false if the page did not exist, otherwise
* resolves/rejects with data from the edit
*/
logCSD: function ( options ) {
var deferred = $.Deferred(),
logPage = new AFCH.Page( 'کاربر:' + mw.config.get( 'wgUserName' ) + '/' +
( window.Twinkle && window.Twinkle.getPref( 'speedyLogPageName' ) || 'سیاهه حذف سریع' ) );
// Abort if user disabled in preferences
if ( !AFCH.prefs.logCsd ) {
return;
}
logPage.getText().done( function ( logText ) {
var status,
date = new Date(),
monthNames = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ],
headerRe = new RegExp( '^==+\\s*' + monthNames[ date.getMonth() ] + '\\s+' + date.getUTCFullYear() + '\\s*==+', 'm' ),
appendText = '';
// Don't edit if the page has doesn't exist or has no text
if ( !logText ) {
deferred.resolve( false );
return;
}
// Add header for new month if necessary
if ( !headerRe.test( logText ) ) {
appendText += '\n\n=== ' + monthNames[ date.getMonth() ] + ' ' + date.getUTCFullYear() + ' ===';
}
appendText += '\n# [[:' + options.title + ']]: ' + options.reason;
if ( options.usersNotified && options.usersNotified.length ) {
appendText += '؛ {{Userv|1=' + options.usersNotified.shift() + '}} آگاهسازی شد';
$.each( options.usersNotified, function ( _, user ) {
appendText += ', {{Userv|1=' + user + '}}';
} );
}
appendText += ' ~~' + '~~' + '~\n';
logPage.edit( {
contents: appendText,
mode: 'appendtext',
summary: 'ثبت سیاههٔ نامزدی حذف سریع [[' + options.title + ']]',
statusText: 'ثبت سیاههٔ نامزدی حذف سریع'
} ).done( function ( data ) {
deferred.resolve( data );
} ).fail( function ( data ) {
deferred.reject( data );
} );
} );
return deferred;
},
/**
* If user is allowed, marks a given recentchanges ID as patrolled
*
* @param {string|number} rcid rcid to mark as patrolled
* @param {string} title Prettier title to display. If not specified, falls back to just
* displaying the rcid instead.
* @return {jQuery.Deferred}
*/
patrolRcid: function ( rcid, title ) {
var request, deferred = $.Deferred(),
status = new AFCH.status.Element( 'در حال گشتزدن $1...',
{ $1: AFCH.makeLinkElementToPage( title ) || 'صفحه با شناسهٔ #' + rcid } );
request = {
action: 'patrol',
rcid: rcid
};
if ( AFCH.consts.mockItUp ) {
AFCH.log( request );
deferred.resolve();
return deferred;
}
AFCH.api.postWithToken( 'patrol', request ).done( function ( data ) {
if ( data.patrol && data.patrol.rcid ) {
status.update( '$1 گشت زده شد' );
deferred.resolve( data );
} else {
status.update( 'ناتوان در گشت زدن $1: ' + JSON.stringify( data.patrol ) );
deferred.reject( data );
}
} ).fail( function ( data ) {
status.update( 'ناتوان در گشت زدن $1: ' + JSON.stringify( data ) );
deferred.reject( data );
} );
return deferred;
}
},
/**
* Series of functions for logging statuses and whatnot
*/
status: {
/**
* Represents the status container, created ub init()
*/
container: false,
/**
* Creates the status container
*
* @param {selector} location String/jQuery selector for where the
* status container should be prepended
*/
init: function ( location ) {
AFCH.status.container = $( '<div>' )
.attr( 'id', 'afchStatus' )
.addClass( 'afchStatus' )
.prependTo( location || '#mw-content-text' );
},
/**
* Represents an element in the status container
*
* @param {string} initialText Initial text of the element
* @param {Object} substitutions key-value pairs of strings that should be replaced by something
* else. For example, { '$2': mw.user.getUser() }. If not redefined, $1
* will be equal to the current page name.
*/
Element: function ( initialText, substitutions ) {
/**
* Replace the status element with new html content
*
* @param {jQuery|string} html Content of the element
* Can use $1 to represent the page name
*/
this.update = function ( html ) {
// Convert to HTML first if necessary
if ( html.jquery ) {
html = AFCH.jQueryToHtml( html );
}
// First run the substutions
$.each( this.substitutions, function ( key, value ) {
// If we are passed a jQuery object, convert it to regular HTML first
if ( value.jquery ) {
value = AFCH.jQueryToHtml( value );
}
html = html.replace( key, value );
} );
// Then update the element
this.element.html( html );
};
/**
* Remove the element from the status container
*/
this.remove = function () {
this.update( '' );
};
// Sanity check, there better be a status container
if ( !AFCH.status.container ) {
AFCH.status.init();
}
if ( !substitutions ) {
substitutions = { $1: AFCH.consts.pagelink };
} else {
substitutions = $.extend( {}, { $1: AFCH.consts.pagelink }, substitutions );
}
this.substitutions = substitutions;
this.element = $( '<li>' )
.appendTo( AFCH.status.container );
this.update( initialText );
}
},
/**
* A simple framework for getting/setting interface messages.
* Not every message necessarily needs to go through here. But
* it's nice to separate long messages from the code itself.
*
* @type {Object}
*/
msg: {
/**
* AFCH messages loaded by default for all subscripts.
*
* @type {Object}
*/
store: {},
/**
* Retrieve the text of a message, or a placeholder if the
* message is not set
*
* @param {string} key Message key
* @param {Object} substitutions replacements to make
* @return {string} Message value
*/
get: function ( key, substitutions ) {
var text = AFCH.msg.store[ key ] || '<' + key + '>';
// Perform substitutions if necessary
if ( substitutions ) {
$.each( substitutions, function ( original, replacement ) {
text = text.replace(
// Escape the original substitution key, then make it a global regex
new RegExp( original.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' ), 'g' ),
replacement
);
} );
}
return text;
},
/**
* Set a new message or messages
*
* @param {string | Object} key
* @param {string} value if key is a string, value
*/
set: function ( key, value ) {
if ( typeof key === 'object' ) {
$.extend( AFCH.msg.store, key );
} else {
AFCH.msg.store[ key ] = value;
}
}
},
/**
* Store persistent data for the user. Data is stored over
* several layers: window-locally, in a variable; broswer-locally,
* via localStorage, and finally not-so-locally-at-all, via
* mw.user.options.
*
* == REDUNDANCY, EXPLAINED ==
* The reason for this redundancy is because of an obnoxious
* little thing called caching. Ideally the script would simply
* use mw.user.options, but *apparently* MediaWiki doesn't always
* provide the most updated mw.user.options on page load -- in some
* instances, it will provide an stale, cached version instead.
* This is most certainly a MediaWiki bug, but in the meantime, we
* circumvent it by adding numerous layers of redundancy to the whole
* getup. In this manner, hopefully by the time we have to rely on
* mw.user.options, the cache will have been invalidated and the world
* won't explode. *sighs repeatedly* --Theopolisme, 26 May 2014
*
* @type {Object}
*/
userData: {
/** @internal */
_prefix: 'userjs-afch-',
/**
* @internal
* This is used to cache the updated values of recently set
* (through AFCH.userData.set) options, since mw.user.options.get
* won't include items set after the page was first loaded.
* @type {Object}
*/
_optsCache: {},
/**
* Set a value in the data store
*
* @param {string} key
* @param {Mixed} value
* @return {jQuery.Deferred} success
*/
set: function ( key, value ) {
var deferred = $.Deferred(),
fullKey = AFCH.userData._prefix + key,
fullValue = JSON.stringify( value );
// Update cache so AFCH.userData.get() will have updated
// information if the page isn't reloaded first. If for
// some reason the post fails...oh well...
AFCH.userData._optsCache[ fullKey ] = fullValue;
// Also update localStorage cache for more redundancy.
// See note in AFCH.userData docs for why this is necessary.
if ( window.localStorage ) {
window.localStorage[ fullKey ] = fullValue;
}
AFCH.api.postWithEditToken( {
action: 'options',
optionname: fullKey,
optionvalue: fullValue
} ).done( function ( data ) {
deferred.resolve( data );
} );
return deferred;
},
/**
* Gets a value from the data store
*
* @param {string} key
* @param {Mixed} fallback fallback if option not present
* @return {Mixed} value
*/
get: function ( key, fallback ) {
var value,
fullKey = AFCH.userData._prefix + key,
cachedWindow = AFCH.userData._optsCache[ fullKey ],
cachedLocal = window.localStorage && window.localStorage[ fullKey ];
// Use cached value if possible, see explanation in AFCH.userData docs.
value = cachedWindow || cachedLocal;
if ( value ) {
return JSON.parse( value );
}
// Otherwise just use mw.user.options (with fallback).
return JSON.parse( mw.user.options.get( fullKey, JSON.stringify( fallback || false ) ) );
}
},
/**
* AFCH.Preferences is a mechanism for accessing and altering user
* preferences in regards to the script.
*
* Preferences are edited by the user via a jquery.ui.dialog and are
* saved and persist for the user using AFCH.userData.
*
* Typical usage:
* AFCH.preferences = new AFCH.Preferences();
* AFCH.preferences.initLink( $( '.put-prefs-link-here' ) );
*
* @type {Object}
*/
Preferences: function () {
var prefs = this;
/**
* Default values for user preferences; details for each preference can be
* found inline in `templates/tpl-preferences.html`.
*
* @type {Object}
*/
this.prefDefaults = {
autoOpen: false,
logCsd: true,
launchLinkPosition: 'p-cactions'
};
/**
* Current user's preferences
*
* @type {Object}
*/
this.prefStore = $.extend( {}, this.prefDefaults, AFCH.userData.get( 'preferences', {} ) );
/**
* Initializes the preferences modification dialog
*/
this.initDialog = function () {
var $spinner = $.createSpinner( {
size: 'large',
type: 'block'
} ).css( 'padding', '20px' );
if ( !this.$dialog ) {
// Initialize the $dialog div
this.$dialog = $( '<div>' );
}
// Until we finish lazy-loading the prefs interface,
// show a spinner in its place.
this.$dialog.empty().append( $spinner );
this.$dialog.dialog( {
width: 500,
autoOpen: false,
title: 'AFCH Preferences',
modal: true,
buttons: [
{
text: 'لغو',
click: function () {
prefs.$dialog.dialog( 'close' );
}
},
{
text: 'ذخیرهسازی ترجیحات',
click: function () {
prefs.save();
prefs.$dialog.empty().append( $spinner );
}
}
]
} );
// If we've already fetched the template, render immediately
if ( this.views ) {
this.renderMain();
} else {
// Otherwise, load the template file and *then* render
$.ajax( {
type: 'GET',
url: AFCH.consts.baseurl + '/tpl-preferences.js',
dataType: 'text'
} ).done( function ( data ) {
prefs.views = new AFCH.Views( data );
prefs.renderMain();
} );
}
};
/**
* Renders the main preferences menu in the $dialog
*/
this.renderMain = function () {
if ( !( this.views && this.$dialog ) ) {
return;
}
// Empty the dialog and render the preferences view. Provides the values of all
// of the preferences as variables, as well as an additional few used in other locations.
this.$dialog.empty().append(
this.views.renderView( 'preferences', $.extend( {}, this.prefStore, {
version: AFCH.consts.version,
versionName: AFCH.consts.versionName,
userAgent: window.navigator.userAgent
} ) )
);
// Manually handle selecting the desired value in <select> menus
this.$dialog.find( 'select' ).each( function () {
var $select = $( this ),
id = $select.attr( 'id' ),
value = prefs.prefStore[ id ];
$select.find( 'option[value="' + value + '"]' ).prop( 'selected', true );
} );
};
/**
* Updates prefs based on data in the dialog which
* is created in AFCH.preferences.init().
*/
this.save = function () {
// First, hide the buttons so the user won't start multiple actions
this.$dialog.dialog( { buttons: [] } );
// Now update the prefStore
$.extend( this.prefStore, AFCH.getFormValues( this.$dialog.find( '.afch-input' ) ) );
// Set the new userData value
AFCH.userData.set( 'preferences', this.prefStore ).done( function () {
// When we're done, close the dialog and notify the user
prefs.$dialog.dialog( 'close' );
mw.notify( 'ابزار مبا: ترجیحات با موفقیت ذخیره شد! این تنظیمات به محض بارگیری مجدد این صفحه ' +
'یا هرگاه که به صفحهای دیگر بروید، اعمال خواهند شد.' );
} );
};
/**
* Adds a link to launch the preferences modification dialog
*
* @param {jQuery} $element element to append the link to
* @param {string} linkText text to display in the link
*/
this.initLink = function ( $element, linkText ) {
$( '<span>' )
.text( linkText || 'روزآمدسازی ترجیحات' )
.addClass( 'preferences-link link' )
.appendTo( $element )
.click( function () {
prefs.initDialog();
prefs.$dialog.dialog( 'open' );
} );
};
},
/**
* Represents a series of "views", aka templateable thingamajigs.
* When creating a set of views, they are loaded from a given piece of
* text. Uses <hogan.js>.
*
* Views on the cheap! Just use one mega template and divide it up into
* lots of baby templates :)
*
* @param {string} [src] text to parse for template contents initially
*/
Views: function ( src ) {
this.views = {};
this.setView = function ( name, content ) {
this.views[ name ] = content;
};
this.renderView = function ( name, data ) {
var view = this.views[ name ],
template = Hogan.compile( view );
return template.render( data );
};
this.loadFromSrc = function ( src ) {
var viewRegex = /<!--\s(.*?)\s-->\r?\n([\s\S]*?)<!--\s\/(.*?)\s-->/g,
match = viewRegex.exec( src );
while ( match !== null ) {
var key = match[ 1 ],
content = match[ 2 ];
this.setView( key, content );
// Increment the match
match = viewRegex.exec( src );
}
};
this.loadFromSrc( src );
},
/**
* Represents a specific window into an AFCH.Views object
*
* @param {AFCH.Views} views location where the views are gleaned
* @param {jQuery} $element
*/
Viewer: function ( views, $element ) {
this.views = views;
this.$element = $element;
this.previousState = false;
this.loadView = function ( view, data ) {
var code = this.views.renderView( view, data );
// Update the view cache
this.previousState = this.$element.clone( true );
this.$element.html( code );
};
this.loadPrevious = function () {
this.$element.replaceWith( this.previousState );
this.$element = this.previousState;
};
},
/**
* Removes a key from a given object and returns the value of the key
*
* @param object
* @param {string} key
* @return {Mixed}
*/
getAndDelete: function ( object, key ) {
var v = object[ key ];
delete object[ key ];
return v;
},
/**
* Removes all occurences of a value from an array
*
* @param {Array} array
* @param {Mixed} value
*/
removeFromArray: function ( array, value ) {
var index = $.inArray( value, array );
while ( index !== -1 ) {
array.splice( index, 1 );
index = $.inArray( value, array );
}
},
/**
* Gets the values of all elements matched by a selector, including
* converting checkboxes to bools, providing textual values of select
* elements, ignoring placeholder elements, and more.
*
* For a radio button group, pass in the container element, which must
* be a fieldset with the appropriate "name" attribute. Its id will
* be used as the key in the data object.
*
* @param {jQuery} $selector elements to get values from
* @return {Object} object of values, with the ids as keys
*/
getFormValues: function ( $selector ) {
var data = {};
$selector.each( function ( _, element ) {
var value, allTexts,
$element = $( element );
if ( element.type === 'checkbox' ) {
value = element.checked;
} else if ( element.type === 'fieldset' ) {
value = $element.find( ':checked' ).val();
} else {
value = $element.val();
// Ignore placeholder text
if ( value === $element.attr( 'placeholder' ) ) {
value = '';
}
// For <select multiple> with nothing selected, jQuery returns null...
// convert that to an empty array so that $.each() won't explode later
if ( value === null ) {
value = [];
}
// Also provide the full text of the selected options in <select>.
// Primary use for this is the edit summary in handleDecline().
if ( element.nodeName.toLowerCase() === 'select' ) {
allTexts = [];
$element.find( 'option:selected' ).each( function () {
allTexts.push( $( this ).text() );
} );
data[ element.id + 'Texts' ] = allTexts;
}
}
data[ element.id ] = value;
} );
return data;
},
/**
* Creates an <a> element that links to a given page.
*
* @param {string} pagename - The title of the page.
* @param {string} displayTitle - What gets shown by the link.
* @param {boolean} [newTab=true] - Whether to open page in a new tab.
* @return {jQuery} <a> element
*/
makeLinkElementToPage: function ( pagename, displayTitle, newTab ) {
var actualTitle = pagename.replace( /_/g, ' ' );
// newTab is an optional parameter.
newTab = ( typeof newTab === 'undefined' ) ? true : newTab;
return $( '<a>' )
.attr( 'href', mw.util.getUrl( actualTitle ) )
.attr( 'id', 'afch-cat-link-' + pagename.toLowerCase().replace( / /g, '-' ).replace( /\//g, '-' ) )
.attr( 'title', actualTitle )
.text( displayTitle || actualTitle )
.attr( 'target', newTab ? '_blank' : '_self' );
},
/**
* Creates an <a> element that links to a random page in the given category.
*
* @param {string} pagename - The name of the category (without the namespace).
* @param {string} displayTitle - What gets shown by the link.
* @return {jQuery} <a> element
*/
makeLinkElementToCategory: function ( pagename, displayTitle ) {
var linkElement = AFCH.makeLinkElementToPage( 'ویژه:تصادفی_در_رده/' + pagename, displayTitle, false ),
linkText = displayTitle || pagename.replace( /_/g, ' ' ),
request = {
action: 'query',
titles: 'رده:' + pagename,
prop: 'categoryinfo'
},
linkSpan = $( '<span>' ).append( linkElement ),
countSpanId = 'afch-cat-count-' + pagename
.toLowerCase()
.replace( / /g, '-' )
.replace( /\//g, '-' );
linkSpan.append( $( '<span>' ).attr( 'id', countSpanId ) );
AFCH.api.get( request )
.done( function ( data ) {
if ( data.query.pages && !data.query.pages[ '-1' ] ) {
var pageKey = Object.keys( data.query.pages )[ 0 ],
pagesCount = data.query.pages[ pageKey ].categoryinfo.pages;
$( '#' + countSpanId ).text( ' (' + pagesCount + ')' );
// Disable link if there aren't any pages
$( '#afch-cat-link-' + pagename.toLowerCase().replace( / /g, '-' ).replace( /\//g, '-' ) ).replaceWith( displayTitle );
}
} );
return linkSpan;
},
/**
* Converts [[wikilink]] -> <a>
*
* @param {string} wikicode
* @return {string}
*/
convertWikilinksToHTML: function ( wikicode ) {
var newCode = wikicode,
wikilinkRegex = /\[\[(.*?)\s*(?:\|\s*(.*?))?\]\]/g,
wikilinkMatch = wikilinkRegex.exec( wikicode );
while ( wikilinkMatch ) {
var title = wikilinkMatch[ 1 ],
displayTitle = wikilinkMatch[ 2 ],
newLink = AFCH.makeLinkElementToPage( title, displayTitle );
// Replace the wikilink with the new <a> element
newCode = newCode.replace( wikilinkMatch[ 0 ], AFCH.jQueryToHtml( newLink ) );
// Increment match
wikilinkMatch = wikilinkRegex.exec( wikicode );
}
return newCode;
},
/**
* Returns the relative time that has elapsed between an oldDate and a nowDate
*
* @param {Date|string} old (if it is a string it will be assumed to be a
* MediaWiki timestamp and converted to a Date first)
* @param {Date} now optional, defaults to `new Date()`
* @return {string}
*/
relativeTimeSince: function ( old, now ) {
var oldDate = typeof old === 'object' ? old : AFCH.mwTimestampToDate( old ),
nowDate = typeof now === 'object' ? now : new Date(),
msPerMinute = 60 * 1000,
msPerHour = msPerMinute * 60,
msPerDay = msPerHour * 24,
msPerMonth = msPerDay * 30,
msPerYear = msPerDay * 365,
elapsed = nowDate - oldDate,
amount, unit;
if ( elapsed < msPerMinute ) {
amount = Math.round( elapsed / 1000 );
unit = 'ثانیه';
} else if ( elapsed < msPerHour ) {
amount = Math.round( elapsed / msPerMinute );
unit = 'دقیقه';
} else if ( elapsed < msPerDay ) {
amount = Math.round( elapsed / msPerHour );
unit = 'ساعت';
} else if ( elapsed < msPerMonth ) {
amount = Math.round( elapsed / msPerDay );
unit = 'روز';
} else if ( elapsed < msPerYear ) {
amount = Math.round( elapsed / msPerMonth );
unit = 'ماه';
} else {
amount = Math.round( elapsed / msPerYear );
unit = 'سال';
}
if ( amount !== 1 ) {
unit += 's';
}
return [ mw.language.convertNumber(amount), unit, 'پیش' ].join( ' ' );
},
/**
* Converts an element into a toggle for another element
*
* @param {string} toggleSelector When clicked, will show/hide elementSelector
* @param {string} elementSelector Element(s) to be shown or hidden
* @param {string} showText e.g. "Show the div"
* @param {string} hideText e.g. "Hide the div"
*/
makeToggle: function ( toggleSelector, elementSelector, showText, hideText ) {
// Remove current click handlers
$( toggleSelector ).off( 'click' );
// If show is true, we make the element visible and display hideText in
// the toggle. Otherwise, we hide the element and display showText.
function toggleState( show ) {
$( elementSelector ).toggleClass( 'hidden', !show );
$( toggleSelector ).text( show ? hideText : showText );
}
// Update everythign to match current state of the element
toggleState( $( elementSelector ).is( ':visible' ) );
// Add the new click handler
$( document ).on( 'click', toggleSelector, function () {
toggleState( $( elementSelector ).hasClass( 'hidden' ) );
} );
},
/**
* Gets the full raw HTML content of a jQuery object
*
* @param {jQuery} $element
* @return {string}
*/
jQueryToHtml: function ( $element ) {
return $( '<div>' ).append( $element ).html();
},
/**
* Given a string, returns by default a Date() object
* or, if mwstyle is true, a MediaWiki-style timestamp
*
* If there is no match, return false
*
* @param {string} string string to parse
* @param mwstyle
* @return {Date|integer}
*/
parseForTimestamp: function ( string, mwstyle ) {
var exp, match, date;
exp = new RegExp( '(\\d{1,2}):(\\d{2}), (\\d{1,2}) ' +
'(January|February|March|April|May|June|July|August|September|October|November|December) ' +
'(\\d{4}) \\(UTC\\)', 'g' );
match = exp.exec( string );
if ( !match ) {
return false;
}
date = new Date();
date.setUTCFullYear( match[ 5 ] );
date.setUTCMonth( mw.config.get( 'wgMonthNames' ).indexOf( match[ 4 ] ) - 1 ); // stupid javascript
date.setUTCDate( match[ 3 ] );
date.setUTCHours( match[ 1 ] );
date.setUTCMinutes( match[ 2 ] );
date.setUTCSeconds( 0 );
if ( mwstyle ) {
return AFCH.dateToMwTimestamp( date );
}
return date;
},
/**
* Parses a MediaWiki internal YYYYMMDDHHMMSS timestamp
*
* @param {string} string
* @return {Date|bool} if unable to parse, returns false
*/
mwTimestampToDate: function ( string ) {
var date, dateMatches = /(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/.exec( string );
// If it *isn't* actually a MediaWiki-style timestamp, pass directly to date
if ( dateMatches === null ) {
date = new Date( string );
// Otherwise use Date.UTC to assemble a date object using UTC time
} else {
date = new Date( Date.UTC(
dateMatches[ 1 ], dateMatches[ 2 ] - 1, dateMatches[ 3 ], dateMatches[ 4 ], dateMatches[ 5 ], dateMatches[ 6 ]
) );
}
// If invalid, return false
if ( isNaN( date.getUTCMilliseconds() ) ) {
return false;
}
return date;
},
/**
* Converts a Date object to YYYYMMDDHHMMSS format
*
* @param {Date} date
* @return {number}
*/
dateToMwTimestamp: function ( date ) {
return +( date.getUTCFullYear() +
( '0' + ( date.getUTCMonth() + 1 ) ).slice( -2 ) +
( '0' + date.getUTCDate() ).slice( -2 ) +
( '0' + date.getUTCHours() ).slice( -2 ) +
( '0' + date.getUTCMinutes() ).slice( -2 ) +
( '0' + date.getUTCSeconds() ).slice( -2 ) );
},
/**
* Returns the value of the specified URL parameter. By default it uses
* the current window's address. Optionally you can pass it a custom location.
* It returns null if the parameter is not present, or an empty string if the
* parameter is empty.
*
* @param {string} name parameter to get
* @param {string} url optional; custom url to search
* @return {string|null} value, or null if not present
*/
getParam: function () {
return mw.util.getParamValue.apply( this, arguments );
},
/**
* Given a code for an AfC decline reason (e.g. "v"), returns some HTML code
* describing the reason.
*
* @param {string} code an AfC decline reason code
* @return {jQuery.Deferred} Resolves with the requested HTML
*/
getReason: function ( code ) {
var deferred = $.Deferred();
$.post( 'https://fa.wikipedia.org/api/rest_v1/transform/wikitext/to/html',
'wikitext={{AfC submission/comments|' + code + '}}&body_only=true',
function ( data ) {
deferred.resolve( data );
}
);
return deferred;
}
} );
}( AFCH, jQuery, mediaWiki ) );
//</nowiki>