mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
fix: count default black fill when detecting tintable SVGs
This commit is contained in:
parent
91d0ca44ac
commit
6bdea5b465
2 changed files with 82 additions and 5 deletions
|
|
@ -66,6 +66,28 @@ describe('isMonochromeSvg', () => {
|
|||
'<svg viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="#fff" /><path fill="#000" d="M6 6h12v12H6z" /></svg>';
|
||||
expect(isMonochromeSvg(svg)).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects an explicit light fill combined with a default-black glyph', () => {
|
||||
const svg =
|
||||
'<svg viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="#fff" /><path d="M6 6h12v12H6z" /></svg>';
|
||||
expect(isMonochromeSvg(svg)).toBe(false);
|
||||
});
|
||||
|
||||
it('still tints an explicit black fill combined with a default-black glyph', () => {
|
||||
const svg =
|
||||
'<svg viewBox="0 0 24 24"><path d="M2 2h4v4H2z" fill="#000" /><path d="M6 6h12v12H6z" /></svg>';
|
||||
expect(isMonochromeSvg(svg)).toBe(true);
|
||||
});
|
||||
|
||||
it('does not add an implicit fill for an explicitly stroked path', () => {
|
||||
const svg = '<svg viewBox="0 0 24 24"><path d="M4 12h16" stroke="#333" /></svg>';
|
||||
expect(isMonochromeSvg(svg)).toBe(true);
|
||||
});
|
||||
|
||||
it('inherits an ancestor fill instead of assuming the default black', () => {
|
||||
const svg = '<svg viewBox="0 0 24 24"><g fill="#333"><path d="M4 4h16v16H4z" /></g></svg>';
|
||||
expect(isMonochromeSvg(svg)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multi-color icons (colors preserved)', () => {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,12 @@ const GRAY_LEVELS = new Map<string, number>([
|
|||
/** Paint properties whose color values determine whether an SVG is monochrome. */
|
||||
const PAINT_PROPS = ['fill', 'stroke', 'stop-color'];
|
||||
|
||||
/** Area shapes that render with SVG's default black fill when none is supplied. */
|
||||
const FILLABLE_SHAPES = 'path, rect, circle, ellipse, polygon, text';
|
||||
|
||||
/** Containers whose descendants are templates and never render on their own. */
|
||||
const NON_RENDERING = new Set(['defs', 'clippath', 'mask', 'symbol', 'marker', 'pattern']);
|
||||
|
||||
/** Matches color declarations inside a `<style>` block. */
|
||||
const CSS_COLOR_REGEX = /(?:fill|stroke|stop-color|color)\s*:\s*([^;}]+)/gi;
|
||||
|
||||
|
|
@ -235,6 +241,56 @@ function collectColors(root: Element): string[] {
|
|||
.filter((color) => color.length > 0 && !IGNORABLE_COLORS.has(color));
|
||||
}
|
||||
|
||||
/** True when the element or any ancestor sets an explicit `fill` (inherited by SVG). */
|
||||
function inheritsExplicitFill(el: Element): boolean {
|
||||
let current: Element | null = el;
|
||||
while (current != null) {
|
||||
const fill = readPaint(current, 'fill');
|
||||
if (fill != null && fill.trim() !== '') {
|
||||
return true;
|
||||
}
|
||||
if (current.nodeName.toLowerCase() === 'svg') {
|
||||
break;
|
||||
}
|
||||
current = current.parentElement;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function inNonRenderingContainer(el: Element, root: Element): boolean {
|
||||
let current = el.parentElement;
|
||||
while (current != null && current !== root) {
|
||||
if (NON_RENDERING.has(current.nodeName.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
current = current.parentElement;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* True when a rendered shape paints with SVG's default black fill: it has no
|
||||
* explicit fill or stroke, inherits no fill from an ancestor, and is not a
|
||||
* non-rendering template. Such a shape contributes a black tone that explicit
|
||||
* paint values alone do not capture. Skipped when a `<style>` block is present,
|
||||
* since a CSS rule may color the shape and cannot be resolved without matching.
|
||||
*/
|
||||
function hasDefaultBlackShape(root: Element): boolean {
|
||||
if (root.querySelector('style') != null) {
|
||||
return false;
|
||||
}
|
||||
for (const el of Array.from(root.querySelectorAll(FILLABLE_SHAPES))) {
|
||||
if (readPaint(el, 'fill') != null || readPaint(el, 'stroke') != null) {
|
||||
continue;
|
||||
}
|
||||
if (inheritsExplicitFill(el) || inNonRenderingContainer(el, root)) {
|
||||
continue;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when an SVG can be safely tinted to match the theme: it embeds no
|
||||
* raster (`<image>`) or foreign (`<foreignObject>`) content, has no opaque
|
||||
|
|
@ -254,18 +310,17 @@ export function isMonochromeSvg(svg: string): boolean {
|
|||
if (hasOpaqueBackground(root)) {
|
||||
return false;
|
||||
}
|
||||
const colors = collectColors(root);
|
||||
if (colors.length === 0) {
|
||||
return true;
|
||||
}
|
||||
const levels = new Set<number>();
|
||||
for (const color of colors) {
|
||||
for (const color of collectColors(root)) {
|
||||
const level = grayLevel(color);
|
||||
if (level === null) {
|
||||
return false;
|
||||
}
|
||||
levels.add(level);
|
||||
}
|
||||
if (hasDefaultBlackShape(root)) {
|
||||
levels.add(0);
|
||||
}
|
||||
return levels.size <= 1;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue