Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 44 additions & 10 deletions packages/app-expo/assets/reader/reader.html
Original file line number Diff line number Diff line change
Expand Up @@ -2093,12 +2093,50 @@
const ps = Math.round(currentParagraphSpacing * layoutScale);

// When useBookFonts is enabled (default), do not force the reader
// font family onto every element so the book's own fonts (e.g.
// embedded @font-face families) render where the book specifies them.
const bodyStarFontOverride = currentUseBookFonts
? ''
: `:root:not(.vrtl):not(.vltr) body *:not(svg):not(svg *):not(math):not(math *):not(pre):not(pre *):not(code):not(code *):not(kbd):not(kbd *):not(samp):not(samp *) {
// font family onto html/body with !important: the book's own
// font-family (on html, body, or any element) must win where specified,
// so embedded @font-face families render. The reader font stays as a
// zero-specificity fallback via :where(html). Target only html (not
// body): body then INHERITS html's font-family, so when the book sets
// one on html it propagates to body text instead of body being pinned
// to the reader font by a direct rule. When disabled, force the reader
// font on html/body and every descendant.
//
// The monospace rule swaps sides with the same toggle: with book fonts
// honored it is zero-specificity (a book's own `pre { font-family: ... }`
// — its embedded code font — wins), and when overriding it needs BOTH
// !important AND higher specificity, because specificity still breaks
// ties between important author declarations: a plain `pre, code, kbd`
// at (0,0,1) loses to an authored `body pre { ... !important }` at
// (0,0,2). The guarded `:root ... body :is(...)` sits at (0,2,2).
// Inner code/kbd/samp INSIDE a pre get an explicit `inherit` instead:
// the UA stylesheet declares `code { font-family: monospace }` and a
// direct declaration (even a UA one) always beats inheritance, so
// exclusion alone would still cut `<pre><code>` off from the book's
// font declared on pre. The zero-specificity inherit restores the
// chain while any author declaration on the element still wins it.
// Standalone inline code keeps the monospace fallback.
// Vertical writing (.vrtl/.vltr) keeps its own behavior: the forced
// overrides carry the horizontal guard like the pre-existing rules.
const readerFontOverride = currentUseBookFonts
? `:where(html) {
font-family: var(--readany-font-family);
}
:where(pre, :not(pre) > code, :not(pre) > kbd, :not(pre) > samp) {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
:where(pre :is(code, kbd, samp)) {
font-family: inherit;
}`
: `:root:not(.vrtl):not(.vltr),
:root:not(.vrtl):not(.vltr) body {
font-family: var(--readany-font-family) !important;
}
:root:not(.vrtl):not(.vltr) body *:not(svg):not(svg *):not(math):not(math *):not(pre):not(pre *):not(code):not(code *):not(kbd):not(kbd *):not(samp):not(samp *) {
font-family: var(--readany-font-family) !important;
}
:root:not(.vrtl):not(.vltr) body :is(pre, code, kbd, samp) {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace !important;
}`;

const baseStyles = `
Expand All @@ -2107,12 +2145,11 @@
}
:root:not(.vrtl):not(.vltr),
:root:not(.vrtl):not(.vltr) body {
font-family: var(--readany-font-family) !important;
font-size: ${fSize}px !important;
-webkit-text-size-adjust: none;
text-size-adjust: none;
}
${bodyStarFontOverride}
${readerFontOverride}
:root:not(.vrtl):not(.vltr) body *:not(svg):not(svg *):not(math):not(math *):not(pre):not(pre *):not(code):not(code *):not(kbd):not(kbd *):not(samp):not(samp *):not(rt):not(rp) {
font-size: ${fSize}px !important;
}
Expand Down Expand Up @@ -2145,9 +2182,6 @@
:root:not(.vrtl):not(.vltr) h6 {
line-height: ${lh} !important;
}
pre, code, kbd, samp {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace !important;
}
:root:not(.vrtl):not(.vltr) p {
margin-top: ${ps}px !important;
margin-bottom: ${ps}px !important;
Expand Down
54 changes: 44 additions & 10 deletions packages/app-expo/assets/reader/reader.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -1918,12 +1918,50 @@
const ps = Math.round(currentParagraphSpacing * layoutScale);

// When useBookFonts is enabled (default), do not force the reader
// font family onto every element so the book's own fonts (e.g.
// embedded @font-face families) render where the book specifies them.
const bodyStarFontOverride = currentUseBookFonts
? ''
: `:root:not(.vrtl):not(.vltr) body *:not(svg):not(svg *):not(math):not(math *):not(pre):not(pre *):not(code):not(code *):not(kbd):not(kbd *):not(samp):not(samp *) {
// font family onto html/body with !important: the book's own
// font-family (on html, body, or any element) must win where specified,
// so embedded @font-face families render. The reader font stays as a
// zero-specificity fallback via :where(html). Target only html (not
// body): body then INHERITS html's font-family, so when the book sets
// one on html it propagates to body text instead of body being pinned
// to the reader font by a direct rule. When disabled, force the reader
// font on html/body and every descendant.
//
// The monospace rule swaps sides with the same toggle: with book fonts
// honored it is zero-specificity (a book's own `pre { font-family: ... }`
// — its embedded code font — wins), and when overriding it needs BOTH
// !important AND higher specificity, because specificity still breaks
// ties between important author declarations: a plain `pre, code, kbd`
// at (0,0,1) loses to an authored `body pre { ... !important }` at
// (0,0,2). The guarded `:root ... body :is(...)` sits at (0,2,2).
// Inner code/kbd/samp INSIDE a pre get an explicit `inherit` instead:
// the UA stylesheet declares `code { font-family: monospace }` and a
// direct declaration (even a UA one) always beats inheritance, so
// exclusion alone would still cut `<pre><code>` off from the book's
// font declared on pre. The zero-specificity inherit restores the
// chain while any author declaration on the element still wins it.
// Standalone inline code keeps the monospace fallback.
// Vertical writing (.vrtl/.vltr) keeps its own behavior: the forced
// overrides carry the horizontal guard like the pre-existing rules.
const readerFontOverride = currentUseBookFonts
? `:where(html) {
font-family: var(--readany-font-family);
}
:where(pre, :not(pre) > code, :not(pre) > kbd, :not(pre) > samp) {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
:where(pre :is(code, kbd, samp)) {
font-family: inherit;
}`
: `:root:not(.vrtl):not(.vltr),
:root:not(.vrtl):not(.vltr) body {
font-family: var(--readany-font-family) !important;
}
:root:not(.vrtl):not(.vltr) body *:not(svg):not(svg *):not(math):not(math *):not(pre):not(pre *):not(code):not(code *):not(kbd):not(kbd *):not(samp):not(samp *) {
font-family: var(--readany-font-family) !important;
}
:root:not(.vrtl):not(.vltr) body :is(pre, code, kbd, samp) {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace !important;
}`;

const baseStyles = `
Expand All @@ -1932,12 +1970,11 @@
}
:root:not(.vrtl):not(.vltr),
:root:not(.vrtl):not(.vltr) body {
font-family: var(--readany-font-family) !important;
font-size: ${fSize}px !important;
-webkit-text-size-adjust: none;
text-size-adjust: none;
}
${bodyStarFontOverride}
${readerFontOverride}
:root:not(.vrtl):not(.vltr) body *:not(svg):not(svg *):not(math):not(math *):not(pre):not(pre *):not(code):not(code *):not(kbd):not(kbd *):not(samp):not(samp *):not(rt):not(rp) {
font-size: ${fSize}px !important;
}
Expand Down Expand Up @@ -1970,9 +2007,6 @@
:root:not(.vrtl):not(.vltr) h6 {
line-height: ${lh} !important;
}
pre, code, kbd, samp {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace !important;
}
:root:not(.vrtl):not(.vltr) p {
margin-top: ${ps}px !important;
margin-bottom: ${ps}px !important;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";

const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../../../..");

function readSource(relativePath: string): string {
return readFileSync(resolve(repositoryRoot, relativePath), "utf8");
}

// The useBookFonts cascade recipe exists in three places that must not drift:
// the mobile template, its generated artifact, and the desktop generator.
// The behavior hinges on subtle specificity/!important interactions
// (zero-specificity :where() fallback vs. (0,0,3)/(0,2,2) forced override),
// so the contract pins the rule SHAPES per toggle state.
describe("useBookFonts font cascade contract", () => {
const template = readSource("packages/app-expo/assets/reader/reader.template.html");
const generated = readSource("packages/app-expo/assets/reader/reader.html");
const desktop = readSource("packages/app/src/components/reader/FoliateViewer.tsx");

it("honors book fonts by default: zero-specificity :where() fallbacks, no !important", () => {
for (const [name, source] of [
["template", template],
["generated", generated],
["desktop", desktop],
] as const) {
expect(source, name).toMatch(/:where\(html\)\s*\{\s*font-family: var\(--readany-font-family\);/);
// Inner code/kbd/samp inside a pre are excluded so they keep inheriting
// the book's font from pre instead of getting a direct declaration.
expect(source, name).toMatch(
/:where\(pre, :not\(pre\) > code, :not\(pre\) > kbd, :not\(pre\) > samp\)\s*\{\s*font-family: ui-monospace[^}]*;/,
);
// The UA stylesheet declares `code { font-family: monospace }` and any
// direct declaration beats inheritance — this zero-specificity inherit
// restores the `<pre><code>` chain to the book's font on pre.
expect(source, name).toMatch(
/:where\(pre :is\(code, kbd, samp\)\)\s*\{\s*font-family: inherit;/,
);
}
// The honoring branch must not carry !important anywhere.
expect(template).toMatch(/currentUseBookFonts\s*\?\s*`:where\(html\)[^`]*`/);
expect(template).not.toMatch(/:where\(html\)\s*\{[^}]*!important/);
expect(desktop).not.toMatch(/:where\(pre[^)]*\)\s*\{[^}]*!important/);
});

it("forces the reader font when the toggle is off, above authored !important rules", () => {
// Desktop: html body :is(...) at (0,0,3) — outranks an authored
// `body pre { ... !important }` at (0,0,2), which a bare `pre, code, kbd`
// at (0,0,1) would lose to despite its own !important.
expect(desktop).toMatch(
/html body :is\(pre, code, kbd, samp\)\s*\{\s*font-family: ui-monospace[^}]*!important/,
);
// Mobile: the same idea guarded to horizontal writing so the vertical
// branch keeps its pre-existing behavior (no descendant forcing).
expect(template).toMatch(
/:root:not\(\.vrtl\):not\(\.vltr\),\s*\n\s*:root:not\(\.vrtl\):not\(\.vltr\) body\s*\{\s*font-family: var\(--readany-font-family\) !important/,
);
expect(template).toMatch(
/:root:not\(\.vrtl\):not\(\.vltr\) body :is\(pre, code, kbd, samp\)\s*\{\s*font-family: ui-monospace[^}]*!important/,
);
// The forced body-star rule keeps the horizontal guard too.
expect(template).toMatch(
/:root:not\(\.vrtl\):not\(\.vltr\) body \*:not\(svg\)[^{]*\{\s*font-family: var\(--readany-font-family\) !important/,
);
// And the generated artifact carries the same shapes.
expect(generated).toMatch(/:root:not\(\.vrtl\):not\(\.vltr\) body :is\(pre, code, kbd, samp\)/);
expect(generated).toMatch(/:where\(pre, :not\(pre\) > code/);
});

it("emits exactly one unconditional monospace rule in the desktop output", () => {
// The legacy unconditional `pre, code, kbd, samp { ... !important }` must
// stay gone from the desktop generator — its reintroduction would silently
// override book code fonts again.
expect(desktop).not.toMatch(/(?<!:where\()pre, code, kbd, samp,?\s*\{[^}]*!important/);
});
});
25 changes: 21 additions & 4 deletions packages/app/src/components/reader/FoliateViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3474,6 +3474,18 @@ function getRendererStyles(settings: ViewSettings, theme: AppTheme): string {
// book sets one on html it propagates to body text instead of body being
// pinned to the reader font by a direct rule. When disabled, force the
// reader font on html/body and every descendant.
//
// The monospace rule swaps sides with the same toggle: with book fonts
// honored it is zero-specificity (a book's own `pre { font-family: ... }` —
// its embedded code font — wins), and when overriding it needs BOTH
// !important AND higher specificity, because specificity still breaks ties
// between important author declarations: a plain `pre, code, kbd` at (0,0,1)
// loses to an authored `body pre { ... !important }` at (0,0,2).
// `html body :is(...)` at (0,0,3) outranks both.
// Inner code/kbd/samp INSIDE a pre are excluded from the fallback so they
// keep INHERITING the book's font from pre (a direct declaration on `code`
// would cut the inheritance — `<pre><code>` is the dominant code markup);
// standalone inline code still gets the fallback.
const readerFontOverride =
settings.useBookFonts === false
? `html, body {
Expand All @@ -3482,10 +3494,19 @@ function getRendererStyles(settings: ViewSettings, theme: AppTheme): string {
body *:not(svg):not(svg *):not(math):not(math *):not(pre):not(pre *):not(code):not(code *):not(kbd):not(kbd *):not(samp):not(samp *) {
font-family: var(--readany-font-family) !important;
}
html body :is(pre, code, kbd, samp) {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace !important;
}
`
: `:where(html) {
font-family: var(--readany-font-family);
}
:where(pre, :not(pre) > code, :not(pre) > kbd, :not(pre) > samp) {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
:where(pre :is(code, kbd, samp)) {
font-family: inherit;
}
`;

return `${settings.customFontFaceCSS ? `/* Custom font faces */\n${settings.customFontFaceCSS}\n\n` : ""}/* Font styles */
Expand All @@ -3510,10 +3531,6 @@ body :not(#__readany_font_size_override):not(svg):not(svg *):not(math):not(math
font-size: ${settings.fontSize}px !important;
}

pre, code, kbd, samp {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace !important;
}

/* Line height for text blocks */
p, div, blockquote, dd, li, span {
line-height: ${settings.lineHeight} !important;
Expand Down