From db5a460f6ff2102dfa064ab828405403e17099af Mon Sep 17 00:00:00 2001 From: boomzero Date: Fri, 2 Oct 2026 22:29:05 +0800 Subject: [PATCH 01/30] fix: preserve TeX and line breaks when copying contest Markdown --- XMOJ.user.js | 54 ++++++++++++++++++++++----- tests/markdown-copy.html | 80 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 tests/markdown-copy.html diff --git a/XMOJ.user.js b/XMOJ.user.js index 731c52ad..a1216d47 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -7683,8 +7683,30 @@ function GetMDText(element) { 'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR' ]); const cellTags = new Set(['TD', 'TH']); + const mathItems = new Map(); + const mathDocument = (typeof unsafeWindow === 'undefined' ? window : unsafeWindow).MathJax?.startup?.document; + // XMOJ also serves MathJax 3.0, before getMathItemsWithin was available. + const items = typeof mathDocument?.getMathItemsWithin === 'function' ? + mathDocument.getMathItemsWithin([element]) : mathDocument?.math || []; + for (const math of items) { + if (math.typesetRoot && element.contains(math.typesetRoot) && typeof math.math === 'string') { + mathItems.set(math.typesetRoot, math); + } + } + + function AppendBreak(count) { + if (!result) return; + const trailing = result.match(/\n*$/)[0].length; + if (trailing < count) result += '\n'.repeat(count - trailing); + } - function traverse(node) { + function AppendMath(tex, display) { + if (display) AppendBreak(2); + result += display ? '$$\n' + tex + '\n$$' : '$' + tex + '$'; + if (display) AppendBreak(2); + } + + function Traverse(node, inPre = false) { if (node.nodeType === Node.TEXT_NODE) { result += node.textContent; return; @@ -7696,9 +7718,24 @@ function GetMDText(element) { const tag = node.nodeName.toUpperCase(); + // Copy the source once, rather than the visual and accessibility trees. + const math = mathItems.get(node); + if (math) { + AppendMath(math.math, math.display); + return; + } + if (node.classList.contains('katex') || node.classList.contains('katex-display') || tag === 'MATH') { + const annotation = node.querySelector('annotation[encoding="application/x-tex"]'); + if (annotation) { + AppendMath(annotation.textContent, node.classList.contains('katex-display') || + node.parentElement?.classList.contains('katex-display') || node.getAttribute('display') === 'block'); + return; + } + } + // Preserve line breaks for
if (tag === 'BR') { - result += '\n'; + result += inPre ? '\n' : ' \n'; return; } @@ -7719,10 +7756,9 @@ function GetMDText(element) { const isBlock = blockTags.has(tag); const isCell = cellTags.has(tag); + const breaks = tag === 'LI' || tag === 'TR' ? 1 : 2; - if (isBlock && !result.endsWith('\n')) { - result += '\n'; - } + if (isBlock && !inPre) AppendBreak(breaks); // Keep table cells visually separated when copied as plain text. if (isCell && result.length > 0 && !result.endsWith('\n') && !result.endsWith('\t') && !result.endsWith(' ')) { @@ -7730,19 +7766,17 @@ function GetMDText(element) { } for (let child of node.childNodes) { - traverse(child); + Traverse(child, inPre || tag === 'PRE'); } if (isCell && !result.endsWith('\n') && !result.endsWith('\t')) { result += '\t'; } - if (isBlock && !result.endsWith('\n')) { - result += '\n'; - } + if (isBlock && !inPre) AppendBreak(breaks); } - traverse(element); + Traverse(element); return result; } diff --git a/tests/markdown-copy.html b/tests/markdown-copy.html new file mode 100644 index 00000000..ce45c280 --- /dev/null +++ b/tests/markdown-copy.html @@ -0,0 +1,80 @@ + + +Markdown copy regression tests +
Running…
+ From 2174975aa6f132259a815a7aabc570e6775a78ff Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 2 Oct 2026 14:30:16 +0000 Subject: [PATCH 02/30] 3.7.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 36d1d15b..0f76e5ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xmoj-script", - "version": "3.7.1", + "version": "3.7.2", "description": "an improvement script for xmoj.tech", "main": "AddonScript.js", "scripts": { From 32945a4e16a8f140b12c2eccebcba58984ea58cf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 2 Oct 2026 14:30:23 +0000 Subject: [PATCH 03/30] Update version info to 3.7.2 --- Update.json | 11 +++++++++++ XMOJ.user.js | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Update.json b/Update.json index bf40a3c4..d294b991 100644 --- a/Update.json +++ b/Update.json @@ -3791,6 +3791,17 @@ } ], "Notes": "No release notes were provided for this release." + }, + "3.7.2": { + "UpdateDate": 1790951417624, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 1031, + "Description": "fix: preserve TeX and line breaks in contest Markdown copy" + } + ], + "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 a1216d47..b29c455f 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name XMOJ -// @version 3.7.1 +// @version 3.7.2 // @description XMOJ增强脚本 // @author @XMOJ-Script-dev, @langningchen and the community // @namespace https://github/langningchen From ddad45a2b47f0b84c9a2583c99e6aa6ea4d26f12 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 2 Oct 2026 14:33:37 +0000 Subject: [PATCH 04/30] Update time and description of 3.7.2 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index d294b991..e14a675f 100644 --- a/Update.json +++ b/Update.json @@ -3793,7 +3793,7 @@ "Notes": "No release notes were provided for this release." }, "3.7.2": { - "UpdateDate": 1790951417624, + "UpdateDate": 1790951611448, "Prerelease": true, "UpdateContents": [ { From 01e8c72f2fbe74afa0f9760f3dd892fc6c56e2e1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 2 Oct 2026 14:34:33 +0000 Subject: [PATCH 05/30] Update time and description of 3.7.2 --- Update.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Update.json b/Update.json index e14a675f..1d0b7d25 100644 --- a/Update.json +++ b/Update.json @@ -3793,7 +3793,7 @@ "Notes": "No release notes were provided for this release." }, "3.7.2": { - "UpdateDate": 1790951611448, + "UpdateDate": 1790951668200, "Prerelease": true, "UpdateContents": [ { From 29f29882b2344c3761a403b7fb82b16a60f0684e Mon Sep 17 00:00:00 2001 From: boomzero Date: Fri, 2 Oct 2026 23:02:19 +0800 Subject: [PATCH 06/30] fix: address Markdown copy review feedback and automate browser tests --- .github/workflows/Test.yml | 2 ++ XMOJ.user.js | 30 +++++++++++-------- package.json | 5 +++- tests/markdown-copy.html | 46 ++++++++++++++++++++++++----- tests/markdown-copy.test.cjs | 57 ++++++++++++++++++++++++++++++++++++ 5 files changed, 119 insertions(+), 21 deletions(-) create mode 100644 tests/markdown-copy.test.cjs diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index 5f9faf8a..b7688d4f 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -15,4 +15,6 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 22 + - run: npm install --ignore-scripts + - run: npx playwright install --with-deps chromium - run: npm test diff --git a/XMOJ.user.js b/XMOJ.user.js index b29c455f..5e30c4c7 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -3898,7 +3898,7 @@ async function main() { CopyMDButton.type = "button"; document.querySelectorAll(".cnt-row-head.title")[i].appendChild(CopyMDButton); CopyMDButton.addEventListener("click", () => { - GM_setClipboard(GetMDText(Temp[i].children[0]).trim().replaceAll("\n\t", "\n").replaceAll("\n\n", "\n")); + GM_setClipboard(GetMDText(Temp[i].children[0]).trim().replaceAll("\n\t", "\n")); CopyMDButton.innerText = "复制成功"; setTimeout(() => { CopyMDButton.innerText = "复制"; @@ -7700,14 +7700,20 @@ function GetMDText(element) { if (trailing < count) result += '\n'.repeat(count - trailing); } - function AppendMath(tex, display) { - if (display) AppendBreak(2); - result += display ? '$$\n' + tex + '\n$$' : '$' + tex + '$'; - if (display) AppendBreak(2); + function AppendMath(tex, display, inCell) { + // Keep a table cell on its row, including its trailing tab separator. + if (display && !inCell) AppendBreak(2); + result += display ? (inCell ? '$$' + tex + '$$' : '$$\n' + tex + '\n$$') : '$' + tex + '$'; + if (display && !inCell) AppendBreak(2); } - function Traverse(node, inPre = false) { + function Traverse(node, inPre = false, inCell = false) { if (node.nodeType === Node.TEXT_NODE) { + // Serialized HTML often has indentation between blocks. Keep spaces + // between inline elements and all preformatted text, but skip that indentation. + if (!inPre && /^[ \t\r\n]*$/.test(node.textContent) && + (blockTags.has(node.previousSibling?.nodeName) || blockTags.has(node.nextSibling?.nodeName) || + (blockTags.has(node.parentElement?.nodeName) && (!node.previousSibling || !node.nextSibling)))) return; result += node.textContent; return; } @@ -7721,14 +7727,14 @@ function GetMDText(element) { // Copy the source once, rather than the visual and accessibility trees. const math = mathItems.get(node); if (math) { - AppendMath(math.math, math.display); + AppendMath(math.math, math.display, inCell); return; } if (node.classList.contains('katex') || node.classList.contains('katex-display') || tag === 'MATH') { const annotation = node.querySelector('annotation[encoding="application/x-tex"]'); if (annotation) { AppendMath(annotation.textContent, node.classList.contains('katex-display') || - node.parentElement?.classList.contains('katex-display') || node.getAttribute('display') === 'block'); + node.parentElement?.classList.contains('katex-display') || node.getAttribute('display') === 'block', inCell); return; } } @@ -7756,9 +7762,9 @@ function GetMDText(element) { const isBlock = blockTags.has(tag); const isCell = cellTags.has(tag); - const breaks = tag === 'LI' || tag === 'TR' ? 1 : 2; + const breaks = tag === 'TR' ? 1 : 2; - if (isBlock && !inPre) AppendBreak(breaks); + if (isBlock && !inPre && !inCell) AppendBreak(breaks); // Keep table cells visually separated when copied as plain text. if (isCell && result.length > 0 && !result.endsWith('\n') && !result.endsWith('\t') && !result.endsWith(' ')) { @@ -7766,14 +7772,14 @@ function GetMDText(element) { } for (let child of node.childNodes) { - Traverse(child, inPre || tag === 'PRE'); + Traverse(child, inPre || tag === 'PRE', inCell || isCell); } if (isCell && !result.endsWith('\n') && !result.endsWith('\t')) { result += '\t'; } - if (isBlock && !inPre) AppendBreak(breaks); + if (isBlock && !inPre && !inCell) AppendBreak(breaks); } Traverse(element); diff --git a/package.json b/package.json index 0f76e5ca..505eaa71 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,10 @@ "description": "an improvement script for xmoj.tech", "main": "AddonScript.js", "scripts": { - "test": "node --test tests/contest-web.test.cjs" + "test": "node --test tests/*.test.cjs" + }, + "devDependencies": { + "playwright": "1.59.1" }, "repository": { "type": "git", diff --git a/tests/markdown-copy.html b/tests/markdown-copy.html index ce45c280..0c22a339 100644 --- a/tests/markdown-copy.html +++ b/tests/markdown-copy.html @@ -4,21 +4,20 @@
Running…