2
0
mirror of https://github.com/iconify/iconify.git synced 2024-12-12 21:57:50 +00:00

feat: icon to css functions in utils

This commit is contained in:
Vjacheslav Trushkin 2022-11-26 13:24:42 +02:00
parent 60541cffb7
commit afefaf410e
10 changed files with 1007 additions and 0 deletions

View File

@ -52,6 +52,31 @@
"import": "./lib/colors/types.mjs",
"types": "./lib/colors/types.d.ts"
},
"./lib/css/common": {
"require": "./lib/css/common.cjs",
"import": "./lib/css/common.mjs",
"types": "./lib/css/common.d.ts"
},
"./lib/css/format": {
"require": "./lib/css/format.cjs",
"import": "./lib/css/format.mjs",
"types": "./lib/css/format.d.ts"
},
"./lib/css/icon": {
"require": "./lib/css/icon.cjs",
"import": "./lib/css/icon.mjs",
"types": "./lib/css/icon.d.ts"
},
"./lib/css/icons": {
"require": "./lib/css/icons.cjs",
"import": "./lib/css/icons.mjs",
"types": "./lib/css/icons.d.ts"
},
"./lib/css/types": {
"require": "./lib/css/types.cjs",
"import": "./lib/css/types.mjs",
"types": "./lib/css/types.d.ts"
},
"./lib/customisations/bool": {
"require": "./lib/customisations/bool.cjs",
"import": "./lib/customisations/bool.mjs",

View File

@ -0,0 +1,88 @@
import type { IconifyIcon } from '@iconify/types';
import { iconToHTML } from '../svg/html';
import { calculateSize } from '../svg/size';
import { svgToURL } from '../svg/url';
import type { IconCSSCommonCodeOptions, IconCSSItemOptions } from './types';
/**
* Generates common CSS rules for multiple icons
*/
export function getCommonCSSRules(
options: IconCSSCommonCodeOptions
): Record<string, string> {
const result = {
display: 'inline-block',
width: '1em',
height: '1em',
} as Record<string, string>;
const varName = options.varName;
if (options.pseudoSelector) {
result['content'] = "''";
}
switch (options.mode) {
case 'background':
result['background'] = 'no-repeat center / 100%';
if (varName) {
result['background-image'] = 'var(--' + varName + ')';
}
break;
case 'mask':
result['background-color'] = 'currentColor';
result['mask'] = result['-webkit-mask'] = 'no-repeat center / 100%';
if (varName) {
result['mask-image'] = result['-webkit-mask-image'] =
'var(--' + varName + ')';
}
break;
}
return result;
}
/**
* Generate CSS rules for one icon
*
* This function excludes common rules
*/
export function generateItemCSSRules(
icon: Required<IconifyIcon>,
options: IconCSSItemOptions
): Record<string, string> {
const result = {} as Record<string, string>;
const varName = options.varName;
// Calculate width
if (!options.forceSquare && icon.width !== icon.height) {
result['width'] = calculateSize('1em', icon.width / icon.height);
}
// Get SVG
const svg = iconToHTML(icon.body.replace(/currentColor/g, '#000'), {
viewBox: `${icon.left} ${icon.top} ${icon.width} ${icon.height}`,
width: icon.width.toString(),
height: icon.height.toString(),
});
// Generate URL
const url = svgToURL(svg);
// Generate result
if (varName) {
result['--' + varName] = url;
} else {
switch (options.mode) {
case 'background':
result['background-image'] = url;
break;
case 'mask':
result['mask-image'] = result['-webkit-mask-image'] = url;
break;
}
}
return result;
}

View File

@ -0,0 +1,62 @@
import type { CSSFormatMode, CSSUnformattedItem } from './types';
type Item = Record<CSSFormatMode, string>;
interface FormatData {
selectorStart: Item;
selectorEnd: Item;
rule: Item;
}
const format: FormatData = {
selectorStart: {
compressed: '{',
compact: ' {',
expanded: ' {',
},
selectorEnd: {
compressed: '}',
compact: '; }\n',
expanded: ';\n}\n',
},
rule: {
compressed: '{key}:',
compact: ' {key}: ',
expanded: '\n {key}: ',
},
};
/**
* Format data
*
* Key is selector, value is list of rules
*/
export function formatCSS(
data: CSSUnformattedItem[],
mode: CSSFormatMode = 'expanded'
): string {
const results: string[] = [];
for (let i = 0; i < data.length; i++) {
const { selector, rules } = data[i];
const fullSelector =
selector instanceof Array
? selector.join(mode === 'compressed' ? ',' : ', ')
: selector;
let entry = fullSelector + format.selectorStart[mode];
let firstRule = true;
for (const key in rules) {
if (!firstRule) {
entry += ';';
}
entry += format.rule[mode].replace('{key}', key) + rules[key];
firstRule = false;
}
entry += format.selectorEnd[mode];
results.push(entry);
}
return results.join(mode === 'compressed' ? '' : '\n');
}

View File

@ -0,0 +1,53 @@
import type { IconifyIcon } from '@iconify/types';
import { defaultIconProps } from '../icon/defaults';
import { generateItemCSSRules, getCommonCSSRules } from './common';
import { formatCSS } from './format';
import type { IconCSSIconOptions } from './types';
/**
* Get CSS for icon
*/
export function getIconCSS(
icon: IconifyIcon,
options: IconCSSIconOptions
): string {
// Get mode
const mode =
options.mode ||
(icon.body.indexOf('currentColor') === -1 ? 'background' : 'mask');
// Get variable name
let varName = options.varName;
if (varName === void 0 && mode === 'mask') {
// Use 'svg' variable for masks to reduce duplication
varName = 'svg';
}
// Clone options
const newOptions = {
...options,
// Override mode and varName
mode,
varName,
};
if (mode === 'background') {
// Variable is not needed for background
delete newOptions.varName;
}
const rules = {
...getCommonCSSRules(newOptions),
...generateItemCSSRules({ ...defaultIconProps, ...icon }, newOptions),
};
const selector = options.iconSelector || '.icon';
return formatCSS(
[
{
selector,
rules,
},
],
newOptions.format
);
}

View File

@ -0,0 +1,134 @@
import type { IconifyJSON } from '@iconify/types';
import { getIconData } from '../icon-set/get-icon';
import { defaultIconProps } from '../icon/defaults';
import { generateItemCSSRules, getCommonCSSRules } from './common';
import { formatCSS } from './format';
import type {
CSSUnformattedItem,
IconCSSIconSetOptions,
IconCSSSelectorOptions,
} from './types';
// Default selectors
const commonSelector = '.icon--{prefix}';
const iconSelector = '.icon--{prefix}--{name}';
const defaultSelectors: IconCSSSelectorOptions = {
commonSelector,
iconSelector,
overrideSelector: commonSelector + iconSelector,
};
/**
* Get CSS for icon
*/
export function getIconsCSS(
iconSet: IconifyJSON,
names: string[],
options: IconCSSIconSetOptions
): string {
const css: CSSUnformattedItem[] = [];
const errors: string[] = [];
// Get mode
const palette = iconSet.info?.palette;
let mode =
options.mode ||
(typeof palette === 'boolean' && (palette ? 'background' : 'mask'));
if (!mode) {
// Cannot get mode: need either mode set in options or icon set with info
mode = 'mask';
errors.push(
'/* cannot detect icon mode: not set in options and icon set is missing info, rendering as ' +
mode +
' */'
);
}
// Get variable name
let varName = options.varName;
if (varName === void 0 && mode === 'mask') {
// Use 'svg' variable for masks to reduce duplication
varName = 'svg';
}
// Clone options
const newOptions = {
...options,
// Override mode and varName
mode,
varName,
};
const { commonSelector, iconSelector, overrideSelector } =
newOptions.iconSelector ? newOptions : defaultSelectors;
const iconSelectorWithPrefix = (iconSelector as string).replace(
/{prefix}/g,
iconSet.prefix
);
// Get common CSS
const commonRules = getCommonCSSRules(newOptions);
const hasCommonRules = commonSelector && commonSelector !== iconSelector;
const commonSelectors: Set<string> = new Set();
if (hasCommonRules) {
css.push({
selector: commonSelector.replace(/{prefix}/g, iconSet.prefix),
rules: commonRules,
});
}
// Parse all icons
for (let i = 0; i < names.length; i++) {
const name = names[i];
const iconData = getIconData(iconSet, name);
if (!iconData) {
errors.push('/* Could not find icon: ' + name + ' */');
continue;
}
const rules = generateItemCSSRules(
{ ...defaultIconProps, ...iconData },
newOptions
);
let requiresOverride = false;
if (hasCommonRules && overrideSelector) {
for (const key in rules) {
if (key in commonRules) {
requiresOverride = true;
}
}
}
const selector = (
requiresOverride && overrideSelector
? overrideSelector.replace(/{prefix}/g, iconSet.prefix)
: iconSelectorWithPrefix
).replace(/{name}/g, name);
css.push({
selector,
rules,
});
if (!hasCommonRules) {
commonSelectors.add(selector);
}
}
// Add common stuff
if (!hasCommonRules && commonSelectors.size) {
const selector = Array.from(commonSelectors).join(
newOptions.format === 'compressed' ? ',' : ', '
);
css.unshift({
selector,
rules: commonRules,
});
}
// Format
return (
formatCSS(css, newOptions.format) +
(errors.length ? '\n' + errors.join('\n') + '\n' : '')
);
}

View File

@ -0,0 +1,115 @@
/**
* Icon mode
*/
export type IconCSSMode = 'mask' | 'background';
/**
* Selector for icon
*/
export interface IconCSSIconSelectorOptions {
// True if selector is a pseudo-selector
pseudoSelector?: boolean;
// Selector used for icon
iconSelector?: string;
}
/**
* Selector for icon when generating data from icon set
*/
export interface IconCSSSelectorOptions extends IconCSSIconSelectorOptions {
// `iconSelector` is inherited from parent interface.
// Can contain {name} for icon name.
// If not set, other options from this interface are ignored
// Selector used for common elements
// Used only when set
commonSelector?: string;
// Selector for rules in icon that override common rules. Can contain {name} for icon name
// Used only when both `commonSelector` and `overrideSelector` are set
overrideSelector?: string;
}
/**
* Options common for both multiple icons and single icon
*/
export interface IconCSSSharedOptions {
// Variable name, null to disable
varName?: string | null;
// If true, result will always be square item
forceSquare?: boolean;
}
/**
* Mode
*/
export interface IconCSSModeOptions {
mode?: IconCSSMode;
}
/**
* Options for generating common code
*
* Requires mode
*/
export interface IconCSSCommonCodeOptions
extends IconCSSSharedOptions,
IconCSSIconSelectorOptions,
Required<IconCSSModeOptions> {
//
}
/**
* Options for generating data for one icon
*/
export interface IconCSSItemOptions
extends IconCSSSharedOptions,
IconCSSSelectorOptions,
Required<IconCSSModeOptions> {
//
}
/**
* Formatting modes. Same as in SASS
*/
export type CSSFormatMode = 'expanded' | 'compact' | 'compressed';
/**
* Item to format
*/
export interface CSSUnformattedItem {
selector: string | string[];
rules: Record<string, string>;
}
/**
* Formatting options
*/
export interface IconCSSFormatOptions {
// Formatter
format?: CSSFormatMode;
}
/**
* Options for generating data for one icon
*/
export interface IconCSSIconOptions
extends IconCSSSharedOptions,
IconCSSIconSelectorOptions,
IconCSSModeOptions,
IconCSSFormatOptions {
//
}
/**
* Options for generating multiple icons
*/
export interface IconCSSIconSetOptions
extends IconCSSSharedOptions,
IconCSSSelectorOptions,
IconCSSModeOptions,
IconCSSFormatOptions {
//
}

View File

@ -64,6 +64,10 @@ export { svgToURL } from './svg/url';
export { colorKeywords } from './colors/keywords';
export { stringToColor, compareColors, colorToString } from './colors/index';
// CSS generator
export { getIconCSS } from './css/icon';
export { getIconsCSS } from './css/icons';
// SVG Icon loader
export type {
CustomIconLoader,

View File

@ -0,0 +1,74 @@
import { formatCSS } from '../lib/css/format';
describe('Testing formatCSS', () => {
test('Various modes', () => {
expect(
formatCSS(
[
{
selector: '.foo',
rules: {
'color': 'red',
'font-size': '16px',
},
},
{
selector: '.bar',
rules: { color: 'blue' },
},
],
'expanded'
)
).toBe(`.foo {
color: red;
font-size: 16px;
}
.bar {
color: blue;
}
`);
expect(
formatCSS(
[
{
selector: '.foo',
rules: { 'color': 'red', 'font-size': '16px' },
},
{
selector: '.bar',
rules: { color: 'blue' },
},
],
'compact'
)
).toBe(`.foo { color: red; font-size: 16px; }
.bar { color: blue; }
`);
expect(
formatCSS(
[
{
selector: '.foo',
rules: {
'color': 'red',
'font-size': '16px',
},
},
{
selector: '.bar',
rules: {
color: 'blue',
},
},
],
'compressed'
)
).toBe(`.foo{color:red;font-size:16px}.bar{color:blue}`);
});
});

View File

@ -0,0 +1,114 @@
import { svgToURL } from '../lib/svg/url';
import { getIconCSS } from '../lib/css/icon';
import type { IconifyIcon } from '@iconify/types';
describe('Testing CSS for icon', () => {
test('Background', () => {
const icon: IconifyIcon = {
body: '<path d="M0 0h16v16z" fill="#f80" />',
width: 24,
height: 16,
};
const expectedURL = svgToURL(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 16" width="24" height="16">${icon.body}</svg>`
);
expect(
getIconCSS(icon, {
format: 'expanded',
})
).toBe(`.icon {
display: inline-block;
width: 1.5em;
height: 1em;
background: no-repeat center / 100%;
background-image: ${expectedURL};
}
`);
});
test('Background with options', () => {
const icon: IconifyIcon = {
body: '<path d="M0 0h16v16z" fill="#f80" />',
width: 24,
height: 16,
};
const expectedURL = svgToURL(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 16" width="24" height="16">${icon.body}</svg>`
);
expect(
getIconCSS(icon, {
iconSelector: '.test-icon:after',
pseudoSelector: true,
varName: 'svg',
forceSquare: true,
format: 'expanded',
})
).toBe(`.test-icon:after {
display: inline-block;
width: 1em;
height: 1em;
content: '';
background: no-repeat center / 100%;
background-image: ${expectedURL};
}
`);
});
test('Mask', () => {
const icon: IconifyIcon = {
body: '<path d="M0 0h16v16z" fill="currentColor" stroke="currentColor" stroke-width="1" />',
};
const expectedURL = svgToURL(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">${icon.body.replace(
/currentColor/g,
'#000'
)}</svg>`
);
expect(
getIconCSS(icon, {
format: 'expanded',
})
).toBe(`.icon {
display: inline-block;
width: 1em;
height: 1em;
background-color: currentColor;
-webkit-mask: no-repeat center / 100%;
mask: no-repeat center / 100%;
-webkit-mask-image: var(--svg);
mask-image: var(--svg);
--svg: ${expectedURL};
}
`);
});
test('Mask with options', () => {
const icon: IconifyIcon = {
body: '<path d="M0 0h16v16z" fill="#f00" />',
};
const expectedURL = svgToURL(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">${icon.body}</svg>`
);
expect(
getIconCSS(icon, {
format: 'expanded',
varName: null,
mode: 'mask',
})
).toBe(`.icon {
display: inline-block;
width: 1em;
height: 1em;
background-color: currentColor;
-webkit-mask: no-repeat center / 100%;
mask: no-repeat center / 100%;
-webkit-mask-image: ${expectedURL};
mask-image: ${expectedURL};
}
`);
});
});

