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

MediaWiki:ScmcScanner.js

Страница интерфейса MediaWiki

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

  • 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 () {
    var scannerRoot = document.querySelector('.scmc-link-scanner');
    if (!scannerRoot) return;

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

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

    function titleKey(title) {
        return normalizeTitle(title).toLowerCase();
    }

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

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

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

    function splitDataList(value) {
        return String(value || '')
            .split('|')
            .map(normalizeTitle)
            .filter(Boolean);
    }

    function uniqueSorted(list) {
        var seen = {};
        list.forEach(function (item) {
            var title = normalizeTitle(item);
            if (title) seen[titleKey(title)] = title;
        });

        return Object.keys(seen)
            .map(function (key) {
                return seen[key];
            })
            .sort(function (a, b) {
                return a.localeCompare(b, 'ru');
            });
    }

    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 extraExcludedTitles = splitDataList(container.getAttribute('data-exclude'));

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

            if (!clean) return true;

            if (clean.indexOf('#') !== -1) {
                clean = normalizeTitle(clean.split('#')[0]);
            }

            if (extraExcludedTitles.some(function (excluded) {
                return titleKey(excluded) === titleKey(clean);
            })) {
                return true;
            }

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

        function isValidLink(link) {
            if (!link || !link.title) return false;
            if (typeof link.ns === 'number' && link.ns !== 0) return false;
            if (isExcludedTitle(link.title)) return false;
            return true;
        }

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

        function getPageLinks(api, requestedTitle) {
            var links = [];
            var redirects = [];
            var normalizedRequested = normalizeTitle(requestedTitle);

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

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

                return api.get(params).then(function (data) {
                    if (data.query && data.query.redirects) {
                        data.query.redirects.forEach(function (redirect) {
                            redirects.push({
                                from: normalizeTitle(redirect.from),
                                to: normalizeTitle(redirect.to)
                            });
                        });
                    }

                    var pages = data.query && data.query.pages ? data.query.pages : [];
                    var page = pages[0];

                    if (!page) {
                        return {
                            requestedTitle: normalizedRequested,
                            finalTitle: normalizedRequested,
                            exists: false,
                            links: [],
                            redirects: redirects
                        };
                    }

                    var finalTitle = normalizeTitle(page.title || normalizedRequested);

                    if (page.missing !== undefined) {
                        return {
                            requestedTitle: normalizedRequested,
                            finalTitle: finalTitle,
                            exists: false,
                            links: [],
                            redirects: redirects
                        };
                    }

                    if (typeof page.ns === 'number' && page.ns !== 0) {
                        return {
                            requestedTitle: normalizedRequested,
                            finalTitle: finalTitle,
                            exists: true,
                            links: [],
                            redirects: redirects
                        };
                    }

                    if (page.links) {
                        page.links.forEach(function (link) {
                            if (isValidLink(link)) {
                                links.push(normalizeTitle(link.title));
                            }
                        });
                    }

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

                    return {
                        requestedTitle: normalizedRequested,
                        finalTitle: finalTitle,
                        exists: true,
                        links: uniqueSorted(links),
                        redirects: redirects
                    };
                });
            }

            return request();
        }

        var header = makeEl('div', 'scmc-scan-header');
        var title = makeEl('div', 'scmc-scan-title', 'Сканер ссылок Marine Corps');
        var subtitle = makeEl(
            'div',
            'scmc-scan-subtitle',
            'Старт: ' + rootTitle + ' · глубина: ' + maxDepth + ' · лимит страниц: ' + maxPages
        );

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

        var controls = makeEl('div', 'scmc-scan-controls');
        var scanBtn = makeButton('Сканировать ссылки');
        var clearBtn = makeButton('Очистить', 'scmc-scan-btn scmc-scan-btn-secondary');

        controls.appendChild(scanBtn);
        controls.appendChild(clearBtn);

        var statusBox = makeEl('div', 'scmc-scan-status', 'Ожидает запуска');
        var statsBox = makeEl('div', 'scmc-scan-stats');

        var resultWrap = makeEl('div', 'scmc-scan-result');
        var treeBox = makeEl('div', 'scmc-scan-box');
        var listBox = makeEl('div', 'scmc-scan-box');
        var redirectBox = makeEl('div', 'scmc-scan-box scmc-scan-box-wide');

        resultWrap.appendChild(treeBox);
        resultWrap.appendChild(listBox);
        resultWrap.appendChild(redirectBox);

        container.innerHTML = '';
        container.appendChild(header);
        container.appendChild(controls);
        container.appendChild(statusBox);
        container.appendChild(statsBox);
        container.appendChild(resultWrap);

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

        function clearResults() {
            treeBox.innerHTML = '';
            listBox.innerHTML = '';
            redirectBox.innerHTML = '';
            statsBox.innerHTML = '';
            setStatus('Ожидает запуска', '');
        }

        function renderTree(root, childrenMap, pageInfo, firstParent, alsoLinkedFrom) {
            treeBox.innerHTML = '';

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

            function makeNode(title, path) {
                var key = titleKey(title);
                var info = pageInfo[key];

                var node = makeEl('div', 'scmc-scan-node');
                var line = makeEl('div', 'scmc-scan-node-line');

                var link = document.createElement('a');
                link.href = titleToUrl(title);
                link.textContent = title;

                line.appendChild(link);

                if (info) {
                    line.appendChild(makeEl('span', 'scmc-scan-depth', 'ур. ' + info.depth));

                    if (info.requestedTitle && titleKey(info.requestedTitle) !== titleKey(info.finalTitle)) {
                        line.appendChild(makeEl('span', 'scmc-scan-redirect-mini', info.requestedTitle + ' → ' + info.finalTitle));
                    }

                    if (info.exists === false) {
                        line.appendChild(makeEl('span', 'scmc-scan-missing', 'нет страницы'));
                    }
                }

                node.appendChild(line);

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

                var extraLinks = alsoLinkedFrom[key] || [];
                if (extraLinks.length) {
                    node.appendChild(makeEl('div', 'scmc-scan-also', 'Ещё ссылки из: ' + extraLinks.join(', ')));
                }

                var nextPath = Object.assign({}, path);
                nextPath[key] = true;

                var children = childrenMap[key] || [];

                if (children.length) {
                    var childrenWrap = makeEl('div', 'scmc-scan-children');

                    children.forEach(function (childTitle) {
                        var childKey = titleKey(childTitle);

                        if (firstParent[childKey] && titleKey(firstParent[childKey]) !== key) {
                            var refNode = makeEl('div', 'scmc-scan-node');
                            var refLine = makeEl('div', 'scmc-scan-node-line scmc-scan-node-ref');

                            var refLink = document.createElement('a');
                            refLink.href = titleToUrl(childTitle);
                            refLink.textContent = childTitle;

                            refLine.appendChild(refLink);
                            refLine.appendChild(makeEl('span', 'scmc-scan-ref', 'уже найдено в: ' + firstParent[childKey]));
                            refNode.appendChild(refLine);
                            childrenWrap.appendChild(refNode);
                            return;
                        }

                        childrenWrap.appendChild(makeNode(childTitle, nextPath));
                    });

                    node.appendChild(childrenWrap);
                }

                return node;
            }

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

        function renderList(pageInfo) {
            listBox.innerHTML = '';

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

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

            var pages = Object.keys(pageInfo)
                .map(function (key) {
                    return pageInfo[key];
                })
                .sort(function (a, b) {
                    if (a.depth !== b.depth) return a.depth - b.depth;
                    return a.finalTitle.localeCompare(b.finalTitle, 'ru');
                });

            pages.forEach(function (info) {
                var li = document.createElement('li');

                var link = document.createElement('a');
                link.href = titleToUrl(info.finalTitle);
                link.textContent = info.finalTitle;

                li.appendChild(link);
                li.appendChild(makeEl('span', 'scmc-scan-depth', 'ур. ' + info.depth));

                if (info.requestedTitle && titleKey(info.requestedTitle) !== titleKey(info.finalTitle)) {
                    li.appendChild(makeEl('span', 'scmc-scan-redirect-mini', 'найдено как: ' + info.requestedTitle));
                }

                if (info.exists === false) {
                    li.appendChild(makeEl('span', 'scmc-scan-missing', 'нет страницы'));
                }

                list.appendChild(li);
            });

            listBox.appendChild(list);
        }

        function renderRedirects(redirectsFound) {
            redirectBox.innerHTML = '';

            redirectBox.appendChild(makeEl('div', 'scmc-scan-box-title', 'Редиректы'));

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

            if (!keys.length) {
                redirectBox.appendChild(makeEl('div', 'scmc-scan-empty', 'Редиректы не найдены.'));
                return;
            }

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

            keys.forEach(function (from) {
                var to = redirectsFound[from];

                var li = document.createElement('li');

                var fromLink = document.createElement('a');
                fromLink.href = titleToUrl(from);
                fromLink.textContent = from;

                var toLink = document.createElement('a');
                toLink.href = titleToUrl(to);
                toLink.textContent = to;

                li.appendChild(fromLink);
                li.appendChild(document.createTextNode(' → '));
                li.appendChild(toLink);

                list.appendChild(li);
            });

            redirectBox.appendChild(list);
        }

        function scan() {
            clearResults();

            var api = getApi();

            var queue = [{
                requestedTitle: rootTitle,
                depth: 0,
                parent: null
            }];

            var scanned = {};
            var queued = {};
            var pageInfo = {};
            var childrenMap = {};
            var firstParent = {};
            var alsoLinkedFrom = {};
            var redirectsFound = {};
            var stoppedByLimit = false;

            queued[titleKey(rootTitle)] = true;

            scanBtn.disabled = true;
            clearBtn.disabled = true;

            setStatus('Сканирование...', 'scanning');

            function addAlsoLinked(childTitle, parentTitle) {
                var childKey = titleKey(childTitle);

                if (!alsoLinkedFrom[childKey]) {
                    alsoLinkedFrom[childKey] = [];
                }

                if (alsoLinkedFrom[childKey].indexOf(parentTitle) === -1) {
                    alsoLinkedFrom[childKey].push(parentTitle);
                }
            }

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

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

                var item = queue.shift();
                var requestedTitle = normalizeTitle(item.requestedTitle);
                var requestedKey = titleKey(requestedTitle);

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

                scanned[requestedKey] = true;

                setStatus('Сканирую: ' + requestedTitle + ' · уровень ' + item.depth, 'scanning');

                getPageLinks(api, requestedTitle).then(function (data) {
                    data.redirects.forEach(function (redirect) {
                        if (redirect.from && redirect.to) {
                            redirectsFound[redirect.from] = redirect.to;
                        }
                    });

                    var finalTitle = normalizeTitle(data.finalTitle || requestedTitle);
                    var finalKey = titleKey(finalTitle);

                    if (!pageInfo[finalKey]) {
                        pageInfo[finalKey] = {
                            requestedTitle: requestedTitle,
                            finalTitle: finalTitle,
                            depth: item.depth,
                            exists: data.exists
                        };
                    } else if (item.depth < pageInfo[finalKey].depth) {
                        pageInfo[finalKey].depth = item.depth;
                    }

                    if (item.parent && !firstParent[finalKey]) {
                        firstParent[finalKey] = item.parent;
                    }

                    childrenMap[finalKey] = data.links;

                    data.links.forEach(function (linkTitle) {
                        var linkKey = titleKey(linkTitle);

                        if (!firstParent[linkKey]) {
                            firstParent[linkKey] = finalTitle;
                        } else if (titleKey(firstParent[linkKey]) !== finalKey) {
                            addAlsoLinked(linkTitle, finalTitle);
                        }

                        if (!pageInfo[linkKey]) {
                            pageInfo[linkKey] = {
                                requestedTitle: linkTitle,
                                finalTitle: linkTitle,
                                depth: item.depth + 1,
                                exists: true
                            };
                        } else if (item.depth + 1 < pageInfo[linkKey].depth) {
                            pageInfo[linkKey].depth = item.depth + 1;
                        }

                        if (item.depth < maxDepth && !queued[linkKey] && !isExcludedTitle(linkTitle)) {
                            queued[linkKey] = true;

                            queue.push({
                                requestedTitle: linkTitle,
                                depth: item.depth + 1,
                                parent: finalTitle
                            });
                        }
                    });

                    statsBox.textContent =
                        'Просканировано: ' + Object.keys(scanned).length +
                        ' · В очереди: ' + queue.length +
                        ' · Найдено страниц: ' + Object.keys(pageInfo).length +
                        ' · Редиректов: ' + Object.keys(redirectsFound).length;

                    setTimeout(step, 120);
                }).catch(function (error) {
                    console.error(error);

                    setStatus('Ошибка при сканировании: ' + requestedTitle, 'error');

                    scanBtn.disabled = false;
                    clearBtn.disabled = false;
                });
            }

            function finish() {
                var rootFinal = rootTitle;

                Object.keys(pageInfo).some(function (key) {
                    var info = pageInfo[key];
                    if (titleKey(info.requestedTitle) === titleKey(rootTitle)) {
                        rootFinal = info.finalTitle;
                        return true;
                    }
                    return false;
                });

                renderTree(rootFinal, childrenMap, pageInfo, firstParent, alsoLinkedFrom);
                renderList(pageInfo);
                renderRedirects(redirectsFound);

                var finalText =
                    'Готово. Найдено страниц: ' + Object.keys(pageInfo).length +
                    '. Просканировано: ' + Object.keys(scanned).length +
                    '. Редиректов: ' + Object.keys(redirectsFound).length + '.';

                if (stoppedByLimit) {
                    finalText += ' Остановлено по лимиту страниц.';
                }

                statsBox.textContent = finalText;
                setStatus('Готово', 'done');

                scanBtn.disabled = false;
                clearBtn.disabled = false;
            }

            step();
        }

        scanBtn.addEventListener('click', scan);
        clearBtn.addEventListener('click', clearResults);
    }

    buildScanner(scannerRoot);
})();