Fix ReplaceXM (小明 → 高老师) on the /web contest pages

BO
1d ago
+46-3

What does this PR aim to accomplish?:

With "将网站中所有"小明"和"我"关键字替换为"高老师"…" (ReplaceXM) turned on, nothing was replaced on the /web contest pages (e.g. https://www.xmoj.tech/web/contest).

Cause: the /web app returns early into InitializeContestWebApp() and never reaches the body.innerHTML rewrite in main(). That is intentional, since rewriting the body would break Vue, but it left ReplaceXM with no /web implementation.

How does this PR accomplish the above?:

  • New ReplaceXMText() applies the same substitutions, in the same order, as main() (including the 我们 / 自我 fix-ups).
  • InitializeContestWebApp() gets a ReplaceXM() that walks the text nodes of #app and updates document.title. It runs at the end of every Enhance() pass, so whatever Vue re-renders is replaced again through the existing MutationObserver. It only writes a node when its text actually changes, so it does not loop.
  • pre, code, textarea, script, style and the Monaco editor are skipped, so samples and code that users copy stay unchanged. (The legacy innerHTML rewrite does touch those.)
  • The legacy main() path is unchanged. tests/account-settings.test.cjs slices main() out on its own, so it can't call the helper.
  • Added a unit test in tests/contest-web.test.cjs: 我们 / 自我 are preserved and re-applying the replacement changes nothing.

Does this PR need a documentation update?:

No


By submitting this pull request, I confirm the following:

  1. I have read and understood the contributor's guide, as well as this entire template. I understand which branch to base my commits and Pull Requests against. (dev for XMOJ-Script, master for everything else).
  2. I have commented on my proposed changes within the code.
  3. I have tested my changes.
  4. I am willing to help maintain this change if there are issues with it later.
  5. It is compatible with the GNU General Public License v3.0
  6. I have squashed any insignificant commits. (git rebase)
  7. I have checked that another pull request for this purpose does not exist.
  8. I have considered and confirmed that this submission will be valuable to others.
  9. I accept that this submission may not be used, and the pull request can be closed at the will of the maintainer.
  10. I give this submission freely and claim no ownership to its content.
  11. I have verified that my changes work correctly in all four UI combinations:
    • the script's monochrome UI on XMOJ's new UI (/web pages)
    • the script's monochrome UI on XMOJ's old/classic pages
    • the script's classic UI on XMOJ's new UI (/web pages)
    • the script's classic UI on XMOJ's old/classic pages

[!WARNING]
Item 11 is not verified yet. node --test tests/*.test.cjs passes (100/100). I ran tests/userscript-harness.cjs with {"ReplaceXM":true,"MonochromeUI":false} and with {"ReplaceXM":true,"MonochromeUI":true}. Both runs had no script errors, but they were logged out: every page, /web included, redirected to /loginpage.php, so the new code never ran on a /web page. A maintainer needs to check /web/contest, a contest, a problem and the rank page with ReplaceXM on, in both MonochromeUI modes, while logged in.


  • I have read the above and my PR is ready for review. Check this box to confirm

🤖 Generated with Claude Code

Summary by Sourcery

Extend ReplaceXM support to /web pages without disrupting Vue rendering.

Bug Fixes:

  • Fix ReplaceXM substitutions on Vue-based /web pages, including contest pages, while preserving the intended handling of 我们 and 自我.

Enhancements:

  • Apply replacements to rendered application text and document titles after each enhancement pass without altering code, samples, editor content, or other excluded elements.

Build:

  • Bump the project and userscript versions to 4.0.6.

Tests:

  • Add coverage verifying ReplaceXM substitutions remain stable when reapplied and preserve 我们 and 自我.

Chores:

  • Record the 4.0.6 update metadata.

Summary by cubic

Fixes ReplaceXM on /web contest pages, where the Vue app bypassed the legacy page-wide rewrite. Text in #app and the document title now uses the same substitutions without disrupting Vue rendering.

  • Reapplies replacements after Vue updates through the existing enhancement observer.
  • Skips code, samples, textareas, scripts, styles, and Monaco editor content.
  • Preserves 我们 and 自我 and verifies repeated replacement is stable.