img.has-text-siblings forced vertical-align: baseline on every inline image with text siblings, out-specifying a book's own value (for example a CJK glyph-substitution image nudged with vertical-align: -0.15em). Because baseline is the CSS initial value, the declaration only ever mattered when it clobbered an authored value. Keep baseline as a default only: move it to a new has-text-siblings-baseline class that applyImageStyle adds only when the image has no author-set vertical-align (detected via getComputedStyle). Refactor applyImageStyle to a two-phase read-then-write pass to avoid a getComputedStyle-after-write style recalc. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -417,6 +417,33 @@ describe('getStyles', () => {
|
|||||||
expect(css).toContain('background-color: #ffffff !important');
|
expect(css).toContain('background-color: #ffffff !important');
|
||||||
expect(css).toContain('color: #171717 !important');
|
expect(css).toContain('color: #171717 !important');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('scopes the inline-image baseline default to its own class so author values win #4866', () => {
|
||||||
|
const vs = makeViewSettings();
|
||||||
|
const themeCode: ThemeCode = {
|
||||||
|
bg: '#ffffff',
|
||||||
|
fg: '#171717',
|
||||||
|
primary: '#0066cc',
|
||||||
|
palette: {
|
||||||
|
'base-100': '#ffffff',
|
||||||
|
'base-200': '#f2f2f2',
|
||||||
|
'base-300': '#e0e0e0',
|
||||||
|
'base-content': '#171717',
|
||||||
|
neutral: '#cccccc',
|
||||||
|
'neutral-content': '#444444',
|
||||||
|
primary: '#0066cc',
|
||||||
|
secondary: '#3399ff',
|
||||||
|
accent: '#0055aa',
|
||||||
|
},
|
||||||
|
isDarkMode: false,
|
||||||
|
};
|
||||||
|
const css = getStyles(vs, themeCode);
|
||||||
|
// Baseline is opt-in via a dedicated class, not forced on every inline image.
|
||||||
|
expect(css).toMatch(/img\.has-text-siblings-baseline\s*\{[^}]*vertical-align:\s*baseline/);
|
||||||
|
// The size-clamp rule must not carry vertical-align anymore.
|
||||||
|
const clampRule = css.match(/\n\s*img\.has-text-siblings\s*\{[^}]*\}/)![0];
|
||||||
|
expect(clampRule).not.toContain('vertical-align');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -479,6 +506,23 @@ describe('applyImageStyle', () => {
|
|||||||
const img = document.querySelector('img')!;
|
const img = document.querySelector('img')!;
|
||||||
expect(img.classList.contains('has-text-siblings')).toBe(false);
|
expect(img.classList.contains('has-text-siblings')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('applies the baseline default when the image has no author vertical-align', () => {
|
||||||
|
document.body.innerHTML = '<p>Some text <img src="test.png" /> more text</p>';
|
||||||
|
applyImageStyle(document);
|
||||||
|
const img = document.querySelector('img')!;
|
||||||
|
expect(img.classList.contains('has-text-siblings')).toBe(true);
|
||||||
|
expect(img.classList.contains('has-text-siblings-baseline')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('respects an author-set vertical-align (no baseline default) #4866', () => {
|
||||||
|
document.body.innerHTML =
|
||||||
|
'<p>Some text <img src="test.png" style="vertical-align: -0.15em" /> more text</p>';
|
||||||
|
applyImageStyle(document);
|
||||||
|
const img = document.querySelector('img')!;
|
||||||
|
expect(img.classList.contains('has-text-siblings')).toBe(true);
|
||||||
|
expect(img.classList.contains('has-text-siblings-baseline')).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -472,6 +472,12 @@ const getPageLayoutStyles = (
|
|||||||
}
|
}
|
||||||
img.has-text-siblings {
|
img.has-text-siblings {
|
||||||
${vertical ? 'width: 1em;' : 'height: 1em;'}
|
${vertical ? 'width: 1em;' : 'height: 1em;'}
|
||||||
|
}
|
||||||
|
/* Baseline is only Readest's default: applyImageStyle adds this class when the
|
||||||
|
book leaves vertical-align at its initial value, so an author-set value
|
||||||
|
(e.g. a CJK glyph-substitution image nudged with vertical-align: -0.15em)
|
||||||
|
keeps winning. See #4866. */
|
||||||
|
img.has-text-siblings-baseline {
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
}
|
}
|
||||||
:is(div) > img.has-text-siblings[style*="object-fit"] {
|
:is(div) > img.has-text-siblings[style*="object-fit"] {
|
||||||
@@ -1148,37 +1154,60 @@ export const applyScrollbarStyle = (document: Document, hideScrollbar: boolean)
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const applyImageStyle = (document: Document) => {
|
export const applyImageStyle = (document: Document) => {
|
||||||
document.querySelectorAll('img').forEach((img) => {
|
const win = document.defaultView ?? window;
|
||||||
|
// Two-phase (read then write): reading getComputedStyle after a class/style
|
||||||
|
// write forces a style recalc, so gather every decision first and apply the
|
||||||
|
// mutations afterwards (same reasoning as keepTextAlignment's split).
|
||||||
|
const plans = Array.from(document.querySelectorAll('img')).map((img) => {
|
||||||
const widthAttr = img.getAttribute('width');
|
const widthAttr = img.getAttribute('width');
|
||||||
if (widthAttr && (widthAttr.endsWith('%') || widthAttr.endsWith('vw'))) {
|
const percentWidth =
|
||||||
const percentage = parseFloat(widthAttr);
|
widthAttr && (widthAttr.endsWith('%') || widthAttr.endsWith('vw'))
|
||||||
if (!isNaN(percentage)) {
|
? parseFloat(widthAttr)
|
||||||
img.style.width = `${(percentage / 100) * window.innerWidth}px`;
|
: NaN;
|
||||||
img.removeAttribute('width');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const heightAttr = img.getAttribute('height');
|
const heightAttr = img.getAttribute('height');
|
||||||
if (heightAttr && (heightAttr.endsWith('%') || heightAttr.endsWith('vh'))) {
|
const percentHeight =
|
||||||
const percentage = parseFloat(heightAttr);
|
heightAttr && (heightAttr.endsWith('%') || heightAttr.endsWith('vh'))
|
||||||
if (!isNaN(percentage)) {
|
? parseFloat(heightAttr)
|
||||||
img.style.height = `${(percentage / 100) * window.innerHeight}px`;
|
: NaN;
|
||||||
img.removeAttribute('height');
|
|
||||||
|
let inlineWithText = false;
|
||||||
|
let keepBaseline = false;
|
||||||
|
const parent = img.parentNode;
|
||||||
|
if (parent && parent.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
const childNodes = Array.from(parent.childNodes);
|
||||||
|
const hasTextSiblings = childNodes.some(
|
||||||
|
(node) => node.nodeType === Node.TEXT_NODE && node.textContent?.trim(),
|
||||||
|
);
|
||||||
|
const isInline = childNodes.every(
|
||||||
|
(node) => node.nodeType !== Node.ELEMENT_NODE || (node as Element).tagName !== 'BR',
|
||||||
|
);
|
||||||
|
inlineWithText = hasTextSiblings && isInline;
|
||||||
|
if (inlineWithText) {
|
||||||
|
// Only supply Readest's baseline default when the book leaves
|
||||||
|
// vertical-align at its initial value; an author-set value (e.g. a CJK
|
||||||
|
// glyph-substitution image nudged with `vertical-align: -0.15em`) must
|
||||||
|
// win. Empty string covers environments that report unset props as ''.
|
||||||
|
const valign = win.getComputedStyle(img).verticalAlign;
|
||||||
|
keepBaseline = valign === '' || valign === 'baseline';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return { img, percentWidth, percentHeight, inlineWithText, keepBaseline };
|
||||||
const parent = img.parentNode;
|
|
||||||
if (!parent || parent.nodeType !== Node.ELEMENT_NODE) return;
|
|
||||||
const hasTextSiblings = Array.from(parent.childNodes).some(
|
|
||||||
(node) => node.nodeType === Node.TEXT_NODE && node.textContent?.trim(),
|
|
||||||
);
|
|
||||||
const isInline = Array.from(parent.childNodes).every(
|
|
||||||
(node) => node.nodeType !== Node.ELEMENT_NODE || (node as Element).tagName !== 'BR',
|
|
||||||
);
|
|
||||||
if (hasTextSiblings && isInline) {
|
|
||||||
img.classList.add('has-text-siblings');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const { img, percentWidth, percentHeight, inlineWithText, keepBaseline } of plans) {
|
||||||
|
if (!isNaN(percentWidth)) {
|
||||||
|
img.style.width = `${(percentWidth / 100) * window.innerWidth}px`;
|
||||||
|
img.removeAttribute('width');
|
||||||
|
}
|
||||||
|
if (!isNaN(percentHeight)) {
|
||||||
|
img.style.height = `${(percentHeight / 100) * window.innerHeight}px`;
|
||||||
|
img.removeAttribute('height');
|
||||||
|
}
|
||||||
|
if (inlineWithText) {
|
||||||
|
img.classList.add('has-text-siblings');
|
||||||
|
if (keepBaseline) img.classList.add('has-text-siblings-baseline');
|
||||||
|
}
|
||||||
|
}
|
||||||
document.querySelectorAll('hr').forEach((hr) => {
|
document.querySelectorAll('hr').forEach((hr) => {
|
||||||
const computedStyle = window.getComputedStyle(hr);
|
const computedStyle = window.getComputedStyle(hr);
|
||||||
if (computedStyle.backgroundImage && computedStyle.backgroundImage !== 'none') {
|
if (computedStyle.backgroundImage && computedStyle.backgroundImage !== 'none') {
|
||||||
|
|||||||
Reference in New Issue
Block a user