User:ChasingAir/js/QuickArchive.js
Appearance
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
* QuickArchive - 在每个小节旁显示 [编辑源代码 | 删除 | 存档]
* - 删除:清空该段(按章节 section 编辑)
* - 存档:追加该段源码到“本页面/{{archiveto|1=...}}”子页面,然后清空该段
*/
// <nowiki>
(function (mw, $) {
'use strict';
if (!mw || !$) return;
// 仅在阅读界面运行
if (mw.config.get('wgAction') !== 'view') return;
if (!mw.config.get('wgPageName')) return;
var api = null;
var archivetoConfigCache = null; // Promise<{archiveTitle:string|null, showDelete:boolean, allowedHeadingTags:Set<string>|null}>
function getParseTextHtml(parseData) {
if (!parseData || !parseData.parse) return '';
// formatversion=2: parse.text is a string
if (typeof parseData.parse.text === 'string') return parseData.parse.text;
// formatversion=1: parse.text['*']
if (parseData.parse.text && typeof parseData.parse.text['*'] === 'string') return parseData.parse.text['*'];
return '';
}
function extractTemplateInvocation(wikitext, templateName) {
if (!wikitext) return null;
var lower = wikitext.toLowerCase();
var needle = '{{' + templateName.toLowerCase();
var start = lower.indexOf(needle);
if (start < 0) return null;
var depth = 0;
for (var i = start; i < wikitext.length - 1; i++) {
var two = wikitext.substr(i, 2);
if (two === '{{') {
depth++;
i++;
continue;
}
if (two === '}}') {
depth--;
i++;
if (depth === 0) {
return wikitext.slice(start, i + 1);
}
continue;
}
}
return null;
}
function splitTopLevel(text, sepChar) {
var parts = [];
var buf = '';
var curly = 0;
var square = 0;
for (var i = 0; i < text.length; i++) {
var two = text.substr(i, 2);
if (two === '{{') {
curly++;
buf += two;
i++;
continue;
}
if (two === '}}') {
if (curly > 0) curly--;
buf += two;
i++;
continue;
}
if (two === '[[') {
square++;
buf += two;
i++;
continue;
}
if (two === ']]') {
if (square > 0) square--;
buf += two;
i++;
continue;
}
var ch = text.charAt(i);
if (ch === sepChar && curly === 0 && square === 0) {
parts.push(buf);
buf = '';
continue;
}
buf += ch;
}
parts.push(buf);
return parts;
}
function splitKeyValueTopLevel(paramText) {
var curly = 0;
var square = 0;
for (var i = 0; i < paramText.length; i++) {
var two = paramText.substr(i, 2);
if (two === '{{') {
curly++;
i++;
continue;
}
if (two === '}}') {
if (curly > 0) curly--;
i++;
continue;
}
if (two === '[[') {
square++;
i++;
continue;
}
if (two === ']]') {
if (square > 0) square--;
i++;
continue;
}
if (paramText.charAt(i) === '=' && curly === 0 && square === 0) {
return {
key: paramText.slice(0, i).trim(),
value: paramText.slice(i + 1).trim()
};
}
}
return null;
}
function truthyParam(value) {
var v = (value || '').toString().trim().toLowerCase();
return v === '1' || v === 'yes' || v === 'true' || v === 'y';
}
function falsyParam(value) {
var v = (value || '').toString().trim().toLowerCase();
return v === '0' || v === 'no' || v === 'false' || v === 'n';
}
function computeArchiveTitle(pageName, resolvedTarget) {
if (!resolvedTarget) return null;
// 兼容两种写法:
// 1) 仅给出子页面名(如“2026年”)=> [[本页面/2026年]]
// 2) 给出完整标题(如“Wikivoyage:.../存档/2026年”)=> 直接使用
if (resolvedTarget.indexOf(':') !== -1 || resolvedTarget.indexOf('/') !== -1) {
return resolvedTarget;
}
return pageName + '/' + resolvedTarget;
}
async function getArchivetoConfig() {
if (archivetoConfigCache) return archivetoConfigCache;
archivetoConfigCache = (async function () {
var pageName = mw.config.get('wgPageName');
var data = await api.get({
action: 'query',
prop: 'revisions',
rvprop: 'content',
rvslots: 'main',
titles: pageName,
formatversion: 2
});
var page = data && data.query && data.query.pages && data.query.pages[0];
if (!page || !page.revisions || !page.revisions[0]) return null;
var rev = page.revisions[0];
var wikitext = '';
if (rev.slots && rev.slots.main && typeof rev.slots.main.content === 'string') {
wikitext = rev.slots.main.content;
} else if (typeof rev.content === 'string') {
wikitext = rev.content;
}
if (!wikitext) return null;
var invocation = extractTemplateInvocation(wikitext, 'archiveto');
if (!invocation) {
return { archiveTitle: null, showDelete: true, allowedHeadingTags: null };
}
var inner = invocation.slice(2, -2);
inner = inner.replace(/^\s*archiveto\s*/i, '');
if (inner.charAt(0) === '|') inner = inner.slice(1);
var rawParams = splitTopLevel(inner, '|');
var positional = [];
var named = Object.create(null);
for (var i = 0; i < rawParams.length; i++) {
var p = (rawParams[i] || '').trim();
if (!p) continue;
var kv = splitKeyValueTopLevel(p);
if (kv) {
named[(kv.key || '').trim().toLowerCase()] = kv.value;
} else {
positional.push(p);
}
}
var param1 = null;
if (Object.prototype.hasOwnProperty.call(named, '1')) param1 = named['1'];
else if (positional.length) param1 = positional[0];
if (!param1) {
return { archiveTitle: null, showDelete: true, allowedHeadingTags: null };
}
var showDelete = true;
if (Object.prototype.hasOwnProperty.call(named, 'delete-link')) {
showDelete = !falsyParam(named['delete-link']);
}
var allowedHeadingTags = null;
var tagKeys = ['h2', 'h3', 'h4', 'h5', 'h6'];
for (var k = 0; k < tagKeys.length; k++) {
if (Object.prototype.hasOwnProperty.call(named, tagKeys[k]) && truthyParam(named[tagKeys[k]])) {
if (!allowedHeadingTags) allowedHeadingTags = new Set();
allowedHeadingTags.add(tagKeys[k]);
}
}
// 展开 magic words / 模板(例如 {{CURRENTYEAR}})
var parsed = await api.get({
action: 'parse',
title: pageName,
text: param1,
prop: 'text',
disableeditsection: 1,
disablelimitreport: 1,
formatversion: 2
});
var html = getParseTextHtml(parsed);
var resolved = $('<div>').html(html).text();
resolved = (resolved || '').replace(/\s+/g, ' ').trim();
var archiveTitle = computeArchiveTitle(pageName, resolved || null);
return { archiveTitle: archiveTitle, showDelete: showDelete, allowedHeadingTags: allowedHeadingTags };
})();
return archivetoConfigCache;
}
function getSectionIdFromHeading(headingNode) {
if (!headingNode) return null;
var editNode = headingNode.querySelector('.mw-editsection') || (headingNode.parentNode && headingNode.parentNode.querySelector('.mw-editsection'));
if (!editNode) return null;
var editLink = editNode.querySelector('a[href*="section="]');
if (!editLink || !editLink.href) return null;
var m = editLink.href.match(/section=(\d+)/);
return m ? m[1] : null;
}
function findEditSectionNode(headingNode) {
if (!headingNode) return null;
return headingNode.querySelector('.mw-editsection') || (headingNode.parentNode && headingNode.parentNode.querySelector('.mw-editsection'));
}
function setLinkDisabled($link, disabled) {
if (disabled) {
$link.addClass('quickarchive-disabled');
$link.css({ 'pointer-events': 'none', 'opacity': 0.6 });
} else {
$link.removeClass('quickarchive-disabled');
$link.css({ 'pointer-events': '', 'opacity': '' });
}
}
async function fetchSectionContent(pageName, sectionId) {
var sectionData = await api.get({
action: 'query',
prop: 'revisions',
rvprop: ['content', 'timestamp'],
rvslots: 'main',
titles: pageName,
rvsection: sectionId,
formatversion: 2
});
var page = sectionData && sectionData.query && sectionData.query.pages && sectionData.query.pages[0];
if (!page || !page.revisions || !page.revisions[0]) throw new Error('无法读取章节内容');
var rev = page.revisions[0];
var content = '';
if (rev.slots && rev.slots.main && typeof rev.slots.main.content === 'string') content = rev.slots.main.content;
else if (typeof rev.content === 'string') content = rev.content;
return { content: content || '', timestamp: rev.timestamp };
}
async function deleteSection(pageName, sectionId) {
var sec = await fetchSectionContent(pageName, sectionId);
await api.postWithToken('csrf', {
action: 'edit',
title: pageName,
section: sectionId,
text: '',
basetimestamp: sec.timestamp,
summary: '删除段落',
format: 'json'
});
}
async function archiveSection(pageName, sectionId) {
var cfg = await getArchivetoConfig();
if (!cfg || !cfg.archiveTitle) throw new Error('未找到 {{archiveto|1=}} 或参数1为空');
var archiveTitle = cfg.archiveTitle;
var sec = await fetchSectionContent(pageName, sectionId);
var sectionText = (sec.content || '').replace(/\s+$/g, '');
if (!sectionText.trim()) throw new Error('该段内容为空,无法存档');
// 1) 追加到存档页
await api.postWithToken('csrf', {
action: 'edit',
title: archiveTitle,
appendtext: '\n\n' + sectionText,
summary: '存档自[[' + pageName + ']]',
format: 'json'
});
// 2) 清空本页该段
await api.postWithToken('csrf', {
action: 'edit',
title: pageName,
section: sectionId,
text: '',
basetimestamp: sec.timestamp,
summary: '存档到[[' + archiveTitle + ']]',
format: 'json'
});
}
function insertActionLinks(headingNode, cfg) {
var pageName = mw.config.get('wgPageName');
var tag = (headingNode && headingNode.tagName ? headingNode.tagName.toLowerCase() : '');
if (cfg && cfg.allowedHeadingTags && !cfg.allowedHeadingTags.has(tag)) return;
var sectionId = getSectionIdFromHeading(headingNode);
if (!sectionId) return;
var editNode = findEditSectionNode(headingNode);
if (!editNode) return;
if (editNode.getAttribute('data-quickarchive') === '1') return;
editNode.setAttribute('data-quickarchive', '1');
var closeBracket = editNode.querySelectorAll('.mw-editsection-bracket');
closeBracket = closeBracket && closeBracket.length ? closeBracket[closeBracket.length - 1] : null;
function insertBeforeClose(node) {
if (closeBracket && closeBracket.parentNode === editNode) {
editNode.insertBefore(node, closeBracket);
} else {
editNode.appendChild(node);
}
}
var links = [];
if (!cfg || cfg.showDelete !== false) {
var delLink = document.createElement('a');
delLink.href = '#';
delLink.textContent = '删除';
delLink.className = 'quickarchive-delete';
$(delLink).on('click', async function (e) {
e.preventDefault();
var $self = $(this);
if ($self.hasClass('quickarchive-disabled')) return false;
setLinkDisabled($self, true);
try {
await deleteSection(pageName, sectionId);
mw.notify('删除成功:已清空该段。', { type: 'success', autoHide: true });
setTimeout(function () { window.location.reload(); }, 800);
} catch (err) {
console.error(err);
mw.notify('删除失败:' + (err && err.message ? err.message : err), { type: 'error' });
setLinkDisabled($self, false);
}
return false;
});
links.push(delLink);
}
var archLink = document.createElement('a');
archLink.href = '#';
archLink.textContent = '存档';
archLink.className = 'quickarchive-archive';
$(archLink).on('click', async function (e) {
e.preventDefault();
var $self = $(this);
if ($self.hasClass('quickarchive-disabled')) return false;
setLinkDisabled($self, true);
try {
var c = await getArchivetoConfig();
await archiveSection(pageName, sectionId);
mw.notify('存档成功:已存档到[[' + ((c && c.archiveTitle) ? c.archiveTitle : (pageName + '/…')) + ']]。', { type: 'success', autoHide: true });
setTimeout(function () { window.location.reload(); }, 800);
} catch (err) {
console.error(err);
mw.notify('存档失败:' + (err && err.message ? err.message : err), { type: 'error' });
setLinkDisabled($self, false);
}
return false;
});
links.push(archLink);
if (!links.length) return;
// 统一插入:" | ..."(置于右括号前)
insertBeforeClose(document.createTextNode(' | '));
for (var li = 0; li < links.length; li++) {
if (li > 0) insertBeforeClose(document.createTextNode(' | '));
insertBeforeClose(links[li]);
}
}
function init($content, cfg) {
// 只给章节标题加(h2-h6),避免引入额外“段落”定义
var $headings = ($content || $(document)).find('.mw-parser-output').find('h2, h3, h4, h5, h6');
$headings.each(function () {
insertActionLinks(this, cfg);
});
}
function main() {
mw.loader.using(['mediawiki.api']).then(function () {
api = new mw.Api();
getArchivetoConfig().then(function (cfg) {
init($(document), cfg);
});
});
// 兼容部分皮肤/动态刷新
mw.hook('wikipage.content').add(function ($content) {
if (!api) return;
getArchivetoConfig().then(function (cfg) {
init($content, cfg);
});
});
}
$(main);
}(mediaWiki, jQuery));
// </nowiki>