From 6e4ee101a47380d6540a9e846ddd0a8a29114da6 Mon Sep 17 00:00:00 2001 From: boomzero Date: Sun, 18 Oct 2026 21:54:59 +0800 Subject: [PATCH 1/3] Apply ReplaceXM on the /web contest app The /web app returns before main(), whose body.innerHTML rewrite would break Vue, so ReplaceXM never ran there. Replace text nodes in #app and the title on every enhance pass instead, skipping code, samples and editors. Co-Authored-By: Claude Opus 5.5 --- XMOJ.user.js | 27 ++++++++++++++++++++++++++- tests/contest-web.test.cjs | 7 +++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index fe2d2685..93edaaff 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -159,6 +159,14 @@ function IsContestWebApp(pathname = location.pathname) { return pathname === "/web" || pathname.startsWith("/web/"); } +// The ReplaceXM substitutions of main(), in the same order, for the /web app's +// text nodes. The last two undo what "我" → "高老师" does to "我们" and "自我". +function ReplaceXMText(text) { + return String(text).replaceAll("我", "高老师").replaceAll("小明", "高老师").replaceAll("下海", "上海").replaceAll("海上", "上海") + .replaceAll("小红", "徐师娘").replaceAll("小粉", "彩虹").replaceAll("提交上节课的代码", "自动提交当年代码") + .replaceAll("高老师们", "我们").replaceAll("自高老师", "自我"); +} + function GetContestRoute(url = location.href) { const path = new URL(url, location.origin).pathname; const match = path.match(/^\/web\/contest(?:\/(\d+)(?:\/([^/]+)(?:\/(std|solution))?)?)?\/?$/); @@ -3470,6 +3478,20 @@ async function InitializeContestWebApp() { } } + // The legacy pages rewrite body.innerHTML, which would break Vue. Edit the text + // nodes instead; when Vue re-renders one, the observer brings us back here. + function ReplaceXM() { + const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { + acceptNode: node => node.parentElement.closest("script, style, textarea, pre, code, .monaco-editor") ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT + }); + for (let node = walker.nextNode(); node; node = walker.nextNode()) { + const text = ReplaceXMText(node.nodeValue); + if (text !== node.nodeValue) node.nodeValue = text; + } + const title = ReplaceXMText(document.title); + if (title !== document.title) document.title = title; + } + function ClearRoute() { controller?.abort(); countdowns.clear(); @@ -3844,7 +3866,10 @@ async function InitializeContestWebApp() { } } UpdateCountdowns(); - } finally { observer.observe(root, {childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ["class", "style"]}); } + } finally { + if (UtilityEnabled("ReplaceXM")) ReplaceXM(); + observer.observe(root, {childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ["class", "style"]}); + } } function ScheduleEnhance() { diff --git a/tests/contest-web.test.cjs b/tests/contest-web.test.cjs index 7b7d94f1..05a868e3 100644 --- a/tests/contest-web.test.cjs +++ b/tests/contest-web.test.cjs @@ -220,3 +220,10 @@ test('web and classic pages load the same monochrome fonts before either Bootstr assert.equal(styles.filter(node => node.id === 'xmoj-monochrome-fonts').length, 0, 'do not load fonts for a disabled skin'); } }); + +test('ReplaceXM substitutions keep 我们 and 自我, and are stable when re-applied', () => { + const {scope} = context(); + const once = scope.ReplaceXMText('小明和我们自我介绍:我在下海,小红和小粉也在海上'); + assert.equal(once, '高老师和我们自我介绍:高老师在上海,徐师娘和彩虹也在上海'); + assert.equal(scope.ReplaceXMText(once), once); +}); From a30a6a4868754f507cdf5beafce1e85f292a33c5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 18 Oct 2026 13:55:38 +0000 Subject: [PATCH 2/3] 4.0.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2cd9c93f..4818c7c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xmoj-script", - "version": "4.0.5", + "version": "4.0.6", "description": "an improvement script for xmoj.tech", "main": "AddonScript.js", "scripts": { From 91870689803a82940d44c56162ea78bb353b0db4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 18 Oct 2026 13:55:45 +0000 Subject: [PATCH 3/3] Update version info to 4.0.6 --- Update.json | 11 +++++++++++ XMOJ.user.js | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Update.json b/Update.json index 0ec791c5..415770ad 100644 --- a/Update.json +++ b/Update.json @@ -3999,6 +3999,17 @@ } ], "Notes": "调试模式现在欢迎所有人开启:开启后可以抢先体验测试版,出错时会用中文提示并告诉你如何反馈。
\n本地调试模式仅供开发者使用:开启时每个页面底部都会显示提示和一键关闭按钮,且不再参与云同步。" + }, + "4.0.6": { + "UpdateDate": 1792331739148, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 1071, + "Description": "Fix ReplaceXM (小明 → 高老师) on the /web contest pages" + } + ], + "Notes": "No release notes were provided for this release." } } } \ No newline at end of file diff --git a/XMOJ.user.js b/XMOJ.user.js index 93edaaff..fec4f881 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name XMOJ -// @version 4.0.5 +// @version 4.0.6 // @description XMOJ增强脚本 // @author @XMOJ-Script-dev, @langningchen and the community // @namespace https://github/langningchen