diff --git a/apps/readest-app/src/__tests__/utils/style-get-styles.test.ts b/apps/readest-app/src/__tests__/utils/style-get-styles.test.ts index c7434ff1..435de828 100644 --- a/apps/readest-app/src/__tests__/utils/style-get-styles.test.ts +++ b/apps/readest-app/src/__tests__/utils/style-get-styles.test.ts @@ -790,13 +790,26 @@ describe('custom @font-face inlining (via getStyles)', () => { expect(css).toContain('blob:http://localhost/my-test-font'); }); - it('inlines the @font-face rules ahead of the rest of the stylesheet', () => { + it('keeps @namespace epub ahead of every @font-face rule (#4438)', () => { const vs = makeViewSettings(); const css = getStyles(vs, theme, [makeCustomFont()]); - // Paginator writes this CSS into the iframe before its first paint, - // so the custom @font-face must precede the font-family declarations - // that reference it (layout styles begin with `@namespace epub`). - expect(css.indexOf('@font-face')).toBeLessThan(css.indexOf('@namespace epub')); + // A `@namespace` rule is only honored when it precedes all style and + // `@font-face` rules; a misplaced one is silently ignored, which drops + // the namespaced `aside[epub|type~="footnote"]` selector and reveals the + // footnote aside's border as a stray horizontal line (#4438). The custom + // `@font-face` rules must therefore come after the namespace declaration. + expect(css).toContain('@namespace epub'); + expect(css).toContain('@font-face'); + expect(css.indexOf('@namespace epub')).toBeLessThan(css.indexOf('@font-face')); + }); + + it('still inlines custom @font-face ahead of the font-family declarations', () => { + const vs = makeViewSettings(); + const css = getStyles(vs, theme, [makeCustomFont()]); + // Paginator writes this CSS into the iframe before its first paint, so the + // custom `@font-face` rules should still precede the `--serif`/`--sans-serif` + // font lists that reference them. + expect(css.indexOf('@font-face')).toBeLessThan(css.indexOf('--serif:')); }); it('emits one @font-face per loaded font', () => { diff --git a/apps/readest-app/src/utils/style.ts b/apps/readest-app/src/utils/style.ts index f403498a..f7e1d5db 100644 --- a/apps/readest-app/src/utils/style.ts +++ b/apps/readest-app/src/utils/style.ts @@ -347,7 +347,6 @@ const getPageLayoutStyles = ( writingMode: string, vertical: boolean, ) => ` - @namespace epub "http://www.idpf.org/2007/ops"; html { --margin-top: ${marginTop}px; --margin-right: ${marginRight}px; @@ -867,7 +866,13 @@ export const getStyles = ( const warichuStyles = getWarichuStyles(); const rubyStyles = getRubyStyles(); const userStylesheet = viewSettings.userStylesheet!; - return `${customFontFaces}\n${pageLayoutStyles}\n${paragraphLayoutStyles}\n${fontStyles}\n${colorStyles}\n${translationStyles}\n${warichuStyles}\n${rubyStyles}\n${userStylesheet}`; + // The `@namespace` declaration must lead the stylesheet: a `@namespace` rule + // placed after any style or `@font-face` rule is invalid and silently ignored, + // which drops the namespaced `aside[epub|type~="footnote"]` selector and lets + // the footnote aside's border show as a stray horizontal line (#4438). Keep it + // ahead of the inlined custom `@font-face` rules. + const epubNamespace = `@namespace epub "http://www.idpf.org/2007/ops";`; + return `${epubNamespace}\n${customFontFaces}\n${pageLayoutStyles}\n${paragraphLayoutStyles}\n${fontStyles}\n${colorStyles}\n${translationStyles}\n${warichuStyles}\n${rubyStyles}\n${userStylesheet}`; }; // Build a CSS chunk of `@font-face` rules for the given user custom