MediaWiki:Gadget-morfeusz-linker-special.js

Uwaga: aby zobaczyć zmiany po opublikowaniu, może zajść potrzeba wyczyszczenia pamięci podręcznej przeglądarki.

  • Firefox / Safari: Przytrzymaj Shift podczas klikania Odśwież bieżącą stronę, lub naciśnij klawisze Ctrl+F5, lub Ctrl+R (⌘-R na komputerze Mac)
  • Google Chrome: Naciśnij Ctrl-Shift-R (⌘-Shift-R na komputerze Mac)
  • Internet Explorer / Edge: Przytrzymaj Ctrl, jednocześnie klikając Odśwież, lub naciśnij klawisze Ctrl+F5
  • Opera: Naciśnij klawisze Ctrl+F5.
var morfeusz,
	MAX_CHARACTERS = 500,
	REQUEST_DELAY_WARN = 2500, // ms
	PREVIEW_DELAY = 250, // ms
	issueTracker = 'Wikidyskusja:Narzędzia/Linkowanie automatyczne',
	api = new mw.Api( { parameters: { formatversion: 2 } } );

mw.libs.MorfeuszAnalyzer = require( 'ext.gadget.morfeusz-analyzer' ).MorfeuszAnalyzer;
morfeusz = new mw.libs.MorfeuszAnalyzer();

if ( !!Number( mw.user.options.get( 'gadget-false-blue-links' ) ) ) {
	mw.loader.using( 'ext.gadget.false-blue-links' ).done( function ( require ) {
		mw.libs.falseBlueLinks = require( 'ext.gadget.false-blue-links' );
	} );
}

mw.messages.set( {
	'ml-special-fieldset-label': 'Linkowanie polskich słów',
	'ml-special-fieldset-description': 'Wprowadzony niżej niesformatowany tekst zostanie przepuszczony przez ' +
									   '<a href="http://morfeusz.sgjp.pl/doc/about/" target=_blank>analizator morfologiczny Morfeusz</a>, ' +
									   'zamieniając rozpoznane słowa na <a href="' + mw.util.getUrl( 'WS:Linki' ) +
									   '" target=_blank>linki w składni wiki</a>. ' +
									   'Program Morfeusz oraz dane fleksyjne SGJP/Polimorf udostępniono na licencji BSD 2-Clause ' +
									   '(<a href="http://morfeusz.sgjp.pl/doc/license/" target=_blank>warunki korzystania</a>).',
	'ml-special-input-label': 'Tekst bez wikilinków (maks. {{PLURAL:$1|$1 znak|$1 znaki|$1 znaków}}):',
	'ml-special-input-placeholder': 'Wpisz tekst.',
	'ml-special-send-button': 'Wyślij',
	'ml-special-report-issue': 'Zgłoś problem',
	'ml-special-progress-bar-label': 'Trwa wysyłanie danych...',
	'ml-special-failed-request': 'Zapytanie zwróciło następujące błędy: <ul>$1</ul>',
	'ml-special-failed-request-empty': 'brak wyników',
	'ml-special-warn-long-request': 'Zapytanie trwa dłużej niż zwykle. Możesz spróbować wysłać dane ponownie.',
	'ml-special-wikitext-header': 'Rezultat',
	'ml-special-wikitext-label': 'Poniższy kod można swobodnie edytować.',
	'ml-special-wikitext-help': '<p>Zostały wyróżnione <span class="ml-special-wikilink-more">linki</span>, ' +
								'którym można przyporządkować więcej niż jedną formę podstawową, ' +
								'oraz <span class="ml-special-wikilink-unknown">słowa</span> nienotowane ' +
								'w słowniku. Naciśnięcie na pierwsze skutkuje wyświetleniem dymku z polem ' +
								'listy i dostępnymi opcjami do wyboru.</p>' +
								'<p>Słownik: <strong id="ml-special-dict-id">?</strong>.</p>' +
								'<p>Wersja programu Morfeusz: <strong id="ml-special-version">?</strong>.</p>',
	'ml-special-clipboard-button': 'Skopiuj do schowka',
	'ml-special-clipboard-notice': 'Skopiowano do schowka',
	'ml-special-preview-header': 'Podgląd'
} );

function processItems( items ) {
	var out = [];
	
	function appendString( s ) {
		var lastItem = out[ out.length - 1 ];
		
		if ( typeof lastItem === 'string' ) {
			out[ out.length - 1 ] += s;
		} else {
			out.push( s );
		}
	}
	
	items.forEach( function ( item ) {
		var uniqueLemmas, $span;
		
		if ( item.unknown ) {
			// non-linkable element with special styling
			out.push( $span = $( '<span>' ).addClass( 'ml-special-wikilink-unknown' ).text( item.form ) );
		} else if ( !( 'lemmas' in item ) ) {
			// regular non-linkable element
			appendString( item.form );
		} else {
			uniqueLemmas = item.lemmas.map( function ( obj ) {
				return obj.lemma;
			} ).filter( function ( lemma, index, arr ) {
				return arr.indexOf( lemma ) == index; // remove duplicates
			} );
			
			if ( uniqueLemmas.length === 1 ) {
				// linkable, but no need to disambiguate; generate link and append to result
				appendString( mw.libs.MorfeuszAnalyzer.makeWikilink( uniqueLemmas[ 0 ], item.form ) );
			} else {
				// linkable with selector for disambiguation
				$span = $( '<span>' )
					.addClass( 'ml-special-wikilink-more' )
					.text( mw.libs.MorfeuszAnalyzer.makeWikilink( uniqueLemmas[ 0 ], item.form ) );
				
				out.push( $span.data( 'ml-data', {
					form: item.form,
					lemmas: uniqueLemmas,
					selected: uniqueLemmas[ 0 ],
					$el: $span
				} ) );
			}
		}
		
		if ( $span ) {
			$span.attr( 'data-ml-initial', $span.text() );
		}
	} );
	
	return out;
}

// https://stackoverflow.com/a/6150060
function selectEditableContents( el ) {
	var range, sel;
	range = document.createRange();
	range.selectNodeContents( el );
	sel = window.getSelection();
	sel.removeAllRanges();
	sel.addRange(range);
}

function previewWikitext( text, $container ) {
	var request = api.get( {
		action: 'parse',
		text: text,
		prop: [ 'text', 'links' ],
		contentmodel: 'wikitext',
		disablelimitreport: true
	} );
	
	return request.then( function ( data ) {
		var links,
			$parsed = $( data.parse.text );
		
		$parsed.find( 'a' ).each( function () {
			var $this = $( this );
			$this.attr( 'href', $this.attr( 'href' ) + '#pl' );
		} );
		
		if ( 'falseBlueLinks' in mw.libs ) {
			links = data.parse.links.filter( function ( obj ) {
				return obj.ns === 0 && obj.exists;
			} ).map( function ( obj ) {
				return obj.title;
			} );
			
			if ( links.length ) {
				return mw.libs.falseBlueLinks.inspectTitles( links ).then( function () {
					mw.libs.falseBlueLinks.processElements( $parsed );
					return $parsed;
				} );
			}
		}
		
		return $parsed;
	} ).done( function ( $parsed ) {
		$container.html( $parsed.html() );
	} ).promise( {
		abort: request.abort
	} );
}

function handleRequestFailure( code, data, messageBox ) {
	var messageHtml,
		errors = [];
	
	if ( code === undefined || ( code === 'http' && data.textStatus && data.textStatus === 'abort' ) ) {
		return false;
	} else if ( code === 'http' ) {
		errors.push( data.textStatus );
	} else if ( code === 'empty' || code === 'malformed' ) {
		errors.push( mw.msg( 'ml-special-failed-request-empty' ) );
	} else if ( code === 'error' ) {
		errors.concat( data.errors );
	} else {
		errors.push( code );
	}
	
	errors = errors.map( function ( error ) {
		return mw.format( '<li>$1</li>', error );
	} );
	
	messageHtml = mw.msg( 'ml-special-failed-request', errors.join( '' ) );
	messageBox.setLabel( new OO.ui.HtmlSnippet( messageHtml ) );
	messageBox.setType( 'error' );
	return true;
}

