diff --git a/Update.json b/Update.json index a456d0f0..8dc5b4f8 100644 --- a/Update.json +++ b/Update.json @@ -3828,6 +3828,17 @@ } ], "Notes": "

修复新版比赛页面和旧版界面顶部导航栏持续增加窗口缩放监听器的问题,减少长时间打开页面后的内存占用和缩放卡顿;页面导航栏被替换时会清理旧监听器。修复下载页面的卡片被插入导航栏占位元素、导致首行被顶部导航栏遮挡的问题。新版 /web 页面与旧版页面共享黑白主题字体加载,避免菜单栏使用不同字体。

" + }, + "3.8.2": { + "UpdateDate": 1791075550636, + "Prerelease": true, + "UpdateContents": [ + { + "PR": 1039, + "Description": "fix: match Chinese navbar fonts between web and classic UI" + } + ], + "Notes": "

修复新版比赛页面与旧版页面导航栏中文字体不一致的问题。

" } } } \ No newline at end of file diff --git a/XMOJ.user.js b/XMOJ.user.js index 745489f9..ea1c87d8 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -1,6 +1,6 @@ // ==UserScript== // @name XMOJ -// @version 3.8.1 +// @version 3.8.2 // @description XMOJ增强脚本 // @author @XMOJ-Script-dev, @langningchen and the community // @namespace https://github/langningchen @@ -143,6 +143,9 @@ const MonochromeSkinCSS = ` --mono-border-thin: 1px solid var(--mono-gray-300); --mono-font-heading: 'Playfair Display', Georgia, serif; --mono-font-body: 'Source Serif 4', 'Source Serif Pro', Georgia, serif; + /* Source Serif has no Chinese glyphs. An explicit navbar fallback + avoids different fonts under legacy lang=en and /web lang=zh-CN. */ + --mono-font-navbar: 'Source Serif 4', 'Source Serif Pro', Georgia, 'PingFang SC', 'Microsoft YaHei', 'Noto Sans CJK SC', serif; --mono-font-mono: 'JetBrains Mono', 'Consolas', monospace; --mono-transition: 100ms ease; } @@ -197,6 +200,7 @@ const MonochromeSkinCSS = ` /* Navbar */ .navbar, nav.navbar { + font-family: var(--mono-font-navbar) !important; border-bottom: 4px solid var(--mono-black) !important; background-color: var(--mono-white) !important; opacity: 1 !important; @@ -204,7 +208,7 @@ const MonochromeSkinCSS = ` .navbar .nav-link { color: var(--mono-black) !important; text-decoration: none !important; - font-family: var(--mono-font-body) !important; + font-family: var(--mono-font-navbar) !important; text-transform: uppercase !important; letter-spacing: 0.05em !important; font-size: 0.85rem !important; @@ -2823,7 +2827,7 @@ class NavbarStyler { this.navbar = navbar; this.addedClasses = ['fixed-top', 'container', 'ml-auto'].filter(name => !navbar.classList.contains(name)); this.restoreNavbarStyles = this.preserveStyles(navbar, [ - 'position', 'border-top-left-radius', 'border-top-right-radius', + 'position', 'top', 'border-top-left-radius', 'border-top-right-radius', 'border-bottom-left-radius', 'border-bottom-right-radius', 'box-shadow', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left', 'max-width', 'background-color', 'opacity', 'z-index' @@ -2882,6 +2886,9 @@ class NavbarStyler { try { let n = this.navbar; n.classList.add('fixed-top', 'container', 'ml-auto'); + // The CDN fallback may still be loading when the top bar starts. Without + // Bootstrap's .fixed-top rule, top:auto moves it down with the spacer. + n.style.top = '0'; if (UtilityEnabled("MonochromeUI")) { Object.assign(n.style, { position: 'fixed', diff --git a/package.json b/package.json index cddb8886..834ca508 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xmoj-script", - "version": "3.8.1", + "version": "3.8.2", "description": "an improvement script for xmoj.tech", "main": "AddonScript.js", "scripts": { diff --git a/tests/fixtures/fonts/OFL.txt b/tests/fixtures/fonts/OFL.txt new file mode 100644 index 00000000..318074ec --- /dev/null +++ b/tests/fixtures/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2014 The Source Serif 4 Project Authors (https://github.com/adobe-fonts/source-serif) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/tests/fixtures/fonts/README.md b/tests/fixtures/fonts/README.md new file mode 100644 index 00000000..b4de304b --- /dev/null +++ b/tests/fixtures/fonts/README.md @@ -0,0 +1,10 @@ +This is the unmodified Latin subset of Source Serif 4 (regular, v15) served by +Google Fonts, the same font used by the monochrome theme. It is distributed under +the SIL Open Font License in OFL.txt. + +Source: https://fonts.gstatic.com/s/sourceserif4/v15/vEFy2_tTDB4M7-auWDN0ahZJW3IX2ih5nk3AucvUHf6OAVIJmeUDygwjihdqnhtdDs4DrjTMXw.woff2 +License: https://github.com/google/fonts/blob/main/ofl/sourceserif4/OFL.txt + +The navbar rendering regression embeds this 20 KB font offline. A local font or +an unloaded font does not reproduce Chromium's language-dependent Chinese +fallback for this webfont; checking computed font-family alone also misses it. diff --git a/tests/fixtures/fonts/source-serif-4-latin.woff2 b/tests/fixtures/fonts/source-serif-4-latin.woff2 new file mode 100644 index 00000000..12f5b101 Binary files /dev/null and b/tests/fixtures/fonts/source-serif-4-latin.woff2 differ diff --git a/tests/navbar-styler.test.cjs b/tests/navbar-styler.test.cjs index 371f2a48..2cdb28fe 100644 --- a/tests/navbar-styler.test.cjs +++ b/tests/navbar-styler.test.cjs @@ -186,3 +186,81 @@ test('downloads stay in the page container below the fixed navbar', {timeout: 60 await browser.close(); } }); + +test('Chinese navbar glyphs use the same fonts under legacy and web document languages', {timeout: 60000}, async () => { + const skin = source.match(/const MonochromeSkinCSS = `([\s\S]*?)`;/)[1]; + const font = fs.readFileSync(path.join(__dirname, "fixtures/fonts/source-serif-4-latin.woff2")).toString("base64"); + const browser = await chromium.launch({executablePath: process.env.XMOJ_CHROMIUM || undefined}); + try { + const rendered = []; + for (const language of ['en', 'zh-CN']) { + const page = await browser.newPage(); + // The public legacy page declares en, while /web declares zh-CN. Source + // Serif 4 has no Chinese glyphs: compare the fonts Chromium actually uses, + // not just getComputedStyle().fontFamily, which matched before the fix. + const html = ``; + await page.route("https://navbar-font.test/**", route => route.fulfill({contentType: "text/html; charset=utf-8", body: html})); + await page.goto("https://navbar-font.test/" + language); + await page.evaluate(() => document.fonts.ready); + const session = await page.context().newCDPSession(page); + await session.send('DOM.enable'); + await session.send('CSS.enable'); + const {root} = await session.send('DOM.getDocument'); + const fonts = {}; + for (const selector of ['.navbar-brand', '.navbar-nav .nav-link', '.dropdown-toggle span', '.dropdown-item']) { + const {nodeId} = await session.send('DOM.querySelector', {nodeId: root.nodeId, selector}); + const result = await session.send('CSS.getPlatformFontsForNode', {nodeId}); + fonts[selector] = result.fonts.map(font => ({name: font.familyName, glyphs: font.glyphCount})).sort((a, b) => a.name.localeCompare(b.name)); + assert.ok(fonts[selector].length, 'must inspect rendered glyphs: ' + selector); + } + assert.ok(fonts[".navbar-brand"].some(font => font.name === "Source Serif 4"), "the Latin webfont must be loaded to exercise Chinese fallback"); + rendered.push(fonts); + await session.detach(); + await page.close(); + } + assert.deepEqual(rendered[1], rendered[0], 'document language must not change the navbar Chinese fallback font'); + } finally { + await browser.close(); + } +}); + +test('navbar reserves content space before Bootstrap CSS loads and restores its original top', {timeout: 60000}, async () => { + const start = 'let navbarStyler = null;'; + const end = '// Wrapped in an async IIFE'; + const navbarSource = source.slice(source.indexOf(start), source.indexOf(end)); + const browser = await chromium.launch({executablePath: process.env.XMOJ_CHROMIUM || undefined}); + try { + for (const width of [375, 1440]) { + const page = await browser.newPage({viewport: {width, height: 800}}); + for (const mono of [false, true]) { + // The resource cache can be empty and the CDN fallback can be slow + // or fail. No .fixed-top rule is available yet in this fixture. + await page.setContent('
Page content
'); + await page.evaluate(mono => { window.UtilityEnabled = name => name === 'MonochromeUI' ? mono : name === 'NewTopBar'; }, mono); + await page.addScriptTag({content: '{\n' + navbarSource + '\nUpdateNavbarStyler(); window.testStyler = navbarStyler;\n}'}); + const Measure = () => page.evaluate(() => ({ + navbarTop: document.querySelector('nav').getBoundingClientRect().top, + navbarBottom: document.querySelector('nav').getBoundingClientRect().bottom, + contentTop: document.getElementById('content').getBoundingClientRect().top + })); + const before = await Measure(); + assert.equal(before.navbarTop, mono ? 0 : 16, 'the navbar must anchor to the viewport without Bootstrap'); + assert.ok(before.contentTop >= before.navbarBottom, 'the navbar must not cover content while CSS is unavailable'); + // When Bootstrap finally arrives, the reserved space still works. + await page.addStyleTag({content: '.fixed-top { position: fixed; top: 0; right: 0; left: 0; }'}); + const after = await Measure(); + assert.ok(after.contentTop >= after.navbarBottom); + await page.evaluate(() => window.testStyler.destroy()); + assert.equal(await page.locator('nav').evaluate(node => node.style.top), '', 'remove the owned offset on teardown'); + } + await page.close(); + } + const page = await browser.newPage(); + await page.setContent(''); + await page.evaluate(() => { window.UtilityEnabled = () => true; }); + await page.addScriptTag({content: navbarSource + '\nUpdateNavbarStyler(); navbarStyler.destroy();'}); + assert.deepEqual(await page.locator('nav').evaluate(node => [node.style.top, node.style.getPropertyPriority('top')]), ['7px', 'important']); + } finally { + await browser.close(); + } +});