mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-10 00:03:03 +00:00
fix: treat currentColor as a tone and stroked closed shapes as filled
This commit is contained in:
parent
6bdea5b465
commit
89ee7fc8c6
2 changed files with 63 additions and 10 deletions
|
|
@ -79,17 +79,41 @@ describe('isMonochromeSvg', () => {
|
|||
expect(isMonochromeSvg(svg)).toBe(true);
|
||||
});
|
||||
|
||||
it('does not add an implicit fill for an explicitly stroked path', () => {
|
||||
it('does not add an implicit fill for a stroked open path (no enclosed area)', () => {
|
||||
const svg = '<svg viewBox="0 0 24 24"><path d="M4 12h16" stroke="#333" /></svg>';
|
||||
expect(isMonochromeSvg(svg)).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects a closed stroked path that still renders its default black fill', () => {
|
||||
const svg = '<svg viewBox="0 0 24 24"><path d="M4 4h16v16H4z" stroke="#fff" /></svg>';
|
||||
expect(isMonochromeSvg(svg)).toBe(false);
|
||||
});
|
||||
|
||||
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('currentColor (a visible, theme-following tone)', () => {
|
||||
it('tints an icon painted entirely with currentColor', () => {
|
||||
const svg = '<svg viewBox="0 0 24 24"><path fill="currentColor" d="M4 4h16v16H4z" /></svg>';
|
||||
expect(isMonochromeSvg(svg)).toBe(true);
|
||||
});
|
||||
|
||||
it('preserves currentColor mixed with an explicit light paint', () => {
|
||||
const svg =
|
||||
'<svg viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="#fff" /><path fill="currentColor" d="M6 6h12v12H6z" /></svg>';
|
||||
expect(isMonochromeSvg(svg)).toBe(false);
|
||||
});
|
||||
|
||||
it('preserves currentColor mixed with a default-black glyph', () => {
|
||||
const svg =
|
||||
'<svg viewBox="0 0 24 24"><path fill="currentColor" d="M2 2h4v4H2z" /><path d="M6 6h12v12H6z" /></svg>';
|
||||
expect(isMonochromeSvg(svg)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multi-color icons (colors preserved)', () => {
|
||||
it('treats a saturated hex color as multi-color', () => {
|
||||
const svg = '<svg><path fill="#ff0000" /></svg>';
|
||||
|
|
|
|||
|
|
@ -8,8 +8,12 @@ import DOMPurify from 'dompurify';
|
|||
* with regexes.
|
||||
*/
|
||||
|
||||
/** Color keywords that carry no chromatic information and are ignored. */
|
||||
const IGNORABLE_COLORS = new Set(['none', 'transparent', 'inherit', 'currentcolor']);
|
||||
/** Color keywords that paint nothing (or defer) and so contribute no tone. */
|
||||
const IGNORABLE_COLORS = new Set(['none', 'transparent', 'inherit']);
|
||||
|
||||
/** `currentColor` is a visible paint that follows the theme; it has no fixed level. */
|
||||
const CURRENT_COLOR = 'currentcolor';
|
||||
const CURRENT_COLOR_TONE = -1;
|
||||
|
||||
/** Named CSS grayscale colors mapped to their 0-255 level. Unknown names are chromatic. */
|
||||
const GRAY_LEVELS = new Map<string, number>([
|
||||
|
|
@ -269,24 +273,45 @@ function inNonRenderingContainer(el: Element, root: Element): boolean {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* True when a shape encloses a fillable area, so its default fill is actually
|
||||
* painted. Open paths (and paths with no `d`) enclose nothing, so their default
|
||||
* fill is invisible even when the shape is stroked.
|
||||
*/
|
||||
function rendersFillArea(el: Element): boolean {
|
||||
const tag = el.nodeName.toLowerCase();
|
||||
if (tag === 'path') {
|
||||
const d = el.getAttribute('d');
|
||||
return d != null && /[zZ]/.test(d);
|
||||
}
|
||||
if (tag === 'text') {
|
||||
return (el.textContent ?? '').trim().length > 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* True when a rendered shape paints with SVG's default black fill: it sets no
|
||||
* explicit fill, inherits none from an ancestor, is not a non-rendering template,
|
||||
* and encloses a fillable area. A stroke does not suppress the default fill, so a
|
||||
* closed stroked shape still renders black. 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) {
|
||||
if (readPaint(el, 'fill') != null) {
|
||||
continue;
|
||||
}
|
||||
if (inheritsExplicitFill(el) || inNonRenderingContainer(el, root)) {
|
||||
continue;
|
||||
}
|
||||
return true;
|
||||
if (rendersFillArea(el)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -312,6 +337,10 @@ export function isMonochromeSvg(svg: string): boolean {
|
|||
}
|
||||
const levels = new Set<number>();
|
||||
for (const color of collectColors(root)) {
|
||||
if (color === CURRENT_COLOR) {
|
||||
levels.add(CURRENT_COLOR_TONE);
|
||||
continue;
|
||||
}
|
||||
const level = grayLevel(color);
|
||||
if (level === null) {
|
||||
return false;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue