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