Участница:Megitsune-chan/close-mistakes.js

// <nowiki>
/**
 * close-mistakes.js - Скрипт для закрытия сообщений об ошибках
 * Написан с использованием нейросетей claude-3-7-sonnet-20250219 и chatgpt-4o-latest-20250326
 * Версия: 2.6
 */

$(function () {
    const DEBUG = true;

    function debug(...args) {
        if (DEBUG) console.log('[CloseMistakes]', ...args);
    }

    const targetPages = [
        'Википедия:Сообщения_об_ошибках',
        'Участник:EyeBot/Сообщения_об_ошибках',
        'Википедия:Фильтр_правок/Срабатывания',
        'Википедия:Сообщения_об_ошибках_в_файлах_с_Викисклада',
        'Участница:Megitsune-chan/Черновик' //Для тестов
    ];

    const currentPage = mw.config.get('wgPageName');
    if (!targetPages.includes(currentPage)) return;
    if (mw.config.get('wgUserName') === null) return;
    if (mw.user.isTemp() === true) return;

    debug('Скрипт запущен на странице:', currentPage);

    $('<style>').text(`
        .error-closer-button {
            margin-left: 10px;
            font-size: 0.8em;
            cursor: pointer;
            padding: 2px 5px;
            border: 1px solid #999;
            border-radius: 3px;
            background: #f8f9fa;
        }
        .error-closer-button:hover {
            background: #eaecf0;
        }
        .error-closer-modal {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 20px;
            border: 1px solid #a2a9b1;
            border-radius: 2px;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
            z-index: 1000;
            width: 500px;
            max-width: 90%;
        }
        .error-closer-overlay {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(0, 0, 0, 0.5);
            z-index: 999;
        }
        .error-closer-textarea {
            width: 100%;
            min-height: 150px;
            margin: 10px 0;
            padding: 8px;
            border: 1px solid #a2a9b1;
        }
        .error-closer-buttons {
            text-align: right;
        }
        .error-closer-button-submit,
        .error-closer-button-cancel {
            margin-left: 10px;
            padding: 5px 15px;
            border: 1px solid #a2a9b1;
            border-radius: 2px;
            cursor: pointer;
        }
        .error-closer-button-submit {
            background: #36c;
            color: white;
            font-weight: bold;
        }
        .error-closer-button-cancel {
            background: #f8f9fa;
        }
    `).appendTo('head');

    // Загрузка полного текста страницы из кэша MediaWiki
    new mw.Api().get({
        action: 'parse',
        page: currentPage,
        prop: 'wikitext',
        formatversion: 2
    }).then(function (data) {
        if (!data.parse?.wikitext) {
            debug('Не удалось получить вики-текст страницы');
            return;
        }

        const pageText = data.parse.wikitext;

        const sectionRegex = /^(==\s*(.+?)\s*==)/gm;
        const sections = [];
        let match;

        while ((match = sectionRegex.exec(pageText)) !== null) {
            const fullHeader = match[1];
            const title = match[2];
            const position = match.index;
            sections.push({ fullHeader, title, position });
        }

        debug(`Разделов найдено: ${sections.length}`);

        const visibleHeaders = $('.mw-parser-output h2[id]');
		processSectionsById(visibleHeaders);
    }).catch(function (error) {
        debug('Ошибка получения текста:', error);
    });
    
    function insertAtCursor($textarea, textToInsert) {
    	const textarea = $textarea[0]; // Получаем DOM-элемент

    	if (!textarea) return;

    	const start = textarea.selectionStart;
    	const end = textarea.selectionEnd;
    	const original = textarea.value;

    	// Вставляем текст в текущее положение курсора
    	const newValue = original.slice(0, start) + textToInsert + original.slice(end);
    	textarea.value = newValue;

    	// Перемещаем курсор за вставленный текст
    	const newPos = start + textToInsert.length;
    	textarea.setSelectionRange(newPos, newPos);
    	textarea.focus();
	}

    /**
     * Основная обработка — вставка кнопок + фильтрация закрытых разделов
     */
    function processSectionsById(visibleHeaders) {
    	new mw.Api().get({
        	action: 'parse',
        	page: mw.config.get('wgPageName'),
        	prop: 'sections',
        	formatversion: 2
    	}).then(function (data) {
        	const sections = data.parse.sections;
        	if (!sections || !sections.length) {
            	debug('Нет доступных секций');
            	return;
        	}

        	debug(`Получено секций из API: ${sections.length}`);

        	sections.forEach(section => {
            	// Находим заголовок в DOM по id (anchor)
            	const sectionId = decodeURIComponent(section.anchor);
            	const header = visibleHeaders.filter(function () {
					 return $(this).attr('id') === section.anchor;
				}).first();

            	if (!header.length) {
                	debug('Заголовок не найден в DOM для anchor', section.anchor);
                	return;
            	}

            	const sectionIndex = parseInt(section.index, 10);
            	const sectionTitle = section.line;

            	// Фильтрация по скрытым запросам (дополнительно запрашиваем текст секции)
            	new mw.Api().get({
                	action: 'parse',
                	page: mw.config.get('wgPageName'),
                	section: sectionIndex,
                	prop: 'wikitext',
                	formatversion: 2
            	}).then(function (textResult) {
                	const text = textResult?.parse?.wikitext || '';
                	const lower = text.toLowerCase();

                	if (
                    	lower.includes('{{закрыто') ||
                    	lower.includes('{{closed') ||
                    	lower.includes('{{cls') ||
                    	lower.includes('{{ecs')
                	) {
                    	//debug('Пропущен закрытый раздел:', sectionTitle);
                    	return;
                	}

                	// Добавляем кнопку
                	const closeButton = $('<span>')
                    	.addClass('error-closer-button')
                    	.text('Закрыть')
                    	.on('click', function () {
                        	openCloseDialog(sectionTitle, sectionIndex, section);
                    	});

                	header.append(closeButton);
                	//debug(`Добавлена кнопка к разделу #${sectionIndex}: ${sectionTitle}`);
            	});
        	});
    	});
	}

    /**
     * Получение текста конкретного раздела слоями (на основе срезов из полного текста)
     */
    function extractSectionTextFromWikitext(fullText, sections, index) {
        const start = sections[index].position;
        const end = index + 1 < sections.length
            ? sections[index + 1].position
            : fullText.length;
        return fullText.slice(start, end);
    }

	/**
     * Кнопки быстрых ответов
     */
    const quickReplies = [
    	{
    		label: 'Сделано',
        	text: '{{сделано}}'
    	},
    	{
    	    label: 'Исправлено',
    	    text: '{{исправлено}}'
    	},
    	{
    	    label: 'Возвращено',
    	    text: '{{сделано|Ваша правка возвращена}}'
    	},
    	{
    		label: 'Добавлено',
        	text: '{{добавлено}}'
    	},
    	{
    		label: 'АИ',
        	text: '[[ВП:АИ|]]'
    	},
    	{
    	    label: 'Не сделано',
    	    text: '{{не сделано}}'
    	},
    	{
    		label: 'Не ошибка',
        	text: '{{не ошибка}}'
    	},
    	{
    		label: 'Отклонено',
        	text: '{{Отклонено}}'
    	},
	];
    

    /**
     * Модальное окно с ответом и подтверждением
     */
    function openCloseDialog(sectionTitle, sectionIndex, sectionInfo) {
        debug('Открытие окна для раздела:', sectionTitle);

        const overlay = $('<div>').addClass('error-closer-overlay');
        const modal = $('<div>').addClass('error-closer-modal');

        modal.append($('<h3>').text('Закрытие запроса: ' + sectionTitle));
        
        const quickReplyContainer = $('<div>').css({
    		marginBottom: '5px',
    		display: 'flex',
    		flexWrap: 'wrap',
    		gap: '5px'
		});

		quickReplies.forEach(reply => {
    		const btn = $('<button>')
    		    .addClass('error-closer-button')
        		.attr('type', 'button')
        		.text(reply.label)
        		.on('click', () => {
            		insertAtCursor(textarea, reply.text);
        		});
    		quickReplyContainer.append(btn);
		});

		modal.append(quickReplyContainer);

        const textarea = $('<textarea>')
            .addClass('error-closer-textarea')
            .attr('placeholder', 'Введите ваш ответ (не обязательно). Подпись ставить не нужно!');
        modal.append(textarea);

        const buttonsDiv = $('<div>').addClass('error-closer-buttons');

        const cancelBtn = $('<button>')
            .addClass('error-closer-button-cancel')
            .text('Отмена')
            .on('click', () => {
                overlay.remove();
                modal.remove();
            });

        const submitBtn = $('<button>')
            .addClass('error-closer-button-submit')
            .text('Закрыть запрос')
            .on('click', () => {
                const responseText = textarea.val().trim();
                closeErrorSection(sectionIndex, sectionTitle, responseText);
				overlay.remove();
				modal.remove();
            });

        buttonsDiv.append(cancelBtn, submitBtn);
        modal.append(buttonsDiv);
        $('body').append(overlay, modal);
        textarea.focus();
    }

    /**
     * Закрывает запрос шаблонами и ответом
     */
    function closeErrorSection(sectionIndex, sectionTitle, responseText) {
        debug('Закрытие секции:', sectionTitle);

        new mw.Api().get({
            action: 'parse',
            page: mw.config.get('wgPageName'),
            section: sectionIndex,
            prop: 'wikitext',
            formatversion: 2
        }).then(function (sectionData) {
            const oldText = sectionData?.parse?.wikitext;
            if (!oldText) {
                alert('Не удалось получить текущий текст.');
                return;
            }

            let newText = oldText.replace(
    			/^(==\s*.+?\s*==\s*)/,
    			'$1{{закрыто}}\n'
			);

			if (responseText) {
    			newText += '\n\n* ' + responseText + ' ~~~~\n';
			}

			newText += '{{закрыто-конец}}';

            const cleanTitle = cleanWikilinks(sectionTitle);

            new mw.Api().postWithToken('csrf', {
                action: 'edit',
                title: mw.config.get('wgPageName'),
                section: sectionIndex,
                text: newText,
                summary: 'Закрытие запроса «[[' + mw.config.get('wgPageName') + '#' + cleanTitle + '|' + cleanTitle + ']]» скриптом [[Участница:Megitsune-chan/close-mistakes|close-mistakes]]',
                formatversion: 2
            }).then(() => {
                location.reload();
            }).catch(function (error) {
                alert('Ошибка при сохранении: ' + error);
                debug('Ошибка API:', error);
            });
        }).catch(function (error) {
            alert('Ошибка загрузки текста раздела.');
            debug('Ошибка получения текста:', error);
        });
    }

    /**
     * Убирает вики-ссылки: [[Ссылка]] → Ссылка и [[Ссылка|Текст]] → Текст
     */
    function cleanWikilinks(text) {
        return text.replace(/\[\[([^\]|]+?\|)?([^\]]+?)\]\]/g, '$2');
    }
});
// </nowiki>
Prefix: a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9

Portal di Ensiklopedia Dunia

Kembali kehalaman sebelumnya