// ==UserScript== // @name SPXXMB // @description Minecraft.net & X.com blog article to BBCode converter, adapted to MineBBS // @namespace npmjs.com/package/@spxxmb/userscript // @author Cinder & SPGoding & SPX Fellow // @connect * // @connect feedback.minecraft.com // @connect help.minecraft.net // @connect raw.githubusercontent.com // @homepage https://github.com/cinder0601/SPXXMB // @match https://www.minecraft.net/en-us/article/* // @match https://www.minecraft.net/zh-hans/article/* // @match https://x.com/*/status/* // @match https://feedback.minecraft.net/hc/en-us/articles/* // @match https://help.minecraft.net/hc/en-us/articles/* // @require https://fastly.jsdelivr.net/gh/sizzlemctwizzle/GM_config@2207c5c1322ebb56e401f03c2e581719f909762a/gm_config.js // @icon https://www.minecraft.net/etc.clientlibs/minecraft/clientlibs/main/resources/favicon.ico // @version 1.0.0 // @grant GM_getValue // @grant GM_setValue // @grant GM_setClipboard // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @license MIT // @downloadURL none // ==/UserScript== (function () { 'use strict'; var GM_config = new GM_configStruct(); GM_config.init({ id: 'spxxmb', title: 'SPXXMB 用户脚本', fields: { translator: { label: '译者名', type: 'text', default: '<默认译者>' }, bugSource: { label: '选择翻译源', type: 'select', options: ['Github', '自定义'], default: 'Github' }, bugCenterTranslation: { label: '漏洞翻译源', type: 'text', default: 'https://raw.githubusercontent.com/SPXFellow/spxx-translation-database/crowdin/zh-CN/zh_CN.json' }, bugCenterTranslator: { label: '漏洞译者源', type: 'text', default: 'https://raw.githubusercontent.com/SPXFellow/spxx-translation-database/master/translator.json' }, bugCenterColor: { label: '漏洞颜色源', type: 'text', default: 'https://raw.githubusercontent.com/SPXFellow/spxx-translation-database/master/color.json' } } }); GM_registerMenuCommand('编辑配置', () => GM_config.open()); const src = GM_config.get('bugSource'); let tr = ""; let tor = ""; let c = ""; if (src == "Github") { console.log("[SPXXMB] 正在使用 Github 漏洞中心"); tr = 'https://raw.githubusercontent.com/SPXFellow/spxx-translation-database/crowdin/zh-CN/zh_CN.json'; tor = 'https://raw.githubusercontent.com/SPXFellow/spxx-translation-database/master/translator.json'; c = 'https://raw.githubusercontent.com/SPXFellow/spxx-translation-database/master/color.json'; }else if (src == "自定义") { console.log("[SPXXMB] 正在使用自定义漏洞中心"); tr = GM_config.get('bugCenterTranslation'); tor = GM_config.get('bugCenterTranslator'); c = GM_config.get('bugCenterColor'); } const config = { translator: GM_config.get('translator'), bugCenter: { translation: tr, translator: tor, color: c } }; var version = "1.0.0"; function getVersionType(url) { const lowerUrl = url.toLowerCase(); if (lowerUrl.includes('snapshot')) { return VersionType.Snapshot; } else if (lowerUrl.includes('pre-release')) { return VersionType.PreRelease; } else if (lowerUrl.includes('release-candidate')) { return VersionType.ReleaseCandidate; } else if (lowerUrl.includes('minecraft-java-edition') && !lowerUrl.includes('snapshot')) { return VersionType.Release; } else if (lowerUrl.includes('minecraft-preview') || lowerUrl.includes('minecraft-beta-preview') || lowerUrl.includes('minecraft-beta')) { return VersionType.BedrockBeta; } else if (lowerUrl.includes('bedrock')) { return VersionType.BedrockRelease; } else { return VersionType.Normal; } } const bugsCenter = config.bugCenter.translation; const bugsTranslatorsTable = config.bugCenter.translator; const translatorColorTable = config.bugCenter.color; const spxxmbVersion = version; const url1 = window.location.href; function getVersionCode(url){ const lowerUrl = url.toLowerCase(); if (lowerUrl.includes('snapshot')) { const versionRegex = /-([0-9a-zA-Z]+)$/; const match = url.match(versionRegex); if (match && match[1]) { return match[1]; } } else if (lowerUrl.includes('minecraft-beta-preview')) { const versionRegex = /\-beta-preview\-(\d+)\-(\d+)\-(\d+)\-(\d+)/; const match = lowerUrl.match(versionRegex); if (match && match.length >= 5) { const Version1 = match[1]; const Version2 = match[2]; const Version3 = match[3]; const Version4 = match[4]; const formattedVersion = `${Version1}.${Version2}.${Version3}.${Version4}`; return formattedVersion; } } else if (lowerUrl.includes('minecraft-preview') && !lowerUrl.includes('beta')) { const versionRegex = /\-preview\-(\d+)\-(\d+)\-(\d+)\-(\d+)/; const match = lowerUrl.match(versionRegex); if (match && match.length >= 5) { const Version1 = match[1]; const Version2 = match[2]; const Version3 = match[3]; const Version4 = match[4]; const formattedVersion = `${Version1}.${Version2}.${Version3}.${Version4}`; return formattedVersion; } } else if (lowerUrl.includes('minecraft-beta') && !lowerUrl.includes('preview')) { const versionRegex = /\-beta\-(\d+)\-(\d+)\-(\d+)\-(\d+)/; const match = lowerUrl.match(versionRegex); if (match && match.length >= 5) { const Version1 = match[1]; const Version2 = match[2]; const Version3 = match[3]; const Version4 = match[4]; const formattedVersion = `${Version1}.${Version2}.${Version3}.${Version4}`; return formattedVersion; } } } let versioncode = getVersionCode(url1); function getHeader(articleType, type) { if (articleType.toLowerCase() !== 'news') { return `[left][color=#388e3c][size=5]|[/size][/color][size=4]本文内容按照 [/size][url=https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh-hans][size=4][color=#2e8b57][u]CC BY-NC-SA 4.0[/u][/color][/size][/url][size=4] 协议进行授权,[b]转载本帖时须注明[color=#ff0000]原作者[/color]以及[color=#ff0000]本帖地址[/color][/b]。[/size][/left][hr]\n`; } switch (type) { case VersionType.Snapshot: return `[color=#388e3c][size=5]|[/size][/color][size=4][b]Minecraft Java 版[/b]是指 Windows、Mac OS 与 Linux 平台上,使用 Java 语言开发的 Minecraft 版本。[/size] [color=#388e3c][size=5]|[/size][/color][size=4][b]每周快照[/b]是 Minecraft Java 版的测试机制,主要用于下一个正式版的特性预览。[/size] [color=#f44336][size=5]|[/size][/color][size=4]然而,[b]每周快照[/b]主要用于新特性展示,通常存在大量漏洞。因此对于普通玩家建议仅做[color=Red][b]测试尝鲜[/b][/color]用。在快照中打开存档前请务必[color=Red][b]进行备份[/b][/color]。[b]适用于正式版的 Mod 不兼容快照,且大多数 Mod 都不对每周快照提供支持[/b]。 [/size] [color=#f44336][size=5]|[/size][/color][size=4]Minecraft Java 版 <正式版版本号> 仍未发布,${versioncode} 为其第 <计数> 个预览版。[/size] [color=#388e3c][size=5]|[/size][/color][size=4]本文内容按照 [/size][url=https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh-hans][size=4][color=#2e8b57][u]CC BY-NC-SA 4.0[/u][/color][/size][/url][size=4] 协议进行授权,[b]转载本帖时须注明[color=#ff0000]原作者[/color]以及[color=#ff0000]本帖地址[/color][/b]。[/size][hr]\n`; case VersionType.PreRelease: return `[color=#388e3c][size=5]|[/size][/color][size=4][b]Minecraft Java 版[/b]是指 Windows、Mac OS 与 Linux 平台上,使用 Java 语言开发的 Minecraft 版本。[/size] [color=#388e3c][size=5]|[/size][/color][size=4][b]预发布版[/b]是 Minecraft Java 版的测试机制,如果该版本作为正式版发布,那么预发布版的游戏文件将与启动器推送的正式版完全相同。[/size] [color=#f44336][size=5]|[/size][/color][size=4]然而,预发布版主要用于服主和 Mod 制作者的预先体验,如果发现重大漏洞,该预发布版会被新的预发布版代替。因此建议普通玩家[color=Red]持观望态度[/color]。 [/size] [color=#f44336][size=5]|[/size][/color][size=4]Minecraft Java 版 <正式版版本号> 仍未发布,<当前版本号> 为其第 <计数> 个预览版。[/size] [color=#388e3c][size=5]|[/size][/color][size=4]本文内容按照 [/size][url=https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh-hans][size=4][color=#2e8b57][u]CC BY-NC-SA 4.0[/u][/color][/size][/url][size=4] 协议进行授权,[b]转载本帖时须注明[color=#ff0000]原作者[/color]以及[color=#ff0000]本帖地址[/color][/b]。[/size][hr]\n`; case VersionType.ReleaseCandidate: return `[color=#388e3c][size=5]|[/size][/color][size=4][b]Minecraft Java 版[/b]是指运行在 Windows、Mac OS 与 Linux 平台上,使用 Java 语言开发的 Minecraft 版本。[/size] [color=#388e3c][size=5]|[/size][/color][size=4][b]候选版[/b]是 Minecraft Java 版正式版的候选版本,如果发现重大漏洞,该候选版会被新的候选版代替。如果一切正常,该版本将会作为正式版发布。[/size] [color=#f44336][size=5]|[/size][/color][size=4]候选版已可供普通玩家进行抢鲜体验,但仍需当心可能存在的漏洞。[/size] [color=#f44336][size=5]|[/size][/color][size=4]Minecraft Java 版 <正式版版本号> 仍未发布,<当前版本号> 为其第 <计数> 个预览版。[/size] [color=#388e3c][size=5]|[/size][/color][size=4]本文内容按照 [/size][url=https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh-hans][size=4][color=#2e8b57][u]CC BY-NC-SA 4.0[/u][/color][/size][/url][size=4] 协议进行授权,[b]转载本帖时须注明[color=#ff0000]原作者[/color]以及[color=#ff0000]本帖地址[/color][/b]。[/size][hr]\n`; case VersionType.Release: return `[color=#388e3c][size=5]|[/size][/color][size=4][b]Minecraft Java 版[/b]是指运行在 Windows、Mac OS 与 Linux 平台上,使用 Java 语言开发的 Minecraft 版本。[/size] [color=#f44336][size=5]|[/size][/color][size=4][b]正式版[/b]是 Minecraft Java 版经过一段时间的预览版测试后得到的稳定版本,也是众多纹理、Mod 与服务器插件会逐渐跟进的版本。官方启动器也会第一时间进行推送。 [/size] [color=#f44336][size=5]|[/size][/color][size=4]建议玩家与服主关注其相关服务端、Mod 与插件的更新,迎接新的正式版吧!专注于单人原版游戏的玩家可立即更新,多人游戏玩家请关注您所在服务器的通知。[/size] [color=#388e3c][size=5]|[/size][/color][size=4]本文内容按照 [/size][url=https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh-hans][size=4][color=#2e8b57][u]CC BY-NC-SA 4.0[/u][/color][/size][/url][size=4] 协议进行授权,[b]转载本帖时须注明[color=#ff0000]原作者[/color]以及[color=#ff0000]本帖地址[/color][/b]。[/size][hr]\n`; case VersionType.BedrockRelease: return `[color=#388e3c][size=5]|[/size][/color][size=4][b]Minecraft 基岩版[/b]是指运行在移动平台(Android、iOS)、Windows 10/11、主机(Xbox One、Switch、PlayStation 4/5)上,使用「基岩引擎」(C++语言)开发的 Minecraft 版本。[/size] [color=#f44336][size=5]|[/size][/color][size=4][b]正式版[/b]是 Minecraft 基岩版经过一段时间的测试版测试之后得到的稳定版本,也是众多纹理、附加包和 Realms 会逐渐跟进的版本。与此同时 Google Play、Microsoft Store 等官方软件商店也会推送此次更新。 [/size] [color=#388e3c][size=5]|[/size][/color][size=4]本文内容按照 [/size][url=https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh-hans][size=4][color=#2e8b57][u]CC BY-NC-SA 4.0[/u][/color][/size][/url][size=4] 协议进行授权,[b]转载本帖时须注明[color=#ff0000]原作者[/color]以及[color=#ff0000]本帖地址[/color][/b]。[/size][hr]\n`; case VersionType.BedrockBeta: return `[color=#388e3c][size=5]|[/size][/color][size=4][b]Minecraft 基岩版[/b]是指运行在移动平台(Android、iOS)、Windows 10/11、主机(Xbox One、Switch、PlayStation 4/5)上,使用「基岩引擎」(C++语言)开发的 Minecraft 版本。[/size] [color=#388e3c][size=5]|[/size][/color][size=4][b]测试版[/b]是 Minecraft 基岩版的测试机制,主要用于下一个正式版的特性预览。[/size] [color=#f44336][size=5]|[/size][/color][size=4][b]然而,测试版主要用于新特性展示,通常存在大量漏洞。因此对于普通玩家建议仅做测试尝鲜用。使用测试版打开存档前请务必备份。适用于正式版的领域服务器与测试版不兼容。[/b] [/size] [color=#f44336][size=5]|[/size][/color][size=4]如果在测试版中遇到旧版存档无法使用的问题,测试版将允许你将存档上传以供开发团队查找问题。[/size] [color=#f44336][size=5]|[/size][/color][size=4]Minecraft 基岩版 <正式版版本号> 仍未发布,Beta & Preview ${versioncode} 为其第 <计数> 个测试版。[/size] [color=#388e3c][size=5]|[/size][/color][size=4]本文内容按照 [/size][url=https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh-hans][size=4][color=#2e8b57][u]CC BY-NC-SA 4.0[/u][/color][/size][/url][size=4] 协议进行授权,[b]转载本帖时须注明[color=#ff0000]原作者[/color]以及[color=#ff0000]本帖地址[/color][/b]。[/size][hr]\n`; case VersionType.Normal: default: return `[color=#388e3c][size=5]|[/size][/color][size=4]本文内容按照 [/size][url=https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh-hans][size=4][color=#2e8b57][u]CC BY-NC-SA 4.0[/u][/color][/size][/url][size=4] 协议进行授权,[b]转载本帖时须注明[color=#ff0000]原作者[/color]以及[color=#ff0000]本帖地址[/color][/b]。[/size][hr]\n`; } } function getFooter(articleType, type) { const time = new Date(); function padTime(time) { return time.toString().padStart(2, '0'); } function toHoursAndMinutes(totalMinutes) { const m = Math.abs(totalMinutes); const minutes = m % 60; const hours = Math.floor(m / 60); return `${totalMinutes < 0 ? '+' : '-'}${padTime(hours)}${padTime(minutes)}`; } const poweredBy = `\n[center][size=1][color=Silver]Powered by SPXXMB ${spxxmbVersion} with love Converted at ${time.getFullYear()}-${padTime(time.getMonth() + 1) // why +1 javascript }-${padTime(time.getDate())} ${padTime(time.getHours())}:${padTime(time.getMinutes())} ${toHoursAndMinutes(time.getTimezoneOffset())}[/color][/size][/center]`; /*Same contents,change if necessary.**/ switch (type) { case VersionType.Snapshot: return `\n${poweredBy}\n[hr][color=#388e3c][size=5]|[/size][/color][size=4][b]想了解更多游戏资讯?[/b][/size][list][*][size=3][url=https://www.minebbs.com/forums/news/][color=#388e3c][u]MineBBS - 新闻资讯版块[/u][/color][/url][/size][/list]`; case VersionType.PreRelease: return `\n${poweredBy}\n[hr][color=#388e3c][size=5]|[/size][/color][size=4][b]想了解更多游戏资讯?[/b][/size][list][*][size=3][url=https://www.minebbs.com/forums/news/][color=#388e3c][u]MineBBS - 新闻资讯版块[/u][/color][/url][/size][/list]`; case VersionType.ReleaseCandidate: return `\n${poweredBy}\n[hr][color=#388e3c][size=5]|[/size][/color][size=4][b]想了解更多游戏资讯?[/b][/size][list][*][size=3][url=https://www.minebbs.com/forums/news/][color=#388e3c][u]MineBBS - 新闻资讯版块[/u][/color][/url][/size][/list]`; case VersionType.Release: return `\n${poweredBy}\n[hr][color=#388e3c][size=5]|[/size][/color][size=4][b]想了解更多游戏资讯?[/b][/size][list][*][size=3][url=https://www.minebbs.com/forums/news/][color=#388e3c][u]MineBBS - 新闻资讯版块[/u][/color][/url][/size][/list]`; case VersionType.BedrockRelease: return `\n${poweredBy}\n[hr][color=#388e3c][size=5]|[/size][/color][size=4][b]想了解更多游戏资讯?[/b][/size][list][*][size=3][url=https://www.minebbs.com/forums/news/][color=#388e3c][u]MineBBS - 新闻资讯版块[/u][/color][/url][/size][/list]`; case VersionType.BedrockBeta: return `\n${poweredBy}\n[hr][color=#388e3c][size=5]|[/size][/color][size=4][b]想了解更多游戏资讯?[/b][/size][list][*][size=3][url=https://www.minebbs.com/forums/news/][color=#388e3c][u]MineBBS - 新闻资讯版块[/u][/color][/url][/size][/list]`; case VersionType.Normal: default: return `\n${poweredBy}\n[hr][color=#388e3c][size=5]|[/size][/color][size=4][b]想了解更多游戏资讯?[/b][/size][list][*][size=3][url=https://www.minebbs.com/forums/news/][color=#388e3c][u]MineBBS - 新闻资讯版块[/u][/color][/url][/size][/list]`; } } let VersionType; (function (VersionType) { VersionType[VersionType["Snapshot"] = 1] = "Snapshot"; VersionType[VersionType["PreRelease"] = 2] = "PreRelease"; VersionType[VersionType["ReleaseCandidate"] = 3] = "ReleaseCandidate"; VersionType[VersionType["Release"] = 4] = "Release"; VersionType[VersionType["Normal"] = 5] = "Normal"; VersionType[VersionType["BedrockBeta"] = 6] = "BedrockBeta"; VersionType[VersionType["BedrockRelease"] = 7] = "BedrockRelease"; })(VersionType || (VersionType = {})); const translators = { headings: (input, ctx) => { return translator(input, ctx, [// Minecraft.net titles [/Block of the Week: /gi, '本周方块:'], [/Taking Inventory: /gi, '背包盘点:'], [/Around the Block: /gi, '群系漫游:'], [/A Minecraft Java Snapshot/gi, 'Minecraft Java版 快照'], [/A Minecraft Java Pre-Release/gi, 'Minecraft Java版 预发布版'], [/A Minecraft Java Release Candidate/gi, 'Minecraft Java版 候选版本'], // Bedrock Edition titles [/Minecraft Beta (?:-|——) (.*?) \((.*?)\)/gi, 'Minecraft 基岩版 Beta $1($2)'], [/Minecraft Beta & Preview - (.*?)/g, 'Minecraft 基岩版 Beta & Preview $1'], [/Minecraft (?:-|——) (.*?) \(Bedrock\)/gi, 'Minecraft 基岩版 $1'], [/Minecraft (?:-|——) (.*?) \((.*?) Only\)/gi, 'Minecraft 基岩版 $1(仅$2)'], [/Minecraft (?:-|——) (.*?) \((.*?)\)/gi, 'Minecraft 基岩版 $1(仅$2)'], // BE subheadings [/Marketplace/gi, '市场'], [/Data-Driven/gi, '数据驱动'], [/Graphical/gi, '图像'], [/Player/gi, '玩家'], [/Experimental Features/gi, '实验性特性'], [/Mobs/gi, '生物'], [/Features and Bug Fixes/gi, '特性和漏洞修复'], [/ADVANCEMENTS/gi, '进度'], [/Accessibility/gi, '辅助功能'], [/Gameplay/gi, '玩法'], [/Items/gi, '物品'], [/Blocks/gi, '方块'], [/User Interface/gi, '用户界面'], [/Commands/gi, '命令'], [/Known Issues/gi, '已知问题'], [/Character Creator/gi, '角色创建器'], [/Components/gi, '组件'], [/General/gi, '通用'], [/Technical Experimental Updates/gi, '实验性技术性更新'], [/Gametest Framework/gi, 'Gametest 框架'], [/Gametest Framework (experimental)/gi, 'Gametest 框架(实验性)'], // JE subheadings [/Minecraft Snapshot /gi, 'Minecraft 快照 '], [/ Pre-Release /gi, '-pre'], [/ Release Candidate /gi, '-rc'], [/Release Candidate/gi, '候选版本'], [/New Features in ([^\r\n]+)/gi, '$1 的新增特性'], [/Technical changes in ([^\r\n]+)/gi, '$1 的技术性修改'], [/Changes in ([^\r\n]+)/gi, '$1 的修改内容'], [/Fixed bugs in ([^\r\n]+)/gi, '$1 修复的漏洞'], [/STABILITY AND PERFORMANCE/gi, '性能与稳定性'], [/FEATURES AND BUG FIXES/gi, '特性和漏洞修复'],[/LOOT/gi, '战利品'], [/PARITY/gi, '趋同'], [/ADD-ONS AND SCRIPT ENGINE/gi, '附加包和脚本引擎'], [/DRESSING ROOM/gi, '更衣室'], [/Item/gi, '物品'], [/CHANGES/gi, '改动'], [/SOUNDS/gi, '音效'], [/DATA PACK VERSION/gi, '数据包版本'], [/PREDICATES/gi, '谓词'], [/ENTITY/gi, '实体'], [/ENCHANTMENTS/gi, '附魔'], [/TAGS/gi, '标签'], [/TYPE/gi, '类型'], [/MUSIC/gi, '音乐'], [/GAME TIPS/gi, '游戏提示'], [/NEW FEATURE/gi, '新特性'], [/USER INTERFACE/gi, '用户界面'], [/EDITOR/gi, '编辑器'], [/FIXES/gi, '修复'], [/IMPROVEMENTS/gi, '改进'], [/RESOURCE PACK VERSION/gi, '资源包版本'], [/SHADERS/gi, '着色器'], [/PARTICLES/gi, '粒子效果'], [/TOUCH CONTROLS/gi, '触控'], [/TECHNICAL UPDATES/gi, '技术性更新'], [/PROJECTILES/gi, '弹射物'], [/ENTITIES/gi, '实体'], [/FUNCTIONS/gi, '函数']]); }, imgCredits: (input, ctx) => { return translator(input, ctx, [// Creative Commons image credits [/Image credit:/gi, '图片来源:'], [/CC BY-NC-ND/gi, '知识共享 署名-非商业性使用-禁止演绎'], [/CC BY-NC-SA/gi, '知识共享 署名-非商业性使用-相同方式共享'], [/CC BY-NC/gi, '知识共享 署名-非商业性使用'], [/CC BY-ND/gi, '知识共享 署名-禁止演绎'], [/CC BY-SA/gi, '知识共享 署名-相同方式共享'], [/CC BY/gi, '知识共享 署名'], [/Public Domain/gi, '公有领域']]); }, punctuation: (input, ctx) => { return translator(input, ctx, [[/\[i\]/gi, '[font=楷体]'], [/\[\/i\]/g, '[/font]'], ...(ctx.disablePunctuationConverter ? [] : [[/,( |$)/g, ','], [/!( |$)/g, '!'], [/\.\.\.( |$)/g, '…'], [/\.( |$)/g, '。'], [/\?( |$)/g, '?'], [/( |^)-( |$)/g, ' —— ']])], input => { return quoteTreatment(input, [['“', '”', /"/]]); }); }, code: (input, ctx) => { return quoteTreatment(input, [['[color=Silver][/color]', '`', /`/]]); } }; function translate(input, ctx, type) { if (typeof type === 'string') { type = [type]; } for (const t of type) { input = translators[t](input, ctx); } return input; } function quoteTreatment(input, quoteArrays) { for (const quoteArray of quoteArrays) { const split = input.split(quoteArray[2]); input = ''; for (let i = 0; i < split.length - 1; i++) { const element = split[i]; input += element + quoteArray[i % 2]; } input += split[split.length - 1]; } return input; } function translator(input, ctx, mappings, treatment = input => input) { // REPLACE!!!!1 for (const mapping of mappings) { input = input.replace(mapping[0], mapping[1]); } treatment(input, ctx); return input; } const converters = { /** * Converts a ChildNode to a BBCode string according to the type of the node. */ convert: async (node, ctx) => { if (node.classList?.contains('spxxmb-userscript-ignored')) { return ''; } // Listing all possible elements in the document switch (node.nodeName) { case 'A': return converters.a(node, ctx); case 'B': case 'STRONG': return converters.strong(node, ctx); case 'BLOCKQUOTE': return converters.blockquote(node, ctx); case 'BR': return converters.br(); case 'CITE': return converters.cite(node, ctx); case 'CODE': return converters.code(node, ctx); case 'DIV': case 'SECTION': return converters.div(node, ctx); case 'DD': return converters.dd(node, ctx); case 'DL': return converters.dl(node, ctx); case 'DT': return converters.dt(); case 'EM': return converters.em(node, ctx); case 'H1': return converters.h1(node, ctx); case 'H2': return converters.h2(node, ctx); case 'H3': return converters.h3(node, ctx); case 'H4': return converters.h4(node, ctx); case 'I': return converters.i(node, ctx); case 'IMG': return converters.img(node); case 'LI': return converters.li(node, ctx); case 'OL': return converters.ol(node, ctx); case 'P': return converters.p(node, ctx); case 'PICTURE': return converters.picture(node, ctx); case 'PRE': return converters.pre(node, ctx); case 'SPAN': return converters.span(node, ctx); case 'TABLE': return converters.table(node, ctx); case 'TBODY': return converters.tbody(node, ctx); case 'TH': case 'TD': return converters.td(node, ctx); case 'TR': return converters.tr(node, ctx); case 'UL': return converters.ul(node, ctx); case '#text': if (node) { if (ctx.multiLineCode) { return node.textContent ? node.textContent : ''; } else return node.textContent.replace(/[\n\r\t]+/g, '').replace(/\s{2,}/g, ''); } else { return ''; } case 'BUTTON': case 'H5': case 'NAV': case 'svg': case 'SCRIPT': if (node) { return node.textContent ? node.textContent : ''; } else { return ''; } default: console.warn(`Unknown type: '${node.nodeName}'.`); if (node) { return node.textContent ? node.textContent : ''; } else { return ''; } } }, /** * Convert child nodes of an HTMLElement to a BBCode string. */ recurse: async (ele, ctx) => { let ans = ''; if (!ele) { return ans; } for (const child of Array.from(ele.childNodes)) { ans += await converters.convert(child, ctx); } return ans; }, a: async (anchor, ctx) => { const url = resolveUrl(anchor.href); let ans; if (url) { ans = `[url=${url}][color=#388d40][u]${await converters.recurse(anchor, ctx)}[/u][/color][/url]`; } else { ans = await converters.recurse(anchor, ctx); } return ans; }, blockquote: async (ele, ctx) => { const prefix = ''; const suffix = ''; const ans = `${prefix}${await converters.recurse(ele, ctx)}${suffix}`; return ans; }, br: async () => { const ans = '\n'; return ans; }, cite: async (ele, ctx) => { const prefix = '—— '; const suffix = ''; const ans = `${prefix}${await converters.recurse(ele, ctx)}${suffix}`; return ans; }, code: async (ele, ctx) => { const prefix = ctx.multiLineCode ? '[code]' : '[bgcolor=#f1edec][color=#7824c5]'; const suffix = ctx.multiLineCode ? '[/code]' : '[/color][/bgcolor]'; const ans = `${prefix}${await converters.recurse(ele, { ...ctx, disablePunctuationConverter: true })}${suffix}`; return ans; }, div: async (ele, ctx) => { let ans = await converters.recurse(ele, ctx); if (ele.classList.contains('text-center')) { ans = `[center]${ans}[/center]\n`; } else if (ele.classList.contains('article-image-carousel')) { /*const prefix = `[album]\n`; const suffix = `\n[/album]\n`;*/ const slides = []; const findSlides = async ele => { if (ele.classList.contains('slick-cloned')) { return; } if (ele.nodeName === 'IMG' && ele.classList.contains('article-image-carousel__image')) { slides.push([resolveUrl(ele.src), ' ']); } else if (ele.nodeName === 'DIV' && ele.classList.contains('article-image-carousel__caption')) { if (slides.length > 0) { slides[slides.length - 1][1] = `[b]${await converters.recurse(ele, ctx)}[/b]`; } } else { for (const child of Array.from(ele.childNodes)) { if (child.nodeName === 'DIV' || child.nodeName === 'IMG') { await findSlides(child); } } } }; await findSlides(ele); /*if (shouldUseAlbum(slides)) { ans = `${prefix}${slides.map(([url, caption]) => `[aimg=${url}]${caption}[/aimg]`).join('\n')}${suffix}`; } else */if (slides.length > 0) { ans = `[center]${slides.map(([url, caption]) => `[img]${url}[/img]\n${caption}`).join('\n')}[/center]\n`; } else { ans = ''; } } else if (ele.classList.contains('video')) { // Video. ans = '\n[center]<无法获取的视频,如有可用视频源,请在此处插入>\n<对于B站视频,可使用 [bilibili] 代码>[/center]\n'; } else if (ele.classList.contains('quote') || ele.classList.contains('attributed-quote')) { ans = `\n[quote]\n${ans}\n[/quote]\n`; } else if (ele.classList.contains('article-social')) { // End of the content. ans = ''; } else if (ele.classList.contains('modal')) { // Unknown useless content ans = ''; } return ans; }, dt: async () => { // const ans = `${converters.rescure(ele)}:` // return ans return ''; }, dl: async (ele, ctx) => { const ans = `\n\n${await converters.recurse(ele, ctx)}\n【本文排版借助了:[url=https://github.com/cinder0601/SPXXMB][color=#388d40][u]SPXXMB[/u][/color][/url] 用户脚本 v${spxxmbVersion}】\n\n`; return ans; }, dd: async (ele, ctx) => { let ans = ''; if (ele.classList.contains('pubDate')) { // Published: // `pubDate` is like '2019-03-08T10:00:00.876+0000'. const date = ele.attributes.getNamedItem('data-value'); if (date) { ans = `[b]【${ctx.translator} 译自[url=${ctx.url}][color=#388d40][u]官网 ${date.value.slice(0, 4)} 年 ${date.value.slice(5, 7)} 月 ${date.value.slice(8, 10)} 日发布的 ${ctx.title}[/u][/color][/url];原作者 ${ctx.author}】[/b]`; } else { ans = `[b]【${ctx.translator} 译自[url=${ctx.url}][color=#388d40][u]官网 * 年 * 月 * 日发布的 ${ctx.title}[/u][/color][/url]】[/b]`; } } else { // Written by: ctx.author = await converters.recurse(ele, ctx); } return ans; }, em: async (ele, ctx) => { const ans = `[i]${await converters.recurse(ele, ctx)}[/i]`; return ans; }, h1: async (ele, ctx) => { const prefix = '[size=6][b]'; const suffix = '[/b][/size]'; const rawInner = await converters.recurse(ele, ctx); const inner = makeUppercaseHeader(rawInner); const ans = `${prefix}[color=Silver]${usingSilver(inner).replace(/[\n\r]+/g, ' ')}[/color]${suffix}\n${prefix}${translate(`${inner}`, ctx, ['headings', 'punctuation']).replace(/[\n\r]+/g, ' ')}${suffix}\n\n`; return ans; }, h2: async (ele, ctx) => { if (isBlocklisted(ele.textContent)) return ''; const prefix = '[size=5][b]'; const suffix = '[/b][/size]'; const rawInner = await converters.recurse(ele, ctx); const inner = makeUppercaseHeader(rawInner); const ans = `\n${prefix}[color=Silver]${usingSilver(inner).replace(/[\n\r]+/g, ' ')}[/color]${suffix}\n${prefix}${translate(`${inner}`, ctx, ['headings', 'punctuation']).replace(/[\n\r]+/g, ' ')}${suffix}\n\n`; return ans; }, h3: async (ele, ctx) => { const prefix = '[size=4][b]'; const suffix = '[/b][/size]'; const rawInner = await converters.recurse(ele, ctx); const inner = makeUppercaseHeader(rawInner); const ans = `\n${prefix}[color=Silver]${usingSilver(inner).replace(/[\n\r]+/g, ' ')}[/color]${suffix}\n${prefix}${translate(`${inner}`, ctx, ['headings', 'punctuation']).replace(/[\n\r]+/g, ' ')}${suffix}\n\n`; return ans; }, h4: async (ele, ctx) => { const prefix = '[size=3][b]'; const suffix = '[/b][/size]'; const rawInner = await converters.recurse(ele, ctx); const inner = makeUppercaseHeader(rawInner); const ans = `\n${prefix}[color=Silver]${usingSilver(inner).replace(/[\n\r]+/g, ' ')}[/color]${suffix}\n${prefix}${translate(`${inner}`, ctx, ['headings', 'punctuation']).replace(/[\n\r]+/g, ' ')}${suffix}\n\n`; return ans; }, i: async (ele, ctx) => { const ans = `[i]${await converters.recurse(ele, ctx)}[/i]`; return ans; }, img: async img => { if (img.alt === 'Author image') { return ''; } let w; let h; if (img.classList.contains('attributed-quote__image')) { // for in-quote avatar image h = 92; w = 53; } else if (img.classList.contains('mr-3')) { // for attributor avatar image h = 121; w = 82; } const prefix = w && h ? `[img=${w},${h}]` : '[img]'; const imgUrl = resolveUrl(img.src); if (imgUrl === '') return ''; // in case of empty image let ans; if (img.classList.contains('attributed-quote__image') || img.classList.contains('mr-3')) { // Attributed quote author avatar. ans = `\n[left]${prefix}${imgUrl}[/img][/left]`; } else { ans = `\n\n[center]${prefix}${imgUrl}[/img][/center]\n`; } return ans; }, li: async (ele, ctx) => { let ans; let nestedList = false; for (const child of ele.childNodes) { if (child.nodeName === 'OL' || child.nodeName === 'UL') { nestedList = true; } } if (nestedList) { // Nested lists. let theParagragh = ''; let theList = ''; let addingList = false; for (let i = 0; i < ele.childNodes.length - 1; i++) { let nodeName = ele.childNodes[i].nodeName; if (nodeName === 'OL' || nodeName === 'UL') { addingList = true; } if (!addingList) { const paragraghNode = await converters.convert(ele.childNodes[i], { ...ctx, inList: true }); theParagragh = `${theParagragh}${paragraghNode}`; } else { const listNode = await converters.convert(ele.childNodes[i], { ...ctx, inList: true }); theList = `${theList}${listNode}`; } } ans = `[*][color=Silver]${usingSilver(theParagragh)}[/color]\n[*]${translate(translateBugs(theParagragh, ctx), ctx, 'code')}\n${theList}`; } else if (isBlocklisted(ele.textContent)) { return ''; } else { const inner = await converters.recurse(ele, { ...ctx, inList: true }); ans = `[*][color=Silver]${usingSilver(inner)}[/color]\n[*]${translate(translateBugs(inner, ctx), ctx, 'code')}\n`; } return ans; }, ol: async (ele, ctx) => { const inner = await converters.recurse(ele, ctx); const ans = `[list=1]\n${inner}[/list]\n`; return ans; }, p: async (ele, ctx) => { const inner = await converters.recurse(ele, ctx); let ans; if (ele.classList.contains('lead')) { ans = `[size=4][b][size=2][color=Silver]${inner}[/color][/size][/b][/size]\n[size=4][b]${translate(inner, ctx, 'headings')}[/b][/size]\n\n`; } else if (ele.querySelector('strong') !== null && ele.querySelector('strong').textContent === 'Posted:') { return ''; } else if (isBlocklisted(ele.textContent)) { return ''; } else if (ele.innerHTML === ' ') { return '\n'; } else if (/\s{0,}/.test(ele.textContent) && ele.querySelectorAll('img').length === 1) { return inner; } else { if (ctx.inList) { ans = inner; } else { ans = `[size=2][color=Silver]${usingSilver(inner)}[/color][/size]\n${translate(inner, ctx, ['punctuation', 'imgCredits'])}\n\n`; } } return ans; }, picture: async (ele, ctx) => { const ans = await converters.recurse(ele, ctx); return ans; }, pre: async (ele, ctx) => { const ans = await converters.recurse(ele, { ...ctx, multiLineCode: true }); return ans; }, span: async (ele, ctx) => { const ans = await converters.recurse(ele, ctx); if (ele.classList.contains('bedrock-server')) { // Inline code. const prefix = '[bgcolor=#f1edec][color=#7824c5]'; const suffix = '[/color][/bgcolor]'; return `${prefix}${await converters.recurse(ele, { ...ctx, disablePunctuationConverter: true })}${suffix}`; } else if (ele.classList.contains('strikethrough')) { // Strikethrough text. const prefix = '[s]'; const suffix = '[/s]'; return `${prefix}${ans}${suffix}`; } else if (ele.childElementCount === 1 && ele.firstElementChild.nodeName === 'IMG') { // Image. const img = ele.firstElementChild; return await converters.img(img); } return ans; }, strong: async (ele, ctx) => { const ans = `[b]${await converters.recurse(ele, ctx)}[/b]`; return ans; }, table: async (ele, ctx) => { const ans = `\n[table]\n${await converters.recurse(ele, ctx)}[/table]\n`; return ans; }, tbody: async (ele, ctx) => { const ans = await converters.recurse(ele, ctx); return ans; }, td: async (ele, ctx) => { const ans = `[td]${await converters.recurse(ele, ctx)}[/td]`; return ans; }, tr: async (ele, ctx) => { const ans = `[tr]${await converters.recurse(ele, ctx)}[/tr]\n`; return ans; }, ul: async (ele, ctx) => { const inner = await converters.recurse(ele, ctx); const ans = `[list]\n${inner}[/list]\n`; return ans; } }; /** * Resolve relative URLs. */ function resolveUrl(url) { if (url[0] === '/') { return `https://${location.host}${url}`; } else { return url; } } function usingSilver(text) { return text.replace(/#388d40/g, 'Silver').replace(/#7824c5/g, 'Silver'); } function makeUppercaseHeader(header) { let retStr = ''; let idx = 0; let bracket = 0; for (let i = 0; i < header.length; i++) { if (header[i] == '[') { if (bracket == 0) { retStr = retStr.concat(header.substring(idx, i).toUpperCase()); idx = i; } bracket++; } else if (header[i] == ']') { if (bracket <= 1) { retStr = retStr.concat(header.substring(idx, i + 1)); idx = i + 1; } bracket = Math.max(0, bracket - 1); } } if (bracket > 0) { console.error('bracket not closed!'); retStr = retStr.concat(header.substring(idx, header.length)); } else { retStr = retStr.concat(header.substring(idx, header.length).toUpperCase()); } return retStr; } /** * Get bugs from BugCenter. * Guangyao and github source are down, so I deleted them. */ async function getBugs() { return new Promise((rs, rj) => { GM_xmlhttpRequest({ method: 'GET', url: bugsCenter, fetch: true, nocache: true, timeout: 7_000, onload: r => { try { rs(JSON.parse(r.responseText)); } catch (e) { rj(e); } }, onabort: () => rj(new Error('Aborted')), onerror: e => rj(e), ontimeout: () => rj(new Error('Time out')) }); }); } async function getBugsTranslators() { return new Promise((rs, rj) => { GM_xmlhttpRequest({ method: 'GET', url: bugsTranslatorsTable, fetch: true, nocache: true, timeout: 7_000, onload: r => { try { rs(JSON.parse(r.responseText)); } catch (e) { rj(e); } }, onabort: () => rj(new Error('Aborted')), onerror: e => rj(e), ontimeout: () => rj(new Error('Time out')) }); }); } async function getTranslatorColor() { return new Promise((rs, rj) => { GM_xmlhttpRequest({ method: 'GET', url: translatorColorTable, fetch: true, nocache: true, timeout: 7_000, onload: r => { try { rs(JSON.parse(r.responseText)); } catch (e) { rj(e); } }, onabort: () => rj(new Error('Aborted')), onerror: e => rj(e), ontimeout: () => rj(new Error('Time out')) }); }); } function markdownToBbcode(value) { return value.replace(/`([^`]+)`/g, '[bgcolor=#f1edec][color=#7824c5]$1[/color][/bgcolor]'); } /** * Replace untranslated bugs. */ function translateBugs(str, ctx) { if (str.startsWith('[url=https://bugs.mojang.com/browse/MC-') && ctx.bugs != null // nullish ) { const id = str.slice(36, str.indexOf(']')); const data = ctx.bugs[id]; if (data) { let bugColor = '#388d40'; if (ctx.bugsTranslators[id]) { const bugTranslator = ctx.bugsTranslators[id]; if (ctx.translatorColor[bugTranslator]) { bugColor = ctx.translatorColor[bugTranslator]; } } const bbcode = markdownToBbcode(data); return `[url=https://bugs.mojang.com/browse/${id}][b][color=${bugColor}]${id}[/color][/b][/url]- ${bbcode}`; } else { return str; } } else { return str; } } /** * MineBBS does NOT support [album], the shouldUseAlbum function is removed temporarily. */ /*function shouldUseAlbum(slides) { return slides.length > 1 && slides.every(([_, caption]) => caption === ' ') ; }*/ function isBlocklisted(text) { const blocklist = ['Information on the Minecraft Preview and Beta:', 'While the version numbers between Preview and Beta are different, there is no difference in game content', 'These work-in-progress versions can be unstable and may not be representative of final version quality', 'Minecraft Preview is available on Xbox, Windows 10/11, and iOS devices. More information can be found at aka.ms/PreviewFAQ', 'The beta is available on Android (Google Play). To join or leave the beta, see aka.ms/JoinMCBeta for detailed instructions']; return blocklist.map(i => { return i.replace(/\p{General_Category=Space_Separator}*/, ''); }).some(block => text.trim().trim().replace(/\p{General_Category=Space_Separator}*/, '').includes(block)); } async function minecraftNet() { const url = document.location.toString(); if (url.match(/^https:\/\/www\.minecraft\.net\/(?:[a-z-]+)\/article\//)) { const pointerModifier = document.getElementsByClassName('article-attribution-container').item(0); pointerModifier.style.pointerEvents = 'inherit'; const button = document.createElement('button'); button.classList.add('mc-button__primary', 'mc-button__green-s1', 'spxxmb-userscript-ignored'); button.innerText = '复制 BBCode'; button.onclick = async () => { button.innerText = '处理中...'; const bbcode = await convertMCArticleToBBCode(document, url); GM_setClipboard(bbcode, { type: 'text', mimetype: 'text/plain' }); button.innerText = '已复制 BBCode!'; setTimeout(() => button.innerText = '复制 BBCode', 5_000); }; const container = document.getElementsByClassName('attribution').item(0); container.append(button); } } async function convertMCArticleToBBCode(html, articleUrl, translator = config.translator) { const articleType = getArticleType(html); const versionType = getVersionType(articleUrl); let bugs; try { bugs = await getBugs(); } catch (e) { bugs = {}; console.error('[convertMCArticleToBBCode#getBugs]', e); } let bugsTranslators; try { bugsTranslators = await getBugsTranslators(); } catch (e) { bugsTranslators = {}; console.error('[convertMCArticleToBBCode#getBugs]', e); } let translatorColor; try { translatorColor = await getTranslatorColor(); } catch (e) { translatorColor = {}; console.error('[convertMCArticleToBBCode#getBugs]', e); } const header = getHeader(articleType, versionType); const heroImage = getHeroImage(html, articleType); const content = await getContent(html, { bugs, bugsTranslators, translatorColor, title: html.title.split(' | ').slice(0, -1).join(' | '), date: null, translator, url: articleUrl }); const footer = getFooter(articleType, versionType); const ans = `${header}${heroImage}${content}${footer}`; return ans; } /** * Returns the type of the article. */ function getArticleType(html) { try { const type = html.getElementsByClassName('article-category__text')?.[0]?.textContent ?? ''; return type.toUpperCase(); } catch (e) { console.error('[getArticleType]', e); } return 'INSIDER'; } /** * Get the hero image (head image) of an article as the form of a BBCode string. * @param html An HTML Document. */ function getHeroImage(html, articleType) { const category = articleType ? `\n[color=White][bgcolor=Black][b]${articleType}[/b][/bgcolor][/color][/center]` : ''; const img = html.getElementsByClassName('article-head__image')[0]; if (!img) { return `\n[center]${category}\n`; } const src = img.src; const ans = `[center][img=1200,513]${resolveUrl(src)}[/img]\n${category}\n`; return ans; } /** * Get the content of an article as the form of a BBCode string. * @param html An HTML Document. */ async function getContent(html, ctx) { const rootDiv = html.getElementsByClassName('article-body')[0]; let ans = await converters.recurse(rootDiv, ctx); // Get the server URL if it exists. const serverUrls = ans.match(/(https:\/\/piston-data.mojang.com\/.+\/server.jar)/); let serverUrl = ''; if (serverUrls) { serverUrl = serverUrls[0]; } // Remove the text after '】' ans = ans.slice(0, ans.lastIndexOf('】') + 1); // Remove 'GET THE SNAPSHOT/PRE-RELEASE/RELEASE-CANDIDATE/RELEASE' for releasing let index = ans.toLowerCase().search(/\[size=\d]\[b\]\[color=silver\](\[b\])?get the (pre-release|release|release candidate|snapshot)(\[\/b\])?\[\/color\]\[\/b\]\[\/size\]/); if (index !== -1) { ans = ans.slice(0, index); const attribution = await converters.recurse(document.querySelector('.attribution'), ctx); ans = `${ans}${attribution}`; } // Add spaces between texts and '[x'. ans = ans.replace(/([a-zA-Z0-9\-._])(\[[A-Za-z])/g, '$1 $2'); // Add spaces between '[/x]' and texts. ans = ans.replace(/(\[\/[^\]]+?\])([a-zA-Z0-9\-._])/g, '$1 $2'); // Append the server URL if it exists. return ans; } function getZendesk(controlDOM, titleSlice, contentClass, versionType) { const button = document.createElement('a'); button.classList.add('navLink'); button.innerText = '复制 BBCode'; button.onclick = async () => { button.innerText = '处理中...'; const bbcode = await convertZendeskArticleToBBCode(document, location.href, config.translator, titleSlice, contentClass, versionType); GM_setClipboard(bbcode, { type: 'text', mimetype: 'text/plain' }); button.innerText = '已复制 BBCode!'; setTimeout(() => button.innerText = '复制 BBCode', 5_000); }; controlDOM(button); } async function converthelpElementsToBBCode(elements, ctx) { let bbcode = ''; const seenImages = new Set(); for (let element of elements) { try { let converted = await converters.recurse(element, ctx); let imgTags = converted.match(/\[img\](.*?)\[\/img\]/g); if (imgTags) { for (let imgTag of imgTags) { let imgUrl = imgTag.match(/\[img\](.*?)\[\/img\]/)[1]; if (seenImages.has(imgUrl)) { converted = converted.replace(imgTag, ''); } else { seenImages.add(imgUrl); } } } bbcode += converted + '\n'; } catch (error) { console.error("Error converting content to BBCode:", error); } } return bbcode; } function getHelpContent(controlDOM) { const ctx = { multiLineCode: false, disablePunctuationConverter: false, translator: config.translator, url: window.location.href, inList: false }; const heading = document.getElementsByClassName('article-page-heading'); const content = document.getElementsByClassName('article-page-body'); const button = document.createElement('a'); button.classList.add('navLink'); button.innerText = '复制 BBCode'; button.onclick = async () => { button.innerText = '处理中...'; let bbcode = await converthelpElementsToBBCode(heading, ctx); let title = bbcode; title = title.replace(/\n/g, ''); bbcode = `[left][color=#388e3c][size=5]|[/size][/color][size=4]本文内容按照 [/size][url=https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh-hans][size=4][color=#2e8b57][u]CC BY-NC-SA 4.0[/u][/color][/size][/url][size=4] 协议进行授权,[b]转载本帖时须注明[color=#ff0000]原作者[/color]以及[color=#ff0000]本帖地址[/color][/b]。[/size][/left][/left][/left][/left][hr]\n[size=6][b][color=silver]${bbcode}[/color][/b][/size][size=6][b]${bbcode}[/b][/size]\n` bbcode += await converthelpElementsToBBCode(content, ctx); bbcode += `[b]【${ctx.translator} 译自[url=${ctx.url}][color=#388d40][u] help.minecraft.net 上的 ${title}[/u][/color][/url]】[/b]\n【本文排版借助了:[url=https://github.com/cinder0601/SPXXMB][color=#388d40][u]SPXXMB[/u][/color][/url] 用户脚本 v${version}】\n`; bbcode += getFooter('INSIDER',VersionType.Normal); GM_setClipboard(bbcode, { type: 'text', mimetype: 'text/plain' }); button.innerText = '已复制 BBCode!'; setTimeout(() => button.innerText = '复制 BBCode', 5000); }; controlDOM(button); } async function convertZendeskArticleToBBCode(html, articleUrl, translator = config.translator, titleSlice, contentClass, versionType) { const title = html.title.slice(0, html.title.lastIndexOf(titleSlice)); const ctx = { bugs: {}, title: title, date: null, translator, url: articleUrl }; const content = await getZendeskContent(html, ctx, contentClass); const posted = await getZendeskDate(location.href); const header = versionType ? getHeader('news', versionType) : ''; const footer = versionType ? getFooter('news', versionType) : ''; const ans = `${header}[center][size=6][b][color=Silver]${title}[/color][/b][/size] ${translate(`[size=6][b]${title}[/b][/size]`, ctx, 'headings')}[/center]\n\n${content}\n [b]【${ctx.translator} 译自[url=${ctx.url}][color=#388d40][u]${ctx.url.match(/https:\/\/(.*?)\//)[1]} ${posted.year} 年 ${posted.month} 月 ${posted.day} 日发布的 ${ctx.title}[/u][/color][/url]】[/b] 【本文排版借助了:[url=https://github.com/cinder0601/SPXXMB][color=#388d40][u]SPXXMB[/u][/color][/url] 用户脚本 v${spxxmbVersion}】\n\n${footer}`; return ans; } async function getZendeskContent(html, ctx, contentClass) { const rootSection = html.getElementsByClassName(contentClass)[0]; // Yep, this is the only difference. let ans = await converters.recurse(rootSection, ctx); // Add spaces between texts and '[x'. ans = ans.replace(/([a-zA-Z0-9\-._])(\[[A-Za-z])/g, '$1 $2'); // Add spaces between '[/x]' and texts. ans = ans.replace(/(\[\/[^\]]+?\])([a-zA-Z0-9\-._])/g, '$1 $2'); return ans; } async function getZendeskDate(url) { const req = new Promise((rs, rj) => { GM_xmlhttpRequest({ method: 'GET', url: '/api/v2/help_center/en-us/articles/' + url.match(/\/articles\/(\d+)/)[1], fetch: true, nocache: true, timeout: 7_000, onload: r => { try { rs(r.responseText); } catch (e) { rj(e); } }, onabort: () => rj(new Error('Aborted')), onerror: e => rj(e), ontimeout: () => rj(new Error('Time out')) }); }); let res; await req.then(value => { const rsp = JSON.parse(value); res = new Date(rsp.article.created_at); }); const year = res.getFullYear(); const month = res.getMonth() + 1; const day = res.getDate(); return { year, month, day }; } function feedback() { let url = window.location.href; // 获取当前页面的URL let versionType = getVersionType(url); // 调用getVersionType函数确定versionType getZendesk(button => { document.querySelector('.topNavbar nav').append(button); }, ' – Minecraft Feedback', 'article-info', versionType); } function help() { getHelpContent(button => { document.querySelector('.mc-globalbanner').append(button); }); } function twitter() { const ProfilePictures = new Map([ ['Mojang', 'https://s2.loli.net/2024/05/27/tMZKe4BmldgDv2P.jpg'], ['MojangSupport', 'https://s2.loli.net/2024/05/27/tMZKe4BmldgDv2P.jpg'], ['MojangStatus', 'https://s2.loli.net/2024/05/27/tMZKe4BmldgDv2P.jpg'], ['Minecraft', 'https://s2.loli.net/2024/05/27/6QsES9CwgKMLv7I.jpg'], ['henrikkniberg', 'https://s2.loli.net/2024/05/27/h2KGZEBks4XMTFq.png'], ['_LadyAgnes', 'https://s2.loli.net/2024/05/27/ZoJsth1i8randl9.png'], ['kingbdogz', 'https://s2.loli.net/2024/05/27/IH7aVTepiDXBZb2.png'], ['JasperBoerstra', 'https://s2.loli.net/2024/05/27/Jh2doD1eG7A56TR.png'], ['adrian_ivl', 'https://s2.loli.net/2024/05/27/itAL8hsGqk67cxS.png'], ['slicedlime', 'https://s2.loli.net/2024/05/27/DY6gQscuX8HiA9e.jpg'], ['Cojomax99', 'https://s2.loli.net/2024/05/27/DxeZ3rINFgTildA.png'], ['Mojang_Ined', 'https://s2.loli.net/2024/05/27/dzX3pYy8TSa7uJt.png'], ['SeargeDP', 'https://s2.loli.net/2024/05/27/nf7EKltTYXLgsxN.png'], ['Dinnerbone', 'https://s2.loli.net/2024/05/27/Q4ebCE29vFwPmxn.png'], ['Marc_IRL', 'https://s2.loli.net/2024/05/27/bcW5zXfQ84r9IO6.png'], ['Mega_Spud', 'https://s2.loli.net/2024/05/27/TZwzsJBRhnLyuFx.png'], ['CornerHardMC', 'https://s2.loli.net/2024/05/28/o4wLCvuRGi9Yxh7.png'], ['MinecraftWikiEN', 'https://s2.loli.net/2024/06/23/fn1SeWpdmit86lQ.png'], ]);//More pictures can be added manually. function getTweetMetadata() { const tweetMetadata = { date: '', source: '', text: '', rawtext: '', tweetLink: '', urls: '', userName: '', userTag: '', lang: '', }; const url = window.location.href; const regex = /https:\/\/x\.com\/([^/]+)\/status\/\d+/; const match = url.match(regex); tweetMetadata.userTag = match[1]; let posterNameContent = []; const posterNameElements = document.querySelector('div[data-testid="User-Name"] a span').querySelectorAll('span, img[alt]'); for (const element of posterNameElements) { if (element.tagName.toLowerCase() === 'span') { posterNameContent.push(element.textContent); } else if (element.tagName.toLowerCase() === 'img' && element.alt) { posterNameContent.push(element.alt); } } tweetMetadata.userName = posterNameContent.join(''); let texts = []; let rawTexts = []; const articleDivs = document.querySelector('article div[lang]').querySelectorAll('span, img[alt]'); for (const element of articleDivs) { let textContent = ''; let rawContent = ''; if (element.tagName.toLowerCase() === 'span') { textContent = element.innerHTML; rawContent = element.textContent; const links = element.querySelectorAll('a'); links.forEach(link => { const url = link.href; const linkText = link.innerText.trim(); const replacement = `[url=${url}][color=#00bfff][u]${linkText}[/u][/color][/url]`; textContent = textContent.replace(link.outerHTML, replacement); rawContent = rawContent.replace(link.outerHTML, linkText); }); } else if (element.tagName.toLowerCase() === 'img' && element.alt) { textContent = element.alt; rawContent = element.alt; } texts.push(textContent); rawTexts.push(rawContent.replace(/(.*?)<\/a>/g, '$1')); } tweetMetadata.text = texts.join(''); tweetMetadata.rawtext = rawTexts.join(''); //I have tried my best but failed, if it still returns 'http://' or 'https://', please add the link manually. tweetMetadata.lang = document.querySelector('article div[lang]').getAttribute('lang'); tweetMetadata.date = document.querySelector('time').innerHTML; tweetMetadata.source = document.querySelector('article a[role="link"] span').innerText; tweetMetadata.tweetLink = window.location.href; return tweetMetadata; } function getTweetBbcode(tweet, mode) { const attributeColor = '#5B7083'; const backgroundColor = mode === 'dark' ? '#000000' : '#FFFFFF'; const foregroundColor = mode === 'dark' ? '#D9D9D9' : '#0F1419'; const dateString = `${tweet.date} · ${tweet.source} · SPXXMB v${spxxmbVersion} · 转载请注明原作者及本帖地址`; const content = tweet.text; const content1 = tweet.rawtext; return `[center][table=560,${backgroundColor}] [tr][td] [img=44,44]${ProfilePictures.get(tweet.userTag) || '<不支持的头像,请手动添加图片链接>'}[/img][size=15px][b][color=${foregroundColor}]${tweet.userName}[/color][/b] [color=${attributeColor}]@${tweet.userTag}[/color][/size] [color=Silver][size=23px]${content1}[/color][/size] [size=15px][color=${attributeColor}]由 ${GM_config.get('translator')} 翻译自${tweet.lang.startsWith('en') ? '英语' : ` ${tweet.lang}`}[/color][/size] [color=${foregroundColor}][size=23px]${content}[/size] [/color][center]<如有配图,请在此处添加> [/center] [size=15px][url=${tweet.tweetLink}][color=${attributeColor}][u]${dateString}[/u][/color][/url][/size][/td][/tr] [/table][/center]`; } function x() { console.info('[SPXXMB] Activated'); const buttonLight = document.createElement('button'); buttonLight.innerText = '复制 BBCode (浅色)'; buttonLight.style.width = '100%'; buttonLight.onclick = async () => { buttonLight.innerText = '处理中...'; try { const bbcode = getTweetBbcode(getTweetMetadata(), 'light'); GM_setClipboard(bbcode, { type: 'text', mimetype: 'text/plain' }); buttonLight.innerText = '已复制 BBCode!'; } catch (error) { console.error("Error processing BBCode (Light):", error); buttonLight.innerText = '错误!'; } setTimeout(() => buttonLight.innerText = '复制 BBCode (浅色)', 5000); }; const buttonDark = document.createElement('button'); buttonDark.innerText = '复制 BBCode (深色)'; buttonDark.style.width = '100%'; buttonDark.onclick = async () => { buttonDark.innerText = '处理中...'; try { const bbcode = getTweetBbcode(getTweetMetadata(), 'dark'); GM_setClipboard(bbcode, { type: 'text', mimetype: 'text/plain' }); buttonDark.innerText = '已复制 BBCode!'; } catch (error) { console.error("Error processing BBCode (Dark):", error); buttonDark.innerText = '错误!'; } setTimeout(() => buttonDark.innerText = '复制 BBCode (深色)', 5000); }; const checkLoaded = setInterval(() => { const targetDiv = document.querySelector('article div[lang]'); if (targetDiv && !document.querySelector('#spxxmb-buttons')) { const container = document.createElement('div'); container.id = 'spxxmb-buttons'; container.style.display = 'flex'; container.style.flexDirection = 'column'; container.style.width = '100%'; container.style.justifyContent = 'flex-end'; buttonLight.style.backgroundColor = 'rgb(29, 155, 240)'; buttonDark.style.backgroundColor = 'rgb(29, 155, 240)'; buttonLight.style.borderColor = 'rgba(100, 149, 237, 0.5)'; buttonDark.style.borderColor = 'rgba(100, 149, 237, 0.5)'; buttonLight.style.borderRadius = '10px'; buttonDark.style.borderRadius = '10px'; buttonLight.style.textAlign = 'center'; buttonDark.style.textAlign = 'center'; buttonLight.style.marginLeft = 'auto'; buttonDark.style.marginLeft = 'auto'; buttonLight.style.width = '180px'; buttonLight.style.height = '25px'; buttonDark.style.width = '180px'; buttonDark.style.height = '25px'; container.append(buttonLight); container.append(buttonDark); targetDiv.parentElement.append(container); clearInterval(checkLoaded); } }, 300); } x(); } switch (location.host) { case 'www.minecraft.net': minecraftNet(); break; case 'x.com': twitter(); break; case 'feedback.minecraft.net': feedback(); break; case 'help.minecraft.net': help(); } })(); //# sourceMappingURL=bundle.user.js.map