SPXXHi

Minecraft.net & X.com blog article to BBCode converter, adapted to HiMCBBS

  1. // ==UserScript==
  2. // @name SPXXHi
  3. // @description Minecraft.net & X.com blog article to BBCode converter, adapted to HiMCBBS
  4. // @namespace npmjs.com/package/@spxxhi/userscript
  5. // @author Cinder & SPGoding & SPX Fellow
  6. // @connect *
  7. // @connect feedback.minecraft.com
  8. // @connect help.minecraft.net
  9. // @connect raw.githubusercontent.com
  10. // @homepage https://github.com/cinder0601/SPXXHi
  11. // @match https://www.minecraft.net/en-us/article/*
  12. // @match https://www.minecraft.net/zh-hans/article/*
  13. // @match https://x.com/*/status/*
  14. // @match https://feedback.minecraft.net/hc/en-us/articles/*
  15. // @match https://help.minecraft.net/hc/en-us/articles/*
  16. // @require https://fastly.jsdelivr.net/gh/sizzlemctwizzle/GM_config@2207c5c1322ebb56e401f03c2e581719f909762a/gm_config.js
  17. // @icon https://www.minecraft.net/etc.clientlibs/minecraft/clientlibs/main/resources/favicon.ico
  18. // @version 3.2.5
  19. // @grant GM_getValue
  20. // @grant GM_setValue
  21. // @grant GM_setClipboard
  22. // @grant GM_xmlhttpRequest
  23. // @grant GM_registerMenuCommand
  24. // @license MIT
  25. // ==/UserScript==
  26. (function () {
  27. "use strict";
  28.  
  29. var GM_config = new GM_configStruct();
  30. GM_config.init({
  31. id: "spxxhi",
  32. title: "SPXXHi 用户脚本",
  33. fields: {
  34. translator: {
  35. label: "译者名",
  36. type: "text",
  37. default: "<默认译者>",
  38. },
  39. bugSource: {
  40. label: "选择翻译源",
  41. type: "select",
  42. options: ["Github", "自定义"],
  43. default: "Github",
  44. },
  45. bugCenterTranslation: {
  46. label: "漏洞翻译源",
  47. type: "text",
  48. default:
  49. "https://raw.githubusercontent.com/SPXFellow/spxx-translation-database/crowdin/zh-CN/zh_CN.json",
  50. },
  51. bugCenterTranslator: {
  52. label: "漏洞译者源",
  53. type: "text",
  54. default:
  55. "https://raw.githubusercontent.com/SPXFellow/spxx-translation-database/master/translator.json",
  56. },
  57. bugCenterColor: {
  58. label: "漏洞颜色源",
  59. type: "text",
  60. default:
  61. "https://raw.githubusercontent.com/SPXFellow/spxx-translation-database/master/color.json",
  62. },
  63. },
  64. });
  65. GM_registerMenuCommand("编辑配置", () => GM_config.open());
  66. const src = GM_config.get("bugSource");
  67. let tr = "";
  68. let tor = "";
  69. let c = "";
  70.  
  71. if (src == "Github") {
  72. console.log("[SPXXHi] 正在使用 Github 漏洞中心");
  73. tr =
  74. "https://raw.githubusercontent.com/SPXFellow/spxx-translation-database/crowdin/zh-CN/zh_CN.json";
  75. tor =
  76. "https://raw.githubusercontent.com/SPXFellow/spxx-translation-database/master/translator.json";
  77. c =
  78. "https://raw.githubusercontent.com/SPXFellow/spxx-translation-database/master/color.json";
  79. } else if (src == "自定义") {
  80. console.log("[SPXXHi] 正在使用自定义漏洞中心");
  81. tr = GM_config.get("bugCenterTranslation");
  82. tor = GM_config.get("bugCenterTranslator");
  83. c = GM_config.get("bugCenterColor");
  84. }
  85.  
  86. const config = {
  87. translator: GM_config.get("translator"),
  88. bugCenter: {
  89. translation: tr,
  90. translator: tor,
  91. color: c,
  92. },
  93. };
  94.  
  95. var version = "3.2.5";
  96.  
  97. function getVersionType(url) {
  98. const lowerUrl = url.toLowerCase();
  99. if (lowerUrl.includes("snapshot")) {
  100. return VersionType.Snapshot;
  101. } else if (lowerUrl.includes("pre-release")) {
  102. return VersionType.PreRelease;
  103. } else if (lowerUrl.includes("release-candidate")) {
  104. return VersionType.ReleaseCandidate;
  105. } else if (
  106. lowerUrl.includes("minecraft-java-edition") &&
  107. !lowerUrl.includes("snapshot")
  108. ) {
  109. return VersionType.Release;
  110. } else if (
  111. lowerUrl.includes("minecraft-preview") ||
  112. lowerUrl.includes("minecraft-beta-preview") ||
  113. lowerUrl.includes("minecraft-beta")
  114. ) {
  115. return VersionType.BedrockBeta;
  116. } else if (lowerUrl.includes("bedrock")) {
  117. return VersionType.BedrockRelease;
  118. } else {
  119. return VersionType.Normal;
  120. }
  121. }
  122.  
  123. const bugsCenter = config.bugCenter.translation;
  124. const bugsTranslatorsTable = config.bugCenter.translator;
  125. const translatorColorTable = config.bugCenter.color;
  126. const spxxhiVersion = version;
  127. const url1 = window.location.href;
  128.  
  129. function getReleaseVersionCode(url) {
  130. const lowerUrl = url.toLowerCase();
  131. if (lowerUrl.includes("pre-release")) {
  132. const versionRegex = /(\d+)-(\d+)-(\d*)(.+)-release/;
  133. const match = lowerUrl.match(versionRegex);
  134. if (match && match.length >= 3) {
  135. const Version1 = match[1];
  136. const Version2 = match[2];
  137. const Version3 = match[3];
  138. if (Version3 == "") {
  139. const formattedVersion = `${Version1}.${Version2}`;
  140. return formattedVersion;
  141. } else {
  142. const formattedVersion = `${Version1}.${Version2}.${Version3}`;
  143. return formattedVersion;
  144. }
  145. }
  146. } else if (lowerUrl.includes("release-candidate")) {
  147. const versionRegex = /(\d+)-(\d+)-(\d*)(.+)-candidate/;
  148. const match = lowerUrl.match(versionRegex);
  149. if (match && match.length >= 3) {
  150. const Version1 = match[1];
  151. const Version2 = match[2];
  152. const Version3 = match[3];
  153. if (Version3 == "") {
  154. const formattedVersion = `${Version1}.${Version2}`;
  155. return formattedVersion;
  156. } else {
  157. const formattedVersion = `${Version1}.${Version2}.${Version3}`;
  158. return formattedVersion;
  159. }
  160. }
  161. }
  162. }
  163.  
  164. function getVersionCode(url) {
  165. const lowerUrl = url.toLowerCase();
  166. if (lowerUrl.includes("snapshot")) {
  167. const versionRegex = /-([0-9a-zA-Z]+)$/;
  168. const match = url.match(versionRegex);
  169. if (match && match[1]) {
  170. return match[1];
  171. }
  172. } else if (lowerUrl.includes("pre-release")) {
  173. const versionRegex = /(\d+)-(\d+)-(\d*)(.+)-release-(\d+)/;
  174. const match = lowerUrl.match(versionRegex);
  175. if (match && match.length >= 4) {
  176. const Version1 = match[1];
  177. const Version2 = match[2];
  178. const Version3 = match[3];
  179. const Version4 = match[5];
  180. if (Version3 == "") {
  181. const formattedVersion = `${Version1}.${Version2}-pre${Version4}`;
  182. return formattedVersion;
  183. } else {
  184. const formattedVersion = `${Version1}.${Version2}.${Version3}-pre${Version4}`;
  185. return formattedVersion;
  186. }
  187. }
  188. } else if (lowerUrl.includes("release-candidate")) {
  189. const versionRegex = /(\d+)-(\d+)-(\d*)(.+)-candidate-(\d+)/;
  190. const match = lowerUrl.match(versionRegex);
  191. if (match && match.length >= 4) {
  192. const Version1 = match[1];
  193. const Version2 = match[2];
  194. const Version3 = match[3];
  195. const Version4 = match[5];
  196. if (Version3 == "") {
  197. const formattedVersion = `${Version1}.${Version2}-rc${Version4}`;
  198. return formattedVersion;
  199. } else {
  200. const formattedVersion = `${Version1}.${Version2}.${Version3}-rc${Version4}`;
  201. return formattedVersion;
  202. }
  203. }
  204. } else if (lowerUrl.includes("minecraft-beta-preview")) {
  205. const versionRegex = /-beta-preview-(\d+)-(\d+)-(\d+)-(\d+)/;
  206. const match = lowerUrl.match(versionRegex);
  207. if (match && match.length >= 5) {
  208. const Version1 = match[1];
  209. const Version2 = match[2];
  210. const Version3 = match[3];
  211. const Version4 = match[4];
  212. const formattedVersion = `${Version1}.${Version2}.${Version3}.${Version4}`;
  213. return formattedVersion;
  214. }
  215. } else if (
  216. lowerUrl.includes("minecraft-preview") &&
  217. !lowerUrl.includes("beta")
  218. ) {
  219. const versionRegex = /-preview-(\d+)-(\d+)-(\d+)-(\d+)/;
  220. const match = lowerUrl.match(versionRegex);
  221. if (match && match.length >= 5) {
  222. const Version1 = match[1];
  223. const Version2 = match[2];
  224. const Version3 = match[3];
  225. const Version4 = match[4];
  226. const formattedVersion = `${Version1}.${Version2}.${Version3}.${Version4}`;
  227. return formattedVersion;
  228. }
  229. } else if (
  230. lowerUrl.includes("minecraft-beta") &&
  231. !lowerUrl.includes("preview")
  232. ) {
  233. const versionRegex = /-beta-(\d+)-(\d+)-(\d+)-(\d+)/;
  234. const match = lowerUrl.match(versionRegex);
  235. if (match && match.length >= 5) {
  236. const Version1 = match[1];
  237. const Version2 = match[2];
  238. const Version3 = match[3];
  239. const Version4 = match[4];
  240. const formattedVersion = `${Version1}.${Version2}.${Version3}.${Version4}`;
  241. return formattedVersion;
  242. }
  243. }
  244. }
  245.  
  246. function getVersionCount(url) {
  247. const lowerUrl = url.toLowerCase();
  248. if (lowerUrl.includes("pre-release")) {
  249. const versionRegex = /-release-(\d+)/;
  250. const match = lowerUrl.match(versionRegex);
  251. if (match && match[1]) {
  252. return match[1];
  253. }
  254. } else if (lowerUrl.includes("release-candidate")) {
  255. const versionRegex = /-candidate-(\d+)/;
  256. const match = lowerUrl.match(versionRegex);
  257. if (match && match[1]) {
  258. return match[1];
  259. }
  260. }
  261. }
  262.  
  263. const link = document.createElement("link");
  264. link.rel = "stylesheet";
  265. link.href =
  266. "https://www.minecraft.net/etc.clientlibs/minecraftnet/clientlibs/clientlib-site/resources/fonts/MinecraftTen.woff";
  267. document.head.appendChild(link);
  268.  
  269. let releaseversioncode = getReleaseVersionCode(url1);
  270. let versioncode = getVersionCode(url1);
  271. let versioncount = getVersionCount(url1);
  272. function getHeader(articleType, type) {
  273. if (articleType.toLowerCase() !== "news") {
  274. 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`;
  275. }
  276.  
  277. switch (type) {
  278. case VersionType.Snapshot:
  279. return `[color=#388e3c][size=5]|[/size][/color][size=4][b]Minecraft Java 版[/b]是指 WindowsMac OS Linux 平台上,使用 Java 语言开发的 Minecraft 版本。[/size]
  280. [color=#388e3c][size=5]|[/size][/color][size=4][b]每周快照[/b]是 Minecraft Java 版的测试机制,主要用于下一个正式版的特性预览。[/size]
  281. [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]
  282. [color=#f44336][size=5]|[/size][/color][size=4]Minecraft Java <正式版版本号> 仍未发布,${versioncode} 为其第 <计数> 个快照。[/size]
  283. [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`;
  284.  
  285. case VersionType.PreRelease:
  286. return `[color=#388e3c][size=5]|[/size][/color][size=4][b]Minecraft Java 版[/b]是指 WindowsMac OS Linux 平台上,使用 Java 语言开发的 Minecraft 版本。[/size]
  287. [color=#388e3c][size=5]|[/size][/color][size=4][b]预发布版[/b]是 Minecraft Java 版的测试机制,如果该版本作为正式版发布,那么预发布版的游戏文件将与启动器推送的正式版完全相同。[/size]
  288. [color=#f44336][size=5]|[/size][/color][size=4]然而,预发布版主要用于服主和 Mod 制作者的预先体验,如果发现重大漏洞,该预发布版会被新的预发布版代替。因此建议普通玩家[color=Red]持观望态度[/color]。 [/size]
  289. [color=#f44336][size=5]|[/size][/color][size=4]Minecraft Java ${releaseversioncode} 仍未发布,${versioncode} 为其第 ${versioncount} 个预发布版。[/size]
  290. [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`;
  291.  
  292. case VersionType.ReleaseCandidate:
  293. return `[color=#388e3c][size=5]|[/size][/color][size=4][b]Minecraft Java 版[/b]是指运行在 WindowsMac OS Linux 平台上,使用 Java 语言开发的 Minecraft 版本。[/size]
  294. [color=#388e3c][size=5]|[/size][/color][size=4][b]候选版[/b]是 Minecraft Java 版正式版的候选版本,如果发现重大漏洞,该候选版会被新的候选版代替。如果一切正常,该版本将会作为正式版发布。[/size]
  295. [color=#f44336][size=5]|[/size][/color][size=4]候选版已可供普通玩家进行抢鲜体验,但仍需当心可能存在的漏洞。[/size]
  296. [color=#f44336][size=5]|[/size][/color][size=4]Minecraft Java ${releaseversioncode} 仍未发布,${versioncode} 为其第 ${versioncount} 个候选版。[/size]
  297. [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`;
  298.  
  299. case VersionType.Release:
  300. return `[color=#388e3c][size=5]|[/size][/color][size=4][b]Minecraft Java 版[/b]是指运行在 WindowsMac OS Linux 平台上,使用 Java 语言开发的 Minecraft 版本。[/size]
  301. [color=#f44336][size=5]|[/size][/color][size=4][b]正式版[/b]是 Minecraft Java 版经过一段时间的预览版测试后得到的稳定版本,也是众多纹理、Mod 与服务器插件会逐渐跟进的版本。官方启动器也会第一时间进行推送。 [/size]
  302. [color=#f44336][size=5]|[/size][/color][size=4]建议玩家与服主关注其相关服务端、Mod 与插件的更新,迎接新的正式版吧!专注于单人原版游戏的玩家可立即更新,多人游戏玩家请关注您所在服务器的通知。[/size]
  303. [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`;
  304.  
  305. case VersionType.BedrockRelease:
  306. return `[color=#388e3c][size=5]|[/size][/color][size=4][b]Minecraft 基岩版[/b]是指运行在移动平台(Android、iOS)、Windows 10/11、主机(Xbox OneSwitchPlayStation 4/5)上,使用「基岩引擎」(C++语言)开发的 Minecraft 版本。[/size]
  307. [color=#f44336][size=5]|[/size][/color][size=4][b]正式版[/b]是 Minecraft 基岩版经过一段时间的测试版测试之后得到的稳定版本,也是众多纹理、附加包和 Realms 会逐渐跟进的版本。与此同时 Google PlayMicrosoft Store 等官方软件商店也会推送此次更新。 [/size]
  308. [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`;
  309.  
  310. case VersionType.BedrockBeta:
  311. return `[color=#388e3c][size=5]|[/size][/color][size=4][b]Minecraft 基岩版[/b]是指运行在移动平台(Android、iOS)、Windows 10/11、主机(Xbox OneSwitchPlayStation 4/5)上,使用「基岩引擎」(C++语言)开发的 Minecraft 版本。[/size]
  312. [color=#388e3c][size=5]|[/size][/color][size=4][b]测试版[/b]是 Minecraft 基岩版的测试机制,主要用于下一个正式版的特性预览。[/size]
  313. [color=#f44336][size=5]|[/size][/color][size=4][b]然而,测试版主要用于新特性展示,通常存在大量漏洞。因此对于普通玩家建议仅做测试尝鲜用。使用测试版打开存档前请务必备份。适用于正式版的领域服务器与测试版不兼容。[/b] [/size]
  314. [color=#f44336][size=5]|[/size][/color][size=4]如果在测试版中遇到旧版存档无法使用的问题,测试版将允许你将存档上传以供开发团队查找问题。[/size]
  315. [color=#f44336][size=5]|[/size][/color][size=4]Minecraft 基岩版 <正式版版本号> 仍未发布,Beta & Preview ${versioncode} 为其第 <计数> 个测试版。[/size]
  316. [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`;
  317.  
  318. case VersionType.Normal:
  319. default:
  320. 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`;
  321. }
  322. }
  323. function getFooter(articleType, type) {
  324. const time = new Date();
  325.  
  326. function padTime(time) {
  327. return time.toString().padStart(2, "0");
  328. }
  329.  
  330. function toHoursAndMinutes(totalMinutes) {
  331. const m = Math.abs(totalMinutes);
  332. const minutes = m % 60;
  333. const hours = Math.floor(m / 60);
  334. return `${totalMinutes < 0 ? "+" : "-"}${padTime(hours)}${padTime(
  335. minutes
  336. )}`;
  337. }
  338.  
  339. const poweredBy = `\n[center][size=1][color=Silver]Powered by SPXXHi ${spxxhiVersion} with love
  340. Converted at ${time.getFullYear()}-${
  341. padTime(time.getMonth() + 1) // why +1 javascript
  342. }-${padTime(time.getDate())} ${padTime(time.getHours())}:${padTime(
  343. time.getMinutes()
  344. )} ${toHoursAndMinutes(time.getTimezoneOffset())}[/color][/size][/center]`;
  345.  
  346. /*Same contents,change if necessary.**/
  347.  
  348. switch (type) {
  349. case VersionType.Snapshot:
  350. return `\n${poweredBy}\n[hr][color=#388e3c][size=5]|[/size][/color][size=4][b]想了解更多游戏资讯?[/b][/size][list][*][size=3][url=https://www.himcbbs.com/forums/news/][color=#388e3c][u]HiMCBBS - 新闻资讯版块[/u][/color][/url][/size][/list]`;
  351.  
  352. case VersionType.PreRelease:
  353. return `\n${poweredBy}\n[hr][color=#388e3c][size=5]|[/size][/color][size=4][b]想了解更多游戏资讯?[/b][/size][list][*][size=3][url=https://www.himcbbs.com/forums/news/][color=#388e3c][u]HiMCBBS - 新闻资讯版块[/u][/color][/url][/size][/list]`;
  354.  
  355. case VersionType.ReleaseCandidate:
  356. return `\n${poweredBy}\n[hr][color=#388e3c][size=5]|[/size][/color][size=4][b]想了解更多游戏资讯?[/b][/size][list][*][size=3][url=https://www.himcbbs.com/forums/news/][color=#388e3c][u]HiMCBBS - 新闻资讯版块[/u][/color][/url][/size][/list]`;
  357.  
  358. case VersionType.Release:
  359. return `\n${poweredBy}\n[hr][color=#388e3c][size=5]|[/size][/color][size=4][b]想了解更多游戏资讯?[/b][/size][list][*][size=3][url=https://www.himcbbs.com/forums/news/][color=#388e3c][u]HiMCBBS - 新闻资讯版块[/u][/color][/url][/size][/list]`;
  360.  
  361. case VersionType.BedrockRelease:
  362. return `\n${poweredBy}\n[hr][color=#388e3c][size=5]|[/size][/color][size=4][b]想了解更多游戏资讯?[/b][/size][list][*][size=3][url=https://www.himcbbs.com/forums/news/][color=#388e3c][u]HiMCBBS - 新闻资讯版块[/u][/color][/url][/size][/list]`;
  363.  
  364. case VersionType.BedrockBeta:
  365. return `\n${poweredBy}\n[hr][color=#388e3c][size=5]|[/size][/color][size=4][b]想了解更多游戏资讯?[/b][/size][list][*][size=3][url=https://www.himcbbs.com/forums/news/][color=#388e3c][u]HiMCBBS - 新闻资讯版块[/u][/color][/url][/size][/list]`;
  366.  
  367. case VersionType.Normal:
  368. return `\n${poweredBy}\n[hr][color=#388e3c][size=5]|[/size][/color][size=4][b]想了解更多游戏资讯?[/b][/size][list][*][size=3][url=https://www.himcbbs.com/forums/news/][color=#388e3c][u]HiMCBBS - 新闻资讯版块[/u][/color][/url][/size][/list]`;
  369. }
  370. }
  371. let VersionType;
  372.  
  373. (function (VersionType) {
  374. VersionType[(VersionType["Snapshot"] = 1)] = "Snapshot";
  375. VersionType[(VersionType["PreRelease"] = 2)] = "PreRelease";
  376. VersionType[(VersionType["ReleaseCandidate"] = 3)] = "ReleaseCandidate";
  377. VersionType[(VersionType["Release"] = 4)] = "Release";
  378. VersionType[(VersionType["Normal"] = 5)] = "Normal";
  379. VersionType[(VersionType["BedrockBeta"] = 6)] = "BedrockBeta";
  380. VersionType[(VersionType["BedrockRelease"] = 7)] = "BedrockRelease";
  381. })(VersionType || (VersionType = {}));
  382.  
  383. const translators = {
  384. headings: (input, ctx) => {
  385. return translator(input, ctx, [
  386. [/Block of the Week: /gi, "本周方块:"],
  387. [/Taking Inventory: /gi, "背包盘点:"],
  388. [/Around the Block: /gi, "群系漫游:"],
  389. [/A Minecraft Java Snapshot/gi, "Minecraft Java版 快照"],
  390. [/A Minecraft Java Pre-Release/gi, "Minecraft Java版 预发布版"],
  391. [/A Minecraft Java Release Candidate/gi, "Minecraft Java版 候选版本"],
  392. [/Minecraft Beta (?:-|——) (.*?) \((.*?)\)/gi, "Minecraft 基岩版 Beta $1($2)"],
  393. [/Minecraft Beta & Preview - (.*?)/g, "Minecraft 基岩版 Beta & Preview $1"],
  394. [/Minecraft (?:-|——) (.*?) \(Bedrock\)/gi, "Minecraft 基岩版 $1"],
  395. [/Minecraft (?:-|——) (.*?) \((.*?) Only\)/gi, "Minecraft 基岩版 $1(仅$2)"],
  396. [/Minecraft (?:-|——) (.*?) \((.*?)\)/gi, "Minecraft 基岩版 $1(仅$2)"],
  397. [/Marketplace/gi, "市场"],
  398. [/Data-Driven/gi, "数据驱动"],
  399. [/Graphical/gi, "图像"],
  400. [/Vanilla /gi, "原版"],
  401. [/Player/gi, "玩家"],
  402. [/Experimental /gi, "实验性"],
  403. [/Mobs/gi, "生物"],
  404. [/Features and Bug Fixes/gi, "特性和漏洞修复"],
  405. [/ADVANCEMENTS/gi, "进度"],
  406. [/Accessibility/gi, "辅助功能"],
  407. [/Gameplay/gi, "玩法"],
  408. [/Items/gi, "物品"],
  409. [/Blocks/gi, "方块"],
  410. [/User Interface/gi, "用户界面"],
  411. [/Commands/gi, "命令"],
  412. [/Known Issues/gi, "已知问题"],
  413. [/Character Creator/gi, "角色创建器"],
  414. [/ Components/gi, "组件"],
  415. [/General/gi, "通用"],
  416. [/Technical Experimental Updates/gi, "实验性技术性更新"],
  417. [/Gametest Framework/gi, "Gametest 框架"],
  418. [/Gametest Framework (experimental)/gi, "Gametest 框架(实验性)"],
  419. [/Minecraft Snapshot /gi, "Minecraft 快照 "],
  420. [/ Pre-Release /gi, "-pre"],
  421. [/ Release Candidate /gi, "-rc"],
  422. [/Get the Release Candidate/gi, "获取预发布版本"],
  423. [/Get the Release/gi, "获取正式版"],
  424. [/Get the Pre-Release/gi, "获取候选版本"],
  425. [/Get the Snapshot/gi, "获取快照版本"],
  426. [/New Features in ([^\r\n]+)/gi, "$1 的新增特性"],
  427. [/Technical changes in ([^\r\n]+)/gi, "$1 的技术性修改"],
  428. [/Changes in ([^\r\n]+)/gi, "$1 的修改内容"],
  429. [/Fixed bugs in ([^\r\n]+)/gi, "$1 修复的漏洞"],
  430. [/STABILITY AND PERFORMANCE/gi, "性能与稳定性"],
  431. [/FEATURES AND BUG FIXES/gi, "特性和漏洞修复"],
  432. [/LOOT/gi, "战利品"],
  433. [/PARITY/gi, "趋同"],
  434. [/Components/gi, "组件"],
  435. [/ADD-ONS AND SCRIPT ENGINE/gi, "附加包和脚本引擎"],
  436. [/DRESSING ROOM/gi, "更衣室"],
  437. [/Item/gi, "物品"],
  438. [/CHANGES/gi, "改动"],
  439. [/SOUNDS/gi, "音效"],
  440. [/DATA PACK VERSION/gi, "数据包版本"],
  441. [/PREDICATES/gi, "谓词"],
  442. [/ PREDICATE/gi, "谓词"],
  443. [/EFFECT/gi, "效果"],
  444. [/COMMAND/gi, "命令"],
  445. [/ATTRIBUTE/gi, "属性"],
  446. [/BLOCK/gi, "方块"],
  447. [/ENTITY/gi, "实体"],
  448. [/ENCHANTMENTS/gi, "附魔"],
  449. [/ TAGS/gi, "标签"],
  450. [/TAGS/gi, "标签"],
  451. [/TYPE/gi, "类型"],
  452. [/MUSIC/gi, "音乐"],
  453. [/GAME TIPS/gi, "游戏提示"],
  454. [/NEW FEATURES/gi, "新特性"],
  455. [/NEW /gi, "新的"],
  456. [/USER INTERFACE/gi, "用户界面"],
  457. [/EDITOR/gi, "编辑器"],
  458. [/FIXES/gi, "修复"],
  459. [/IMPROVEMENTS/gi, "改进"],
  460. [/RESOURCE PACK VERSION/gi, "资源包版本"],
  461. [/SHADERS/gi, "着色器"],
  462. [/PARTICLES/gi, "粒子效果"],
  463. [/TOUCH CONTROLS/gi, "触控"],
  464. [/TECHNICAL UPDATES/gi, "技术性更新"],
  465. [/ TABLES/gi, "表"],
  466. [/PROJECTILES/gi, "弹射物"],
  467. [/STRUCTURES/gi, "结构"],
  468. [/ENTITIES/gi, "实体"],
  469. [/FUNCTIONS/gi, "函数"],
  470. ]);
  471. },
  472. imgCredits: (input, ctx) => {
  473. return translator(input, ctx, [
  474. [/Image credit:/gi, "图片来源:"],
  475. [/CC BY-NC-ND/gi, "知识共享 署名-非商业性使用-禁止演绎"],
  476. [/CC BY-NC-SA/gi, "知识共享 署名-非商业性使用-相同方式共享"],
  477. [/CC BY-NC/gi, "知识共享 署名-非商业性使用"],
  478. [/CC BY-ND/gi, "知识共享 署名-禁止演绎"],
  479. [/CC BY-SA/gi, "知识共享 署名-相同方式共享"],
  480. [/CC BY/gi, "知识共享 署名"],
  481. [/Public Domain/gi, "公有领域"],
  482. ]);
  483. },
  484. punctuation: (input, ctx) => {
  485. return translator(
  486. input,
  487. ctx,
  488. [
  489. [/\[i]/gi, "[i]"],
  490. [/\[\/i]/g, "[/i]"],
  491. ...(ctx.disablePunctuationConverter
  492. ? []
  493. : [
  494. [/,( |$)/g, ","],
  495. [/!( |$)/g, "!"],
  496. [/\.\.\.( |$)/g, "…"],
  497. [/\.( |$)/g, "。"],
  498. [/\?( |$)/g, "?"],
  499. [/( |^)-( |$)/g, " —— "],
  500. ]),
  501. ],
  502. (input) => {
  503. return quoteTreatment(input, [["“", "”", /"/]]);
  504. }
  505. );
  506. },
  507. code: (input, ctx) => {
  508. return quoteTreatment(input, [
  509. [
  510. '[bgcolor=#f1edec][color=Silver][font=Unifont][/font][/color][/bgcolor]',
  511. "`",
  512. /`/,
  513. ],
  514. ]);
  515. },
  516. };
  517. function translate(input, ctx, type) {
  518. if (typeof type === "string") {
  519. type = [type];
  520. }
  521.  
  522. for (const t of type) {
  523. input = translators[t](input, ctx);
  524. }
  525.  
  526. return input;
  527. }
  528.  
  529. function quoteTreatment(input, quoteArrays) {
  530. for (const quoteArray of quoteArrays) {
  531. const split = input.split(quoteArray[2]);
  532. input = "";
  533.  
  534. for (let i = 0; i < split.length - 1; i++) {
  535. const element = split[i];
  536. input += element + quoteArray[i % 2];
  537. }
  538.  
  539. input += split[split.length - 1];
  540. }
  541.  
  542. return input;
  543. }
  544.  
  545. function translator(input, ctx, mappings, treatment = (input) => input) {
  546. // REPLACE!!!!1
  547. for (const mapping of mappings) {
  548. input = input.replace(mapping[0], mapping[1]);
  549. }
  550.  
  551. treatment(input, ctx);
  552. return input;
  553. }
  554.  
  555. const converters = {
  556. /**
  557. * Converts a ChildNode to a BBCode string according to the type of the node.
  558. */
  559. convert: async (node, ctx) => {
  560. if (node.classList?.contains("spxxhi-userscript-ignored")) {
  561. return "";
  562. } // Listing all possible elements in the document
  563.  
  564. switch (node.nodeName) {
  565. case "A":
  566. return converters.a(node, ctx);
  567.  
  568. case "B":
  569. case "STRONG":
  570. return converters.strong(node, ctx);
  571.  
  572. case "BLOCKQUOTE":
  573. return converters.blockquote(node, ctx);
  574.  
  575. case "BR":
  576. return converters.br();
  577.  
  578. case "CITE":
  579. return converters.cite(node, ctx);
  580.  
  581. case "CODE":
  582. return converters.code(node, ctx);
  583.  
  584. case "DIV":
  585. case "SECTION":
  586. return converters.div(node, ctx);
  587.  
  588. case "DD":
  589. return converters.dd(node, ctx);
  590.  
  591. case "DL":
  592. return converters.dl(node, ctx);
  593.  
  594. case "DT":
  595. return converters.dt();
  596.  
  597. case "EM":
  598. return converters.em(node, ctx);
  599.  
  600. case "H1":
  601. return converters.h1(node, ctx);
  602.  
  603. case "H2":
  604. return converters.h2(node, ctx);
  605.  
  606. case "H3":
  607. return converters.h3(node, ctx);
  608.  
  609. case "H4":
  610. return converters.h4(node, ctx);
  611.  
  612. case "I":
  613. return converters.i(node, ctx);
  614.  
  615. case "IMG":
  616. return converters.img(node);
  617.  
  618. case "LI":
  619. return converters.li(node, ctx);
  620.  
  621. case "OL":
  622. return converters.ol(node, ctx);
  623.  
  624. case "P":
  625. return converters.p(node, ctx);
  626.  
  627. case "PICTURE":
  628. return converters.picture(node, ctx);
  629.  
  630. case "PRE":
  631. return converters.pre(node, ctx);
  632.  
  633. case "SPAN":
  634. return converters.span(node, ctx);
  635.  
  636. case "TABLE":
  637. return converters.table(node, ctx);
  638.  
  639. case "TBODY":
  640. return converters.tbody(node, ctx);
  641.  
  642. case "TH":
  643. case "TD":
  644. return converters.td(node, ctx);
  645.  
  646. case "TR":
  647. return converters.tr(node, ctx);
  648.  
  649. case "UL":
  650. return converters.ul(node, ctx);
  651.  
  652. case "#text":
  653. if (node) {
  654. if (ctx.multiLineCode) {
  655. return node.textContent ? node.textContent : "";
  656. } else
  657. return node.textContent
  658. .replace(/[\n\r\t]+/g, "")
  659. .replace(/\s{2,}/g, "");
  660. } else {
  661. return "";
  662. }
  663.  
  664. case "H5":
  665. return converters.h5(node, ctx);
  666.  
  667. case "BUTTON":
  668. case "NAV":
  669. case "svg":
  670. case "SCRIPT":
  671. if (node) {
  672. return node.textContent ? node.textContent : "";
  673. } else {
  674. return "";
  675. }
  676. case "FIGURE":
  677. return converters.figure(node, ctx);
  678.  
  679. default:
  680. console.warn(`Unknown type: '${node.nodeName}'.`);
  681.  
  682. if (node) {
  683. return node.textContent ? node.textContent : "";
  684. } else {
  685. return "";
  686. }
  687. }
  688. },
  689.  
  690. /**
  691. * Convert child nodes of an HTMLElement to a BBCode string.
  692. */
  693. recurse: async (ele, ctx) => {
  694. let ans = "";
  695.  
  696. if (!ele) {
  697. return ans;
  698. }
  699.  
  700. for (const child of Array.from(ele.childNodes)) {
  701. ans += await converters.convert(child, ctx);
  702. }
  703.  
  704. return ans;
  705. },
  706. a: async (anchor, ctx) => {
  707. const url = resolveUrl(anchor.href);
  708. let ans;
  709.  
  710. if (url) {
  711. ans = `[url=${url}][color=#388d40][u]${await converters.recurse(
  712. anchor,
  713. ctx
  714. )}[/u][/color][/url]`;
  715. } else {
  716. ans = await converters.recurse(anchor, ctx);
  717. }
  718.  
  719. return ans;
  720. },
  721. blockquote: async (ele, ctx) => {
  722. const prefix = "";
  723. const suffix = "";
  724. const ans = `${prefix}${await converters.recurse(ele, ctx)}${suffix}`;
  725. return ans;
  726. },
  727. br: async () => {
  728. const ans = "\n";
  729. return ans;
  730. },
  731. cite: async (ele, ctx) => {
  732. const prefix = "—— ";
  733. const suffix = "";
  734. const ans = `${prefix}${await converters.recurse(ele, ctx)}${suffix}`;
  735. return ans;
  736. },
  737. code: async (ele, ctx) => {
  738. if (!ele || !await converters.recurse(ele, {
  739. ...ctx,
  740. disablePunctuationConverter: true,
  741. })) {
  742. return '';
  743. }
  744. const prefix = ctx.multiLineCode
  745. ? "[code]"
  746. : "[bgcolor=#f1edec][color=#7824c5][font=Unifont]";
  747. const suffix = ctx.multiLineCode
  748. ? "[/code]"
  749. : "[/font][/color][/bgcolor]";
  750. const ans = `${prefix}${await converters.recurse(ele, {
  751. ...ctx,
  752. disablePunctuationConverter: true,
  753. })}${suffix}`;
  754. return ans;
  755. },
  756. div: async (ele, ctx) => {
  757. let ans = await converters.recurse(ele, ctx);
  758.  
  759. if (ele.classList.contains("text-center")) {
  760. ans = `[center]${ans}[/center]\n`;
  761. } else if (ele.classList.contains("article-image-carousel")) {
  762. /*const prefix = `[/indent][/indent][album]\n`;
  763. const suffix = `\n[/album][indent][indent]\n`;*/
  764.  
  765. const slides = [];
  766. const findSlides = async (ele) => {
  767. if (ele.classList.contains("slick-cloned")) {
  768. return;
  769. }
  770.  
  771. if (
  772. ele.nodeName === "IMG" &&
  773. ele.classList.contains("article-image-carousel__image")
  774. ) {
  775. slides.push([resolveUrl(ele.src), " "]);
  776. } else if (
  777. ele.nodeName === "DIV" &&
  778. ele.classList.contains("article-image-carousel__caption")
  779. ) {
  780. if (slides.length > 0) {
  781. slides[slides.length - 1][1] = `[b]${await converters.recurse(
  782. ele,
  783. ctx
  784. )}[/b]`;
  785. }
  786. } else {
  787. for (const child of Array.from(ele.childNodes)) {
  788. if (child.nodeName === "DIV" || child.nodeName === "IMG") {
  789. await findSlides(child);
  790. }
  791. }
  792. }
  793. };
  794.  
  795. await findSlides(ele);
  796.  
  797. if (slides.length > 0) {
  798. ans = `[center]${slides
  799. .map(([url, caption]) => `[img]${url}[/img]\n${caption}`)
  800. .join("\n")}[/center]\n`;
  801. } else {
  802. ans = "";
  803. }
  804. } else if (ele.classList.contains("video")) {
  805. ans = "\n[center]<无法获取的视频,如有可用视频源,请在此处插入>\n<对于B站视频,可使用 [bilibili] 代码>[/center]\n";
  806. } else if (
  807. ele.classList.contains("quote") ||
  808. ele.classList.contains("attributed-quote")
  809. ) {
  810. ans = `\n[quote]\n${ans}\n[/quote]\n`;
  811. } else if (ele.classList.contains("article-social")) {
  812. ans = "";
  813. } else if (ele.classList.contains("modal")) {
  814. ans = "";
  815. }
  816.  
  817. return ans;
  818. },
  819. dt: async () => {
  820. // const ans = `${converters.rescure(ele)}:`
  821. // return ans
  822. return "";
  823. },
  824. dl: async (ele, ctx) => {
  825. const ans = `\n\n${await converters.recurse(
  826. ele,
  827. ctx
  828. )}\n【本文排版借助了:[url=https://github.com/cinder0601/SPXXHi][color=#388d40][u]SPXXHi[/u][/color][/url] 用户脚本 v${spxxhiVersion}】\n\n`;
  829. return ans;
  830. },
  831. dd: async (ele, ctx) => {
  832. let ans = "";
  833.  
  834. if (ele.classList.contains("pubDate")) {
  835. // Published:
  836. // `pubDate` is like '2019-03-08T10:00:00.876+0000'.
  837. const date = ele.attributes.getNamedItem("data-value");
  838.  
  839. if (date) {
  840. ans = `[b]【${ctx.translator} 译自[url=${
  841. ctx.url
  842. }][color=#388d40][u]官网 ${date.value.slice(
  843. 0,
  844. 4
  845. )} ${date.value.slice(5, 7)} ${date.value.slice(
  846. 8,
  847. 10
  848. )} 日发布的 ${ctx.title}[/u][/color][/url];原作者 ${
  849. ctx.author
  850. }】[/b]`;
  851. } else {
  852. ans = `[b]【${ctx.translator} 译自[url=${ctx.url}][color=#388d40][u]官网 * * * 日发布的 ${ctx.title}[/u][/color][/url]】[/b]`;
  853. }
  854. } else {
  855. // Written by:
  856. ctx.author = await converters.recurse(ele, ctx);
  857. }
  858.  
  859. return ans;
  860. },
  861. em: async (ele, ctx) => {
  862. const ans = `[i]${await converters.recurse(ele, ctx)}[/i]`;
  863. return ans;
  864. },
  865. h1: async (ele, ctx) => {
  866. const prefix = "[size=6][b]";
  867. const suffix = "[/b][/size]";
  868. const processNode = async (node) => {
  869. if (node.nodeType === Node.ELEMENT_NODE) {
  870. return await converters[node.tagName.toLowerCase()](node, ctx);
  871. } else if (node.nodeType === Node.TEXT_NODE) {
  872. return node.nodeValue;
  873. }
  874. };
  875. const rawInnerArray = await Promise.all(
  876. Array.from(ele.childNodes).map(processNode)
  877. );
  878. const rawInner = rawInnerArray.join("");
  879. const inner = makeUppercaseHeader(rawInner);
  880. const ans = `${prefix}[color=Silver]${usingSilver(inner).replace(
  881. /[\n\r]+/g,
  882. " "
  883. )}[/color]${suffix}\n${prefix}${translate(`${inner}`, ctx, [
  884. "headings",
  885. "punctuation",
  886. ]).replace(/[\n\r]+/g, " ")}${suffix}\n\n`;
  887. return ans;
  888. },
  889. h2: async (ele, ctx) => {
  890. if (isBlocklisted(ele.textContent)) return "";
  891. const prefix = "[size=5][b]";
  892. const suffix = "[/b][/size]";
  893. const processNode = async (node) => {
  894. if (node.nodeType === Node.ELEMENT_NODE) {
  895. return await converters[node.tagName.toLowerCase()](node, ctx);
  896. } else if (node.nodeType === Node.TEXT_NODE) {
  897. return node.nodeValue;
  898. }
  899. };
  900. const rawInnerArray = await Promise.all(
  901. Array.from(ele.childNodes).map(processNode)
  902. );
  903. const rawInner = rawInnerArray.join("");
  904. const inner = makeUppercaseHeader(rawInner);
  905. const ans = `\n${prefix}[color=Silver]${usingSilver(inner).replace(
  906. /[\n\r]+/g,
  907. " "
  908. )}[/color]${suffix}\n${prefix}${translate(`${inner}`, ctx, [
  909. "headings",
  910. "punctuation",
  911. ]).replace(/[\n\r]+/g, " ")}${suffix}\n\n`;
  912. return ans;
  913. },
  914. h3: async (ele, ctx) => {
  915. const prefix = "[size=4][b]";
  916. const suffix = "[/b][/size]";
  917. const processNode = async (node) => {
  918. if (node.nodeType === Node.ELEMENT_NODE) {
  919. return await converters[node.tagName.toLowerCase()](node, ctx);
  920. } else if (node.nodeType === Node.TEXT_NODE) {
  921. return node.nodeValue;
  922. }
  923. };
  924. const rawInnerArray = await Promise.all(
  925. Array.from(ele.childNodes).map(processNode)
  926. );
  927. const rawInner = rawInnerArray.join("");
  928. const inner = makeUppercaseHeader(rawInner);
  929. const ans = `\n${prefix}[color=Silver]${usingSilver(inner).replace(
  930. /[\n\r]+/g,
  931. " "
  932. )}[/color]${suffix}\n${prefix}${translate(`${inner}`, ctx, [
  933. "headings",
  934. "punctuation",
  935. ]).replace(/[\n\r]+/g, " ")}${suffix}\n\n`;
  936. return ans;
  937. },
  938. h4: async (ele, ctx) => {
  939. const prefix = "[size=3][b]";
  940. const suffix = "[/b][/size]";
  941. const processNode = async (node) => {
  942. if (node.nodeType === Node.ELEMENT_NODE) {
  943. return await converters[node.tagName.toLowerCase()](node, ctx);
  944. } else if (node.nodeType === Node.TEXT_NODE) {
  945. return node.nodeValue;
  946. }
  947. };
  948. const rawInnerArray = await Promise.all(
  949. Array.from(ele.childNodes).map(processNode)
  950. );
  951. const rawInner = rawInnerArray.join("");
  952. const inner = makeUppercaseHeader(rawInner);
  953. const ans = `\n${prefix}[color=Silver]${usingSilver(inner).replace(
  954. /[\n\r]+/g,
  955. " "
  956. )}[/color]${suffix}\n${prefix}${inner}${suffix}\n\n`;
  957. return ans;
  958. },
  959. h5: async (ele, ctx) => {
  960. const prefix = "[size=2][b]";
  961. const suffix = "[/b][/size]";
  962. const processNode = async (node) => {
  963. if (node.nodeType === Node.ELEMENT_NODE) {
  964. return await converters[node.tagName.toLowerCase()](node, ctx);
  965. } else if (node.nodeType === Node.TEXT_NODE) {
  966. return node.nodeValue;
  967. }
  968. };
  969. const rawInnerArray = await Promise.all(
  970. Array.from(ele.childNodes).map(processNode)
  971. );
  972. const rawInner = rawInnerArray.join("");
  973. const inner = makeUppercaseHeader(rawInner);
  974. const ans = `\n${prefix}[color=Silver]${usingSilver(inner).replace(
  975. /[\n\r]+/g,
  976. " "
  977. )}[/color]${suffix}\n${prefix}${inner}${suffix}\n\n`;
  978. return ans;
  979. },
  980. i: async (ele, ctx) => {
  981. const ans = `[i]${await converters.recurse(ele, ctx)}[/i]`;
  982. return ans;
  983. },
  984. img: async (img) => {
  985. let host = location.host;
  986.  
  987. let w;
  988. let h;
  989.  
  990. if (img.classList.contains("attributed-quote__image")) {
  991. // for in-quote avatar image
  992. h = 92;
  993. w = 53;
  994. } else if (img.classList.contains("mr-3")) {
  995. // for attributor avatar image
  996. h = 121;
  997. w = 82;
  998. }
  999.  
  1000. const prefix = w && h ? `[img=${w},${h}]` : "[img]";
  1001. const imgUrl = resolveUrl(img.src);
  1002. if (imgUrl === "") return ""; // in case of empty image
  1003. let ans = `[center]${prefix}${imgUrl}[/img][/center]\n`; //Left aligning is too ugly.
  1004. return ans;
  1005. },
  1006. li: async (ele, ctx) => {
  1007. let ans;
  1008. let nestedList = false;
  1009.  
  1010. for (const child of ele.childNodes) {
  1011. if (child.nodeName === "OL" || child.nodeName === "UL") {
  1012. nestedList = true;
  1013. }
  1014. }
  1015.  
  1016. if (nestedList) {
  1017. // Nested lists.
  1018. let theParagragh = "";
  1019. let theList = "";
  1020. let addingList = false;
  1021.  
  1022. for (let i = 0; i < ele.childNodes.length - 1; i++) {
  1023. let nodeName = ele.childNodes[i].nodeName;
  1024.  
  1025. if (nodeName === "OL" || nodeName === "UL") {
  1026. addingList = true;
  1027. }
  1028.  
  1029. if (!addingList) {
  1030. const paragraghNode = await converters.convert(ele.childNodes[i], {
  1031. ...ctx,
  1032. inList: true,
  1033. });
  1034. theParagragh = `${theParagragh}${paragraghNode}`;
  1035. } else {
  1036. const listNode = await converters.convert(ele.childNodes[i], {
  1037. ...ctx,
  1038. inList: true,
  1039. });
  1040. theList = `${theList}${listNode}`;
  1041. }
  1042. }
  1043.  
  1044. ans = `[*][color=Silver]${usingSilver(
  1045. theParagragh
  1046. )}[/color]\n[*]${translate(
  1047. translateBugs(theParagragh, ctx),
  1048. ctx,
  1049. "code"
  1050. )}\n${theList}`;
  1051. } else if (isBlocklisted(ele.textContent)) {
  1052. return "";
  1053. } else {
  1054. const inner = await converters.recurse(ele, { ...ctx, inList: true });
  1055. ans = `[*][color=Silver]${usingSilver(inner)}[/color]\n[*]${translate(
  1056. translateBugs(inner, ctx),
  1057. ctx,
  1058. "code"
  1059. )}\n`;
  1060. }
  1061.  
  1062. return ans;
  1063. },
  1064. ol: async (ele, ctx) => {
  1065. const inner = await converters.recurse(ele, ctx);
  1066. const ans = `[list=1]\n${inner}[/list]\n`;
  1067. return ans;
  1068. },
  1069. p: async (ele, ctx) => {
  1070. const processNode = async (node) => {
  1071. if (node.nodeType === Node.ELEMENT_NODE) {
  1072. const converter = converters[node.tagName.toLowerCase()];
  1073. if (converter) {
  1074. return await converter(node, ctx);
  1075. }
  1076. } else if (node.nodeType === Node.TEXT_NODE) {
  1077. return node.nodeValue;
  1078. }
  1079. };
  1080.  
  1081. let inner = await converters.recurse(ele, ctx);
  1082. inner = inner ? inner.trim() : "";
  1083.  
  1084. let ans;
  1085.  
  1086. if (inner === "") {
  1087. return "";
  1088. }
  1089.  
  1090. if (ele.style.textAlign === "center") {
  1091. ans = `[center][size=2][color=Silver]${usingSilver(
  1092. inner
  1093. )}[/color][/size]\n${translate(inner, ctx, [
  1094. "punctuation",
  1095. "imgCredits",
  1096. ])}[/center]\n`;
  1097. } else if (ele.classList.contains("lead")) {
  1098. ans = `[b][size=2][color=Silver]${inner}[/color][/size][/b]\n[size=4][b]${translate(
  1099. inner,
  1100. ctx,
  1101. "headings"
  1102. )}[/b][/size]\n`;
  1103. } else if (
  1104. ele.querySelector("strong") !== null &&
  1105. ele.querySelector("strong").textContent === "Posted:"
  1106. ) {
  1107. return "";
  1108. } else if (isBlocklisted(inner)) {
  1109. return "";
  1110. } else if (ele.innerHTML.trim() === "&nbsp;") {
  1111. return "";
  1112. } else if (
  1113. /\s{0,}/.test(inner) &&
  1114. ele.querySelectorAll("img").length === 1
  1115. ) {
  1116. return inner;
  1117. } else {
  1118. if (ctx.inList) {
  1119. ans = inner;
  1120. } else {
  1121. ans = `[size=2][color=Silver]${usingSilver(
  1122. inner
  1123. )}[/color][/size]\n${translate(inner, ctx, [
  1124. "punctuation",
  1125. "imgCredits",
  1126. ])}\n\n`;
  1127. }
  1128. }
  1129.  
  1130. return ans;
  1131. },
  1132. picture: async (ele, ctx) => {
  1133. const ans = await converters.recurse(ele, ctx);
  1134. return ans;
  1135. },
  1136. figure: async (ele, ctx) => {
  1137. const ans = await converters.recurse(ele, ctx);
  1138. return ans;
  1139. },
  1140. pre: async (ele, ctx) => {
  1141. const ans = await converters.recurse(ele, {
  1142. ...ctx,
  1143. multiLineCode: true,
  1144. });
  1145. return ans;
  1146. },
  1147. span: async (ele, ctx) => {
  1148. const ans = await converters.recurse(ele, ctx);
  1149.  
  1150. if (ele.classList.contains("MC_Effect_TextHighlightA")) {
  1151. // Special for MC_Effect_TextHighlightA element.
  1152. const textContent = await converters.recurse(ele, ctx);
  1153. const prefix =
  1154. "[bgcolor=#f1edec][color=#7824c5][font=Unifont]";
  1155. const suffix = "[/font][/color][/bgcolor]";
  1156. return `${prefix}${textContent}${suffix}`;
  1157. } else if (ele.classList.contains("MC_Effect_TextHighlightB")) {
  1158. // Special for MC_Effect_TextHighlightB element.
  1159. const textContent = await converters.recurse(ele, ctx);
  1160. const prefix =
  1161. "[bgcolor=#f1edec][color=#7824c5][font=Unifont]";
  1162. const suffix = "[/font][/color][/bgcolor]";
  1163. return `${prefix}${textContent}${suffix}`;
  1164. } else if (ele.classList.contains("bedrock-server")) {
  1165. // Inline code.
  1166. const prefix =
  1167. "[bgcolor=#f1edec][color=#7824c5][font=Unifont]";
  1168. const suffix = "[/font][/color][/bgcolor]";
  1169. return `${prefix}${await converters.recurse(ele, {
  1170. ...ctx,
  1171. disablePunctuationConverter: true,
  1172. })}${suffix}`;
  1173. } else if (ele.classList.contains("strikethrough")) {
  1174. // Strikethrough text.
  1175. const prefix = "[s]";
  1176. const suffix = "[/s]";
  1177. return `${prefix}${ans}${suffix}`;
  1178. } else if (
  1179. ele.childElementCount === 1 &&
  1180. ele.firstElementChild.nodeName === "IMG"
  1181. ) {
  1182. // Image.
  1183. const img = ele.firstElementChild;
  1184. return await converters.img(img);
  1185. }
  1186.  
  1187. return ans;
  1188. },
  1189. strong: async (ele, ctx) => {
  1190. const ans = `[b]${await converters.recurse(ele, ctx)}[/b]`;
  1191. return ans;
  1192. },
  1193. table: async (ele, ctx) => {
  1194. const ans = `\n[table]\n${await converters.recurse(ele, ctx)}[/table]\n`;
  1195. return ans;
  1196. },
  1197. tbody: async (ele, ctx) => {
  1198. const ans = await converters.recurse(ele, ctx);
  1199. return ans;
  1200. },
  1201. td: async (ele, ctx) => {
  1202. const ans = `[td]${await converters.recurse(ele, ctx)}[/td]`;
  1203. return ans;
  1204. },
  1205. tr: async (ele, ctx) => {
  1206. const ans = `[tr]${await converters.recurse(ele, ctx)}[/tr]\n`;
  1207. return ans;
  1208. },
  1209. ul: async (ele, ctx) => {
  1210. const inner = await converters.recurse(ele, ctx);
  1211. const ans = `[list]\n${inner}[/list]\n`;
  1212. return ans;
  1213. },
  1214. };
  1215. /**
  1216. * Resolve relative URLs.
  1217. */
  1218.  
  1219. function resolveUrl(url) {
  1220. if (url[0] === "/") {
  1221. return `https://${location.host}${url}`;
  1222. } else {
  1223. return url;
  1224. }
  1225. }
  1226. function usingSilver(text) {
  1227. return text.replace(/#388d40/g, "Silver").replace(/#7824c5/g, "Silver");
  1228. }
  1229. function makeUppercaseHeader(header) {
  1230. let retStr = "";
  1231. let idx = 0;
  1232. let bracket = 0;
  1233.  
  1234. for (let i = 0; i < header.length; i++) {
  1235. if (header[i] == "[") {
  1236. if (bracket == 0) {
  1237. retStr = retStr.concat(header.substring(idx, i).toUpperCase());
  1238. idx = i;
  1239. }
  1240.  
  1241. bracket++;
  1242. } else if (header[i] == "]") {
  1243. if (bracket <= 1) {
  1244. retStr = retStr.concat(header.substring(idx, i + 1));
  1245. idx = i + 1;
  1246. }
  1247.  
  1248. bracket = Math.max(0, bracket - 1);
  1249. }
  1250. }
  1251.  
  1252. if (bracket > 0) {
  1253. console.error("bracket not closed!");
  1254. retStr = retStr.concat(header.substring(idx, header.length));
  1255. } else {
  1256. retStr = retStr.concat(
  1257. header.substring(idx, header.length).toUpperCase()
  1258. );
  1259. }
  1260.  
  1261. return retStr;
  1262. }
  1263. /**
  1264. * Get bugs from BugCenter.
  1265. * Guangyao and GitHub source are down, so I deleted them.
  1266. */
  1267.  
  1268. async function getBugs() {
  1269. return new Promise((rs, rj) => {
  1270. GM_xmlhttpRequest({
  1271. method: "GET",
  1272. url: bugsCenter,
  1273. fetch: true,
  1274. nocache: true,
  1275. timeout: 7_000,
  1276. onload: (r) => {
  1277. try {
  1278. rs(JSON.parse(r.responseText));
  1279. } catch (e) {
  1280. rj(e);
  1281. }
  1282. },
  1283. onabort: () => rj(new Error("Aborted")),
  1284. onerror: (e) => rj(e),
  1285. ontimeout: () => rj(new Error("Time out")),
  1286. });
  1287. });
  1288. }
  1289. async function getBugsTranslators() {
  1290. return new Promise((rs, rj) => {
  1291. GM_xmlhttpRequest({
  1292. method: "GET",
  1293. url: bugsTranslatorsTable,
  1294. fetch: true,
  1295. nocache: true,
  1296. timeout: 7_000,
  1297. onload: (r) => {
  1298. try {
  1299. rs(JSON.parse(r.responseText));
  1300. } catch (e) {
  1301. rj(e);
  1302. }
  1303. },
  1304. onabort: () => rj(new Error("Aborted")),
  1305. onerror: (e) => rj(e),
  1306. ontimeout: () => rj(new Error("Time out")),
  1307. });
  1308. });
  1309. }
  1310. async function getTranslatorColor() {
  1311. return new Promise((rs, rj) => {
  1312. GM_xmlhttpRequest({
  1313. method: "GET",
  1314. url: translatorColorTable,
  1315. fetch: true,
  1316. nocache: true,
  1317. timeout: 7_000,
  1318. onload: (r) => {
  1319. try {
  1320. rs(JSON.parse(r.responseText));
  1321. } catch (e) {
  1322. rj(e);
  1323. }
  1324. },
  1325. onabort: () => rj(new Error("Aborted")),
  1326. onerror: (e) => rj(e),
  1327. ontimeout: () => rj(new Error("Time out")),
  1328. });
  1329. });
  1330. }
  1331.  
  1332. function markdownToBbcode(value) {
  1333. return value.replace(
  1334. /`([^`]+)`/g,
  1335. "[bgcolor=#f1edec][color=#7824c5][font=Unifont]$1[/font][/color][/bgcolor]"
  1336. );
  1337. }
  1338. /**
  1339. * Replace untranslated bugs.
  1340. */
  1341.  
  1342. function translateBugs(str, ctx) {
  1343. if (
  1344. str.startsWith("[url=https://bugs.mojang.com/browse/MC-") &&
  1345. ctx.bugs != null // nullish
  1346. ) {
  1347. const id = str.slice(36, str.indexOf("]"));
  1348. const data = ctx.bugs[id];
  1349.  
  1350. if (data) {
  1351. let bugColor = "#388d40";
  1352.  
  1353. if (ctx.bugsTranslators[id]) {
  1354. const bugTranslator = ctx.bugsTranslators[id];
  1355.  
  1356. if (ctx.translatorColor[bugTranslator]) {
  1357. bugColor = ctx.translatorColor[bugTranslator];
  1358. }
  1359. }
  1360.  
  1361. const bbcode = markdownToBbcode(data);
  1362. return `[url=https://bugs.mojang.com/browse/${id}][b][color=${bugColor}]${id}[/color][/b][/url]- ${bbcode}`;
  1363. } else {
  1364. return str;
  1365. }
  1366. } else {
  1367. return str;
  1368. }
  1369. }
  1370. /**
  1371. * HiMCBBS does NOT support [album], the shouldUseAlbum function is removed temporarily.
  1372. */
  1373.  
  1374. /*function shouldUseAlbum(slides) {
  1375. return slides.length > 1 && slides.every(([_, caption]) => caption === ' ')
  1376. ;
  1377. }*/
  1378.  
  1379. function isBlocklisted(text) {
  1380. const blocklist = [
  1381. "Information on the Minecraft Preview and Beta:",
  1382. "While the version numbers between Preview and Beta are different, there is no difference in game content",
  1383. "These work-in-progress versions can be unstable and may not be representative of final version quality",
  1384. "Minecraft Preview is available on Xbox, Windows 10/11, and iOS devices. More information can be found at aka.ms/PreviewFAQ",
  1385. "The beta is available on Android (Google Play). To join or leave the beta, see aka.ms/JoinMCBeta for detailed instructions",
  1386. ];
  1387. return blocklist
  1388. .map((i) => {
  1389. return i.replace(/\p{General_Category=Space_Separator}*/, "");
  1390. })
  1391. .some((block) =>
  1392. text
  1393. .trim()
  1394. .trim()
  1395. .replace(/\p{General_Category=Space_Separator}*/, "")
  1396. .includes(block)
  1397. );
  1398. }
  1399.  
  1400. async function minecraftNet() {
  1401. const url = document.location.toString();
  1402.  
  1403. if (url.match(/^https:\/\/www\.minecraft\.net\/(?:[a-z-]+)\/article\//)) {
  1404. const authorContainer = document.querySelector(
  1405. ".MC_articleHeroA_attribution_author"
  1406. );
  1407. const dateElement = authorContainer.querySelector("dd:nth-child(4)"); // 获取发布日期的 dd 元素
  1408.  
  1409. const button = document.createElement("button");
  1410. button.classList.add("spxxhi-userscript-ignored");
  1411. button.innerText = "复制 BBCode (HiMCBBS)";
  1412. // 按钮样式设置
  1413. button.style.backgroundColor = "#3C8527";
  1414. button.style.color = "#FFFFFF";
  1415. button.style.border = "none";
  1416. button.style.padding = "10px 20px";
  1417. button.style.borderRadius = "5px";
  1418. button.style.fontSize = "16px";
  1419. button.style.cursor = "pointer";
  1420. button.style.transition = "background-color 0.3s ease";
  1421. button.style.fontFamily = "MinecraftTen, sans-serif";
  1422.  
  1423. button.style.width = "140px";
  1424. button.style.height = "70px";
  1425. button.style.textAlign = "center";
  1426. button.style.marginLeft = "auto";
  1427.  
  1428. button.onmouseover = () => {
  1429. button.style.backgroundColor = "#52A535";
  1430. };
  1431. button.onmouseout = () => {
  1432. button.style.backgroundColor = "#3C8527";
  1433. };
  1434. button.onclick = async () => {
  1435. button.innerText = "处理中...";
  1436. const bbcode = await convertMCArticleToBBCode(document, url);
  1437. GM_setClipboard(bbcode, {
  1438. type: "text",
  1439. mimetype: "text/plain",
  1440. });
  1441. button.innerText = "已复制!";
  1442. setTimeout(() => (button.innerText = "复制 BBCode (HiMCBBS)"), 5000);
  1443. };
  1444. const container = document.createElement("div");
  1445. container.id = "spxxhi-buttons";
  1446. container.style.display = "flex";
  1447. container.style.flexDirection = "column";
  1448. container.style.alignItems = "flex-end";
  1449. container.style.width = "100%";
  1450. container.style.padding = "10px";
  1451. container.style.boxSizing = "border-box";
  1452. container.append(button);
  1453. // 将按钮插入到日期下方
  1454. dateElement.insertAdjacentElement("afterend", button);
  1455. }
  1456. }
  1457.  
  1458. async function convertMCArticleToBBCode(
  1459. html,
  1460. articleUrl,
  1461. translator = config.translator
  1462. ) {
  1463. const articleType = getArticleType(html);
  1464. const versionType = getVersionType(articleUrl);
  1465. let bugs;
  1466.  
  1467. try {
  1468. bugs = await getBugs();
  1469. } catch (e) {
  1470. bugs = {};
  1471. console.error("[convertMCArticleToBBCode#getBugs]", e);
  1472. }
  1473.  
  1474. let bugsTranslators;
  1475.  
  1476. try {
  1477. bugsTranslators = await getBugsTranslators();
  1478. } catch (e) {
  1479. bugsTranslators = {};
  1480. console.error("[convertMCArticleToBBCode#getBugs]", e);
  1481. }
  1482.  
  1483. let translatorColor;
  1484.  
  1485. try {
  1486. translatorColor = await getTranslatorColor();
  1487. } catch (e) {
  1488. translatorColor = {};
  1489. console.error("[convertMCArticleToBBCode#getBugs]", e);
  1490. }
  1491.  
  1492. const header = getHeader(articleType, versionType);
  1493. const heroImage = getHeroImage(html, articleType);
  1494. const maintitle = await getMainTitle(html);
  1495. const subtitle = await getSubTitle(html);
  1496. let content = await getContent(html, {
  1497. bugs,
  1498. bugsTranslators,
  1499. translatorColor,
  1500. title: html.title.split(" | ").slice(0, -1).join(" | "),
  1501. date: null,
  1502. translator,
  1503. url: articleUrl,
  1504. });
  1505. const footer = getFooter(articleType, versionType);
  1506. const author = await getAuthor(html);
  1507. const ans = `${header}${heroImage}\n[center][color=silver][size=6][b]${maintitle}[/b][/size][/color][/center]\n[center][size=6][b]${maintitle}[/b][/size][/center]\n[center][color=silver][size=2]${subtitle}[/size][/color][/center]\n[center][size=2]${subtitle}[/size][/center]\n\n${content}[b]${author}\n\n${footer}`;
  1508. return ans;
  1509. }
  1510. /**
  1511. * Returns the type of the article.
  1512. */
  1513.  
  1514. function getArticleType(html) {
  1515. try {
  1516. const type =
  1517. html.getElementsByClassName("MC_articleHeroA_category")?.[0]
  1518. ?.textContent ?? "";
  1519. return type.toUpperCase();
  1520. } catch (e) {
  1521. console.error("[getArticleType]", e);
  1522. }
  1523.  
  1524. return "INSIDER";
  1525. }
  1526.  
  1527. /**
  1528. * Get the hero image (head image) of an article as the form of a BBCode string.
  1529. * @param html An HTML Document.
  1530. */
  1531.  
  1532. function getHeroImage(html, articleType) {
  1533. const category = articleType
  1534. ? `\n[center][bgcolor=Black][color=White][font=Unifont][b]${articleType}[/b][/font][/color][/bgcolor][/center]`
  1535. : "";
  1536. const img = html.getElementsByClassName("article-head__image")[0];
  1537.  
  1538. if (!img) {
  1539. return `\n[center]${category}[/center]\n`;
  1540. }
  1541.  
  1542. const src = img.src;
  1543. const ans = `[center][img=1200,513]${resolveUrl(
  1544. src
  1545. )}[/img]\n${category}[/center]\n`;
  1546. return ans;
  1547. }
  1548. /**
  1549. * Get the content of an article as the form of a BBCode string.
  1550. * @param html An HTML Document.
  1551. */
  1552.  
  1553. async function getSubTitle(html) {
  1554. let con = html.getElementsByClassName(
  1555. "MC_articleHeroA_header_container"
  1556. )[0];
  1557. let subtitle = con.getElementsByClassName(
  1558. "MC_articleHeroA_header_subheadline"
  1559. )[0].innerText;
  1560. return subtitle;
  1561. }
  1562. async function getMainTitle(html) {
  1563. let con = html.getElementsByClassName(
  1564. "MC_articleHeroA_header_container"
  1565. )[0];
  1566. let maintitle = con.getElementsByClassName("MC_Heading_1")[0].innerText;
  1567. return maintitle;
  1568. }
  1569.  
  1570. async function getAuthor(html, translator = config.translator) {
  1571. try {
  1572. let rawauthor = html.getElementsByClassName("MC_articleHeroA_attribution_author")[0];
  1573. if (!rawauthor) {
  1574. console.warn("Author attribution element not found");
  1575. return "Unknown Author";
  1576. }
  1577.  
  1578. let authorImgUrl = "";
  1579. let authorImg = rawauthor.getElementsByTagName("img")[0];
  1580. if (authorImg && authorImg.src) {
  1581. authorImgUrl = authorImg.src;
  1582. }
  1583.  
  1584. let authorName = "Unknown";
  1585. let authorNameElement = rawauthor.getElementsByTagName("dd")[0];
  1586. if (authorNameElement) {
  1587. authorName = authorNameElement.innerText;
  1588. }
  1589.  
  1590. let publishDate = "Unknown Date";
  1591. let publishDateElement = rawauthor.getElementsByTagName("dd")[1];
  1592. if (publishDateElement) {
  1593. publishDate = publishDateElement.innerText;
  1594. }
  1595.  
  1596. let [a, b, c] = publishDate.split("/");
  1597. let year, month, day;
  1598. if (a > 12) {
  1599. year = a;
  1600. month = b;
  1601. day = c;
  1602. } else {
  1603. year = "20" + c;
  1604. month = a;
  1605. day = b;
  1606. }
  1607. let url = window.location.href;
  1608. let title = await getMainTitle(html);
  1609.  
  1610. let ans = `\n${authorImgUrl ? `[left][img]${authorImgUrl}[/img][/left]\n\n\n` : ''}【${translator} 译自[url=${url}][color=#388d40][u]${authorName} ${year} ${month} ${day} 日发布的 ${title}[/u][/color][/url]】[/b]\n【本文排版借助了:[url=https://github.com/cinder0601/SPXXHi][color=#388d40][u]SPXXHi[/u][/color][/url] 用户脚本 v${spxxhiVersion}】`;
  1611. return ans;
  1612. } catch (error) {
  1613. console.error("Error in getAuthor function:", error);
  1614. return "Error retrieving author information";
  1615. }
  1616. }
  1617.  
  1618. async function getContent(html, ctx) {
  1619. let results = [];
  1620. let elements = document.querySelectorAll(
  1621. ".MC_articleGridA_container.MC_articleGridA_grid, .MC_Carousel_track_slide.MC_Theme_Vanilla.MC_Carousel_track_slide__active, .MC_Carousel_track_slide.MC_Theme_Vanilla:not(.MC_Carousel_track_slide__copy), .MC_Carousel_track_slide.MC_Theme_Legends.MC_Carousel_track_slide__active, .MC_Carousel_track_slide.MC_Theme_Legends:not(.MC_Carousel_track_slide__copy)"
  1622. );
  1623. let container = document.createElement("div");
  1624.  
  1625. let seenElements = new Set();
  1626.  
  1627. Array.from(elements).forEach((element) => {
  1628. let identifier = element.querySelector("img")?.src || element.innerHTML;
  1629. if (!seenElements.has(identifier)) {
  1630. seenElements.add(identifier);
  1631. if (element.classList.contains("MC_Carousel_track_slide")) {
  1632. let mediaDiv = element.querySelector(".MC_Carousel_track_slide_media");
  1633. if (mediaDiv) {
  1634. let clonedDiv = document.createElement("div");
  1635. clonedDiv.appendChild(mediaDiv.cloneNode(true));
  1636. container.appendChild(clonedDiv);
  1637. }
  1638. } else {
  1639. container.appendChild(element.cloneNode(true));
  1640. }
  1641. }
  1642. });
  1643.  
  1644. let containerElements = Array.from(container.children);
  1645.  
  1646. for (let i = 0; i < containerElements.length; i++) {
  1647. let rootDiv = containerElements[i];
  1648.  
  1649. let rootDivHTML = rootDiv.outerHTML.replace(
  1650. /<h[1-5]>(?:&nbsp;|\s)*<\/h[1-5]>/g,
  1651. ""
  1652. );
  1653. rootDiv = document.createElement("div");
  1654. rootDiv.innerHTML = rootDivHTML;
  1655.  
  1656. let spanElements = rootDiv.querySelectorAll("span");
  1657. spanElements.forEach((spanElement) => {
  1658. spanElement.innerHTML = spanElement.innerHTML.replace(/\n/g, " ");
  1659. });
  1660.  
  1661. let ans = await converters.recurse(rootDiv, ctx);
  1662. ans = ans
  1663. .replace(/([a-zA-Z0-9\-._])(\[[A-Za-z])/g, "$1 $2")
  1664. .replace(/(\[\/[^\]]+?])([a-zA-Z0-9\-._])/g, "$1 $2");
  1665. results.push(ans);
  1666. }
  1667.  
  1668. return results.join("\n\n");
  1669. }
  1670.  
  1671. function getZendesk(controlDOM, titleSlice, contentClass, versionType) {
  1672. const button = document.createElement("a");
  1673. button.classList.add("spxxhi-userscript-ignored", "navLink");
  1674. button.innerText = "复制 BBCode (HiMCBBS)";
  1675. // 按钮样式设置
  1676. button.style.backgroundColor = "#3C8527";
  1677. button.style.color = "#FFFFFF";
  1678. button.style.border = "none";
  1679. button.style.padding = "5px 10px";
  1680. button.style.borderRadius = "5px";
  1681. button.style.fontSize = "15px";
  1682. button.style.cursor = "pointer";
  1683. button.style.transition = "background-color 0.3s ease";
  1684.  
  1685. button.style.width = "120px";
  1686. button.style.height = "50px";
  1687. button.style.textAlign = "center";
  1688. button.style.marginLeft = "auto";
  1689.  
  1690. button.onmouseover = () => {
  1691. button.style.backgroundColor = "#52A535";
  1692. };
  1693. button.onmouseout = () => {
  1694. button.style.backgroundColor = "#3C8527";
  1695. };
  1696. button.onclick = async () => {
  1697. button.innerText = "处理中...";
  1698. const bbcode = await convertZendeskArticleToBBCode(
  1699. document,
  1700. location.href,
  1701. config.translator,
  1702. titleSlice,
  1703. contentClass,
  1704. versionType
  1705. );
  1706. GM_setClipboard(bbcode, {
  1707. type: "text",
  1708. mimetype: "text/plain",
  1709. });
  1710. button.innerText = "已复制!";
  1711. setTimeout(() => (button.innerText = "复制 BBCode (HiMCBBS)"), 5_000);
  1712. };
  1713. const container = document.createElement("div");
  1714. container.id = "spxxhi-buttons";
  1715. container.style.display = "flex";
  1716. container.style.flexDirection = "column";
  1717. container.style.alignItems = "flex-end";
  1718. container.style.width = "100%";
  1719. container.style.padding = "10px";
  1720. container.style.boxSizing = "border-box";
  1721. container.append(button);
  1722.  
  1723. controlDOM(button);
  1724. }
  1725.  
  1726. async function converthelpElementsToBBCode(elements, ctx) {
  1727. let bbcode = "";
  1728. const seenImages = new Set();
  1729.  
  1730. for (let element of elements) {
  1731. try {
  1732. let converted = await converters.recurse(element, ctx);
  1733. let imgTags = converted.match(/\[img](.*?)\[\/img]/g);
  1734. if (imgTags) {
  1735. for (let imgTag of imgTags) {
  1736. let imgUrl = imgTag.match(/\[img](.*?)\[\/img]/)[1];
  1737. if (seenImages.has(imgUrl)) {
  1738. converted = converted.replace(imgTag, "");
  1739. } else {
  1740. seenImages.add(imgUrl);
  1741. }
  1742. }
  1743. }
  1744.  
  1745. bbcode += converted + "\n";
  1746. } catch (error) {
  1747. console.error("Error converting content to BBCode:", error);
  1748. }
  1749. }
  1750. return bbcode;
  1751. }
  1752.  
  1753. function getHelpContent(controlDOM) {
  1754. const ctx = {
  1755. multiLineCode: false,
  1756. disablePunctuationConverter: false,
  1757. translator: config.translator,
  1758. url: window.location.href,
  1759. inList: false,
  1760. };
  1761. const heading = document.getElementsByClassName("article-page-heading");
  1762. const content = document.getElementsByClassName("article-page-body");
  1763.  
  1764. const button = document.createElement("a");
  1765. button.classList.add("spxxhi-userscript-ignored", "navLink");
  1766. button.innerText = "复制 BBCode (HiMCBBS)";
  1767. // 按钮样式设置
  1768. button.style.backgroundColor = "#3C8527";
  1769. button.style.color = "#FFFFFF";
  1770. button.style.border = "none";
  1771. button.style.padding = "5px 10px";
  1772. button.style.borderRadius = "5px";
  1773. button.style.fontSize = "15px";
  1774. button.style.cursor = "pointer";
  1775. button.style.transition = "background-color 0.3s ease";
  1776.  
  1777. button.style.width = "120px";
  1778. button.style.height = "50px";
  1779. button.style.textAlign = "center";
  1780. button.style.marginLeft = "auto";
  1781.  
  1782. button.onmouseover = () => {
  1783. button.style.backgroundColor = "#52A535";
  1784. };
  1785. button.onmouseout = () => {
  1786. button.style.backgroundColor = "#3C8527";
  1787. };
  1788. button.onclick = async () => {
  1789. button.innerText = "处理中...";
  1790. let bbcode = await converthelpElementsToBBCode(heading, ctx);
  1791. let title = bbcode;
  1792. title = title.replace(/\n/g, "");
  1793. bbcode = `[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[size=6][b][color=silver]${bbcode}[/color][/b][/size][size=6][b]${bbcode}[/b][/size]\n`;
  1794. bbcode += await converthelpElementsToBBCode(content, ctx);
  1795. bbcode += `[b]【${ctx.translator} 译自[url=${ctx.url}][color=#388d40][u] help.minecraft.net 上的 ${title}[/u][/color][/url]】[/b]\n【本文排版借助了:[url=https://github.com/cinder0601/SPXXHi][color=#388d40][u]SPXXHi[/u][/color][/url] 用户脚本 v${version}】\n`;
  1796. bbcode += getFooter("INSIDER", VersionType.Normal);
  1797. GM_setClipboard(bbcode, {
  1798. type: "text",
  1799. mimetype: "text/plain",
  1800. });
  1801. button.innerText = "已复制!";
  1802. setTimeout(() => (button.innerText = "复制 BBCode (HiMCBBS)"), 5000);
  1803. };
  1804. const container = document.createElement("div");
  1805. container.id = "spxxhi-buttons";
  1806. container.style.display = "flex";
  1807. container.style.flexDirection = "column";
  1808. container.style.alignItems = "flex-end";
  1809. container.style.width = "100%";
  1810. container.style.padding = "10px";
  1811. container.style.boxSizing = "border-box";
  1812. container.append(button);
  1813.  
  1814. controlDOM(button);
  1815. }
  1816.  
  1817. async function convertZendeskArticleToBBCode(
  1818. html,
  1819. articleUrl,
  1820. translator = config.translator,
  1821. titleSlice,
  1822. contentClass,
  1823. versionType
  1824. ) {
  1825. const title = html.title.slice(0, html.title.lastIndexOf(titleSlice));
  1826. const ctx = {
  1827. bugs: {},
  1828. title: title,
  1829. date: null,
  1830. translator,
  1831. url: articleUrl,
  1832. };
  1833. const content = await getZendeskContent(html, ctx, contentClass);
  1834. const posted = await getZendeskDate(location.href);
  1835. const header = versionType ? getHeader("news", versionType) : "";
  1836. const footer = versionType ? getFooter("news", versionType) : "";
  1837. const ans = `${header}[center][size=6][b][color=Silver]${title}[/color][/b][/size]
  1838. ${translate(
  1839. `[size=6][b]${title}[/b][/size]`,
  1840. ctx,
  1841. "headings"
  1842. )}[/center]\n\n${content}\n
  1843. [b]【${ctx.translator} 译自[url=${ctx.url}][color=#388d40][u]${
  1844. ctx.url.match(/https:\/\/(.*?)\//)[1]
  1845. } ${posted.year} ${posted.month} ${posted.day} 日发布的 ${
  1846. ctx.title
  1847. }[/u][/color][/url]】[/b]
  1848. 【本文排版借助了:[url=https://github.com/HiMCBBS/SPXXHi][color=#388d40][u]SPXXHi[/u][/color][/url] 用户脚本 v${spxxhiVersion}】\n\n${footer}`;
  1849. return ans;
  1850. }
  1851.  
  1852. async function getZendeskContent(html, ctx, contentClass) {
  1853. const rootSection = html.getElementsByClassName(contentClass)[0]; // Yep, this is the only difference.
  1854.  
  1855. let ans = await converters.recurse(rootSection, ctx); // Add spaces between texts and '[x'.
  1856.  
  1857. ans = ans.replace(/([a-zA-Z0-9\-._])(\[[A-Za-z])/g, "$1 $2"); // Add spaces between '[/x]' and texts.
  1858.  
  1859. ans = ans.replace(/(\[\/[^\]]+?])([a-zA-Z0-9\-._])/g, "$1 $2");
  1860. return ans;
  1861. }
  1862.  
  1863. async function getZendeskDate(url) {
  1864. const req = new Promise((rs, rj) => {
  1865. GM_xmlhttpRequest({
  1866. method: "GET",
  1867. url:
  1868. "/api/v2/help_center/en-us/articles/" +
  1869. url.match(/\/articles\/(\d+)/)[1],
  1870. fetch: true,
  1871. nocache: true,
  1872. timeout: 7_000,
  1873. onload: (r) => {
  1874. try {
  1875. rs(r.responseText);
  1876. } catch (e) {
  1877. rj(e);
  1878. }
  1879. },
  1880. onabort: () => rj(new Error("Aborted")),
  1881. onerror: (e) => rj(e),
  1882. ontimeout: () => rj(new Error("Time out")),
  1883. });
  1884. });
  1885. let res;
  1886. await req.then((value) => {
  1887. const rsp = JSON.parse(value);
  1888. res = new Date(rsp.article.created_at);
  1889. });
  1890. let year, month, day;
  1891. if (res.getFullYear() > 12) {
  1892. year = res.getFullYear();
  1893. month = res.getMonth() + 1;
  1894. day = res.getDate();
  1895. } else {
  1896. year = "20" + res.getDate();
  1897. month = res.getFullYear();
  1898. day = res.getMonth() + 1;
  1899. }
  1900. return {
  1901. year,
  1902. month,
  1903. day,
  1904. };
  1905. }
  1906.  
  1907. function feedback() {
  1908. let url = window.location.href; // 获取当前页面的URL
  1909. let versionType = getVersionType(url); // 调用getVersionType函数确定versionType
  1910.  
  1911. getZendesk(
  1912. (button) => {
  1913. document.querySelector(".topNavbar nav").append(button);
  1914. },
  1915. " – Minecraft Feedback",
  1916. "article-info",
  1917. versionType
  1918. );
  1919. }
  1920.  
  1921. function help() {
  1922. getHelpContent((button) => {
  1923. document.querySelector(".mc-globalbanner").append(button);
  1924. });
  1925. }
  1926. function twitter() {
  1927. const ProfilePictures = new Map([
  1928. ["Mojang", "https://s2.loli.net/2024/05/27/tMZKe4BmldgDv2P.jpg"],
  1929. ["MojangSupport", "https://s2.loli.net/2024/05/27/tMZKe4BmldgDv2P.jpg"],
  1930. ["MojangStatus", "https://s2.loli.net/2024/05/27/tMZKe4BmldgDv2P.jpg"],
  1931. ["Minecraft", "https://s2.loli.net/2024/05/27/6QsES9CwgKMLv7I.jpg"],
  1932. ["henrikkniberg", "https://s2.loli.net/2024/05/27/h2KGZEBks4XMTFq.png"],
  1933. ["_LadyAgnes", "https://s2.loli.net/2024/05/27/ZoJsth1i8randl9.png"],
  1934. ["kingbdogz", "https://s2.loli.net/2024/05/27/IH7aVTepiDXBZb2.png"],
  1935. ["JasperBoerstra", "https://s2.loli.net/2024/05/27/Jh2doD1eG7A56TR.png"],
  1936. ["adrian_ivl", "https://s2.loli.net/2024/05/27/itAL8hsGqk67cxS.png"],
  1937. ["slicedlime", "https://s2.loli.net/2024/05/27/DY6gQscuX8HiA9e.jpg"],
  1938. ["Cojomax99", "https://s2.loli.net/2024/05/27/DxeZ3rINFgTildA.png"],
  1939. ["Mojang_Ined", "https://s2.loli.net/2024/05/27/dzX3pYy8TSa7uJt.png"],
  1940. ["SeargeDP", "https://s2.loli.net/2024/05/27/nf7EKltTYXLgsxN.png"],
  1941. ["Dinnerbone", "https://s2.loli.net/2024/05/27/Q4ebCE29vFwPmxn.png"],
  1942. ["Marc_IRL", "https://s2.loli.net/2024/05/27/bcW5zXfQ84r9IO6.png"],
  1943. ["Mega_Spud", "https://s2.loli.net/2024/05/27/TZwzsJBRhnLyuFx.png"],
  1944. ["CornerHardMC", "https://s2.loli.net/2024/05/28/o4wLCvuRGi9Yxh7.png"],
  1945. ["MinecraftWikiEN", "https://s2.loli.net/2024/06/23/fn1SeWpdmit86lQ.png"],
  1946. ]); //More pictures can be added manually.
  1947. function getTweetMetadata() {
  1948. const tweetMetadata = {
  1949. date: "",
  1950. source: "",
  1951. text: "",
  1952. rawtext: "",
  1953. tweetLink: "",
  1954. urls: "",
  1955. userName: "",
  1956. userTag: "",
  1957. lang: "",
  1958. };
  1959. const url = window.location.href;
  1960. const regex = /https:\/\/x\.com\/([^/]+)\/status\/\d+/;
  1961. const match = url.match(regex);
  1962. tweetMetadata.userTag = match[1];
  1963. let posterNameContent = [];
  1964. const posterNameElements = document
  1965. .querySelector('div[data-testid="User-Name"] a span')
  1966. .querySelectorAll("span, img[alt]");
  1967. for (const element of posterNameElements) {
  1968. if (element.tagName.toLowerCase() === "span") {
  1969. posterNameContent.push(element.textContent);
  1970. } else if (element.tagName.toLowerCase() === "img" && element.alt) {
  1971. posterNameContent.push(element.alt);
  1972. }
  1973. }
  1974. tweetMetadata.userName = posterNameContent.join("");
  1975. let texts = [];
  1976. let rawTexts = [];
  1977.  
  1978. const articleDivs = document
  1979. .querySelector("article div[lang]")
  1980. .querySelectorAll("a, span, img[alt]");
  1981.  
  1982. for (const element of articleDivs) {
  1983. let textContent = "";
  1984. let rawContent = "";
  1985.  
  1986. if (element.tagName.toLowerCase() === "a") {
  1987. const url = element.href;
  1988. let linkText = element.textContent.trim();
  1989. const span = element.querySelector("span");
  1990. if (span) {
  1991. let spanContent = span.textContent.trim();
  1992. linkText = linkText.replace(spanContent, "").trim();
  1993. }
  1994. textContent = `[url=${url}][color=#00bfff][u]${linkText}[/u][/color][/url]`;
  1995. rawContent = linkText;
  1996. } else if (element.tagName.toLowerCase() === "span") {
  1997. if (
  1998. !element.closest("a") &&
  1999. element.querySelectorAll("a").length === 0
  2000. ) {
  2001. textContent = element.innerHTML;
  2002. rawContent = element.textContent;
  2003. }
  2004. } else if (element.tagName.toLowerCase() === "img" && element.alt) {
  2005. textContent = element.alt;
  2006. rawContent = element.alt;
  2007. }
  2008. if (textContent.trim()) {
  2009. texts.push(textContent);
  2010. rawTexts.push(rawContent.replace(/<a.*?>(.*?)<\/a>/g, "$1"));
  2011. }
  2012. }
  2013. tweetMetadata.text = texts.join("");
  2014. tweetMetadata.rawtext = rawTexts.join("");
  2015. //I have tried my best but failed, if it still returns 'http://' or 'https://', please add the link manually.
  2016. tweetMetadata.lang = document
  2017. .querySelector("article div[lang]")
  2018. .getAttribute("lang");
  2019. tweetMetadata.date = document.querySelector("time").innerHTML;
  2020. tweetMetadata.source = document.querySelector(
  2021. 'article a[role="link"] span'
  2022. ).innerText;
  2023. tweetMetadata.tweetLink = window.location.href;
  2024.  
  2025. return tweetMetadata;
  2026. }
  2027.  
  2028. function getTweetBbcode(tweet, mode) {
  2029. const attributeColor = "#5B7083";
  2030. const backgroundColor = mode === "dark" ? "#000000" : "#FFFFFF";
  2031. const foregroundColor = mode === "dark" ? "#D9D9D9" : "#0F1419";
  2032. const dateString = `${tweet.date} · ${tweet.source} · SPXXHi v${spxxhiVersion} · 转载请注明原作者及本帖地址`;
  2033. const content = tweet.text;
  2034. const content1 = tweet.rawtext;
  2035.  
  2036. return `[center][table=560,${backgroundColor}]
  2037. [tr][td][indent][font=Arial]
  2038. [left][img=44,44]${
  2039. ProfilePictures.get(tweet.userTag) || "<不支持的头像,请手动添加图片链接>"
  2040. }[/img][/left][size=15px][b][color=${foregroundColor}]${
  2041. tweet.userName
  2042. }[/color][/b]
  2043. [color=${attributeColor}]@${tweet.userTag}[/color][/size]
  2044.  
  2045. [color=Silver][size=23px]${content1}[/color][/size]
  2046.  
  2047. [size=15px][color=${attributeColor}]由 ${GM_config.get("translator")} 翻译自${
  2048. tweet.lang.startsWith("en") ? "英语" : ` ${tweet.lang}`
  2049. }[/color][/size]
  2050.  
  2051. [color=${foregroundColor}][size=23px]${content}[/size]
  2052. [/size][/color][/indent][center]<如有配图,请在此处添加>[/center]
  2053. [indent][size=15px][url=${
  2054. tweet.tweetLink
  2055. }][color=${attributeColor}][u]${dateString}[/u][/color][/url][/size][/indent][/td][/tr]
  2056. [/table][/center]`;
  2057. }
  2058.  
  2059. function x() {
  2060. console.info("[SPXXHi] Activated");
  2061.  
  2062. const buttonLight = document.createElement("button");
  2063. buttonLight.style.backgroundColor = "rgb(255, 255, 255)";
  2064. buttonLight.style.color = "#000000";
  2065. buttonLight.style.border = "none";
  2066. buttonLight.style.padding = "5px 10px";
  2067. buttonLight.style.borderRadius = "5px";
  2068. buttonLight.style.fontSize = "15px";
  2069. buttonLight.style.cursor = "pointer";
  2070. buttonLight.style.transition = "background-color 0.3s ease";
  2071. buttonLight.style.width = "180px";
  2072. buttonLight.style.height = "50px";
  2073. buttonLight.style.textAlign = "center";
  2074. buttonLight.style.marginLeft = "auto";
  2075.  
  2076. buttonLight.onmouseover = () => {
  2077. buttonLight.style.backgroundColor = "rgb(223, 223, 223)";
  2078. };
  2079. buttonLight.onmouseout = () => {
  2080. buttonLight.style.backgroundColor = "rgb(255, 255, 255)";
  2081. };
  2082. buttonLight.innerText = "复制 BBCode (HiMCBBS)(浅色)";
  2083. buttonLight.onclick = async () => {
  2084. buttonLight.innerText = "处理中...";
  2085. try {
  2086. const bbcode = getTweetBbcode(getTweetMetadata(), "light");
  2087. GM_setClipboard(bbcode, { type: "text", mimetype: "text/plain" });
  2088. buttonLight.innerText = "已复制!";
  2089. } catch (error) {
  2090. console.error("Error processing BBCode (Light):", error);
  2091. buttonLight.innerText = "错误!";
  2092. }
  2093. setTimeout(() => (buttonLight.innerText = "复制 BBCode (HiMCBBS)(浅色)"), 5000);
  2094. };
  2095.  
  2096. const buttonDark = document.createElement("button");
  2097. buttonDark.style.backgroundColor = "rgb(32, 32, 32)";
  2098. buttonDark.style.color = "#FFFFFF";
  2099. buttonDark.style.border = "none";
  2100. buttonDark.style.padding = "5px 10px";
  2101. buttonDark.style.borderRadius = "5px";
  2102. buttonDark.style.fontSize = "15px";
  2103. buttonDark.style.cursor = "pointer";
  2104. buttonDark.style.transition = "background-color 0.3s ease";
  2105. buttonDark.style.width = "180px";
  2106. buttonDark.style.height = "50px";
  2107. buttonDark.style.textAlign = "center";
  2108. buttonDark.style.marginLeft = "auto";
  2109.  
  2110. buttonDark.onmouseover = () => {
  2111. buttonDark.style.backgroundColor = "rgb(42, 42, 42)";
  2112. };
  2113. buttonDark.onmouseout = () => {
  2114. buttonDark.style.backgroundColor = "rgb(32, 32, 32)";
  2115. };
  2116. buttonDark.innerText = "复制 BBCode (HiMCBBS)(深色)";
  2117. buttonDark.onclick = async () => {
  2118. buttonDark.innerText = "处理中...";
  2119. try {
  2120. const bbcode = getTweetBbcode(getTweetMetadata(), "dark");
  2121. GM_setClipboard(bbcode, { type: "text", mimetype: "text/plain" });
  2122. buttonDark.innerText = "已复制!";
  2123. } catch (error) {
  2124. console.error("Error processing BBCode (Dark):", error);
  2125. buttonDark.innerText = "错误!";
  2126. }
  2127. setTimeout(() => (buttonDark.innerText = "复制 BBCode (HiMCBBS)(深色)"), 5000);
  2128. };
  2129.  
  2130. const checkLoaded = setInterval(() => {
  2131. const targetDiv = document.querySelector("article div[lang]");
  2132. if (targetDiv && !document.querySelector("#spxxhi-buttons")) {
  2133. const container = document.createElement("div");
  2134. container.id = "spxxhi-buttons";
  2135. container.style.display = "flex";
  2136. container.style.flexDirection = "column";
  2137. container.style.alignItems = "flex-end";
  2138. container.style.width = "100%";
  2139. container.style.padding = "10px";
  2140. container.style.boxSizing = "border-box";
  2141. container.append(buttonLight);
  2142. container.append(buttonDark);
  2143. targetDiv.parentElement.append(container);
  2144. clearInterval(checkLoaded);
  2145. }
  2146. }, 300);
  2147. }
  2148.  
  2149. x();
  2150. }
  2151. switch (location.host) {
  2152. case "www.minecraft.net": //Fuck minecraft.net what the heck are you doing.
  2153. minecraftNet();
  2154. break;
  2155.  
  2156. case "x.com":
  2157. twitter();
  2158. break;
  2159.  
  2160. case "feedback.minecraft.net":
  2161. feedback();
  2162. break;
  2163.  
  2164. case "help.minecraft.net":
  2165. help();
  2166. break;
  2167. }
  2168. })();
  2169. //# sourceMappingURL=bundle.user.js.map