跳转到内容
主菜单
主菜单
移至侧栏
隐藏
导航
首页
分类索引
最近更改
随便看看
灵兰秘典
捐助本站
帮助
帮助
联系我们
关于本站
MediaWiki帮助
中医百科
搜索
搜索
外观
登录
个人工具
登录
查看“︁MediaWiki:Gadget-AdvancedSiteNotices.js”︁的源代码
系统消息
讨论
English
阅读
查看源代码
查看历史
工具
工具
移至侧栏
隐藏
操作
阅读
查看源代码
查看历史
清除缓存
常规
链入页面
相关更改
特殊页面
页面信息
Cargo数据
短URL
外观
移至侧栏
隐藏
←
MediaWiki:Gadget-AdvancedSiteNotices.js
因为以下原因,您没有权限编辑该页面:
您请求的操作仅限属于该用户组的用户执行:
用户
此页面为本wiki上的软件提供界面文本,并受到保护以防止滥用。 如欲修改所有wiki的翻译,请访问
translatewiki.net
上的MediaWiki本地化项目。
您无权编辑此JavaScript页面,因为编辑此页面可能会影响所有访问者。
您可以查看和复制此页面的源代码。
// <nowiki> /* * **************************************************************************** * * >>>>> 小工具导入者:导入前请阅读 <<<<< * * * 请确保通告页面 ([[Template:AdvancedSiteNotices/ajax]]) * * * 在您的维基上已受到适当保护。 * * * 虽然已尽力正确处理 JavaScript 表达式, * * * 但它们未经充分的实战测试。 * * **************************************************************************** * */ Promise.all([ $.ready ]).then(() => { const geo = null; // 明确设为 null,兼容后续逻辑 if ( $("#siteNotice").length < 0 || mw.config.get("wgAction") === "edit" || mw.config.get("wgAction") === "submit" ) { return; } const { conv } = require("ext.gadget.HanAssist"); let customASNInterval = window.customASNInterval || 15; const COOKIE_NAME = "dismissASN"; let cookieVal = Number.parseInt(mw.cookie.get(COOKIE_NAME) || "-1", 10); let revisionId = 0; let timeoutId = null; let $asnRoot = $("<div>", { id: "asn-dismissable-notice" }); let $asnBody = $("<div>", { id: "advancedSiteNotices", class: "mw-parser-output", }); let $asnClose = $("<button>", { title: conv({ hans: "关闭", hant: "關閉" }), "aria-label": conv({ hans: "关闭", hant: "關閉" }), class: "asn-close-button", }); $asnRoot.append($asnBody, $asnClose); $asnClose.click(() => { $asnClose.prop("disabled", true); mw.cookie.set(COOKIE_NAME, revisionId, { expires: 60 * 60 * 24 * 30, path: "/", secure: true, }); clearTimeout(timeoutId); $asnRoot.fadeOut(() => { $asnRoot.remove(); }); }); /** * @typedef {Object} TokenizeStatus * @property {string} expression * @property {RegExp} startTokenRe * @property {number} index * @property {((item: any) => void) & { __orig__?: (item: any) => void }} append */ /** * 条件解析器。仅支持 JavaScript 的一个小子集。 */ class CriteriaExecutor { /** * @param {Record<string, (...args: any[]) => any>} functions */ constructor(functions = {}) { this.functions = functions; } /** * 将表达式解析为标记 * * @param {string} expression * @return {any[]} */ _tokenizeExpression(expression) { expression = expression.trim(); if (!expression) { return []; } const result = []; /** @type {TokenizeStatus} */ const status = { expression, startTokenRe: /\|\||&&|\b(?:true|false)\b|[('"!\s]/g, index: 0, append(item) { result.push(item); }, }; let match = status.startTokenRe.exec(expression); const skipSpace = () => { while (/\s/.test(expression[status.index] || "")) { status.index++; } }; while (match) { const token = match[0]; if (status.startTokenRe.lastIndex - token.length !== status.index) { if (token === "(") { // 处理函数调用 const parsed = this._handleFunctionCall(status); if (!parsed) { break; } } else { break; } } else { let parsed = false; switch (token) { case "(": parsed = this._handleExpressionStatement(status); break; case "||": case "&&": parsed = this._handleLogicalOperator(status, token); break; case "!": parsed = this._handleUnaryExpression(status, token); break; case "'": case '"': parsed = this._handleStringLiteral(status, token); break; case "true": case "false": parsed = this._handleBooleanLiteral(status, token); break; } if (!parsed) { break; } } skipSpace(); status.startTokenRe.lastIndex = status.index; match = status.startTokenRe.exec(expression); } if (status.index < expression.length - 1) { throw new SyntaxError("Unexpected token."); } return result; } /** * 处理函数调用 * * @param {TokenizeStatus} status * @return {boolean} */ _handleFunctionCall(status) { const functionName = status.expression .slice(status.index, status.startTokenRe.lastIndex - 1) .trim(); if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(functionName)) { return false; } const argsStartIndex = status.startTokenRe.lastIndex - 1; const endIndex = this._findNextBalanceItem( status.expression, "(", ")", argsStartIndex ); if (endIndex === -1) { throw new SyntaxError("Unbalanced parentheses in function call"); } const rawArgs = status.expression .slice(argsStartIndex + 1, endIndex) .trim(); const args = !rawArgs.length ? [] : this._balanceSplit(rawArgs, ",", "(", ")"); status.append({ type: "FunctionCall", functionName, args: args.map((arg) => this._tokenizeExpression(arg)), }); status.index = endIndex + 1; return true; } /** * 处理逻辑运算符 * * @param {TokenizeStatus} status * @param {string} token * @return {boolean} */ _handleLogicalOperator(status, token) { status.append({ type: "LogicalExpression", operator: token, }); status.index = status.startTokenRe.lastIndex; return true; } /** * 处理一元表达式 * * @param {TokenizeStatus} status * @param {string} token * @return {boolean} */ _handleUnaryExpression(status, token) { const item = { type: "UnaryExpression", operator: token, argument: undefined, }; status.append(item); const origAppend = status.append.__orig__ || status.append; status.append = function (nextItem) { if (nextItem.type === "LogicalExpression") { throw new SyntaxError("Unexpected LogicalExpression"); } item.argument = nextItem; status.append = origAppend; }; status.append.__orig__ = origAppend; status.index = status.startTokenRe.lastIndex; return true; } /** * 处理布尔值字面量 * * @param {TokenizeStatus} status * @param {string} token * @return {boolean} */ _handleBooleanLiteral(status, token) { status.append({ type: "BooleanLiteral", value: token === "true", }); status.index = status.startTokenRe.lastIndex; return true; } /** * 处理表达式语句 * * @param {TokenizeStatus} status * @return {boolean} */ _handleExpressionStatement(status) { const endIndex = this._findNextBalanceItem( status.expression, "(", ")", status.index ); if (endIndex === -1) { throw new SyntaxError("Unbalanced parentheses in expression."); } status.append({ type: "ExpressionStatement", expression: this._tokenizeExpression( status.expression.slice(status.index + 1, endIndex) ), }); status.index = endIndex + 1; return true; } /** * 处理字符串字面量 * * @param {TokenizeStatus} status * @param {string} delimiter * @return {boolean} */ _handleStringLiteral(status, delimiter) { const endIndex = this._findNextTokenWithoutEscape( status.expression, delimiter, status.index + 1 ); if (endIndex === -1) { throw new SyntaxError("Unterminated string literal."); } status.append({ type: "StringLiteral", value: this._parseString( status.expression.slice(status.index + 1, endIndex) ), }); status.index = endIndex + 1; return true; } /** * 查找下一个平衡项 * * @param {string} expression * @param {string} startToken * @param {string} endToken * @param {number} currentIndex * @return {number} */ _findNextBalanceItem(expression, startToken, endToken, currentIndex) { let count = 1; while (++currentIndex < expression.length) { if (expression[currentIndex] === startToken) { count++; } else if (expression[currentIndex] === endToken) { count--; } if (count === 0) { return currentIndex; } } return -1; } /** * 分割平衡子表达式 * * @param {string} expression * @param {string} startToken * @param {string} endToken * @param {string} splitToken * @return {string[]} */ _balanceSplit(expression, splitToken, startToken, endToken) { const result = []; let balance = 0; let current = ""; for (const char of expression) { if (char === startToken) { balance++; } else if (char === endToken) { balance--; } if (char === splitToken && balance === 0) { result.push(current.trim()); current = ""; } else { current += char; } } if (current) { result.push(current.trim()); } return result; } /** * 查找未转义的标记 * * @param {string} expression * @param {string} token * @param {number} currentIndex * @return {number} */ _findNextTokenWithoutEscape(expression, token, currentIndex) { while (currentIndex < expression.length) { const foundIndex = expression.indexOf(token, currentIndex); if (foundIndex === -1) { return -1; } else if (expression[foundIndex - 1] !== "\\") { return foundIndex; } currentIndex = foundIndex + 1; } return -1; } /** * 解析字符串并处理转义字符 * * @param {string} input * @return {string} */ _parseString(input) { return input.replace( /\\(n|t|r|b|f|x[0-9A-Fa-f]{2}|u\{[0-9A-Fa-f]+\}|u[0-9A-Fa-f]{4}|.)/g, (_, esc) => { switch (esc[0]) { case "n": return "\n"; case "t": return "\t"; case "r": return "\r"; case "b": return "\b"; case "f": return "\f"; case "x": if (esc === "x") { throw new SyntaxError("Invalid hexadecimal escape sequence."); } return String.fromCharCode(parseInt(esc.slice(1), 16)); case "u": if (esc === "u") { throw new SyntaxError("Invalid Unicode escape sequence."); } else if (esc[1] === "{") { const codePoint = Number.parseInt(esc.slice(2, -1), 16); if (codePoint > 0x10ffff) { throw new SyntaxError( `Undefined Unicode code-point: \\${esc}.` ); } return String.fromCodePoint(codePoint); } return String.fromCharCode(Number.parseInt(esc.slice(1), 16)); default: return esc; } } ); } /** * @param {any[]} tokens */ _tokensToAst(tokens) { if (!tokens.length) { throw new TypeError("Token list is empty."); } // 先處理 && 再處理 || let logicalItemIndex = tokens.findIndex((t) => t.type === "LogicalExpression" && t.operator === '&&'); if (logicalItemIndex === -1) { logicalItemIndex = tokens.findIndex((t) => t.type === "LogicalExpression" && t.operator === '||'); } if (logicalItemIndex === -1) { if (tokens.length === 1) { return this._tokenToAst(tokens[0]); } throw new SyntaxError(`Unexpected ${tokens[1].type}.`); } else if (logicalItemIndex === 0) { throw new SyntaxError(`Unexpected LogicalExpression`); } else { const left = this._tokensToAst(tokens.slice(0, logicalItemIndex)); const right = this._tokensToAst(tokens.slice(logicalItemIndex + 1)); return { type: "LogicalExpression", operator: tokens[logicalItemIndex].operator, left, right, }; } } /** * @param {any} token */ _tokenToAst(token) { if (!token) { throw new TypeError('token is undefined or null.'); } const result = Object.assign({}, token); if (token.type === "UnaryExpression") { if (!result.argument) { throw new SyntaxError("Unexpected UnaryExpression."); } result.argument = this._tokenToAst(token.argument); } else if (token.type === "FunctionCall") { result.args = token.args.map((argument) => this._tokensToAst(argument) ); } else if (token.type === "ExpressionStatement") { result.expression = this._tokensToAst(token.expression); } return result; } /** * @param {string} expression */ toAst(expression) { const tokenizes = this._tokenizeExpression(expression); return this._tokensToAst(tokenizes); } /** * @param {string} functionName * @param {any[]} args */ _executeFunction(functionName, args) { if (!Object.prototype.hasOwnProperty.call(this.functions, functionName)) { throw new Error(`Function ${functionName} is not allowed.`); } return this.functions[functionName](...args); } /** * @param {any} ast */ evaluate(ast) { switch (ast.type) { case "ExpressionStatement": return this.evaluate(ast.expression); case "UnaryExpression": if (ast.operator === "!") { return !this.evaluate(ast.argument); } throw new Error(`Unknown UnaryExpression operator: ${ast.operator}`); case "LogicalExpression": if (ast.operator === "&&" || ast.operator === "||") { const left = this.evaluate(ast.left); if (ast.operator === "&&" && !left) { return false; } else if (ast.operator === "||" && left) { return true; } return this.evaluate(ast.right); } throw new Error(`Unknown LogicalExpression operator: ${ast.operator}`); case "FunctionCall": const args = ast.args.map((arg) => this.evaluate(arg)); let returnValue = this._executeFunction(ast.functionName, args); if (typeof returnValue === 'undefined' || returnValue === null) { returnValue = false; } else if (typeof returnValue !== 'string' && typeof returnValue !== 'boolean') { console.warn( '[AdvancedSiteNotices]: The return type %s of the function %s() is unsafe and has been forcibly converted to a string.', typeof returnValue, ast.functionName ); returnValue = String(returnValue); } return returnValue; case "StringLiteral": case "BooleanLiteral": return ast.value; default: throw new Error(`Unknown AST node type: ${ast.type}`); } } } const functions = {}; // 帶參數 if (geo) { functions.in_country = (...counties) => counties.includes(geo.country); functions.in_region = (...regions) => regions.includes(geo.region); functions.in_city = (...cities) => cities.includes(geo.city); } else { // 无 geoIP 时,跳过地区筛选(或改为 return false 禁用) functions.in_country = functions.in_region = functions.in_city = (...args) => args.length ? true : false; } const configs = mw.config.get(["wgUserGroups", "wgUserLanguage"]); functions.in_group = (...groups) => groups.some(group => configs.wgUserGroups.includes(group)); functions.in_group_every = (...groups) => groups.every(group => configs.wgUserGroups.includes(group)); functions.in_lang = (...useLangs) => useLangs.includes(configs.wgUserLanguage); // 不帶參數 // 錯誤示範: // functions.is_anon = mw.user.isAnon; // 不安全,無法確定 mw.user.isAnon 是否有被修改過可以傳奇怪的值進去 // functions.is_anon = () => mw.user.isAnon(); // 不安全,返回值為 boolean 的,只要無法保證一定返回 boolean,就應該全部強制轉換成 boolean functions.is_anon = () => !!mw.user.isAnon(); // 支持较旧的 MediaWiki 版本(通常是其他维基) // mw.user.isTemp 和 mw.user.isNamed 在 MediaWiki 1.40 中添加 functions.is_temp = () => typeof mw.user.isTemp === "function" ? !!mw.user.isTemp() : false; functions.is_named = () => typeof mw.user.isNamed === "function" ? !!mw.user.isNamed() : !mw.user.isAnon(); const parser = new CriteriaExecutor(functions); const cache = new WeakMap(); function getCache($element, key) { const element = $element.get(0); if (cache.has(element)) { return cache.get(element)[key]; } } function setCache($element, key, value) { const element = $element.get(0); if (cache.has(element)) { cache.get(element)[key] = value; } else { cache.set(element, { [key]: value, }); } } function matchCriteria($noticeItem) { let cache = getCache($noticeItem, "asn-cache"); if (cache !== undefined) { return cache; } let criteria = $noticeItem.attr("data-asn-criteria"); let result; if (criteria !== undefined) { if (criteria === "") { result = true; } else { try { criteria = decodeURIComponent(criteria.replace(/\+/g, "%20")).trim(); const ast = parser.toAst(criteria); result = !!parser.evaluate(ast); } catch (error) { console.warn( '[AdvancedSiteNotices]: Fail to parse or evaluate criteria "%s":', criteria, error ); result = false; } } } else { const testList = []; if ($noticeItem.hasClass("only_sysop")) { testList.push(functions.in_group("sysop")); } if ( $noticeItem.hasClass("only_logged_in") || $noticeItem.hasClass("only_logged") /* 已弃用 */ || $noticeItem.hasClass("is_named") ) { testList.push(functions.is_named()); } if ( $noticeItem.hasClass("only_logged_out") || $noticeItem.hasClass("only_anon") /* 已弃用 */ ) { testList.push(!functions.is_named()); } if ($noticeItem.hasClass("is_temp")) { testList.push(functions.is_temp()); } if ($noticeItem.hasClass("is_anon")) { testList.push(functions.is_anon()); } if ($noticeItem.hasClass("only_zh_cn")) { testList.push(functions.in_lang("zh-cn")); } if ($noticeItem.hasClass("only_zh_hk")) { testList.push(functions.in_lang("zh-hk")); } if ($noticeItem.hasClass("only_zh_sg")) { testList.push(functions.in_lang("zh-sg")); } if ($noticeItem.hasClass("only_zh_tw")) { testList.push(functions.in_lang("zh-tw")); } result = !testList.length || testList.every((v) => !!v); } setCache($noticeItem, "asn-cache", result); return result; } function getNoticeElement($noticeItem) { let $cache = getCache($noticeItem, "asn-element"); if ($cache !== undefined) { return $cache; } $cache = $("<div>").append($noticeItem.contents().clone()); setCache($noticeItem, "asn-element", $cache); return $cache; } let isSetMinHeightCalled = false; /** * 设置 ASN 的高度为所有条目的最大高度,以防止在切换时出现大量布局偏移。 */ function setMinHeight($noticeList) { let minHeight = -1; $noticeList.each((_, nt) => { let $nt = $(nt); $asnBody.replaceWith($nt); minHeight = Math.max(minHeight, $nt.height()); $nt.replaceWith($asnBody); }); $asnRoot.css("min-height", `${minHeight}px`); if (!isSetMinHeightCalled) { isSetMinHeightCalled = true; window.addEventListener( "resize", mw.util.debounce(() => setMinHeight($noticeList), 300) ); } } function loadNotice($noticeList, pos) { const $noticeItem = $noticeList.eq(pos); let nextPos = pos + 1; if (nextPos === $noticeList.length) { nextPos = 0; } const $noticeElement = getNoticeElement($noticeItem); if ($asnBody.children().length) { $asnBody.stop().fadeOut(() => { $asnBody.empty().append($noticeElement); // 动画 try/catch 以避免 TypeError: (Animation.tweeners[prop]||[]).concat is not a function 错误在生产环境中出现 try { $asnBody.fadeIn(); } catch (_) { } }); } else { $asnBody.append($noticeElement).fadeIn(); } if ($noticeList.length > 1) { timeoutId = setTimeout(() => { loadNotice($noticeList, nextPos); }, customASNInterval * 1000); } } function initialNotices($noticeList) { if (!$asnRoot.length || !$noticeList.length || revisionId === cookieVal) { return; } mw.cookie.set(COOKIE_NAME, null); $asnRoot.appendTo($("#siteNotice")); setMinHeight($noticeList); loadNotice($noticeList, Math.floor(Math.random() * $noticeList.length)); } new mw.Api({ ajax: { headers: { "Api-User-Agent": "w:zh:MediaWiki:Gadget-AdvancedSiteNotices.js", }, }, }) .parse(new mw.Title("Template:AdvancedSiteNotices/ajax"), { variant: mw.config.get("wgUserVariant"), maxage: 3600, smaxage: 3600, }) .then((html) => { let $json = $("ul.sitents", $.parseHTML(html)); let $noticeList = $("li", $json).filter((_, li) => matchCriteria($(li))); revisionId = $json.data("asn-version"); initialNotices($noticeList); }) .catch((e) => { console.error("[AdvancedSiteNotices]: error ", e); }); }); // </nowiki>
返回
MediaWiki:Gadget-AdvancedSiteNotices.js
。