View File

@ -0,0 +1,338 @@
import { svgToURL } from '../lib/svg/url';
import { getIconsCSS } from '../lib/css/icons';
import type { IconifyJSON } from '@iconify/types';
describe('Testing CSS for multiple icons', () => {
test('Background', () => {
const iconSet: IconifyJSON = {
prefix: 'test-prefix',
icons: {
'123': {
body: '<path fill="currentColor" d="M2.873 11.297V4.142H1.699L0 5.379v1.137l1.64-1.18h.06v5.961h1.174Zm3.213-5.09v-.063c0-.618.44-1.169 1.196-1.169c.676 0 1.174.44 1.174 1.106c0 .624-.42 1.101-.807 1.526L4.99 10.553v.744h4.78v-.99H6.643v-.069L8.41 8.252c.65-.724 1.237-1.332 1.237-2.27C9.646 4.849 8.723 4 7.308 4c-1.573 0-2.36 1.064-2.36 2.15v.057h1.138Zm6.559 1.883h.786c.823 0 1.374.481 1.379 1.179c.01.707-.55 1.216-1.421 1.21c-.77-.005-1.326-.419-1.379-.953h-1.095c.042 1.053.938 1.918 2.464 1.918c1.478 0 2.642-.839 2.62-2.144c-.02-1.143-.922-1.651-1.551-1.714v-.063c.535-.09 1.347-.66 1.326-1.678c-.026-1.053-.933-1.855-2.359-1.845c-1.5.005-2.317.88-2.348 1.898h1.116c.032-.498.498-.944 1.206-.944c.703 0 1.206.435 1.206 1.07c.005.64-.504 1.106-1.2 1.106h-.75v.96Z"/>',
},
'activity': {
body: '<path fill="currentColor" fill-rule="evenodd" d="M6 2a.5.5 0 0 1 .47.33L10 12.036l1.53-4.208A.5.5 0 0 1 12 7.5h3.5a.5.5 0 0 1 0 1h-3.15l-1.88 5.17a.5.5 0 0 1-.94 0L6 3.964L4.47 8.171A.5.5 0 0 1 4 8.5H.5a.5.5 0 0 1 0-1h3.15l1.88-5.17A.5.5 0 0 1 6 2Z"/>',
},
'airplane': {
body: '<path fill="currentColor" d="M6.428 1.151C6.708.591 7.213 0 8 0s1.292.592 1.572 1.151C9.861 1.73 10 2.431 10 3v3.691l5.17 2.585a1.5 1.5 0 0 1 .83 1.342V12a.5.5 0 0 1-.582.493l-5.507-.918l-.375 2.253l1.318 1.318A.5.5 0 0 1 10.5 16h-5a.5.5 0 0 1-.354-.854l1.319-1.318l-.376-2.253l-5.507.918A.5.5 0 0 1 0 12v-1.382a1.5 1.5 0 0 1 .83-1.342L6 6.691V3c0-.568.14-1.271.428-1.849Zm.894.448C7.111 2.02 7 2.569 7 3v4a.5.5 0 0 1-.276.447l-5.448 2.724a.5.5 0 0 0-.276.447v.792l5.418-.903a.5.5 0 0 1 .575.41l.5 3a.5.5 0 0 1-.14.437L6.708 15h2.586l-.647-.646a.5.5 0 0 1-.14-.436l.5-3a.5.5 0 0 1 .576-.411L15 11.41v-.792a.5.5 0 0 0-.276-.447L9.276 7.447A.5.5 0 0 1 9 7V3c0-.432-.11-.979-.322-1.401C8.458 1.159 8.213 1 8 1c-.213 0-.458.158-.678.599Z"/>',
},
'airplane-engines': {
body: '<path fill="currentColor" d="M8 0c-.787 0-1.292.592-1.572 1.151A4.347 4.347 0 0 0 6 3v3.691l-2 1V7.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.191l-1.17.585A1.5 1.5 0 0 0 0 10.618V12a.5.5 0 0 0 .582.493l1.631-.272l.313.937a.5.5 0 0 0 .948 0l.405-1.214l2.21-.369l.375 2.253l-1.318 1.318A.5.5 0 0 0 5.5 16h5a.5.5 0 0 0 .354-.854l-1.318-1.318l.375-2.253l2.21.369l.405 1.214a.5.5 0 0 0 .948 0l.313-.937l1.63.272A.5.5 0 0 0 16 12v-1.382a1.5 1.5 0 0 0-.83-1.342L14 8.691V7.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v.191l-2-1V3c0-.568-.14-1.271-.428-1.849C9.292.591 8.787 0 8 0ZM7 3c0-.432.11-.979.322-1.401C7.542 1.159 7.787 1 8 1c.213 0 .458.158.678.599C8.889 2.02 9 2.569 9 3v4a.5.5 0 0 0 .276.447l5.448 2.724a.5.5 0 0 1 .276.447v.792l-5.418-.903a.5.5 0 0 0-.575.41l-.5 3a.5.5 0 0 0 .14.437l.646.646H6.707l.647-.646a.5.5 0 0 0 .14-.436l-.5-3a.5.5 0 0 0-.576-.411L1 11.41v-.792a.5.5 0 0 1 .276-.447l5.448-2.724A.5.5 0 0 0 7 7V3Z"/>',
},
'empty': {
body: '<g />',
},
},
};
const expectedURL = (name: string) =>
svgToURL(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">${iconSet.icons[
name
].body.replace(/currentColor/g, '#000')}</svg>`
);
expect(
getIconsCSS(iconSet, ['empty', '123', 'airplane'], {
format: 'expanded',
mode: 'background',
})
).toBe(`.icon--test-prefix {
display: inline-block;
width: 1em;
height: 1em;
background: no-repeat center / 100%;
}
.icon--test-prefix--empty {
background-image: ${expectedURL('empty')};
}
.icon--test-prefix--123 {
background-image: ${expectedURL('123')};
}
.icon--test-prefix--airplane {
background-image: ${expectedURL('airplane')};
}
`);
});
test('Mask', () => {
const iconSet: IconifyJSON = {
prefix: 'bi',
info: {
name: 'Bootstrap Icons',
total: 1953,
version: '1.10.2',
author: {
name: 'The Bootstrap Authors',
url: 'https://github.com/twbs/icons',
},
license: {
title: 'MIT',
spdx: 'MIT',
url: 'https://github.com/twbs/icons/blob/main/LICENSE.md',
},
samples: ['graph-up', 'card-image', 'code-slash'],
height: 16,
category: 'General',
palette: false,
},
icons: {
'123': {
body: '<path fill="currentColor" d="M2.873 11.297V4.142H1.699L0 5.379v1.137l1.64-1.18h.06v5.961h1.174Zm3.213-5.09v-.063c0-.618.44-1.169 1.196-1.169c.676 0 1.174.44 1.174 1.106c0 .624-.42 1.101-.807 1.526L4.99 10.553v.744h4.78v-.99H6.643v-.069L8.41 8.252c.65-.724 1.237-1.332 1.237-2.27C9.646 4.849 8.723 4 7.308 4c-1.573 0-2.36 1.064-2.36 2.15v.057h1.138Zm6.559 1.883h.786c.823 0 1.374.481 1.379 1.179c.01.707-.55 1.216-1.421 1.21c-.77-.005-1.326-.419-1.379-.953h-1.095c.042 1.053.938 1.918 2.464 1.918c1.478 0 2.642-.839 2.62-2.144c-.02-1.143-.922-1.651-1.551-1.714v-.063c.535-.09 1.347-.66 1.326-1.678c-.026-1.053-.933-1.855-2.359-1.845c-1.5.005-2.317.88-2.348 1.898h1.116c.032-.498.498-.944 1.206-.944c.703 0 1.206.435 1.206 1.07c.005.64-.504 1.106-1.2 1.106h-.75v.96Z"/>',
},
'activity': {
body: '<path fill="currentColor" fill-rule="evenodd" d="M6 2a.5.5 0 0 1 .47.33L10 12.036l1.53-4.208A.5.5 0 0 1 12 7.5h3.5a.5.5 0 0 1 0 1h-3.15l-1.88 5.17a.5.5 0 0 1-.94 0L6 3.964L4.47 8.171A.5.5 0 0 1 4 8.5H.5a.5.5 0 0 1 0-1h3.15l1.88-5.17A.5.5 0 0 1 6 2Z"/>',
},
'airplane': {
body: '<path fill="currentColor" d="M6.428 1.151C6.708.591 7.213 0 8 0s1.292.592 1.572 1.151C9.861 1.73 10 2.431 10 3v3.691l5.17 2.585a1.5 1.5 0 0 1 .83 1.342V12a.5.5 0 0 1-.582.493l-5.507-.918l-.375 2.253l1.318 1.318A.5.5 0 0 1 10.5 16h-5a.5.5 0 0 1-.354-.854l1.319-1.318l-.376-2.253l-5.507.918A.5.5 0 0 1 0 12v-1.382a1.5 1.5 0 0 1 .83-1.342L6 6.691V3c0-.568.14-1.271.428-1.849Zm.894.448C7.111 2.02 7 2.569 7 3v4a.5.5 0 0 1-.276.447l-5.448 2.724a.5.5 0 0 0-.276.447v.792l5.418-.903a.5.5 0 0 1 .575.41l.5 3a.5.5 0 0 1-.14.437L6.708 15h2.586l-.647-.646a.5.5 0 0 1-.14-.436l.5-3a.5.5 0 0 1 .576-.411L15 11.41v-.792a.5.5 0 0 0-.276-.447L9.276 7.447A.5.5 0 0 1 9 7V3c0-.432-.11-.979-.322-1.401C8.458 1.159 8.213 1 8 1c-.213 0-.458.158-.678.599Z"/>',
},
'airplane-engines': {
body: '<path fill="currentColor" d="M8 0c-.787 0-1.292.592-1.572 1.151A4.347 4.347 0 0 0 6 3v3.691l-2 1V7.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.191l-1.17.585A1.5 1.5 0 0 0 0 10.618V12a.5.5 0 0 0 .582.493l1.631-.272l.313.937a.5.5 0 0 0 .948 0l.405-1.214l2.21-.369l.375 2.253l-1.318 1.318A.5.5 0 0 0 5.5 16h5a.5.5 0 0 0 .354-.854l-1.318-1.318l.375-2.253l2.21.369l.405 1.214a.5.5 0 0 0 .948 0l.313-.937l1.63.272A.5.5 0 0 0 16 12v-1.382a1.5 1.5 0 0 0-.83-1.342L14 8.691V7.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v.191l-2-1V3c0-.568-.14-1.271-.428-1.849C9.292.591 8.787 0 8 0ZM7 3c0-.432.11-.979.322-1.401C7.542 1.159 7.787 1 8 1c.213 0 .458.158.678.599C8.889 2.02 9 2.569 9 3v4a.5.5 0 0 0 .276.447l5.448 2.724a.5.5 0 0 1 .276.447v.792l-5.418-.903a.5.5 0 0 0-.575.41l-.5 3a.5.5 0 0 0 .14.437l.646.646H6.707l.647-.646a.5.5 0 0 0 .14-.436l-.5-3a.5.5 0 0 0-.576-.411L1 11.41v-.792a.5.5 0 0 1 .276-.447l5.448-2.724A.5.5 0 0 0 7 7V3Z"/>',
},
},
};
const expectedURL = (name: string) =>
svgToURL(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">${iconSet.icons[
name
].body.replace(/currentColor/g, '#000')}</svg>`
);
expect(
getIconsCSS(iconSet, ['activity', 'airplane-engines'], {
format: 'expanded',
})
).toBe(`.icon--bi {
display: inline-block;
width: 1em;
height: 1em;
background-color: currentColor;
-webkit-mask: no-repeat center / 100%;
mask: no-repeat center / 100%;
-webkit-mask-image: var(--svg);
mask-image: var(--svg);
}
.icon--bi--activity {
--svg: ${expectedURL('activity')};
}
.icon--bi--airplane-engines {
--svg: ${expectedURL('airplane-engines')};
}
`);
});
test('Mask with custom config', () => {
const iconSet: IconifyJSON = {
prefix: 'bi',
info: {
name: 'Bootstrap Icons',
total: 1953,
version: '1.10.2',
author: {
name: 'The Bootstrap Authors',
url: 'https://github.com/twbs/icons',
},
license: {
title: 'MIT',
spdx: 'MIT',
url: 'https://github.com/twbs/icons/blob/main/LICENSE.md',
},
samples: ['graph-up', 'card-image', 'code-slash'],
height: 16,
category: 'General',
palette: false,
},
icons: {
'123': {
body: '<path fill="currentColor" d="M2.873 11.297V4.142H1.699L0 5.379v1.137l1.64-1.18h.06v5.961h1.174Zm3.213-5.09v-.063c0-.618.44-1.169 1.196-1.169c.676 0 1.174.44 1.174 1.106c0 .624-.42 1.101-.807 1.526L4.99 10.553v.744h4.78v-.99H6.643v-.069L8.41 8.252c.65-.724 1.237-1.332 1.237-2.27C9.646 4.849 8.723 4 7.308 4c-1.573 0-2.36 1.064-2.36 2.15v.057h1.138Zm6.559 1.883h.786c.823 0 1.374.481 1.379 1.179c.01.707-.55 1.216-1.421 1.21c-.77-.005-1.326-.419-1.379-.953h-1.095c.042 1.053.938 1.918 2.464 1.918c1.478 0 2.642-.839 2.62-2.144c-.02-1.143-.922-1.651-1.551-1.714v-.063c.535-.09 1.347-.66 1.326-1.678c-.026-1.053-.933-1.855-2.359-1.845c-1.5.005-2.317.88-2.348 1.898h1.116c.032-.498.498-.944 1.206-.944c.703 0 1.206.435 1.206 1.07c.005.64-.504 1.106-1.2 1.106h-.75v.96Z"/>',
},
'activity': {
body: '<path fill="currentColor" fill-rule="evenodd" d="M6 2a.5.5 0 0 1 .47.33L10 12.036l1.53-4.208A.5.5 0 0 1 12 7.5h3.5a.5.5 0 0 1 0 1h-3.15l-1.88 5.17a.5.5 0 0 1-.94 0L6 3.964L4.47 8.171A.5.5 0 0 1 4 8.5H.5a.5.5 0 0 1 0-1h3.15l1.88-5.17A.5.5 0 0 1 6 2Z"/>',
},
'airplane': {
body: '<path fill="currentColor" d="M6.428 1.151C6.708.591 7.213 0 8 0s1.292.592 1.572 1.151C9.861 1.73 10 2.431 10 3v3.691l5.17 2.585a1.5 1.5 0 0 1 .83 1.342V12a.5.5 0 0 1-.582.493l-5.507-.918l-.375 2.253l1.318 1.318A.5.5 0 0 1 10.5 16h-5a.5.5 0 0 1-.354-.854l1.319-1.318l-.376-2.253l-5.507.918A.5.5 0 0 1 0 12v-1.382a1.5 1.5 0 0 1 .83-1.342L6 6.691V3c0-.568.14-1.271.428-1.849Zm.894.448C7.111 2.02 7 2.569 7 3v4a.5.5 0 0 1-.276.447l-5.448 2.724a.5.5 0 0 0-.276.447v.792l5.418-.903a.5.5 0 0 1 .575.41l.5 3a.5.5 0 0 1-.14.437L6.708 15h2.586l-.647-.646a.5.5 0 0 1-.14-.436l.5-3a.5.5 0 0 1 .576-.411L15 11.41v-.792a.5.5 0 0 0-.276-.447L9.276 7.447A.5.5 0 0 1 9 7V3c0-.432-.11-.979-.322-1.401C8.458 1.159 8.213 1 8 1c-.213 0-.458.158-.678.599Z"/>',
},
'airplane-engines': {
body: '<path fill="currentColor" d="M8 0c-.787 0-1.292.592-1.572 1.151A4.347 4.347 0 0 0 6 3v3.691l-2 1V7.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.191l-1.17.585A1.5 1.5 0 0 0 0 10.618V12a.5.5 0 0 0 .582.493l1.631-.272l.313.937a.5.5 0 0 0 .948 0l.405-1.214l2.21-.369l.375 2.253l-1.318 1.318A.5.5 0 0 0 5.5 16h5a.5.5 0 0 0 .354-.854l-1.318-1.318l.375-2.253l2.21.369l.405 1.214a.5.5 0 0 0 .948 0l.313-.937l1.63.272A.5.5 0 0 0 16 12v-1.382a1.5 1.5 0 0 0-.83-1.342L14 8.691V7.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v.191l-2-1V3c0-.568-.14-1.271-.428-1.849C9.292.591 8.787 0 8 0ZM7 3c0-.432.11-.979.322-1.401C7.542 1.159 7.787 1 8 1c.213 0 .458.158.678.599C8.889 2.02 9 2.569 9 3v4a.5.5 0 0 0 .276.447l5.448 2.724a.5.5 0 0 1 .276.447v.792l-5.418-.903a.5.5 0 0 0-.575.41l-.5 3a.5.5 0 0 0 .14.437l.646.646H6.707l.647-.646a.5.5 0 0 0 .14-.436l-.5-3a.5.5 0 0 0-.576-.411L1 11.41v-.792a.5.5 0 0 1 .276-.447l5.448-2.724A.5.5 0 0 0 7 7V3Z"/>',
},
},
width: 24,
};
const expectedURL = (name: string) =>
svgToURL(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 16" width="24" height="16">${iconSet.icons[
name
].body.replace(/currentColor/g, '#000')}</svg>`
);
expect(
getIconsCSS(iconSet, ['activity', 'airplane-engines'], {
format: 'expanded',
varName: null,
})
).toBe(`.icon--bi {
display: inline-block;
width: 1em;
height: 1em;
background-color: currentColor;
-webkit-mask: no-repeat center / 100%;
mask: no-repeat center / 100%;
}
.icon--bi.icon--bi--activity {
width: 1.5em;
-webkit-mask-image: ${expectedURL('activity')};
mask-image: ${expectedURL('activity')};
}
.icon--bi.icon--bi--airplane-engines {
width: 1.5em;
-webkit-mask-image: ${expectedURL('airplane-engines')};
mask-image: ${expectedURL('airplane-engines')};
}
`);
});
test('Mask with custom selector', () => {
const iconSet: IconifyJSON = {
prefix: 'bi',
info: {
name: 'Bootstrap Icons',
total: 1953,
version: '1.10.2',
author: {
name: 'The Bootstrap Authors',
url: 'https://github.com/twbs/icons',
},
license: {
title: 'MIT',
spdx: 'MIT',
url: 'https://github.com/twbs/icons/blob/main/LICENSE.md',
},
samples: ['graph-up', 'card-image', 'code-slash'],
height: 16,
category: 'General',
palette: false,
},
icons: {
'123': {
body: '<path fill="currentColor" d="M2.873 11.297V4.142H1.699L0 5.379v1.137l1.64-1.18h.06v5.961h1.174Zm3.213-5.09v-.063c0-.618.44-1.169 1.196-1.169c.676 0 1.174.44 1.174 1.106c0 .624-.42 1.101-.807 1.526L4.99 10.553v.744h4.78v-.99H6.643v-.069L8.41 8.252c.65-.724 1.237-1.332 1.237-2.27C9.646 4.849 8.723 4 7.308 4c-1.573 0-2.36 1.064-2.36 2.15v.057h1.138Zm6.559 1.883h.786c.823 0 1.374.481 1.379 1.179c.01.707-.55 1.216-1.421 1.21c-.77-.005-1.326-.419-1.379-.953h-1.095c.042 1.053.938 1.918 2.464 1.918c1.478 0 2.642-.839 2.62-2.144c-.02-1.143-.922-1.651-1.551-1.714v-.063c.535-.09 1.347-.66 1.326-1.678c-.026-1.053-.933-1.855-2.359-1.845c-1.5.005-2.317.88-2.348 1.898h1.116c.032-.498.498-.944 1.206-.944c.703 0 1.206.435 1.206 1.07c.005.64-.504 1.106-1.2 1.106h-.75v.96Z"/>',
},
'activity': {
body: '<path fill="currentColor" fill-rule="evenodd" d="M6 2a.5.5 0 0 1 .47.33L10 12.036l1.53-4.208A.5.5 0 0 1 12 7.5h3.5a.5.5 0 0 1 0 1h-3.15l-1.88 5.17a.5.5 0 0 1-.94 0L6 3.964L4.47 8.171A.5.5 0 0 1 4 8.5H.5a.5.5 0 0 1 0-1h3.15l1.88-5.17A.5.5 0 0 1 6 2Z"/>',
},
'airplane': {
body: '<path fill="currentColor" d="M6.428 1.151C6.708.591 7.213 0 8 0s1.292.592 1.572 1.151C9.861 1.73 10 2.431 10 3v3.691l5.17 2.585a1.5 1.5 0 0 1 .83 1.342V12a.5.5 0 0 1-.582.493l-5.507-.918l-.375 2.253l1.318 1.318A.5.5 0 0 1 10.5 16h-5a.5.5 0 0 1-.354-.854l1.319-1.318l-.376-2.253l-5.507.918A.5.5 0 0 1 0 12v-1.382a1.5 1.5 0 0 1 .83-1.342L6 6.691V3c0-.568.14-1.271.428-1.849Zm.894.448C7.111 2.02 7 2.569 7 3v4a.5.5 0 0 1-.276.447l-5.448 2.724a.5.5 0 0 0-.276.447v.792l5.418-.903a.5.5 0 0 1 .575.41l.5 3a.5.5 0 0 1-.14.437L6.708 15h2.586l-.647-.646a.5.5 0 0 1-.14-.436l.5-3a.5.5 0 0 1 .576-.411L15 11.41v-.792a.5.5 0 0 0-.276-.447L9.276 7.447A.5.5 0 0 1 9 7V3c0-.432-.11-.979-.322-1.401C8.458 1.159 8.213 1 8 1c-.213 0-.458.158-.678.599Z"/>',
},
'airplane-engines': {
body: '<path fill="currentColor" d="M8 0c-.787 0-1.292.592-1.572 1.151A4.347 4.347 0 0 0 6 3v3.691l-2 1V7.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.191l-1.17.585A1.5 1.5 0 0 0 0 10.618V12a.5.5 0 0 0 .582.493l1.631-.272l.313.937a.5.5 0 0 0 .948 0l.405-1.214l2.21-.369l.375 2.253l-1.318 1.318A.5.5 0 0 0 5.5 16h5a.5.5 0 0 0 .354-.854l-1.318-1.318l.375-2.253l2.21.369l.405 1.214a.5.5 0 0 0 .948 0l.313-.937l1.63.272A.5.5 0 0 0 16 12v-1.382a1.5 1.5 0 0 0-.83-1.342L14 8.691V7.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v.191l-2-1V3c0-.568-.14-1.271-.428-1.849C9.292.591 8.787 0 8 0ZM7 3c0-.432.11-.979.322-1.401C7.542 1.159 7.787 1 8 1c.213 0 .458.158.678.599C8.889 2.02 9 2.569 9 3v4a.5.5 0 0 0 .276.447l5.448 2.724a.5.5 0 0 1 .276.447v.792l-5.418-.903a.5.5 0 0 0-.575.41l-.5 3a.5.5 0 0 0 .14.437l.646.646H6.707l.647-.646a.5.5 0 0 0 .14-.436l-.5-3a.5.5 0 0 0-.576-.411L1 11.41v-.792a.5.5 0 0 1 .276-.447l5.448-2.724A.5.5 0 0 0 7 7V3Z"/>',
},
},
width: 24,
};
const expectedURL = (name: string) =>
svgToURL(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 16" width="24" height="16">${iconSet.icons[
name
].body.replace(/currentColor/g, '#000')}</svg>`
);
expect(
getIconsCSS(iconSet, ['activity', 'airplane-engines'], {
format: 'expanded',
iconSelector: '.test--{name}',
})
).toBe(`.test--activity, .test--airplane-engines {
display: inline-block;
width: 1em;
height: 1em;
background-color: currentColor;
-webkit-mask: no-repeat center / 100%;
mask: no-repeat center / 100%;
-webkit-mask-image: var(--svg);
mask-image: var(--svg);
}
.test--activity {
width: 1.5em;
--svg: ${expectedURL('activity')};
}
.test--airplane-engines {
width: 1.5em;
--svg: ${expectedURL('airplane-engines')};
}
`);
});
test('Duplicate selectors', () => {
const iconSet: IconifyJSON = {
prefix: 'bi',
info: {
name: 'Bootstrap Icons',
total: 1953,
version: '1.10.2',
author: {
name: 'The Bootstrap Authors',
url: 'https://github.com/twbs/icons',
},
license: {
title: 'MIT',
spdx: 'MIT',
url: 'https://github.com/twbs/icons/blob/main/LICENSE.md',
},
samples: ['graph-up', 'card-image', 'code-slash'],
height: 16,
category: 'General',
palette: false,
},
icons: {
'123': {
body: '<path fill="currentColor" d="M2.873 11.297V4.142H1.699L0 5.379v1.137l1.64-1.18h.06v5.961h1.174Zm3.213-5.09v-.063c0-.618.44-1.169 1.196-1.169c.676 0 1.174.44 1.174 1.106c0 .624-.42 1.101-.807 1.526L4.99 10.553v.744h4.78v-.99H6.643v-.069L8.41 8.252c.65-.724 1.237-1.332 1.237-2.27C9.646 4.849 8.723 4 7.308 4c-1.573 0-2.36 1.064-2.36 2.15v.057h1.138Zm6.559 1.883h.786c.823 0 1.374.481 1.379 1.179c.01.707-.55 1.216-1.421 1.21c-.77-.005-1.326-.419-1.379-.953h-1.095c.042 1.053.938 1.918 2.464 1.918c1.478 0 2.642-.839 2.62-2.144c-.02-1.143-.922-1.651-1.551-1.714v-.063c.535-.09 1.347-.66 1.326-1.678c-.026-1.053-.933-1.855-2.359-1.845c-1.5.005-2.317.88-2.348 1.898h1.116c.032-.498.498-.944 1.206-.944c.703 0 1.206.435 1.206 1.07c.005.64-.504 1.106-1.2 1.106h-.75v.96Z"/>',
},
'activity': {
body: '<path fill="currentColor" fill-rule="evenodd" d="M6 2a.5.5 0 0 1 .47.33L10 12.036l1.53-4.208A.5.5 0 0 1 12 7.5h3.5a.5.5 0 0 1 0 1h-3.15l-1.88 5.17a.5.5 0 0 1-.94 0L6 3.964L4.47 8.171A.5.5 0 0 1 4 8.5H.5a.5.5 0 0 1 0-1h3.15l1.88-5.17A.5.5 0 0 1 6 2Z"/>',
},
'airplane': {
body: '<path fill="currentColor" d="M6.428 1.151C6.708.591 7.213 0 8 0s1.292.592 1.572 1.151C9.861 1.73 10 2.431 10 3v3.691l5.17 2.585a1.5 1.5 0 0 1 .83 1.342V12a.5.5 0 0 1-.582.493l-5.507-.918l-.375 2.253l1.318 1.318A.5.5 0 0 1 10.5 16h-5a.5.5 0 0 1-.354-.854l1.319-1.318l-.376-2.253l-5.507.918A.5.5 0 0 1 0 12v-1.382a1.5 1.5 0 0 1 .83-1.342L6 6.691V3c0-.568.14-1.271.428-1.849Zm.894.448C7.111 2.02 7 2.569 7 3v4a.5.5 0 0 1-.276.447l-5.448 2.724a.5.5 0 0 0-.276.447v.792l5.418-.903a.5.5 0 0 1 .575.41l.5 3a.5.5 0 0 1-.14.437L6.708 15h2.586l-.647-.646a.5.5 0 0 1-.14-.436l.5-3a.5.5 0 0 1 .576-.411L15 11.41v-.792a.5.5 0 0 0-.276-.447L9.276 7.447A.5.5 0 0 1 9 7V3c0-.432-.11-.979-.322-1.401C8.458 1.159 8.213 1 8 1c-.213 0-.458.158-.678.599Z"/>',
},
'airplane-engines': {
body: '<path fill="currentColor" d="M8 0c-.787 0-1.292.592-1.572 1.151A4.347 4.347 0 0 0 6 3v3.691l-2 1V7.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.191l-1.17.585A1.5 1.5 0 0 0 0 10.618V12a.5.5 0 0 0 .582.493l1.631-.272l.313.937a.5.5 0 0 0 .948 0l.405-1.214l2.21-.369l.375 2.253l-1.318 1.318A.5.5 0 0 0 5.5 16h5a.5.5 0 0 0 .354-.854l-1.318-1.318l.375-2.253l2.21.369l.405 1.214a.5.5 0 0 0 .948 0l.313-.937l1.63.272A.5.5 0 0 0 16 12v-1.382a1.5 1.5 0 0 0-.83-1.342L14 8.691V7.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v.191l-2-1V3c0-.568-.14-1.271-.428-1.849C9.292.591 8.787 0 8 0ZM7 3c0-.432.11-.979.322-1.401C7.542 1.159 7.787 1 8 1c.213 0 .458.158.678.599C8.889 2.02 9 2.569 9 3v4a.5.5 0 0 0 .276.447l5.448 2.724a.5.5 0 0 1 .276.447v.792l-5.418-.903a.5.5 0 0 0-.575.41l-.5 3a.5.5 0 0 0 .14.437l.646.646H6.707l.647-.646a.5.5 0 0 0 .14-.436l-.5-3a.5.5 0 0 0-.576-.411L1 11.41v-.792a.5.5 0 0 1 .276-.447l5.448-2.724A.5.5 0 0 0 7 7V3Z"/>',
},
},
width: 24,
};
const expectedURL = (name: string) =>
svgToURL(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 16" width="24" height="16">${iconSet.icons[
name
].body.replace(/currentColor/g, '#000')}</svg>`
);
expect(
getIconsCSS(iconSet, ['activity'], {
format: 'expanded',
iconSelector: '.test--{name}',
})
).toBe(`.test--activity {
display: inline-block;
width: 1em;
height: 1em;
background-color: currentColor;
-webkit-mask: no-repeat center / 100%;
mask: no-repeat center / 100%;
-webkit-mask-image: var(--svg);
mask-image: var(--svg);
}
.test--activity {
width: 1.5em;
--svg: ${expectedURL('activity')};
}
`);
});
});