Benutzer:Schnark/js/veHint.js

aus Wikipedia, der freien Enzyklopädie
Zur Navigation springen Zur Suche springen

Hinweis: Leere nach dem Veröffentlichen den Browser-Cache, um die Änderungen sehen zu können.

  • Firefox/Safari: Umschalttaste drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+F5 oder Strg+R (⌘+R auf dem Mac) drücken
  • Google Chrome: Umschalttaste+Strg+R (⌘+Umschalttaste+R auf dem Mac) drücken
  • Internet Explorer/Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
  • Opera: Strg+F5
//Dokumentation unter [[Benutzer:Schnark/js/veHint]] <nowiki>
/*global mediaWiki, OO, ve*/
(function ($, mw) {
"use strict";

var l10n = {
	en: {
		'schnark-ve-problems-total-count': 'All problems: $1',
		'schnark-ve-problems-find-prev': 'Find previous problem',
		'schnark-ve-problems-find-next': 'Find next problem',
		'schnark-ve-problems-replace': 'Fix problem',
		'schnark-ve-problems-replace-all': 'Fix all problems',
		'schnark-ve-problems-enable-all': 'Enable all problems',
		'schnark-ve-problems-disable-all': 'Disable all problems',
		'schnark-ve-problems-done': 'Done',
		'schnark-ve-problems-title': 'Find problems'
	},
	de: {
		'schnark-ve-problems-total-count': 'Alle Probleme: $1',
		'schnark-ve-problems-find-prev': 'Finde vorheriges Problem',
		'schnark-ve-problems-find-next': 'Finde nächstes Problem',
		'schnark-ve-problems-replace': 'Behebe Problem',
		'schnark-ve-problems-replace-all': 'Behebe alle Probleme',
		'schnark-ve-problems-enable-all': 'Aktiviere alle Probleme',
		'schnark-ve-problems-disable-all': 'Deaktiviere alle Probleme',
		'schnark-ve-problems-done': 'Fertig',
		'schnark-ve-problems-title': 'Finde Probleme'
	}
}, SOURCE = 1, VISUAL = 2, problemsForProjects = {};

function initL10N (l10n) {
	var i, chain = mw.language.getFallbackLanguageChain();
	for (i = chain.length - 1; i >= 0; i--) {
		if (chain[i] in l10n) {
			mw.messages.set(l10n[chain[i]]);
		}
	}
}

function addToToolbar (tool) {
	var i, include;
	try {
		include = ve.init.mw.DesktopArticleTarget.static.actionGroups[1].include;
		i = include.indexOf('findAndReplace');
		if (i === -1) {
			include.push(tool);
		} else {
			include.splice(i + 1, 0, tool);
		}
	} catch (e) {
		mw.log.warn('veHint.js: addToToolbar failed: ' + e);
	}
}

function init (problems) {
//virtual indent

var css =
	'.schnark-ve-problems-current-problem-highlight {' +
		'background-color: #fc3;' +
	'}' +
	'.schnark-multicol-layout {' +
		'padding: 0 0.5em;' +
		'max-height: 8em;' +
		'overflow: auto;' +
	'}' +
	'.schnark-multicol-layout .oo-ui-fieldLayout {' +
		'width: 20em;' +
		'height: 2.5em;' +
		'float: left;' +
	'}';
mw.util.addCSS(css);

function MulticolLayout () {
	MulticolLayout.parent.apply(this, arguments);
	this.$element.addClass('schnark-multicol-layout');
	this.$group.prepend($('<span>')); //don't remove the margin-top from the first widget
}

OO.inheritClass(MulticolLayout, OO.ui.FieldsetLayout);

function FindProblemsDialog () {
	FindProblemsDialog.parent.apply(this, arguments);
}

OO.inheritClass(FindProblemsDialog, ve.ui.FindAndReplaceDialog);

FindProblemsDialog.static.name = 'findProblems';
FindProblemsDialog.static.padded = false; //strange issue with padding + overflow
//TODO besser lösen, ohne padding sieht es blöd aus
FindProblemsDialog.static.problemsVisual = [];
FindProblemsDialog.static.problemsSource = [];
FindProblemsDialog.static.addProblem = function (desc, re, fix, mode) {
	var problem = {desc: desc, rawRe: re, re: new RegExp('^' + re + '$')};
	if (fix !== false) {
		problem.replace = fix;
	}
	if (mode === VISUAL || mode === VISUAL + SOURCE) {
		FindProblemsDialog.static.problemsVisual.push(
			$.extend({index: FindProblemsDialog.static.problemsVisual.length}, problem)
		);
	}
	if (mode === SOURCE || mode === VISUAL + SOURCE) {
		FindProblemsDialog.static.problemsSource.push(
			$.extend({index: FindProblemsDialog.static.problemsSource.length}, problem)
		);
	}
};

FindProblemsDialog.prototype.initialize = function () {
	var problemGroup, navigateGroup, replaceGroup, onoffGroup, doneButton, $problemRow, $navRow;

	ve.ui.FindAndReplaceDialog.parent.prototype.initialize.apply(this, arguments); //eine Hierarchiestufe überspringen

	this.surface = null;
	this.invalidRegex = false;
	this.$findResults = $('<div>').addClass('ve-ui-findAndReplaceDialog-findResults');
	this.initialFragment = null;
	this.startOffset = 0;
	this.fragments = [];
	this.results = 0;
	this.renderedResultsCache = {};
	this.renderedFragments = new ve.Range();
	this.replacing = false;
	this.focusedIndex = 0;
	this.query = null;
	this.problemsForIndexCache = null;

	if (ve.init.target.getSurface().getMode() === 'source') {
		this.constructor.static.problems = this.constructor.static.problemsSource;
	} else {
		this.constructor.static.problems = this.constructor.static.problemsVisual;
	}
	this.problemSwitches = this.constructor.static.problems.map(function (problem, index) {
		var id = 'schnark-ve-problems-problem-' + index,
			toggleSwitch = new OO.ui.CheckboxInputWidget({
				selected: true
			}), fieldLayout = new OO.ui.FieldLayout(toggleSwitch, {
				align: 'inline',
				label: new OO.ui.HtmlSnippet(
					'<span id="' + id + '">' + problem.desc + '<span class="count"></span></span>'
				)
			});

		return {toggleSwitch: toggleSwitch, fieldLayout: fieldLayout};
	});

	this.previousButton = new OO.ui.ButtonWidget({
		icon: 'previous',
		title: ve.msg('schnark-ve-problems-find-prev'),
		tabIndex: 1
	});
	this.nextButton = new OO.ui.ButtonWidget({
		icon: 'next',
		title: ve.msg('schnark-ve-problems-find-next'),
		tabIndex: 1
	});
	this.replaceButton = new OO.ui.ButtonWidget({
		label: ve.msg('schnark-ve-problems-replace'),
		tabIndex: 1
	});
	this.replaceAllButton = new OO.ui.ButtonWidget({
		label: ve.msg('schnark-ve-problems-replace-all'),
		tabIndex: 1
	});
	this.enableAllButton = new OO.ui.ButtonWidget({
		label: ve.msg('schnark-ve-problems-enable-all'),
		tabIndex: 1
	});
	this.disableAllButton = new OO.ui.ButtonWidget({
		label: ve.msg('schnark-ve-problems-disable-all'),
		tabIndex: 1
	});

	problemGroup = new MulticolLayout({
		items: this.problemSwitches.map(function (item) {
			return item.fieldLayout;
		}),
		label: new OO.ui.HtmlSnippet(
			ve.msg('schnark-ve-problems-total-count', '<span id="schnark-problems-totalcount">0</span>')
		)
	});
	navigateGroup = new OO.ui.ButtonGroupWidget({
		classes: ['ve-ui-findAndReplaceDialog-cell'],
		items: [
			this.previousButton,
			this.nextButton
		]
	});
	replaceGroup = new OO.ui.ButtonGroupWidget({
		classes: ['ve-ui-findAndReplaceDialog-cell'],
		items: [
			this.replaceButton,
			this.replaceAllButton
		]
	});
	onoffGroup = new OO.ui.ButtonGroupWidget({
		classes: ['ve-ui-findAndReplaceDialog-cell'],
		items: [
			this.enableAllButton,
			this.disableAllButton
		]
	});
	doneButton = new OO.ui.ButtonWidget({
		classes: ['ve-ui-findAndReplaceDialog-cell'],
		label: ve.msg('schnark-ve-problems-done'),
		tabIndex: 1
	});
	$problemRow = $('<div>');
	$navRow = $('<div>').addClass('ve-ui-findAndReplaceDialog-row').css('padding', '0 0.5em');

	this.onWindowScrollThrottled = ve.throttle(this.onWindowScroll.bind(this), 250);
	this.updateFragmentsThrottled = ve.throttle(this.updateFragments.bind(this), 250);
	this.renderFragmentsThrottled = ve.throttle(this.renderFragments.bind(this), 250);
	this.problemSwitches.forEach(function (item) {
		item.toggleSwitch.connect(this, {change: 'onProblemsChange'});
	}, this);
	this.nextButton.connect(this, {click: 'findNext'});
	this.previousButton.connect(this, {click: 'findPrevious'});
	this.replaceButton.connect(this, {click: 'onReplaceButtonClick'});
	this.replaceAllButton.connect(this, {click: 'onReplaceAllButtonClick'});
	this.enableAllButton.connect(this, {click: 'onEnableAllButtonClick'});
	this.disableAllButton.connect(this, {click: 'onDisableAllButtonClick'});
	doneButton.connect(this, {click: 'close'});

	this.tabIndexScope = new ve.ui.TabIndexScope({
		root: this.$element
	});

	this.$content.addClass('ve-ui-findAndReplaceDialog-content');
	this.$body
		.append(
			$navRow.append(navigateGroup.$element, replaceGroup.$element, onoffGroup.$element, doneButton.$element),
			$problemRow.append(problemGroup.$element)
		);
};

FindProblemsDialog.prototype.onProblemsChange = function () {
	this.updateFragments();
	this.clearRenderedResultsCache();
	this.renderFragments();
	this.highlightFocused(true);
};

FindProblemsDialog.prototype.onEnableAllButtonClick = function () {
	this.problemSwitches.forEach(function (item) {
		item.toggleSwitch.setSelected(true);
	}, this);
};

FindProblemsDialog.prototype.onDisableAllButtonClick = function () {
	this.problemSwitches.forEach(function (item) {
		item.toggleSwitch.setSelected(false);
	}, this);
};

FindProblemsDialog.prototype.getProblemSelectionStates = function () {
	return this.problemSwitches.map(function (item) {
		return item.toggleSwitch.isSelected();
	});
};

FindProblemsDialog.prototype.updateFragments = function () {
	var i, l, startIndex,
		surfaceModel = this.surface.getModel(),
		documentModel = surfaceModel.getDocument(),
		isReadOnly = surfaceModel.isReadOnly(),
		ranges = [],
		selectionStates = this.getProblemSelectionStates(),
		problemCounts, countHtml;

	this.query = this.constructor.static.problems.filter(function (el, index) {
		return selectionStates[index];
	}).map(function (problem) {
		return '(?:' + problem.rawRe + ')';
	}).join('|');

	this.fragments = [];
	this.problemsForIndexCache = null;
	if (this.query) {
		this.query = new RegExp(this.query, 'g');
		ranges = documentModel.findText(this.query, {
			noOverlaps: true
		});
		for (i = 0, l = ranges.length; i < l; i++) {
			this.fragments.push(surfaceModel.getLinearFragment(ranges[i], true, true));
			if (startIndex === undefined && ranges[i].start >= this.startOffset) {
				startIndex = this.fragments.length - 1;
			}
		}
	}
	this.results = this.fragments.length;
	this.focusedIndex = startIndex || 0;
	this.nextButton.setDisabled(!this.results);
	this.previousButton.setDisabled(!this.results);
	this.replaceAllButton.setDisabled(!this.results || isReadOnly);

	problemCounts = this.getCounts();
	this.$element.find('#schnark-problems-totalcount').html(this.results);
	for (i = 0; i < problemCounts.length; i++) {
		if (selectionStates[i]) {
			countHtml = ' (' + problemCounts[i] + ')';
		} else {
			countHtml = '';
		}
		this.$element.find('#schnark-ve-problems-problem-' + i + ' .count').html(countHtml);
	}
};

FindProblemsDialog.prototype.getCounts = function () {
	var i, problemCounts = [];

	function add (problem) {
		problemCounts[problem.index]++;
	}

	for (i = 0; i < this.constructor.static.problems.length; i++) {
		problemCounts.push(0);
	}

	for (i = 0; i < this.fragments.length; i++) {
		this.getProblemsForIndex(i).list.forEach(add);
	}
	return problemCounts;
};

FindProblemsDialog.prototype.createProblemsForIndexCache = function () {
	var i;
	this.problemsForIndexCache = [];
	for (i = 0; i < this.fragments.length; i++) {
		this.problemsForIndexCache.push(this.createProblemsForIndex(i));
	}
};

FindProblemsDialog.prototype.createProblemsForIndex = function (index) {
	var text = this.fragments[index].getText(true).replace(/\n/g, '\uFFFC'),
		selectionStates = this.getProblemSelectionStates(),
		fixProblem = false,
		list = this.constructor.static.problems.filter(function (problem, index) {
			//FIXME teilweise sprechen auch falsche REs an
			var matches = selectionStates[index] && text.search(problem.re) === 0;
			if (text.indexOf('\uFFFC') === -1 && matches && fixProblem === false && problem.replace !== undefined) {
				fixProblem = index;
			}
			return matches;
		});
	return {list: list, fixProblem: fixProblem};
};

FindProblemsDialog.prototype.getProblemsForIndex = function (index) {
	if (!this.problemsForIndexCache) {
		this.createProblemsForIndexCache();
	}
	return this.problemsForIndexCache[index] || {list: [], fixProblem: false};
};

FindProblemsDialog.prototype.highlightFocused = function () {
	this.findText = {setLabel: function () {}};
	FindProblemsDialog.parent.prototype.highlightFocused.apply(this, arguments);
	this.$element.find('.schnark-ve-problems-current-problem-highlight')
		.removeClass('schnark-ve-problems-current-problem-highlight');
	var currentProblems = this.getProblemsForIndex(this.focusedIndex);
	currentProblems.list.forEach(function (problem) {
		this.$element.find('#schnark-ve-problems-problem-' + problem.index)
			.addClass('schnark-ve-problems-current-problem-highlight')[0]
			.scrollIntoView(false);
	}, this);
	this.replaceButton.setDisabled(
		currentProblems.fixProblem === false || this.surface.getModel().isReadOnly()
	);
};

FindProblemsDialog.prototype.findFirst = function () {
	var initialFragment = this.surface.getModel().getFragment(null, true);
	this.startOffset = ve.getProp(
		initialFragment.getSelection().getRanges(initialFragment.getDocument()),
		0, 'start'
	) || 0;
	this.onProblemsChange();
};

FindProblemsDialog.prototype.focus = function () {};

FindProblemsDialog.prototype.replace = function (index) {
	var fix = this.getProblemsForIndex(index).fixProblem;
	if (fix === false) {
		return;
	}
	fix = this.constructor.static.problems[fix];
	this.replacing = true;
	this.fragments[index].insertContent(
		this.fragments[index].getText().replace(fix.re, fix.replace),
		true
	);
	setTimeout(function () {
		this.replacing = false;
	}.bind(this));
};

ve.ui.windowFactory.register(FindProblemsDialog);

ve.ui.commandRegistry.register(
	new ve.ui.Command(
		'findProblems', 'window', 'open', {args: ['findProblems', null, 'findFirst']}
	)
);

function FindProblemsTool () {
	FindProblemsTool.parent.apply(this, arguments);
}

OO.inheritClass(FindProblemsTool, ve.ui.FindAndReplaceTool);
FindProblemsTool.static.name = 'findProblems';
FindProblemsTool.static.icon = 'alert';
FindProblemsTool.static.title = ve.msg('schnark-ve-problems-title');
FindProblemsTool.static.commandName = 'findProblems';
ve.ui.toolFactory.register(FindProblemsTool);

addToToolbar('findProblems');

ve.ui.triggerRegistry.register(
	'findProblems', {mac: new ve.ui.Trigger('cmd+p'), pc: new ve.ui.Trigger('ctrl+p')}
);

problems(FindProblemsDialog.static.addProblem);

//virtual outdent
}

problemsForProjects.dewiki = function (add) {
	function allowCapital (re) {
		var first = re.charAt(0),
			firstCapital = first.toUpperCase();
		if (first === firstCapital) {
			return re;
		}
		return '[' + first + firstCapital + ']' + re.slice(1);
	}

	var typo = { //FIXME erweitern
		'aberufen': 'abgerufen',
		'Addresse': 'Adresse',
		'anderere': 'andere',
		'andereren': 'anderen',
		'andererer': 'anderer',
		'Gallerie': 'Galerie',
		'Standart': 'Standard'
	}, words = [ //evt. weitere aus wikilint übernehmen
		'bedauerlicherweise',
		'bedeutendste',
		'bekannte',
		'bekannteste',
		'berühmteste',
		'derzeit',
		'einzigste',
		'ich',
		'in der Regel',
		'in den letzten Jahren',
		'sogar',
		'wichtigste',
		'zum Glück',
		'zurzeit',
		'Insider'
	], abbr = [
		'z. B.',
		'bzw.',
		'd. h.',
		'jährl.',
		'u. a.',
		'hl.',
		'engl.',
		'ital.',
		'span.',
		'lat.',
		'ca.',
		'sog.'
	], months = [
		'Januar',
		'Februar',
		'März',
		'April',
		'Mai',
		'Juni',
		'Juli',
		'August',
		'September',
		'Oktober',
		'November',
		'Dezember'
	],
	opening = '([„«', closing = ')]“»', digitPunct = '.,', punct = ':;?!', object = '\uFFFC';
	add('Wortdopplung', '\\b(\\S+)\\b(\\W+)\\b\\1\\b', '$1$2', SOURCE + VISUAL);
	add('Plenk', '\\s+([' + mw.util.escapeRegExp(digitPunct + punct + closing + '/') +
		'])|([' + mw.util.escapeRegExp(opening + '/') + '])\\s+', '$1$2', SOURCE + VISUAL);
	add('Klemp',
		'([' + mw.util.escapeRegExp(punct) + '])([^ ' + mw.util.escapeRegExp(closing) + object + '])|' +
		'(,)([^ \\d' + mw.util.escapeRegExp(closing) + object + '])|' +
		'(\\.)([^ \\-\\da-z' + mw.util.escapeRegExp(closing) + object + '])',
		'$1$3$5 $2$4$6', VISUAL);
	add('Mehrfache Leerzeichen', ' {2,}', ' ', VISUAL);
	add('Überflüssige Leerzeichen', ' +$', '', VISUAL + SOURCE);
	add('Kleinschreibung', '([.!?]\\s+)([a-zäöü])', function (all, pre, char) {
		return pre + char.toUpperCase();
	}, SOURCE + VISUAL);
	add('Tippfehler', '\\b(?:' + Object.keys(typo).map(mw.util.escapeRegExp).join('|') + ')\\b', function (word) {
		return typo[word];
	}, SOURCE + VISUAL);
	add('Zahlenformat (4-stellig)', '\\b(\\d)\\.(\\d{3})\\b', '$1$2', VISUAL + SOURCE);
	add('Zahlenformat', ' \\d{5,}\\b', function (number) {
		function group (n) {
			if (n.length <= 3) {
				return n;
			}
			return group(n.slice(0, -3)) + '.' + n.slice(-3);
		}
		return ' ' + group(number.slice(1));
	}, SOURCE + VISUAL);
	add('Unerwünschte Wörter', '\\b(?:' + words.map(mw.util.escapeRegExp).map(allowCapital).join('|') + ')\\b',
		false, SOURCE + VISUAL);
	add('Abkürzung', '\\b(?:' + abbr.map(mw.util.escapeRegExp).map(allowCapital).join('|') + ')', false, SOURCE + VISUAL);
	add('Langer Satz', '(?:\\b\\S+\\b[ ,]){50,}', false, SOURCE + VISUAL);
	add('Datum', '\\b([0-3]?[0-9])\\.\\s*([01]?[0-9])\\.\\s*', function (all, day, month) {
		return String(Number(day)) + '. ' + months[Number(month) - 1] + ' ';
	}, SOURCE + VISUAL);
	add('Gedankenstrich', ' - ', ' – ', SOURCE + VISUAL);
	add('Bis-Strich zwischen Jahren', '([^-0-9][12][0-9]{3}) *- *([12][0-9]{3}[^-0-9])', '$1–$2', SOURCE + VISUAL);
	add('Bis-Strich zwischen Seiten', '\\b(Sp?\\.|Seiten?|Spalten?) *(\\d+) *- *', '$1 $2–', SOURCE + VISUAL);
	add('Bis-Strich mit Leerzeichen', '(\\d) +(–) +(\\d)', '$1$2$3', SOURCE + VISUAL);
	add('Auslassungspunkte', '\\.\\.\\.', '…', SOURCE + VISUAL);
	add('Anführungszeichen', '"([^"\\n]{0,100})"', '„$1“', VISUAL);
	add('Apostroph', '\'', '’', VISUAL);
	add('Zusammenstoßende Links', '\\]\\]\\[\\[', false, SOURCE);
	add('Weblinks', '^\\s*(?:[eE]xterner?|[eE]xternal)?\\s*(?:[wW]eblink|[lL]inks?|[wW]ebseiten?|[wW]ebsites?)\\s*$',
		'Weblinks', SOURCE + VISUAL);
};

problemsForProjects['*'] = function (add) {
	mw.log.warn('veHint.js: No config for this project!');
	add('Placeholder', '^.*$', false, SOURCE + VISUAL);
};

//'oojs-ui.styles.icons-movement' nicht aufgeführt, da diese Icons auch vom beerbten Suchdialog verwendet werden
mw.loader.using([
	'mediawiki.language', 'mediawiki.util',
	'ext.visualEditor.core', 'ext.visualEditor.desktopArticleTarget', 'oojs-ui.styles.icons-alerts'
]).then(function () {
	initL10N(l10n);
	init(problemsForProjects[mw.config.get('wgDBname')] || problemsForProjects['*']);
	mw.hook('userjs.script-ready.veHint').fire();
});

})(jQuery, mediaWiki);
//</nowiki>