User:SuperGrey/gadgets/CiteUnseen/main.js
外观
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
// Main page: [[User:SuperGrey/gadgets/CiteUnseen]]
var CiteUnseen = {
/**
* 解析 COinS 字串,並將其轉換為 object。
* @param query {String} - COinS 字串
* @returns {Object} - 解析後的 object
*/
parseCoinsString: function (query) {
const result = {};
// Split the string by '&'
const pairs = query.split('&');
pairs.forEach(pair => {
if (!pair) return;
// Split at the first '='
const index = pair.indexOf('=');
let key, value;
if (index === -1) {
key = pair;
value = "";
} else {
key = pair.substring(0, index);
value = pair.substring(index + 1);
}
// Replace '+' with space and decode both key and value
key = key.replace(/\+/g, ' ');
value = value.replace(/\+/g, ' ');
if (!result[key]) {
result[key] = value;
} else {
if (typeof result[key] === 'string') {
result[key] = [result[key]];
}
result[key].push(value);
}
});
return result;
},
/**
* 解析包含逗號分隔條件的日期規則字串,
* 每個條件包含一個運算符和一個日期。
* 返回一個接受日期(或日期字串)的判斷函式,
* 若日期符合所有條件則返回 true。
* @param ruleString {String} - 日期規則字串,例如 "<2022-01-01,>2020-01-01"
* @returns {Function|null} - 判斷函式,若有任何條件無效則返回 null。
*/
parseDateRule: function (ruleString) {
ruleString = ruleString.trim();
if (ruleString.length === 0) {
return null;
}
// Split the rule string by comma into individual conditions.
let conditionStrings = ruleString.split(',').map(s => s.trim());
// Array to hold predicate functions for each condition.
let predicates = [];
for (let cond of conditionStrings) {
if (cond.length === 0) continue; // Skip empty conditions
// Check if the condition starts with an operator (<, >, or =). Default to "=".
let operator = '=';
if (cond[0] === '<' || cond[0] === '>' || cond[0] === '=') {
operator = cond[0];
cond = cond.substring(1).trim();
}
// Parse the date portion.
let targetDate = new Date(cond);
if (isNaN(targetDate.getTime())) {
// If any condition has an invalid date, return null.
return null;
}
// Create and store a predicate function for this condition.
let predicate;
switch (operator) {
case '<':
predicate = function (date) {
return date < targetDate;
};
break;
case '>':
predicate = function (date) {
return date > targetDate;
};
break;
default: // '='
predicate = function (date) {
return date.getTime() === targetDate.getTime();
};
}
predicates.push(predicate);
}
// Return a predicate function that applies all conditions.
return function (inputDate) {
let date = (inputDate instanceof Date) ? inputDate : new Date(inputDate);
if (isNaN(date.getTime())) {
return false;
}
return predicates.every(fn => fn(date));
};
},
/**
* 將字串中的特殊字符轉義。
* @param string {String} - 字串
* @returns {String} - 轉義後的字串
*/
escapeRegex: function (string) {
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
},
/**
* 建立正規表達式。
* @param string {String} - 字串
* @returns {RegExp} - 正規表達式
*/
urlRegex: function (string) {
// 給定一個網域和路徑,正規表達式會尋找符合以下規則的子字串:
// - 以 http:// 或 https:// 開頭
// - 網域前不能有額外的 /
// - 網域緊接在 :// 之後,或前面有 .(以考慮子網域)
// - 網域和路徑之後,符合以下其中一項:
// - 字串結束
// - 下一個字元是 /
// - 網域以句點結尾(允許像 gov.uk 這樣的頂級域名)
return new RegExp('https?:\\/\\/([^\\/]*\\.)?' + CiteUnseen.escapeRegex(string) + '($|((?<=\\.)|\\/))');
},
/**
* 檢查來源的作者是否符合規則。
* @param coins {Object} - COinS object
* @param rule {Object} - 規則
* @returns {boolean} - 是否符合規則
*/
matchAuthor: function (coins, rule) {
let author = coins['rft.au']; // can be a string or an array
if (coins['rft.aulast']) {
let appendedAuthors = coins['rft.aulast'];
if (typeof appendedAuthors === 'string' && coins['rft.aufirst']) {
appendedAuthors = [coins['rft.aufirst'] + ' ' + appendedAuthors];
} else if (coins['rft.aufirst']) {
for (let i = 0; i < coins['rft.aufirst'].length; i++) {
appendedAuthors[i] = coins['rft.aufirst'][i] + ' ' + appendedAuthors[i];
}
}
if (typeof author === 'string') {
author = [author];
}
author.concat(appendedAuthors);
}
if (!author || !rule['author']) {
return false;
}
let authorRegex = new RegExp(rule['author'], 'i');
if (typeof author === 'string') {
return authorRegex.test(author);
} else {
for (let au of author) {
if (authorRegex.test(au)) {
return true;
}
}
return false;
}
},
/**
* 檢查來源的出版商是否符合規則。
* @param coins {Object} - COinS object
* @param rule {Object} - 規則
* @returns {boolean} - 是否符合規則
*/
matchPublisher: function (coins, rule) {
const coinsPub = coins['rft.pub'] || coins['rft.jtitle'] || null;
if (!coinsPub || !rule['pub']) {
return false;
}
let publisherRegex = new RegExp(rule['pub'], 'i');
if (typeof coinsPub === 'string') {
return publisherRegex.test(coinsPub);
} else {
for (let publisher of coinsPub) {
if (publisherRegex.test(publisher)) {
return true;
}
}
return false;
}
},
/**
* 檢查 COinS 物件中的日期是否符合日期規則。
* @param coins {Object} - COinS 物件。
* @param rule {Object} - 包含日期規則字串的規則物件。
* @returns {boolean} - 如果日期符合規則則返回 true,否則返回 false。
*/
matchDate: function (coins, rule) {
let predicate = CiteUnseen.parseDateRule(rule['date']);
if (!predicate) {
console.log("[Cite Unseen] Invalid date rule: " + rule['date']);
return false;
}
return predicate(coins['rft.date']);
},
/**
* 檢查來源的 URL 是否符合規則。
* @param coins {Object} - COinS object
* @param rule {Object} - 規則
* @returns {boolean} - 是否符合規則
*/
matchUrl: function (coins, rule) {
if (!rule['url'] || !coins['rft_id']) {
return false;
}
let urlRegex = CiteUnseen.urlRegex(rule['url']);
return urlRegex.test(coins['rft_id']);
},
/**
* 檢查來源的 URL 字串是否符合規則。
* @param coins {Object} - COinS object
* @param rule {Object} - 規則
* @returns {boolean} - 是否符合規則
*/
matchUrlString: function (coins, rule) {
if (!rule['url_str'] || !coins['rft_id']) {
return false;
}
let urlString = rule['url_str'];
if (typeof urlString === 'string') {
return coins['rft_id'].includes(urlString);
} else {
for (let str of urlString) {
if (coins['rft_id'].includes(str)) {
return true;
}
}
return false;
}
},
/**
* 檢查來源是否符合規則。
* @param coins {Object} - COinS object
* @param rule {Object} - 規則
* @returns {boolean} - 是否符合規則
*/
match: function (coins, rule) {
if (!rule) {
console.log("[Cite Unseen] There are empty rules in the ruleset.");
return false;
}
const matchFunctions = {
'author': CiteUnseen.matchAuthor,
'pub': CiteUnseen.matchPublisher,
'date': CiteUnseen.matchDate,
'url': CiteUnseen.matchUrl,
'url_str': CiteUnseen.matchUrlString,
};
for (let key of Object.keys(rule)) {
if (!matchFunctions[key]) {
console.log("[Cite Unseen] Unknown rule:");
console.log(rule)
continue;
}
if (!matchFunctions[key](coins, rule)) {
return false;
}
}
return true;
},
/**
* 為引用的來源加上圖示。僅在頁面載入時執行一次。
*/
addIcons: function () {
// 將 categorizedRules 過濾為僅包含頁面引用中出現的連結
// 考慮到我們追蹤的網域數量以及後續使用的正規表達式,這樣可以顯著節省時間
// 在一篇約有 500 個引用的文章上進行快速測試,速度提升約 5 倍
let filteredCategorizedRules = {};
for (let key of Object.keys(CiteUnseen.categorizedRules)) {
filteredCategorizedRules[key] = [];
for (let link of CiteUnseen.refLinks) {
for (let rule of CiteUnseen.categorizedRules[key]) {
let domain = rule['url'];
if (!(CiteUnseen.citeUnseenDomainIgnore[key] && CiteUnseen.citeUnseenDomainIgnore[key].includes(domain)) && link.includes(domain)) {
if (!filteredCategorizedRules[key].includes(rule)) {
filteredCategorizedRules[key].push(rule);
}
}
}
}
}
let typeCategories = CiteUnseen.citeUnseenCategoryTypes.flatMap(x => x[1]);
// 來源是否未被標記
let unknownSet;
CiteUnseen.refs.forEach(function (ref) {
// 在 <cite> 標籤前加入圖示區域
let iconsDiv = document.createElement("div");
iconsDiv.classList.add("cite-unseen-icons");
iconsDiv.style.display = 'inline-flex';
iconsDiv.style.gap = '0 5px';
iconsDiv.style.paddingRight = '5px';
iconsDiv.style.verticalAlign = 'middle';
ref.cite.prepend(iconsDiv);
if (window.cite_unseen_dashboard) {
// 提前放入自訂來源分類的圖示
let iconNode = CiteUnseen.processIcon(iconsDiv, "flag");
iconNode.style.display = 'none'; // 預設隱藏
CiteUnseen.customCategoryIcons.push(iconNode);
}
// 根據 class 名稱判斷來源類型。
const classList = ref.cite.classList;
if (classList.contains("book") || classList.contains("journal") || classList.contains("encyclopaedia") || classList.contains("conference") || classList.contains("thesis") || classList.contains("magazine")) {
if (CiteUnseen.citeUnseenCategories.books) {
CiteUnseen.processIcon(iconsDiv, "books");
}
} else if (classList.contains("pressrelease")) {
if (CiteUnseen.citeUnseenCategories.press) {
CiteUnseen.processIcon(iconsDiv, "press");
}
} else if (classList.contains("episode") || classList.contains("podcast") || classList.contains("media")) {
if (CiteUnseen.citeUnseenCategories.tvPrograms) {
CiteUnseen.processIcon(iconsDiv, "tvPrograms");
}
} else if (ref.coins['rft_id']) {
unknownSet = true;
// 來源檢查表
let checked = false;
for (let checklistTypeData of CiteUnseen.citeUnseenChecklists) {
if (checked) {
break;
}
let checklistType = checklistTypeData[0];
for (let checklist of checklistTypeData[1]) {
if (checked) {
break;
}
let checklistName = checklist[0], checklistID = checklist[1];
for (let rule of filteredCategorizedRules[checklistID]) {
if (CiteUnseen.match(ref.coins, rule)) {
if (CiteUnseen.citeUnseenCategories[checklistID]) {
CiteUnseen.processIcon(iconsDiv, checklistType, checklistName);
checked = true;
unknownSet = false;
break;
}
unknownSet = false;
}
}
}
}
// 類型
for (let category of typeCategories) {
for (let rule of filteredCategorizedRules[category]) {
if (CiteUnseen.match(ref.coins, rule)) {
if (CiteUnseen.citeUnseenCategories[category]) {
CiteUnseen.processIcon(iconsDiv, category);
unknownSet = false;
break;
}
unknownSet = false;
}
}
}
if (CiteUnseen.citeUnseenCategories.unknown && unknownSet) {
// 並未被識別的來源,標記為未知。
CiteUnseen.processIcon(iconsDiv, "unknown");
}
}
});
if (window.cite_unseen_dashboard) {
CiteUnseen.showDashboard();
}
// 結束計時
console.timeEnd('CiteUnseen runtime');
},
/**
* 添加計數。目前無論是否在 reflist 中,都會記錄進去。
* @param node {Element} - 節點
* @param type {String} - 類型
*/
addToCount: function (node, type) {
CiteUnseen.citeUnseenCategoryData[type].count = CiteUnseen.citeUnseenCategoryData[type].count + 1;
},
/**
* 為節點添加圖示、滑鼠懸浮文字。
* @param node {Element} - iconsDiv 節點
* @param type {String} - 類型
* @param checklist {String|null} - 檢查表
* @returns {Element} - iconNode 節點
*/
processIcon: function (node, type, checklist = null) {
let iconNode = document.createElement("img");
iconNode.classList.add("skin-invert");
iconNode.classList.add("cite-unseen-icon-" + type);
iconNode.setAttribute("src", CiteUnseen.citeUnseenCategoryData[type].icon);
let message = CiteUnseen.convByVar({
hant: CiteUnseen.citeUnseenCategoryData[type].hint_hant,
hans: CiteUnseen.citeUnseenCategoryData[type].hint_hans,
});
if (checklist) {
message = CiteUnseen.convByVar({hant: '來自 ', hans: '来自 '}) + checklist + ':' + message;
}
iconNode.setAttribute("alt", message);
iconNode.setAttribute("title", "[Cite Unseen] " + message);
CiteUnseen.addToCount(node, type);
iconNode.style.width = "17px";
iconNode.style.height = "17px";
iconNode.style.objectFit = 'contain';
iconNode.style.cssText += 'max-width: 17px !important;';
node.appendChild(iconNode);
if (!CiteUnseen.refCategories[type]) {
CiteUnseen.refCategories[type] = [];
}
CiteUnseen.refCategories[type].push(node.parentNode);
return iconNode;
},
CustomCategoryDialog: function (config) {
CiteUnseen.CustomCategoryDialog.super.call(this, config);
},
/**
* 建立一個選擇「網址」或「網址字串」的列。
* 此函式會生成一個下拉選單,允許使用者在「網址」與「網址字串」之間切換,
* 並根據選擇更新規則物件。
* @param {Object} rule - 要根據選擇進行更新的規則物件。
*/
createUrlRow: function (rule) {
const chineseUrl = CiteUnseen.convByVar({hant: '網址', hans: '网址'});
const chineseUrlString = CiteUnseen.convByVar({hant: '網址字串', hans: '网址字符串'});
// Create menu options for URL and URL String using convByVar.
var optionURL = new OO.ui.MenuOptionWidget({
data: 'url', label: chineseUrl,
});
var optionURLString = new OO.ui.MenuOptionWidget({
data: 'url_str', label: chineseUrlString,
});
// Initialize the dropdown label based on the current rule.
var initialLabel = rule.hasOwnProperty('url_str') ? chineseUrlString : chineseUrl;
var dropdown = new OO.ui.DropdownWidget({
label: initialLabel, menu: {
items: [optionURL, optionURLString],
},
});
// Update the rule when an option is chosen.
dropdown.getMenu().on('choose', function (item) {
var data = item.getData();
if (data === 'url') {
if (rule.url_str !== undefined) {
rule.url = rule.url_str;
delete rule.url_str;
}
dropdown.setLabel(chineseUrl);
} else if (data === 'url_str') {
if (rule.url !== undefined) {
rule.url_str = rule.url;
delete rule.url;
}
dropdown.setLabel(chineseUrlString);
}
});
// Create the URL text input widget.
var urlValue = rule.url || rule.url_str || '';
var urlInput = new OO.ui.TextInputWidget({
value: urlValue, title: CiteUnseen.convByVar({
hant: '網址應包含網域名,不需要包含 http(s);網址字串則可以包含任何字元。',
hans: '网址应包含网域名,不需要包含 http(s);网址字符串则可以包含任何字符。',
}),
});
// Update rule when the text input value changes.
urlInput.on('change', function (value) {
if (rule.hasOwnProperty('url')) {
rule.url = value;
} else {
rule.url_str = value;
}
});
var $row = $('<div>').addClass('cite-unseen-dialog-url-row');
$row.append(dropdown.$element, urlInput.$element);
return $row;
},
/**
* 建立可選參數的行(例如作者、出版、日期)。
* 此函式會生成一個包含多個可選參數的區域,每個參數都包含一個勾選框和文字輸入框。
* 當勾選框被選中時,對應的文字輸入框會啟用,否則會被禁用。
* @param rule {Object} - 包含參數值的規則物件。
*/
createOptionalParamsRow: function (rule) {
var $paramsContainer = $('<div>').addClass('cite-unseen-dialog-optional-params');
const params = ['author', 'pub', 'date'];
const chineseParams = {
'author': CiteUnseen.convByVar({hant: '作者', hans: '作者'}),
'pub': CiteUnseen.convByVar({hant: '出版', hans: '出版'}),
'date': CiteUnseen.convByVar({hant: '日期', hans: '日期'}),
};
params.forEach(function (param) {
var paramValue = rule[param] || '';
var checkbox = new OO.ui.CheckboxInputWidget({
selected: !!paramValue,
});
var textInput = new OO.ui.TextInputWidget({
value: paramValue,
});
textInput.$element.css('display', !!paramValue ? 'inline-block' : 'none');
textInput.setDisabled(!checkbox.isSelected());
checkbox.on('change', function (isSelected) {
textInput.setDisabled(!isSelected);
textInput.$element.css('display', isSelected ? 'inline-block' : 'none');
if (!isSelected) {
textInput.setValue('');
delete rule[param];
}
});
textInput.on('change', function (value) {
rule[param] = value;
});
var $paramRow = $('<div>')
.addClass('cite-unseen-dialog-param-container')
.css({'margin-top': '5px'});
$paramRow.append(checkbox.$element, $('<span>').text(chineseParams[param]), textInput.$element);
$paramsContainer.append($paramRow);
});
return $paramsContainer;
},
/**
* 為一條規則建立完整的元件。
* 此函式會生成一個包含規則的容器,並附加相關的 UI 元件。
* @param {Object} rule - 包含規則資料的物件。
* @param {OO.ui.FieldsetLayout} parentFieldset - 父級的 Fieldset,用於移除規則時更新 UI。
*/
createRuleWidget: function (rule, parentFieldset) {
var $container = $('<div>').addClass('cite-unseen-dialog-rule-container');
var $urlRow = CiteUnseen.createUrlRow(rule);
var $optionalParams = CiteUnseen.createOptionalParamsRow(rule);
var ruleWidget = new OO.ui.Widget({$element: $container});
// Attach rule data directly to the widget.
ruleWidget.ruleData = rule;
var removeButton = new OO.ui.ButtonWidget({
icon: 'trash', flags: ['destructive'],
});
removeButton.$element.addClass('cite-unseen-dialog-remove-button');
removeButton.on('click', function () {
if (parentFieldset) {
parentFieldset.removeItems([ruleWidget]); // Properly remove from the fieldset.
} else {
ruleWidget.$element.remove();
}
CiteUnseen.updateCustomCategoryDialogHeight();
});
$container.append($urlRow, $optionalParams, removeButton.$element);
return ruleWidget;
},
/**
* 生成自訂來源分類對話框的內容。
* @returns {OO.ui.FieldsetLayout} - 自訂來源分類對話框的內容
*/
generateCustomCategoryDialogRules: function (initialRules) {
var fieldset = new OO.ui.FieldsetLayout({
classes: ['cite-unseen-dialog-rules'],
});
if (initialRules) {
// Create a widget for each rule without updating any global array.
initialRules.forEach(function (rule) {
fieldset.addItems([CiteUnseen.createRuleWidget(rule, fieldset)]);
});
}
var addButton = new OO.ui.ButtonWidget({
label: CiteUnseen.convByVar({
hant: '新增規則', hans: '新增规则',
}), icon: 'add',
});
var addButtonField = new OO.ui.FieldLayout(addButton, {align: 'center'});
addButtonField.$element.addClass('cite-unseen-dialog-add-button');
fieldset.addButtonField = addButtonField;
addButton.on('click', function () {
var newRule = {url: ''};
var newWidget = CiteUnseen.createRuleWidget(newRule, fieldset);
// Insert new rule widget just before the add button.
fieldset.removeItems([fieldset.addButtonField]);
fieldset.addItems([newWidget]);
fieldset.addItems([fieldset.addButtonField]);
CiteUnseen.updateCustomCategoryDialogHeight();
});
fieldset.addItems([addButtonField]);
return fieldset;
},
/**
* 更新自訂來源分類對話框的高度。
*/
updateCustomCategoryDialogHeight: function () {
if (CiteUnseen.customCategoryDialog) {
var maxHeight = $(window).height() * 0.5;
CiteUnseen.customCategoryDialog.panel.$element.css('max-height', maxHeight + 'px');
CiteUnseen.customCategoryDialog.updateSize();
}
},
/**
* 初始化自訂來源分類對話框。
*/
initCustomCategoryDialog: function () {
if (CiteUnseen.CustomCategoryDialogRules === null) {
CiteUnseen.CustomCategoryDialogRules = new OO.ui.FieldsetLayout();
}
OO.inheritClass(CiteUnseen.CustomCategoryDialog, OO.ui.Dialog);
CiteUnseen.CustomCategoryDialog.static.name = 'CustomCategoryDialog';
CiteUnseen.CustomCategoryDialog.prototype.initialize = function () {
CiteUnseen.CustomCategoryDialog.super.prototype.initialize.apply(this, arguments);
this.panel = new OO.ui.PanelLayout({
padded: true, expanded: false,
});
this.panel.$element.addClass('cite-unseen-dialog-panel');
this.content = new OO.ui.FieldsetLayout();
this.content.$element.addClass('cite-unseen-dialog-content');
// 標題
this.titleLabelWidget = new OO.ui.LabelWidget({
label: CiteUnseen.convByVar({
hant: '[Cite Unseen] 添加自訂來源分類規則', hans: '[Cite Unseen] 添加自定义来源分类规则',
}), classes: ['cite-unseen-dialog-title'],
});
this.titleLabelWidget.$element.append($('<span>').text(CiteUnseen.convByVar({
hant: '(此為臨時生效;若需自訂已有的來源分類,請參見「',
hans: '(此为临时生效;若需自定义已有的来源分类,请参见「',
}))
.css({'font-size': '0.75em', 'font-weight': 'normal'})
.append($('<a>')
.text(CiteUnseen.convByVar({hant: '進階自訂教程', hans: '进阶自定义教程'}))
.attr('href', '//zh.wikipedia.org/wiki/User:SuperGrey/gadgets/CiteUnseen'))
.append('」。)'));
// 動作按鈕
this.cancelButton = new OO.ui.ButtonWidget({
label: CiteUnseen.convByVar({hant: '取消', hans: '取消'}), flags: ['safe', 'close'], action: 'cancel',
});
this.saveButton = new OO.ui.ButtonWidget({
label: CiteUnseen.convByVar({hant: '儲存', hans: '保存'}),
flags: ['primary', 'progressive'],
action: 'save',
});
this.actionButtons = new OO.ui.ButtonGroupWidget({
items: [
this.cancelButton, this.saveButton,
],
});
this.actionButtons.$element.addClass('cite-unseen-dialog-action-buttons');
// 為按鈕添加點擊事件
this.cancelButton.$element.on('click', function (e) {
this.close();
}.bind(this));
this.saveButton.$element.on('click', async function (e) {
let response = await CiteUnseen.addCustomCategory();
if (response) {
this.close();
}
}.bind(this));
// 視窗內容
this.ruleContent = CiteUnseen.generateCustomCategoryDialogRules(CiteUnseen.customCategoryRules);
this.content.addItems([
this.titleLabelWidget, this.actionButtons, this.ruleContent,
]);
this.panel.$element.append(this.content.$element);
this.$body.append(this.panel.$element);
// Automatically update height on window resize.
$(window).on('resize.customCategoryDialog', function () {
CiteUnseen.updateCustomCategoryDialogHeight();
});
CiteUnseen.updateCustomCategoryDialogHeight();
}
CiteUnseen.CustomCategoryDialog.prototype.getBodyHeight = function () {
return this.panel.$element.outerHeight(true);
};
CiteUnseen.CustomCategoryDialog.prototype.getTearDownProcess = function (data) {
return CiteUnseen.CustomCategoryDialog.super.prototype.getTearDownProcess.call(this, data);
};
},
/**
* 添加自訂來源分類規則。
* @returns {Promise<boolean>} - 返回一個 Promise,表示操作是否成功。
*/
addCustomCategory: async function () {
if (!CiteUnseen.customCategoryDialog) {
console.error("[Cite Unseen] customCategoryDialog is not initialized when addCustomCategory is called.");
return false;
}
var dialog = CiteUnseen.customCategoryDialog;
var newRules = [];
dialog.ruleContent.getItems().forEach(function (item) {
// Exclude the add button field and only collect widgets with ruleData.
if (item !== dialog.ruleContent.addButtonField && item.ruleData) {
newRules.push(item.ruleData);
}
});
console.log(newRules);
for (let i = 0; i < newRules.length; i++) {
// Ensure the rule has a non-empty URL value.
var rule = newRules[i];
var url = rule.url || rule.url_str;
if (!url || url.trim() === '') {
mw.notify(CiteUnseen.convByVar({
hant: '來源網址無效,請檢查並重新輸入。', hans: '来源网址无效,请检查并重新输入。',
}), {type: 'error', autoHide: true, title: '[Cite Unseen]'});
return false;
}
}
CiteUnseen.customCategoryRules = newRules;
// 根據自訂規則更新來源圖示的 display 狀態。
let customCategoryCitations = [];
for (let i = 0; i < CiteUnseen.refs.length; i++) {
let ref = CiteUnseen.refs[i];
let coins = ref.coins;
let iconNode = CiteUnseen.customCategoryIcons[i];
let matched = false;
for (let rule of CiteUnseen.customCategoryRules) {
if (CiteUnseen.match(coins, rule)) {
iconNode.style.display = 'inline-block';
matched = true;
customCategoryCitations.push(ref.cite);
break;
}
}
if (!matched) {
iconNode.style.display = 'none';
}
}
// 高亮顯示來源
CiteUnseen.highlightCitation('flag', customCategoryCitations);
CiteUnseen.refCategories['flag'] = customCategoryCitations;
return true;
},
/**
* 顯示自訂來源分類對話框。
*/
showCustomCategoryDialog: function () {
if (CiteUnseen.CustomCategoryDialogRules === null) {
CiteUnseen.initCustomCategoryDialog();
}
CiteUnseen.CustomCategoryDialogRules.clearItems();
CiteUnseen.customCategoryDialog = new CiteUnseen.CustomCategoryDialog({padded: true, scrollable: true});
CiteUnseen.CustomCategoryDialogRules.addItems(CiteUnseen.generateCustomCategoryDialogRules());
if (CiteUnseen.windowManager === null) {
CiteUnseen.windowManager = new OO.ui.WindowManager({modal: false, classes: ['cite-unseen-non-modal']});
mw.util.addCSS(`
.cite-unseen-non-modal {
position: -webkit-sticky;
position: sticky;
bottom: 1em;
max-width: 960px;
margin-left: auto;
margin-right: auto;
background-color: white;
}
.cite-unseen-non-modal.oo-ui-windowManager-size-full {
width: 100%;
height: 100%;
bottom: 0;
}
.cite-unseen-non-modal .oo-ui-window {
border: 1px solid #a2a9b1;
box-shadow: 0 0 4px 0 rgba( 0, 0, 0, 0.25 );
}
.cite-unseen-non-modal.oo-ui-windowManager-size-full .oo-ui-window {
border: 0;
box-shadow: unset;
}
.cite-unseen-dialog-panel {
padding-bottom: 0;
}
.cite-unseen-dialog-action-buttons {
position: absolute;
top: 0;
right: -5px;
display: flex;
gap: 0 10px;
flex-wrap: wrap;
}
.cite-unseen-dialog-title {
font-size: 1.12em;
font-weight: bold;
margin-bottom: 10px;
margin-right: 127px;
}
.cite-unseen-dialog-content {
padding-bottom: 16px;
}
.cite-unseen-dialog-rule-container {
position: relative;
width: 20em;
padding: 10px;
background-color: rgb(249, 249, 249);
border: 1px solid rgb(204, 204, 204);
flex-grow: 1;
}
.cite-unseen-dialog-rules .oo-ui-fieldsetLayout-group {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
}
.cite-unseen-dialog-url-row {
display: flex;
align-items: center;
margin-right: 55px;
}
.cite-unseen-dialog-url-row .oo-ui-dropdownWidget {
width: auto !important;
display: inline-block;
vertical-align: middle;
margin-right: 0;
}
.cite-unseen-dialog-url-row .oo-ui-textInputWidget {
max-width: 100%;
display: inline-block;
vertical-align: middle;
margin-left: 8px;
}
.cite-unseen-dialog-optional-params {
display: flex;
align-items: center;
flex-wrap: wrap;
flex-direction: row;
gap: 0 10px;
}
.cite-unseen-dialog-param-container {
display: flex;
align-items: center;
flex-grow: 1;
}
.cite-unseen-dialog-param-container .oo-ui-textInputWidget {
width: 10em;
vertical-align: middle;
margin-left: 8px;
flex-grow: 1;
}
.cite-unseen-dialog-remove-button {
display: block;
position: absolute;
top: 11px;
right: 11px;
}
.cite-unseen-dialog-add-button {
margin-top: 0;
}
`);
$('body').append(CiteUnseen.windowManager.$element);
}
CiteUnseen.windowManager.addWindows({'customCategoryDialog': CiteUnseen.customCategoryDialog});
CiteUnseen.windowManager.openWindow('customCategoryDialog');
},
/**
* 添加來源統計結果展板。
*/
showDashboard: function () {
if (CiteUnseen.refs.length === 0) {
// 沒有來源,不顯示展板。
return;
}
if (CiteUnseen.dashboard === null) {
CiteUnseen.dashboard = {
div: document.createElement('div'),
total: document.createElement('div'),
cats: document.createElement('div'),
flag: document.createElement('a'),
custom: false,
};
CiteUnseen.dashboard.div.style.border = '1px solid #ccc';
CiteUnseen.dashboard.div.style.marginBottom = '1em';
CiteUnseen.dashboard.div.style.borderRadius = '5px';
CiteUnseen.dashboard.div.style.fontSize = '.8em';
CiteUnseen.dashboard.div.style.display = 'flex';
CiteUnseen.dashboard.div.style.gap = '.5em 1em';
CiteUnseen.dashboard.div.style.flexWrap = 'wrap';
CiteUnseen.dashboard.div.style.padding = '5px';
CiteUnseen.dashboard.div.style.justifyContent = 'center';
CiteUnseen.dashboard.div.style.textAlign = 'center';
// 標記的來源總數
CiteUnseen.dashboard.total.innerText = "[Cite Unseen] 共 " + CiteUnseen.refs.length + CiteUnseen.convByVar({
hant: ' 個來源', hans: ' 个来源',
});
CiteUnseen.dashboard.total.style.fontWeight = 'bold';
CiteUnseen.dashboard.div.appendChild(CiteUnseen.dashboard.total);
// 各類型來源
CiteUnseen.dashboard.cats.style.display = 'contents';
CiteUnseen.dashboard.cats.style.textAlign = 'center';
CiteUnseen.dashboard.div.appendChild(CiteUnseen.dashboard.cats);
// 自訂來源分類按鈕
CiteUnseen.dashboard.flag.innerText = CiteUnseen.convByVar({hant: '自訂', hans: '自定义'});
CiteUnseen.dashboard.flag.style.cursor = 'pointer';
const flagIcon = document.createElement('img');
flagIcon.src = CiteUnseen.citeUnseenCategoryData['flag'].icon;
flagIcon.style.width = '17px';
flagIcon.style.height = '17px';
flagIcon.style.objectFit = 'contain';
flagIcon.style.cssText += 'max-width: 17px !important;';
flagIcon.style.marginRight = '5px';
flagIcon.style.verticalAlign = 'middle';
flagIcon.style.display = 'inline-block';
this.dashboard.flag.prepend(flagIcon);
CiteUnseen.dashboard.flag.onclick = function () {
CiteUnseen.showCustomCategoryDialog();
};
CiteUnseen.dashboard.div.appendChild(CiteUnseen.dashboard.flag);
mw.util.addCSS(`
.cite-unseen-highlight {
background-color: rgba(255, 246, 153, 0.5);
-webkit-box-decoration-break: clone;
box-decoration-break: clone;
}
`);
// 在 {{reflist}} 之前插入展板。若無 {{reflist}},則插入在末尾。
document.querySelector('#mw-content-text .mw-parser-output').insertBefore(CiteUnseen.dashboard.div, CiteUnseen.reflistNode);
}
// 清空各類型來源
CiteUnseen.dashboard.cats.innerHTML = '';
// 按順序列出各類型來源
let categoryTypes = CiteUnseen.citeUnseenChecklists.flatMap(x => x[0]);
categoryTypes = categoryTypes.concat(CiteUnseen.citeUnseenCategoryTypes.flatMap(x => x[1]));
categoryTypes.push('unknown');
for (let category of categoryTypes) {
let categoryData = CiteUnseen.citeUnseenCategoryData[category];
if (categoryData.count > 0) {
let countNode = document.createElement('div');
let countIcon = document.createElement('img');
countIcon.alt = CiteUnseen.convByVar({hant: categoryData.hint_hant, hans: categoryData.hint_hans});
countIcon.src = categoryData.icon;
countIcon.width = '17';
countIcon.style.maxHeight = '18px';
let countText = document.createElement('span');
countText.innerText = categoryData.count + ' ' + CiteUnseen.convByVar({
hant: categoryData.label_hant, hans: categoryData.label_hans,
});
countText.style.paddingLeft = '5px';
countText.style.cursor = 'pointer';
countText.onmouseover = function () {
countText.style.textDecoration = 'underline';
}
countText.onmouseout = function () {
countText.style.textDecoration = 'none';
}
countText.onclick = function () {
if (CiteUnseen.dashboardHighlighted === category) {
CiteUnseen.highlightCitation(null); // 取消高亮
} else {
CiteUnseen.highlightCitation(category);
}
}
countNode.appendChild(countIcon);
countNode.appendChild(countText);
CiteUnseen.dashboard.cats.appendChild(countNode);
}
}
},
/**
* 高亮顯示來源。
* @param category - 來源類別
* @param nodes - 節點列表。預設為 null,則使用來源類別中的節點。
*/
highlightCitation: function (category, nodes = null) {
if (CiteUnseen.dashboardHighlighted) {
// 清除之前的高亮
CiteUnseen.refCategories[CiteUnseen.dashboardHighlighted].forEach(function (node) {
node.classList.remove('cite-unseen-highlight');
});
}
CiteUnseen.dashboardHighlighted = category;
if (category) {
nodes = nodes || CiteUnseen.refCategories[category]; // 若未指定節點,則使用來源類別中的節點
nodes.forEach(function (node) {
node.classList.add('cite-unseen-highlight');
});
}
},
/**
* (helper function) 過濾 obj.filter === filterValue。
* @param obj - 要過濾的 object
* @param filter - 過濾的鍵
* @param filterValue - 過濾的值
* @returns {Object} - 過濾後的 object
*/
filterObjectIncludes: function (obj, filter, filterValue) {
return Object.keys(obj).reduce((acc, val) => (obj[val][filter] !== filterValue ? acc : {
...acc, [val]: obj[val],
}), {});
},
/**
* (helper function) 過濾 obj.filter !== filterValue。
* @param obj - 要過濾的 object
* @param filter - 過濾的鍵
* @param filterValue - 過濾的值
* @returns {Object} - 過濾後的 object
*/
filterObjectExcludes: function (obj, filter, filterValue) {
return Object.keys(obj).reduce((acc, val) => (obj[val][filter] === filterValue ? acc : {
...acc, [val]: obj[val],
}), {});
},
/**
* 從 User:<username>/CiteUnseen-Rules.js 導入使用者自訂規則。
*/
importCustomRules: async function () {
try {
await mw.loader.getScript('/w/index.php?title=User:' + encodeURIComponent(mw.config.get('wgUserName')) + '/CiteUnseen-Rules.js&ctype=text/javascript&action=raw');
} catch (err) {
console.log("[Cite Unseen] Error getting Cite Unseen custom rules: " + err.message);
return;
}
try {
// 以前用過的 config 名稱
if (!window.cite_unseen_categories && window.cite_unseen_rules) {
window.cite_unseen_categories = window.cite_unseen_rules;
}
if (!window.cite_unseen_categories && window.cite_unseen_ruleset) {
window.cite_unseen_categories = window.cite_unseen_ruleset;
}
// 獲取使用者的來源分類設定
if (window.cite_unseen_categories && typeof window.cite_unseen_categories === 'object') {
for (let key in window.cite_unseen_categories) {
if (key in CiteUnseen.citeUnseenCategories) {
CiteUnseen.citeUnseenCategories[key] = window.cite_unseen_categories[key];
} else if (key in [
"blacklisted",
"deprecated",
"generallyUnreliable",
"marginallyReliable",
"generallyReliable",
"multi",
]) {
for (let checklistTypeData of CiteUnseen.citeUnseenChecklists) {
if (checklistTypeData[0] === key) {
for (let checklist of checklistTypeData[1]) {
CiteUnseen.citeUnseenCategories[checklist] = window.cite_unseen_categories[key];
}
}
}
}
}
}
// 獲取使用者的網域忽略列表
if (window.cite_unseen_domain_ignore && typeof window.cite_unseen_domain_ignore === 'object') {
for (let key in window.cite_unseen_domain_ignore) {
if (window.cite_unseen_domain_ignore[key].length && key in CiteUnseen.citeUnseenDomainIgnore) {
CiteUnseen.citeUnseenDomainIgnore[key] = window.cite_unseen_domain_ignore[key];
}
}
}
// 獲取使用者的自訂網址列表
if (window.cite_unseen_additional_domains && typeof window.cite_unseen_additional_domains === 'object') {
for (let key in window.cite_unseen_additional_domains) {
if (window.cite_unseen_additional_domains[key].length && key in CiteUnseen.categorizedRules) {
CiteUnseen.categorizedRules[key] = CiteUnseen.categorizedRules[key].concat({
'url': window.cite_unseen_additional_domains[key],
});
}
}
}
// 獲取使用者的自訂網址字串列表
if (window.cite_unseen_additional_strings && typeof window.cite_unseen_additional_strings === 'object') {
for (let key in window.cite_unseen_additional_strings) {
if (window.cite_unseen_additional_strings[key].length && key in CiteUnseen.categorizedRules) {
CiteUnseen.categorizedRules[key] = CiteUnseen.categorizedRules[key].concat({
'url_str': window.cite_unseen_additional_strings[key],
});
}
}
}
// 是否顯示展板。預設顯示。
if (window.cite_unseen_dashboard === undefined) {
window.cite_unseen_dashboard = true;
}
} catch (err) {
console.log('[Cite Unseen] Could not read custom rules due to error: ', err);
}
},
/**
* 導入來源分類資料。
* @returns {Promise<Record<string, Object[]>>}
*/
importDependencies: async function () {
await mw.loader.using('ext.gadget.HanAssist', function (require) {
const {convByVar} = require('ext.gadget.HanAssist');
CiteUnseen.convByVar = convByVar;
});
await mw.loader.getScript('/w/index.php?title=User:SuperGrey/gadgets/CiteUnseen/sources.js&action=raw&ctype=text/javascript');
return await CiteUnseenData.getCategorizedRules();
},
/**
* 尋找所有 <cite> 標籤,並將其解析為 COinS object;找到 {{reflist}} 所在之處。
*/
findCitations: function () {
// 结构:
// <cite class="citation book">...</cite>
// <span title="...">...</span>
// 篩選所有 <cite> 標籤
for (let citeTag of document.querySelectorAll("cite")) {
let coinsTag = citeTag.nextElementSibling;
if (!coinsTag || coinsTag.tagName !== 'SPAN' || !coinsTag.hasAttribute('title')) {
continue;
}
let coinsString = decodeURIComponent(coinsTag.getAttribute('title'));
let coinsObject = CiteUnseen.parseCoinsString(coinsString);
if (!coinsObject['rft_id']) {
let aTag = citeTag.querySelector('a.external')
if (aTag) {
coinsObject['rft_id'] = aTag.getAttribute('href');
}
}
CiteUnseen.refs.push({
cite: citeTag, coins: coinsObject,
});
if (coinsObject['rft_id']) {
CiteUnseen.refLinks.push(coinsObject['rft_id']);
}
}
// 找到 {{reflist}} 所在之處。如有多個,則取最後一個。若無,則插入在末尾。
let reflists = document.querySelectorAll('#mw-content-text .mw-parser-output div.reflist');
if (reflists.length > 0) {
CiteUnseen.reflistNode = reflists[reflists.length - 1];
} else {
CiteUnseen.reflistNode = null; // 插入在末尾
}
},
/**
* 程式入口。
*/
init: function () {
// 開始計時。(在 addIcons() 結尾結束計時。)
console.time('CiteUnseen runtime');
// 導入來源分類資料
CiteUnseen.importDependencies().then(function (categorizedRules) {
CiteUnseen.categorizedRules = categorizedRules;
CiteUnseen.citeUnseenCategories = CiteUnseenData.citeUnseenCategories;
CiteUnseen.citeUnseenCategoryTypes = CiteUnseenData.citeUnseenCategoryTypes;
CiteUnseen.citeUnseenChecklists = CiteUnseenData.citeUnseenChecklists;
CiteUnseen.citeUnseenCategoryData = CiteUnseenData.citeUnseenCategoryData;
// 補上未完備的參數
for (let key of Object.keys(CiteUnseen.categorizedRules)) {
if (CiteUnseen.citeUnseenCategories[key] === undefined) {
CiteUnseen.citeUnseenCategories[key] = true;
}
}
// 導入使用者自訂規則
CiteUnseen.importCustomRules().then(function () {
// 導入完成後,開始處理來源分類。
CiteUnseen.findCitations();
CiteUnseen.addIcons();
});
});
},
categorizedRules: null, // 來源分類規則
citeUnseenCategories: null, // 預設來源分類開關
citeUnseenCategoryTypes: null, // 來源分類的類型
citeUnseenChecklists: null, // 來源檢查表按大類區分
citeUnseenCategoryData: null, // 使用到的分類資料、圖示和計數
citeUnseenDomainIgnore: {}, // 使用者自訂排除的網域
refs: [], // 所有參考文獻
refLinks: [], // 所有來源連結
refCategories: {}, // 所有來源對應的分類
reflistNode: null, // {{reflist}} 所在之處
convByVar: null, // 簡繁轉換
dashboard: null, // 展板
dashboardHighlighted: null, // 當前高亮的來源
CustomCategoryDialogRules: null, // 自訂來源分類對話框內容
customCategoryRules: [], // 自訂來源分類規則
customCategoryIcons: [], // 自訂來源分類圖示
windowManager: null, // Window manager for OOUI dialogs
};
CiteUnseen.init();