User:94rain/js/afch-master.js/core.js
外观
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
/* https://github.com/94rain/afch-zhwp, translated and adapted from
* https://github.com/WPAFC/afch-rewrite */
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;
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( 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 ) );
//<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.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:建立條目專題/沙盒"
pagelink: mw.util.getUrl(),
// Used when status is disabled
nullstatus: {
update: function () {
return;
}
},
// Current user
user: mw.user.getName(),
// Edit summary ad
summaryAd: ' ([[PJ:AFCH|AFCH]])',
// Require users to be on whitelist to use the script
whitelistRequired: true,
// Name of the whitelist page for reviewers
whitelistTitle: 'WikiProject:建立條目/參與者'
},
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, '\\$&' ),
userSysop = $.inArray( 'sysop', mw.config.get( 'wgUserGroups' ) ) > -1,
userNPP = $.inArray( 'patroller', mw.config.get( 'wgUserGroups' ) ) > -1,
userOnWhitelist = ( new RegExp( '\\|\\s*' + sanitizedUser + '\\s*}' ) ).test( text ),
userAllowed = userOnWhitelist || userSysop || userNPP;
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( wgULS( '如果要禁用辅助脚本,', '如果要禁用輔助腳本,' ) )
.append( $( '<a>' )
.text( wgULS( '点击这里', '點擊這裡' ) )
.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已被成功禁用。' );
} );
} ) )
.append( '. ' );
// Otherwise, AFCH is probably installed via common.js/skin.js --
// offer links for easy access.
} else {
$howToDisable = $( '<span>' )
.append(
wgULS(
'如果要禁用帮助程序脚本,则需要手动',
'如果要禁用幫助程序腳本,則需要手動' ) +
wgULS( '从你的', '從你的' ) )
.append( AFCH.makeLinkElementToPage(
'Special:MyPage/common.js', 'common.js' ) )
.append( '或' )
.append( AFCH.makeLinkElementToPage(
'Special:MyPage/skin.js', 'skin.js' ) )
.append( wgULS( '页面中移除。', '頁面中移除。' ) );
}
// Finally, make and push the notification, then explode AFCH
mw.notify(
$( '<div>' )
.append(
wgULS( 'AFCH不能加载,"', 'AFCH不能加載,"' ) + user +
wgULS( '"没有列在', '"沒有列在' ) )
.append( AFCH.makeLinkElementToPage( whitelist.rawTitle ) )
.append( wgULS(
'。您可以在那里申请使用AFC辅助脚本的权限。',
'。您可以在那裡申請使用AFC輔助腳本的權限。' ) )
.append( $howToDisable )
.append( wgULS(
'如果您有任何问题或疑虑,请', '如果您有任何問題或疑慮,請' ) )
.append( AFCH.makeLinkElementToPage(
'WT:AFCH', wgULS( '寻求帮助', '尋求幫助' ) ) )
.append( '!' ),
{
title: wgULS(
'AFCH错误:用户不在允许列表中',
'AFCH錯誤:用戶不在允許列表中' ),
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:94rain/js/afch-master.css',
'text/css' );
// Load dependencies
mw.loader.load( [
// jquery resources
'jquery.chosen', 'jquery.spinner', 'jquery.ui',
// 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:建立條目專題/協助腳本' ),
bugsLink:
'https://wikicn.playgoteam.workers.dev/w/index.php?title=WikiProject_talk:建立條目/協助腳本&action=edit§ion=new',
bugsListLink:
'https://wikicn.playgoteam.workers.dev/w/index.php?title=WikiProject_talk:建立條目/協助腳本'
} );
$( '<span>' )
.text( linkText || wgULS( '提供反馈!', '提供反饋!' ) )
.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( /<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 ? ':?' : '' ) +
'Category:(.*?)\\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.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,
tool: 'AFCH'
};
// 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
* @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 {$.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,
tool: 'AFCH'
};
$.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
* @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
};
// 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( request );
deferred.resolve();
return deferred;
}
AFCH.api.postWithToken( 'edit', 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(
wgULS( '没有对$1作出任何更改', '沒有對$1作出任何更改' ) );
return;
}
// Create a link to the diff of the edit
$diffLink = AFCH.makeLinkElementToPage(
'Special:Diff/' + data.edit.oldrevid + '/' +
data.edit.newrevid,
wgULS( '(差异)', '(差異)' ) )
.addClass( 'text-smaller' );
status.update( '已保存$1的更改' + AFCH.jQueryToHtml( $diffLink ) );
} else {
deferred.reject( data );
// FIXME: get detailed error info from API result??
status.update(
wgULS( '保存$1的更改失败:', '保存$1的更改失敗:' ) +
JSON.stringify( data ) );
}
} )
.fail( function ( err ) {
deferred.reject( err );
status.update(
wgULS( '保存$1的更改失败:', '保存$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 {$.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 {$.Deferred} Resolves with success/failure
*/
movePage: function ( oldTitle, newTitle, reason, additionalParameters, hide ) {
var status, request, deferred = $.Deferred();
if ( !hide ) {
status = new AFCH.status.Element(
wgULS( '正在移动$1至$2...', '正在移動$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
.postWithToken( 'edit', request ) // Move token === edit token
.done( function ( data ) {
if ( data && data.move ) {
status.update( wgULS( '移动$1至$2', '移動$1至$2' ) );
deferred.resolve( data.move );
} else {
// FIXME: get detailed error info from API result??
status.update(
wgULS( '移动$1至$2失败:', '移動$1至$2失敗:' ) +
JSON.stringify( data.error ) );
deferred.reject( data.error );
}
} )
.fail( function ( err ) {
status.update(
wgULS( '移动$1至$2失败:', '移動$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
* @return {$.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
talkPageName = 'User talk:' + user;
AFCH.api.get( { action: 'query', prop: 'info', titles: talkPageName } )
.done( function ( data ) {
var pages = data.query.pages;
var pageId = Object.keys( pages )[ 0 ];
var cm = pages[ pageId ].contentmodel;
if ( cm == 'flow-board' ) {
AFCH.api.postWithToken( 'csrf', {
action: 'flow',
page: talkPageName,
submodule: 'new-topic',
nttopic: options.summary,
ntcontent: options.message,
ntformat: 'wikitext'
} );
var status = new AFCH.status.Element(
( wgULS( '尝试对结构化讨论页面', '嘗試對結構化討論頁面' ) ) +
'$1' +
wgULS(
'做出了编辑,请检查此次', '做出了編輯,請檢查此次' ) +
'$2 $3',
{
$1: AFCH.makeLinkElementToPage( talkPageName ),
$2: AFCH.makeLinkElementToPage(
talkPageName, wgULS( '编辑', ' 編輯' ) ),
$3: AFCH.makeLinkElementToPage(
'WT:AFCH', wgULS( '(错误报告)', '(錯誤報告)' ) )
} );
} else if ( cm == 'wikitext' ) {
userTalkPage.exists().done( function ( exists ) {
userTalkPage.edit( {
contents: ( exists ? '' : '{{Talk header}}' ) + '\n\n' +
options.message,
summary: options.summary || '通知用户',
mode: 'appendtext',
statusText: '通知',
hide: options.hide
} );
} );
} else {
deferred.rejected();
}
deferred.resolved();
} )
.fail( function ( data ) {
deferred.rejected();
} );
return deferred;
},
/**
* Logs a CSD nomination
* @param {object} options
* - title {string}
* - reason {string}
* - usersNotified {array} optional
* @return {$.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(
'User:' + mw.config.get( 'wgUserName' ) + '/' +
( window.Twinkle && window.Twinkle.getPref( 'speedyLogPageName' ) ||
'CSD日志' ) );
// Abort if user disabled in preferences
if ( !AFCH.prefs.logCsd ) {
return;
}
logPage.getText().done( function ( logText ) {
var status, date = new Date(),
headerRe = new RegExp(
'^==+\\s*' + date.getUTCMonthName() + '\\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=== ' + date.getUTCMonthName() + ' ' +
date.getUTCFullYear() + ' ===';
}
appendText += '\n# [[:' + options.title + ']]: ' + options.reason;
if ( options.usersNotified && options.usersNotified.length ) {
appendText +=
'; 通知{{user|1=' + options.usersNotified.shift() + '}}';
$.each( options.usersNotified, function ( _, user ) {
appendText += ', {{user|1=' + user + '}}';
} );
}
appendText += ' ~~' +
'~~' +
'~\n';
logPage
.edit( {
contents: appendText,
mode: 'appendtext',
summary: wgULS( '记录对[[', '記錄對[[' ) + options.title +
']]的快速删除提名',
statusText: wgULS( '记录快速删除提名', '記錄快速刪除提名' )
} )
.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 {$.Deferred}
*/
patrolRcid: function ( rcid, title ) {
var request,
deferred = $.Deferred(),
status = new AFCH.status.Element(
wgULS( '正在将$1标记为已巡查...', '正在將$1標記為已巡查...' ), {
$1: AFCH.makeLinkElementToPage( title ) || 'page with id #' + 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(
wgULS( '将$1标记为已巡查失败:', '將$1標記為已巡查失敗:' ) +
JSON.stringify( data.patrol ) );
deferred.reject( data );
}
} )
.fail( function ( data ) {
status.update(
wgULS( '将$1标记为已巡查失败:', '將$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 );
}
},
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 {$.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
.postWithToken(
'options',
{ 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: wgULS( 'AFCH参数设置', 'AFCH偏好設定' ),
modal: true,
buttons: [
{
text: '取消',
click: function () {
prefs.$dialog.dialog( 'close' );
}
},
{
text: wgULS( '保存设置', '保存設置' ),
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 + wgULS( '/tpl-preferences_zh-hans.js', '/tpl-preferences_zh-hant.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( wgULS(
'AFCH: 参数设置项保存成功!它们将在当前页面重新加载或浏览其他页面时生效。',
'AFCH: 偏好設定項保存成功!它們將在當前頁面重新加載或瀏覽其他頁面時生效。' ) );
} );
};
/**
* 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 || wgULS( '更新设置', '更新設置' ) )
.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-->\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 {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(
'Special:RandomInCategory/' + pagename, displayTitle, false ),
linkText = displayTitle || pagename.replace( /_/g, ' ' ), request = {
action: 'query',
titles: 'Category:' + 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 = wgULS( '分钟', '分钟' );
} else if ( elapsed < msPerDay ) {
amount = Math.round( elapsed / msPerHour );
unit = wgULS( '小时', '小時' );
} 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 = '年';
}
return [ 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
* @return {Date|integer}
*/
parseForTimestamp: function ( string, mwstyle ) {
var exp, match, date;
exp = new RegExp(
'(\\d{1,2}):(\\d{2}), (\\d{1,2}) ' +
'(1月|2月|3月|4月|5月|6月|7月|8月|9月|10月|11月|12月) ' +
'(\\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 {$.Deferred} Resolves with the requested HTML
*/
getReason: function ( code ) {
var deferred = $.Deferred();
$.post(
'https://wikicn.playgoteam.workers.dev/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>