User:MarkTraceur/editDescriptions.js

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search
Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
/**
 * This is an on-site JS hack to make editing and adding descriptions easier.
 * This has only been tested on Information templates, but it probably works elsewhere...
 * To use, add 'importScript( 'User:MarkTraceur/editDescriptions.js' );' to [[Special:MyPage/common.js]]
 * and visit a file page that contains descriptions.
 */

( function ( $, mw ) {
	var api, pageTitle,
		debug = true;
		

	function plog( msg ) {
		if ( debug === true ) {
			console.log( msg );
		}
	}

	function getLanguageDropdown( defaultLang ) {
		return api.get( {
			format: 'json',
			action: 'query',
			list: 'categorymembers',
			cmtitle: 'Category:Language_templates',
			cmprop: 'title',
			cmlimit: 500
		} ).then( function ( data ) {
			var $dropdown = $( '<select>' ),
				tpls = [];

			$.each( data.query.categorymembers, function ( i, tpl ) {
				var title = mw.Title.newFromText( tpl.title ),
					langCode = title.getMain().toLowerCase();

				if ( tpl.ns === 10 && tpl.title !== 'Template:Unknown language' ) {
					$dropdown.append(
						$( '<option>' )
							.prop( 'selected', langCode === defaultLang )
							.text( langCode )
							.val( langCode )
					);
				}
			} );

			return $.Deferred().resolve( $dropdown );
		} );
	}
	
	function getParsedDescription( newText ) {
		return api.post( {
			action: 'parse',
			format: 'json',
			title: pageTitle,
			text: newText,
			prop: 'text'
		} ).then( function ( data ) {
			if ( data && data.parse && data.parse.text ) {
				return $.Deferred().resolve( $( $.parseHTML( data.parse.text['*'] ) ) );
			} else {
				return $.Deferred().reject();
			}
		} );
	}

	function getWikitext() {
		return api.get( {
			format: 'json',
			action: 'query',
			titles: pageTitle,
			prop: 'revisions',
			rvprop: 'content|timestamp',
			indexpageids: true
		} ).then( function ( data ) {
			var revision, timestamp,
				q = data.query,
				pages = q.pages,
				pageids = q.pageids,
				page = pages[pageids[0]];

			$.each( page.revisions, function ( i, rev ) {
				content = rev['*'];
				timestamp = rev.timestamp;
				return false;
			} );

			return $.Deferred().resolve( content, timestamp );
		} );
	}

	function updateWikitext( content, summary, timestamp ) {
		summary = summary || 'Modified descriptions with [[User:MarkTraceur/editDescriptions.js]].';

		return api.postWithToken( 'edit', {
			format: 'json',
			action: 'edit',
			title: pageTitle,
			summary: summary,
			text: content
		} );
	}

	function modifyDescriptions( wt, replaceFn, addFn ) {
		var i, replacementStart, replacementEnd,
			languagesFound = 0,
			curlang, newdesc, replacement,
			descSearchString, start,
			lastIndex = 0,
			replacements = [],
			capturing = '',
			lbraces = 0,
			rbraces = 0,
			lsquares = 0,
			rsquares = 0,
			bracelevels = 0,
			linklevels = 0,
			matches = wt.match( /\|\s*[Dd]escription\s*=/ ),
			newWt = '';

		try {
			descSearchString = matches[0];
			start = wt.indexOf( descSearchString ) + descSearchString.length;
		} catch ( e ) {
			// There was probably no description field. Fail!
			return;
		}

		if ( start < descSearchString.length ) {
			return;
		}

		addFn = addFn || function () { return {}; };

		for ( i = start; i < wt.length; i++ ) {
			c = wt[i];

			if ( c === '{' ) {
				lbraces++;
				plog( 'Now at ' + lbraces + ' left braces.' );
			}

			if ( lbraces === 2 ) {
				bracelevels++;
				linklevels = 0;
				lbraces = 0;
				plog( 'Now at ' + bracelevels + ' template scopes' );
				capturing = '';
			}

			if ( c === '}' ) {
				rbraces++;
				plog( 'Now at ' + rbraces + ' right braces.' );

				if ( rbraces === 2 ) {
					bracelevels--;
					linklevels = 0;
					rsquares = 0;
					lsquares = 0;
					rbraces = 0;
					plog( 'Now at ' + bracelevels + ' template scopes' );

					if ( curlang && capturing ) {
						languagesFound ++;

						newdesc = capturing.split( '=', 2 );

						if ( newdesc.length > 1 ) {
							newdesc = newdesc[1];
						} else {
							newdesc = newdesc[0];
						}

						replacement = replaceFn( curlang, newdesc );

						if ( replacement ) {
							replacementStart = i - 2 - capturing.length - curlang.length;
							replacementEnd = i - 1;

							plog( 'Pushing replacement for "' + wt.substring( replacementStart, replacementEnd ) + '": "' + replacement.join( '|1=' ) );
							replacements.push( {
								start: replacementStart,
								end: replacementEnd,
								value: replacement.join( '|1=' )
							} );
						}

						capturing = '';

						curlang = null;
					} else {
						capturing = '{{' + capturing + '}}';
					}
				}
			}

			if ( c === '|' && bracelevels === 0 && linklevels === 0 ) {
				additions = addFn();

				$.each( additions, function ( lang, content ) {
					replacements.push( {
						start: i - 1,
						end: i - 1,
						value: '{{' + lang + '|1=' + content + '}}'
					} );
				} );
				
				if ( languagesFound === 0 && capturing ) {
					plog( 'Found unset language for description "' + capturing + '".' );
					replacement = replaceFn( 'NO_LANGUAGE_SET', capturing );

					if ( replacement ) {
						plog( 'Pushing replacement for description with no language template: Set language to "' + replacement[0] + '" and set description to "' + replacement[1] + '".' );
						
						if ( replacement[0] === 'NO_LANGUAGE_SET' ) {
							replacement[0] = 'unknown language'; // God you hate to do this, but I don't really have a choice
						}

						replacements.push( {
							start: i - capturing.length,
							end: i - 1,
							value: '{{' + replacement.join( '|1=' ) + '}}'
						} );
					}
				}

				plog( 'Found closing |, leaving loop' );
				break;
			}

			if ( c === '[' ) {
				lsquares++;
				plog( 'Now at ' + lsquares + ' opening square brackets.' );

				if ( lsquares === 2 ) {
					linklevels++;
					plog( 'Now at ' + linklevels + ' link scopes.' );
					lsquares = 0;
				}
			}

			if ( c === ']' ) {
				rsquares++;
				plog( 'Now at ' + rsquares + ' closing square brackets.' );

				if ( rsquares === 2 && linklevels > 0 ) {
					linklevels--;
					plog( 'Now at ' + linklevels + ' link scopes.' );
					rsquares = 0;
				}
			}

			if ( !curlang && c === '|' && linklevels === 0 ) {
				plog( 'Description template call for ' + capturing );
				curlang = capturing;
				capturing = '';
			} else if ( c !== '{' && c !== '}' ) {
				capturing += c;
			}
		}

		if ( replacements.length === 0 ) {
			return wt;
		}

		for ( i = 0; i < replacements.length; i++ ) {
			// Replacements should be in order, so no need to worry about messing
			// that up.
			replacement = replacements[i];
			newWt += wt.substring( lastIndex, replacement.start );
			newWt += replacement.value;
			lastIndex = replacement.end;
			plog( 'Replaced "' + wt.substring( replacement.start, replacement.end ) + '" with "' + replacement.value );
		}

		newWt += wt.substring( lastIndex );

		return newWt;
	}

	function addEditButtons ( $spans ) {
		$spans.after(
			$( '<sup>' ).append(
				$( '<a>' ).attr( 'href', '#' ).text( '[edit]' ).click( function () {
					showEditInterface( $( this ).closest( '.description' ) );
					return false;
				} )
			)
		);
	}

	function showAddInterface() {
		var defaultLang = mw.config.get( 'wgUserLanguage' ) || 'en';

		getLanguageDropdown( defaultLang ).done( function ( $dropdown ) {
			plog( $dropdown );

			$( 'td.description' ).append(
				$( '<div>' ).addClass( 'description' ).append(
					$dropdown,
					$( '<textarea>' ),
					$( '<button>' ).text( 'Save' ).click( function ( e ) {
						var $this = $( this ),
							$desc = $this.closest( '.description' );

						e.stopPropagation();
						e.preventDefault();

						$desc.hide();

						getWikitext().done( function ( content, timestamp ) {
							var lang = $desc.find( 'select' ).val(),
								desc = $desc.find( 'textarea' ).val(),
								newDescWikitext = '{{' + lang + '|1=' + desc + '}}',
								newText = modifyDescriptions( content, function () {}, function () {
									var retval = {};
									retval[lang] = desc;
									return retval;
								} );

							plog( newText );

							updateWikitext(
								newText,
								'Added description for language \'' + lang + '\' with [[User:MarkTraceur/editDescriptions.js]]',
								timestamp
							).done( function () {
								getParsedDescription( newDescWikitext ).done( function ( $newDesc ) {
									$desc.empty().append( $newDesc ).show();
									addEditButtons( $desc.find( 'span.language' ) );
								} );
							} ).fail( function () {
								$desc.show();
							} );
						} );
					} )
				)
			);
		} );
	}

	function showEditInterface( $desc ) {
		var langCode = $desc.attr( 'lang' );

		getWikitext().done( function ( content, timestamp ) {
			getLanguageDropdown( langCode || 'en' ).done( function ( $dropdown ) {
				var $descForm,
					oldDesc = '',
					$langName = $desc.find( 'span.language' ).clone();

				modifyDescriptions( content, function ( lang, existingDesc ) {
					plog( lang + ' - ' + existingDesc );

					if ( lang === langCode ) {
						oldDesc = existingDesc;
					}
				} );

				$descForm = $( '<div>' )
					.addClass( 'mw-description-edit-form' )
					.css( {
						padding: '10px',
						width: '40%'
					} )
					.append(
						$dropdown,
						$( '<textarea>' ).val( oldDesc ),
						$( '<button>' ).text( 'Save' ).click( function ( e ) {
							var desc = $desc.find( 'textarea' ).val(),
								lang = $desc.find( 'select' ).val(),
								newDescWikitext = '{{' + lang + '|1=' + desc + '}}',
								newText = modifyDescriptions( content, function ( thislang ) {
									if ( thislang === langCode ) {
										return [ lang, desc ];
									}
								} );
	
							e.stopPropagation();
							e.preventDefault();
	
							$desc.hide();
	
							plog( newText );
	
							updateWikitext(
								newText,
								'Edited description for language \'' + lang + '\' with [[User:MarkTraceur/editDescriptions.js]]',
								timestamp
							).done( function () {
								getParsedDescription( newDescWikitext ).done( function ( $newDesc ) {
									$desc.empty().append( $newDesc ).show();
									addEditButtons( $desc.find( 'span.language' ) );
								} );
							} ).fail( function () {
								$desc.show();
							} );
						} ),
					$( '<button>' ).text( 'Preview' ).click( function ( e ) {
						var desc = $desc.find( 'textarea' ).val(),
							lang = $desc.find( 'select' ).val(),
							newDescWikitext = '{{' + lang + '|1=' + desc + '}}';
	
						e.stopPropagation();
						e.preventDefault();
	
						$desc.find( '.mw-description-preview' ).remove();
	
						getParsedDescription( newDescWikitext ).done( function ( $newDesc ) {
							$( '<div>' )
								.addClass( 'mw-description-preview' )
								.css( {
									border: '1px solid grey',
									width: '40%',
									padding: '20px',
									backgroundColor: '#DDDDFF'
								} )
								.append( $newDesc )
								.prependTo( $desc );
						} );
					} )
				);

				$desc.empty().append( $descForm );
			} );
		} );
	}

	mw.loader.using( [ 'mediawiki.api', 'mediawiki.Title' ] )
		.done( function () {
			api = new mw.Api();
			pageTitle = mw.Title.newFromText( mw.config.get( 'wgPageName' ) ).getPrefixedDb();
			$( function () {
				var $unattachedDesc, $langSpan,
					$descTd = $( '#fileinfotpl_desc' ).next( 'td' ),
					$descriptionDivs = $descTd.find( 'div.description' );
		
				$( '#fileinfotpl_desc' ).append(
					$( '<a>' )
						.attr( 'href', '#' )
						.text( 'Add another language' )
						.css( {
							fontWeight: 'normal',
							fontSize: '.75em',
							display: 'block'
						} )
						.click( function () {
							showAddInterface();
							return false;
						} )
				);
		
				if ( $descriptionDivs.find( 'span.language' ).length === 0 ) {
					$unattachedDesc = $descTd.contents().detach();
		
					if ( $descriptionDivs.length === 0 ) {
						$descriptionDivs = $( '<div>' ).addClass( 'description' ).attr( 'lang', 'NO_LANGUAGE_SET' ).appendTo( $descTd );
					}
		
					if ( $descriptionDivs.length > 1 ) {
						// Pretty sure this will never happen, but may as well be sure.
						$descriptionDivs = $descriptionDivs.first();
					}
		
					$langSpan = $( '<span>' ).addClass( 'language NO_LANGUAGE_SET' ).html( '<b>Language not set:</b>' ).appendTo( $descriptionDivs );
					$descriptionDivs.append( $unattachedDesc );
				}
		
				addEditButtons( $descriptionDivs.find( 'span.language' ) );
			} );
		} );
}( jQuery, mediaWiki ) );