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

MediaWiki:ScmcScanner.js: различия между версиями

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


     var EXCLUDED_PREFIXES = [
     var EXCLUDED_PREFIXES = [
Строка 32: Строка 26:
         return String(title || '')
         return String(title || '')
             .replace(/_/g, ' ')
             .replace(/_/g, ' ')
            .replace(/\s+/g, ' ')
             .trim();
             .trim();
     }
     }


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


     function isExcludedTitle(title) {
     function titleToUrl(title) {
         var normalized = normalizeTitle(title);
         return mw.util.getUrl(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) {
     function makeEl(tag, className, text) {
         var el = document.createElement(tag);
         var el = document.createElement(tag);
         if (className) el.className = className;
         if (className) el.className = className;
Строка 57: Строка 45:
     }
     }


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


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


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


     function shouldFollowPage(page) {
     function buildScanner(container) {
         if (!page || page.missing !== undefined) return false;
         var rootTitle = normalizeTitle(container.getAttribute('data-root') || 'Marine_Corps');
         if (typeof page.ns === 'number' && page.ns !== 0) return false;
        var maxDepth = parseInt(container.getAttribute('data-depth') || '5', 10);
         if (isExcludedTitle(page.title)) return false;
         var maxPages = parseInt(container.getAttribute('data-max-pages') || '250', 10);
         return true;
         var extraExcludedTitles = splitDataList(container.getAttribute('data-exclude'));
    }
 
         function isExcludedTitle(title) {
            var clean = normalizeTitle(title);


    function getPageLinks(api, title) {
            if (!clean) return true;
        var links = [];
        var normalizedTitle = normalizeTitle(title);


        function request(plcontinue) {
            if (clean.indexOf('#') !== -1) {
            var params = {
                 clean = normalizeTitle(clean.split('#')[0]);
                 action: 'query',
             }
                prop: 'links',
                titles: normalizedTitle,
                pllimit: 'max',
                formatversion: 2
             };


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


             return api.get(params).then(function (data) {
             return EXCLUDED_PREFIXES.some(function (prefix) {
                 var pages = data.query && data.query.pages ? data.query.pages : [];
                 return clean.indexOf(prefix) === 0;
                var page = pages[0];
            });
        }


                if (!shouldFollowPage(page)) {
        function isValidLink(link) {
                    return {
            if (!link || !link.title) return false;
                        title: normalizedTitle,
            if (typeof link.ns === 'number' && link.ns !== 0) return false;
                        exists: !!page && page.missing === undefined,
            if (isExcludedTitle(link.title)) return false;
                        followed: false,
            return true;
                        links: []
        }
                     };
 
        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;
                 }
                 }


                 if (page.links) {
                 return api.get(params).then(function (data) {
                    page.links.forEach(function (link) {
                    if (data.query && data.query.redirects) {
                        if (link.ns === 0 && !isExcludedTitle(link.title)) {
                        data.query.redirects.forEach(function (redirect) {
                             links.push(normalizeTitle(link.title));
                            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 (data.continue && data.continue.plcontinue) {
                    if (page.links) {
                    return request(data.continue.plcontinue);
                        page.links.forEach(function (link) {
                }
                            if (isValidLink(link)) {
                                links.push(normalizeTitle(link.title));
                            }
                        });
                    }


                return {
                     if (data.continue && data.continue.plcontinue) {
                     title: normalizeTitle(page.title || normalizedTitle),
                        return request(data.continue.plcontinue);
                    exists: true,
                    }
                    followed: true,
                    links: uniqueSorted(links)
                };
            });
        }


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


    function buildScanner(container) {
            return request();
        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 header = makeEl('div', 'scmc-scan-header');
         var title = createEl('div', 'scmc-scan-title', 'Сканер ссылок Marine Corps');
         var title = makeEl('div', 'scmc-scan-title', 'Сканер ссылок Marine Corps');
         var subtitle = createEl('div', 'scmc-scan-subtitle', 'Стартовая страница: ' + rootTitle + '. Глубина: ' + maxDepth + '.');
         var subtitle = makeEl(
            'div',
            'scmc-scan-subtitle',
            'Старт: ' + rootTitle + ' · глубина: ' + maxDepth + ' · лимит страниц: ' + maxPages
        );


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


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


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


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


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


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


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


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


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


         function renderList(allPages, pageDepth, skippedByLimit) {
         function renderTree(root, childrenMap, pageInfo, firstParent, alsoLinkedFrom) {
             listBox.innerHTML = '';
             treeBox.innerHTML = '';
 
            treeBox.appendChild(makeEl('div', 'scmc-scan-box-title', 'Дерево ссылок'));
 
            function makeNode(title, path) {
                var key = titleKey(title);
                var info = pageInfo[key];


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


            var list = createEl('ol', 'scmc-scan-page-list');
                var link = document.createElement('a');
                link.href = titleToUrl(title);
                link.textContent = title;


            allPages.forEach(function (title) {
                 line.appendChild(link);
                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]);
                 if (info) {
                    line.appendChild(makeEl('span', 'scmc-scan-depth', 'ур. ' + info.depth));


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


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


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


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


            var boxTitle = createEl('div', 'scmc-scan-box-title', 'Дерево ссылок');
                var extraLinks = alsoLinkedFrom[key] || [];
            treeBox.appendChild(boxTitle);
                if (extraLinks.length) {
                    node.appendChild(makeEl('div', 'scmc-scan-also', 'Ещё ссылки из: ' + extraLinks.join(', ')));
                }


            function makeNode(title, localVisited) {
                var nextPath = Object.assign({}, path);
                 var node = createEl('div', 'scmc-scan-node');
                 nextPath[key] = true;


                 var line = createEl('div', 'scmc-scan-node-line');
                 var children = childrenMap[key] || [];
                var a = document.createElement('a');
                a.href = titleToLink(title);
                a.textContent = title;


                 var depth = createEl('span', 'scmc-scan-depth', 'ур. ' + pageDepth[title]);
                 if (children.length) {
                    var childrenWrap = makeEl('div', 'scmc-scan-children');


                line.appendChild(a);
                    children.forEach(function (childTitle) {
                line.appendChild(depth);
                        var childKey = titleKey(childTitle);
                node.appendChild(line);


                if (localVisited[title]) {
                        if (firstParent[childKey] && titleKey(firstParent[childKey]) !== key) {
                    var loop = createEl('div', 'scmc-scan-loop', '↳ уже встречалась выше');
                            var refNode = makeEl('div', 'scmc-scan-node');
                    node.appendChild(loop);
                            var refLine = makeEl('div', 'scmc-scan-node-line scmc-scan-node-ref');
                    return node;
                }


                var nextVisited = Object.assign({}, localVisited);
                            var refLink = document.createElement('a');
                nextVisited[title] = true;
                            refLink.href = titleToUrl(childTitle);
                            refLink.textContent = childTitle;


                var children = childrenMap[title] || [];
                            refLine.appendChild(refLink);
                if (children.length) {
                            refLine.appendChild(makeEl('span', 'scmc-scan-ref', 'уже найдено в: ' + firstParent[childKey]));
                    var childBox = createEl('div', 'scmc-scan-children');
                            refNode.appendChild(refLine);
                            childrenWrap.appendChild(refNode);
                            return;
                        }


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


                     node.appendChild(childBox);
                     node.appendChild(childrenWrap);
                 }
                 }


Строка 261: Строка 330:


             treeBox.appendChild(makeNode(root, {}));
             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() {
         function scan() {
             clearResult();
             clearResults();


             var api = getApi();
             var api = getApi();
             var queue = [{ title: rootTitle, depth: 0 }];
 
             var queue = [{
                requestedTitle: rootTitle,
                depth: 0,
                parent: null
            }];
 
             var scanned = {};
             var scanned = {};
             var queued = {};
             var queued = {};
             var pageDepth = {};
             var pageInfo = {};
             var childrenMap = {};
             var childrenMap = {};
             var skippedByLimit = false;
             var firstParent = {};
            var alsoLinkedFrom = {};
            var redirectsFound = {};
            var stoppedByLimit = false;


             queued[rootTitle] = true;
             queued[titleKey(rootTitle)] = true;
            pageDepth[rootTitle] = 0;


             startButton.disabled = true;
             scanBtn.disabled = true;
             clearButton.disabled = true;
             clearBtn.disabled = true;
             setStatus(STATUS.scanning, 'scanning');
 
             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() {
             function step() {
Строка 288: Строка 457:


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


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


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


                 scanned[title] = true;
                 scanned[requestedKey] = true;


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


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


                     if (depth < maxDepth) {
                     var finalTitle = normalizeTitle(data.finalTitle || requestedTitle);
                        pageData.links.forEach(function (linkTitle) {
                    var finalKey = titleKey(finalTitle);
                            if (!queued[linkTitle] && !isExcludedTitle(linkTitle)) {
 
                                queued[linkTitle] = true;
                    if (!pageInfo[finalKey]) {
                                pageDepth[linkTitle] = depth + 1;
                        pageInfo[finalKey] = {
                                queue.push({
                            requestedTitle: requestedTitle,
                                    title: linkTitle,
                            finalTitle: finalTitle,
                                    depth: depth + 1
                            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;
                     }
                     }


                     stats.textContent = 'Просканировано страниц: ' + Object.keys(scanned).length + '. В очереди: ' + queue.length + '. Найдено всего: ' + Object.keys(queued).length + '.';
                     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);
                     setTimeout(step, 120);
                 }).catch(function (error) {
                 }).catch(function (error) {
                     console.error(error);
                     console.error(error);
                     setStatus('Ошибка при сканировании страницы: ' + title, 'error');
 
                     startButton.disabled = false;
                     setStatus('Ошибка при сканировании: ' + requestedTitle, 'error');
                     clearButton.disabled = false;
 
                     scanBtn.disabled = false;
                     clearBtn.disabled = false;
                 });
                 });
             }
             }


             function finish() {
             function finish() {
                 var allPages = Object.keys(queued).sort(function (a, b) {
                 var rootFinal = rootTitle;
                     return a.localeCompare(b, 'ru');
 
                Object.keys(pageInfo).some(function (key) {
                     var info = pageInfo[key];
                    if (titleKey(info.requestedTitle) === titleKey(rootTitle)) {
                        rootFinal = info.finalTitle;
                        return true;
                    }
                    return false;
                 });
                 });


                 renderTree(rootTitle, childrenMap, pageDepth);
                 renderTree(rootFinal, childrenMap, pageInfo, firstParent, alsoLinkedFrom);
                 renderList(allPages, pageDepth, skippedByLimit);
                 renderList(pageInfo);
                renderRedirects(redirectsFound);


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


                 startButton.disabled = false;
                 if (stoppedByLimit) {
                 clearButton.disabled = false;
                    finalText += ' Остановлено по лимиту страниц.';
                }
 
                statsBox.textContent = finalText;
                setStatus('Готово', 'done');
 
                scanBtn.disabled = false;
                 clearBtn.disabled = false;
             }
             }


Строка 351: Строка 585:
         }
         }


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


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

Версия от 08:46, 15 июня 2026

(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);
})();