function initialize() {
	var textInput, submitButton, reportButton, progressBarField, messageBox, messageField,
		mainPanel, wikitextFieldset, formPopup, formMenuSelect, clipboardButton,
		$wikitext, $preview, $output, request, requestTimer;
	
	function warnLongRequest() {
		messageBox.setLabel( mw.msg( 'ml-special-warn-long-request' ) );
		messageBox.setType( 'warning' );
		messageField.toggle( true );
	}
	
	textInput = new OO.ui.MultilineTextInputWidget( {
		autocomplete: false,
		autofocus: true,
		autosize: true,
		maxLength: MAX_CHARACTERS,
		rows: 3,
		placeholder: mw.msg( 'ml-special-input-placeholder' ),
		spellcheck: true,
		validate: 'non-empty',
		label: String( MAX_CHARACTERS )
	} ).on( 'change', function ( value ) {
		textInput.setLabel( String( MAX_CHARACTERS - value.length ) );
	} ).on( 'enter', function ( evt ) {
		if ( evt.originalEvent.ctrlKey ) {
			submitButton.emit( 'click' );
		}
	} );
	
	submitButton = new OO.ui.ButtonWidget( {
		label: mw.msg( 'ml-special-send-button' ),
		flags: [ 'primary', 'progressive' ]
	} ).on( 'click', function () {
		if ( request && 'abort' in request ) {
			request.abort();
		}
		
		clearTimeout( requestTimer );
		
		textInput.getValidity().then( function () {
			$output.hide();
			progressBarField.toggle( true );
			messageField.toggle( false );
			request = morfeusz.analyze( textInput.getValue() );
			requestTimer = setTimeout( warnLongRequest, REQUEST_DELAY_WARN );
			return request;
		} ).then( function ( data ) {
			$( '#ml-special-dict-id' ).text( data.dictionaryId );
			$( '#ml-special-version' ).text( data.libraryVersion );
			return processItems( data.items );
		} ).then( function ( items ) {
			var serialized = $( '<p>' ).append( items ).text();
			request = previewWikitext( serialized, $preview );
			
			return request.done( function () {
				items.forEach( function ( item, index, arr ) {
					if ( typeof item === 'string' ) {
						arr[ index ] = item.replace( /\n/g, '<br>' );
					}
				} );
				
				$wikitext.html( items );
				clearTimeout( requestTimer );
				messageField.toggle( false );
				progressBarField.toggle( false );
				$output.show();
			} );
		} ).fail( function ( code, data ) {
			if ( handleRequestFailure( code, data, messageBox ) ) {
				clearTimeout( requestTimer );
				messageField.toggle( true );
				progressBarField.toggle( false );
			}
		} );
	} );
	
	reportButton = new OO.ui.ButtonWidget( {
		label: mw.msg( 'ml-special-report-issue' ),
		framed: false,
		icon: 'feedback'
	} ).on( 'click', function () {
		window.open( mw.util.getUrl( issueTracker ) );
	} );
	
	progressBarField = new OO.ui.FieldLayout( new OO.ui.ProgressBarWidget(), {
		label: mw.msg( 'ml-special-progress-bar-label' ),
		align: 'top'
	} ).toggle( false );
	
	messageBox = new OO.ui.MessageWidget();
	messageField = new OO.ui.FieldLayout( messageBox ).toggle( false );
	
	mainPanel = new OO.ui.PanelLayout( {
		content: [
			new OO.ui.FieldsetLayout( {
				label: mw.msg( 'ml-special-fieldset-label' ),
				items: [
					new OO.ui.FieldLayout( new OO.ui.LabelWidget( {
						label: new OO.ui.HtmlSnippet( mw.msg( 'ml-special-fieldset-description' ) )
					} ) ),
					new OO.ui.FieldLayout( textInput, {
						label: mw.msg( 'ml-special-input-label', MAX_CHARACTERS ),
						align: 'top'
					} ),
					new OO.ui.FieldLayout( new OO.ui.Widget( {
						content: [
							new OO.ui.HorizontalLayout( {
								items: [ submitButton, reportButton ]
							} )
						]
					} ) ),
					progressBarField,
					messageField
				]
			} )
		],
		framed: true,
		padded: true,
		expanded: false
	} );
	
	$wikitext = $( '<div>' )
		.addClass( [ 'mw-editfont-monospace', 'ml-special-wikitext-container' ] )
		.prop( 'contenteditable', true )
		.on( 'select.editable', function () {
			selectEditableContents( this );
		} )
		.on( 'input', function ( evt ) {
			if ( $preview.data( 'parseRequest' ) ) {
				$preview.data( 'parseRequest' ).abort();
			}
			
			$wikitext.find( '[data-ml-initial]' ).each( function () {
				var $wikilink = $( this );
				
				if ( $wikilink.text() !== $wikilink.data( 'ml-initial' ) ) {
					$wikilink.removeClass().removeAttr( 'data-ml-initial' );
				}
			} );
			
			formPopup.toggle( false );
		} )
		.on( 'input', mw.util.debounce( PREVIEW_DELAY, function ( evt ) {
			$preview.data( 'parseRequest', previewWikitext( $wikitext.text(), $preview ) );
		} ) );
	
	formMenuSelect = new OO.ui.MenuSelectWidget( {
		autoHide: false,
		hideOnChoose: false,
		hideWhenOutOfView: false
	} ).on( 'choose', function ( option ) {
		var data = option.getData();
		
		data.selected = option.getLabel();
		data.$el.text( mw.libs.MorfeuszAnalyzer.makeWikilink( data.selected, data.form ) );
		data.$el.attr( 'data-ml-initial', data.$el.text() );
		
		formPopup.toggle( 'false' ); // doesn't work, but...
		$wikitext.trigger( 'click' ); // this hack does the trick
		
		if ( $preview.data( 'parseRequest' ) ) {
			$preview.data( 'parseRequest' ).abort();
		}
		
		$preview.data( 'parseRequest', previewWikitext( $wikitext.text(), $preview ) );
	} );
	
	formPopup = new OO.ui.PopupWidget( {
		content: [ formMenuSelect ],
		autoClose: true
	} );
	
	$( document.body ).append( formPopup.$element );
	
	$wikitext.on( 'click', '.ml-special-wikilink-more', function ( evt ) {
		var $wikilink = $( this ),
			data = $wikilink.data( 'ml-data' );
		
		formMenuSelect.clearItems().addItems( data.lemmas.map( function ( lemma ) {
			return new OO.ui.MenuOptionWidget( {
				data: data,
				label: lemma,
				disabled: lemma === data.selected
			} );
		} ) );
		
		formMenuSelect.toggle( true );
		formMenuSelect.unbindDocumentKeyDownListener();
		formMenuSelect.unbindDocumentKeyPressListener();
		
		formPopup.setFloatableContainer( $wikilink );
		formPopup.toggle( true );
		formPopup.setSize( formMenuSelect.$element.width(), formMenuSelect.$element.height() );
	} );
	
	clipboardButton = new OO.ui.ButtonWidget( {
		label: mw.msg( 'ml-special-clipboard-button' ),
		icon: 'articles'
	} ).on( 'click', function () {
		$wikitext.trigger( 'select.editable' );
		document.execCommand( 'copy' );
		mw.notify( mw.msg( 'ml-special-clipboard-notice' ) );
	} );
	
	wikitextFieldset = new OO.ui.FieldsetLayout( {
		items: [
			new OO.ui.FieldLayout( new OO.ui.Widget( {
				$content: $wikitext
			} ), {
				label: mw.msg( 'ml-special-wikitext-label' ),
				align: 'top',
				help: new OO.ui.HtmlSnippet( mw.msg( 'ml-special-wikitext-help' ) )
			} ),
			new OO.ui.FieldLayout( clipboardButton ),
		]
	} );
	
	$preview = $( '<div>' );
	
	$output = $( '<div>' ).append(
		$( '<h2>' ).text( mw.msg( 'ml-special-wikitext-header' ) ),
		wikitextFieldset.$element,
		$( '<h2>' ).text( mw.msg( 'ml-special-preview-header' ) ),
		$preview
	).hide();
	
	$( '#mw-content-text' ).empty().append( mainPanel.$element, $output );
}

$( initialize );