Открыть меню
Переключить меню настроек
Открыть персональное меню
Вы не представились системе
Ваш IP-адрес будет виден всем, если вы внесёте какие-либо изменения.

MediaWiki:ScmcScanner.js

Страница интерфейса MediaWiki
Версия от 08:39, 15 июня 2026; Defer (обсуждение | вклад) (Новая страница: «(function () { if (!document.querySelector('.scmc-link-scanner')) return; var STATUS = { waiting: 'Ожидает запуска', scanning: 'Сканирование...', done: 'Готово', error: 'Ошибка' }; var EXCLUDED_PREFIXES = [ 'Файл:', 'File:', 'Категория:', 'Category:', 'Шаблон:', 'Template:', 'Участник:',...»)
(разн.) ← Предыдущая версия | Текущая версия (разн.) | Следующая версия → (разн.)

Замечание: Возможно, после публикации вам придётся очистить кэш своего браузера, чтобы увидеть изменения.

  • Firefox / Safari: Удерживая клавишу Shift, нажмите на панели инструментов Обновить либо нажмите Ctrl+F5 или Ctrl+R (⌘+R на Mac)
  • Google Chrome: Нажмите Ctrl+Shift+R (⌘+Shift+R на Mac)
  • Edge: Удерживая Ctrl, нажмите Обновить либо нажмите Ctrl+F5
  • Opera: Нажмите Ctrl+F5.
(function () {
    if (!document.querySelector('.scmc-link-scanner')) return;

    var STATUS = {
        waiting: 'Ожидает запуска',
        scanning: 'Сканирование...',
        done: 'Готово',
        error: 'Ошибка'
    };

    var EXCLUDED_PREFIXES = [
        'Файл:',
        'File:',
        'Категория:',
        'Category:',
        'Шаблон:',
        'Template:',
        'Участник:',
        'User:',
        'Обсуждение:',
        'Talk:',
        'Служебная:',
        'Special:',
        'MediaWiki:',
        'Модуль:',
        'Module:',
        'Справка:',
        'Help:'
    ];

    function normalizeTitle(title) {
        return String(title || '')
            .replace(/_/g, ' ')
            .trim();
    }

    function titleToLink(title) {
        return mw.util.getUrl(title);
    }

    function isExcludedTitle(title) {
        var normalized = normalizeTitle(title);

        if (!normalized) return true;
        if (normalized.indexOf('#') !== -1) normalized = normalized.split('#')[0];

        return EXCLUDED_PREFIXES.some(function (prefix) {
            return normalized.indexOf(prefix) === 0;
        });
    }

    function createEl(tag, className, text) {
        var el = document.createElement(tag);
        if (className) el.className = className;
        if (text !== undefined) el.textContent = text;
        return el;
    }

    function createButton(text, className) {
        var button = document.createElement('button');
        button.type = 'button';
        button.className = className || 'scmc-scan-btn';
        button.textContent = text;
        return button;
    }

    function getApi() {
        return new mw.Api();
    }

    function uniqueSorted(items) {
        var map = {};
        items.forEach(function (item) {
            map[normalizeTitle(item)] = true;
        });
        return Object.keys(map).sort(function (a, b) {
            return a.localeCompare(b, 'ru');
        });
    }

    function shouldFollowPage(page) {
        if (!page || page.missing !== undefined) return false;
        if (typeof page.ns === 'number' && page.ns !== 0) return false;
        if (isExcludedTitle(page.title)) return false;
        return true;
    }

    function getPageLinks(api, title) {
        var links = [];
        var normalizedTitle = normalizeTitle(title);

        function request(plcontinue) {
            var params = {
                action: 'query',
                prop: 'links',
                titles: normalizedTitle,
                pllimit: 'max',
                formatversion: 2
            };

            if (plcontinue) {
                params.plcontinue = plcontinue;
            }

            return api.get(params).then(function (data) {
                var pages = data.query && data.query.pages ? data.query.pages : [];
                var page = pages[0];

                if (!shouldFollowPage(page)) {
                    return {
                        title: normalizedTitle,
                        exists: !!page && page.missing === undefined,
                        followed: false,
                        links: []
                    };
                }

                if (page.links) {
                    page.links.forEach(function (link) {
                        if (link.ns === 0 && !isExcludedTitle(link.title)) {
                            links.push(normalizeTitle(link.title));
                        }
                    });
                }

                if (data.continue && data.continue.plcontinue) {
                    return request(data.continue.plcontinue);
                }

                return {
                    title: normalizeTitle(page.title || normalizedTitle),
                    exists: true,
                    followed: true,
                    links: uniqueSorted(links)
                };
            });
        }

        return request();
    }

    function buildScanner(container) {
        var rootTitle = normalizeTitle(container.getAttribute('data-root') || 'Marine_Corps');
        var maxDepth = parseInt(container.getAttribute('data-depth') || '5', 10);
        var maxPages = parseInt(container.getAttribute('data-max-pages') || '250', 10);

        var header = createEl('div', 'scmc-scan-header');
        var title = createEl('div', 'scmc-scan-title', 'Сканер ссылок Marine Corps');
        var subtitle = createEl('div', 'scmc-scan-subtitle', 'Стартовая страница: ' + rootTitle + '. Глубина: ' + maxDepth + '.');

        header.appendChild(title);
        header.appendChild(subtitle);

        var controls = createEl('div', 'scmc-scan-controls');
        var startButton = createButton('Сканировать ссылки');
        var clearButton = createButton('Очистить', 'scmc-scan-btn scmc-scan-btn-secondary');

        controls.appendChild(startButton);
        controls.appendChild(clearButton);

        var status = createEl('div', 'scmc-scan-status', STATUS.waiting);
        var stats = createEl('div', 'scmc-scan-stats');

        var result = createEl('div', 'scmc-scan-result');
        var treeBox = createEl('div', 'scmc-scan-box');
        var listBox = createEl('div', 'scmc-scan-box');

        result.appendChild(treeBox);
        result.appendChild(listBox);

        container.innerHTML = '';
        container.appendChild(header);
        container.appendChild(controls);
        container.appendChild(status);
        container.appendChild(stats);
        container.appendChild(result);

        function setStatus(text, mode) {
            status.textContent = text;
            status.setAttribute('data-mode', mode || '');
        }

        function clearResult() {
            treeBox.innerHTML = '';
            listBox.innerHTML = '';
            stats.innerHTML = '';
            setStatus(STATUS.waiting, '');
        }

        function renderList(allPages, pageDepth, skippedByLimit) {
            listBox.innerHTML = '';

            var boxTitle = createEl('div', 'scmc-scan-box-title', 'Все найденные страницы');
            listBox.appendChild(boxTitle);

            var list = createEl('ol', 'scmc-scan-page-list');

            allPages.forEach(function (title) {
                var li = document.createElement('li');
                var a = document.createElement('a');
                a.href = titleToLink(title);
                a.textContent = title;

                var depth = createEl('span', 'scmc-scan-depth', 'ур. ' + pageDepth[title]);

                li.appendChild(a);
                li.appendChild(depth);
                list.appendChild(li);
            });

            listBox.appendChild(list);

            if (skippedByLimit) {
                var warning = createEl('div', 'scmc-scan-warning', 'Сканирование остановлено по лимиту страниц. Если нужно больше — увеличь data-max-pages.');
                listBox.appendChild(warning);
            }
        }

        function renderTree(root, childrenMap, pageDepth) {
            treeBox.innerHTML = '';

            var boxTitle = createEl('div', 'scmc-scan-box-title', 'Дерево ссылок');
            treeBox.appendChild(boxTitle);

            function makeNode(title, localVisited) {
                var node = createEl('div', 'scmc-scan-node');

                var line = createEl('div', 'scmc-scan-node-line');
                var a = document.createElement('a');
                a.href = titleToLink(title);
                a.textContent = title;

                var depth = createEl('span', 'scmc-scan-depth', 'ур. ' + pageDepth[title]);

                line.appendChild(a);
                line.appendChild(depth);
                node.appendChild(line);

                if (localVisited[title]) {
                    var loop = createEl('div', 'scmc-scan-loop', '↳ уже встречалась выше');
                    node.appendChild(loop);
                    return node;
                }

                var nextVisited = Object.assign({}, localVisited);
                nextVisited[title] = true;

                var children = childrenMap[title] || [];
                if (children.length) {
                    var childBox = createEl('div', 'scmc-scan-children');

                    children.forEach(function (child) {
                        childBox.appendChild(makeNode(child, nextVisited));
                    });

                    node.appendChild(childBox);
                }

                return node;
            }

            treeBox.appendChild(makeNode(root, {}));
        }

        function scan() {
            clearResult();

            var api = getApi();
            var queue = [{ title: rootTitle, depth: 0 }];
            var scanned = {};
            var queued = {};
            var pageDepth = {};
            var childrenMap = {};
            var skippedByLimit = false;

            queued[rootTitle] = true;
            pageDepth[rootTitle] = 0;

            startButton.disabled = true;
            clearButton.disabled = true;
            setStatus(STATUS.scanning, 'scanning');

            function step() {
                if (!queue.length) {
                    finish();
                    return;
                }

                if (Object.keys(scanned).length >= maxPages) {
                    skippedByLimit = true;
                    finish();
                    return;
                }

                var current = queue.shift();
                var title = current.title;
                var depth = current.depth;

                if (scanned[title]) {
                    step();
                    return;
                }

                scanned[title] = true;

                setStatus('Сканирую: ' + title + ' — уровень ' + depth, 'scanning');

                getPageLinks(api, title).then(function (pageData) {
                    childrenMap[title] = pageData.links || [];

                    if (depth < maxDepth) {
                        pageData.links.forEach(function (linkTitle) {
                            if (!queued[linkTitle] && !isExcludedTitle(linkTitle)) {
                                queued[linkTitle] = true;
                                pageDepth[linkTitle] = depth + 1;
                                queue.push({
                                    title: linkTitle,
                                    depth: depth + 1
                                });
                            }
                        });
                    }

                    stats.textContent = 'Просканировано страниц: ' + Object.keys(scanned).length + '. В очереди: ' + queue.length + '. Найдено всего: ' + Object.keys(queued).length + '.';

                    setTimeout(step, 120);
                }).catch(function (error) {
                    console.error(error);
                    setStatus('Ошибка при сканировании страницы: ' + title, 'error');
                    startButton.disabled = false;
                    clearButton.disabled = false;
                });
            }

            function finish() {
                var allPages = Object.keys(queued).sort(function (a, b) {
                    return a.localeCompare(b, 'ru');
                });

                renderTree(rootTitle, childrenMap, pageDepth);
                renderList(allPages, pageDepth, skippedByLimit);

                stats.textContent = 'Готово. Найдено страниц: ' + allPages.length + '. Просканировано страниц: ' + Object.keys(scanned).length + '.';
                setStatus(STATUS.done, 'done');

                startButton.disabled = false;
                clearButton.disabled = false;
            }

            step();
        }

        startButton.addEventListener('click', scan);
        clearButton.addEventListener('click', clearResult);
    }

    mw.hook('wikipage.content').add(function ($content) {
        $content.find('.scmc-link-scanner').each(function () {
            buildScanner(this);
        });
    });
})();