From c15f8ca0c203c17a3d7bdeeae4deec3808fba03f Mon Sep 17 00:00:00 2001 From: Vjacheslav Trushkin Date: Sun, 17 Sep 2023 20:10:28 +0300 Subject: [PATCH] chore: add functions to generate css as content, add extra rules option for css functions --- packages/utils/src/css/common.ts | 36 +++++++++- packages/utils/src/css/icon.ts | 41 +++++++++++- packages/utils/src/css/icons.ts | 65 ++++++++++++++++-- packages/utils/src/css/types.ts | 81 ++++++++++++++++++++++- packages/utils/src/index.ts | 4 +- packages/utils/tests/icon-to-css-test.ts | 56 +++++++++++++++- packages/utils/tests/icons-to-css-test.ts | 77 ++++++++++++++++++++- 7 files changed, 343 insertions(+), 17 deletions(-) diff --git a/packages/utils/src/css/common.ts b/packages/utils/src/css/common.ts index 096115b..66e1de5 100644 --- a/packages/utils/src/css/common.ts +++ b/packages/utils/src/css/common.ts @@ -2,10 +2,14 @@ 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'; +import type { + IconCSSCommonCodeOptions, + IconCSSItemOptions, + IconContentItemOptions, +} from './types'; /** - * Generates common CSS rules for multiple icons + * Generates common CSS rules for multiple icons, rendered as background/mask */ export function getCommonCSSRules( options: IconCSSCommonCodeOptions @@ -45,7 +49,7 @@ export function getCommonCSSRules( } /** - * Generate CSS rules for one icon + * Generate CSS rules for one icon, rendered as background/mask * * This function excludes common rules */ @@ -91,3 +95,29 @@ export function generateItemCSSRules( return result; } + +/** + * Generate content for one icon, rendered as content of pseudo-selector + */ +export function generateItemContent( + icon: Required, + options: IconContentItemOptions +): string { + // Get dimensions + const height = options.height; + const width = + options.width ?? calculateSize(height, icon.width / icon.height); + + // Get SVG + const svg = iconToHTML( + icon.body.replace(/currentColor/g, options.color || 'black'), + { + viewBox: `${icon.left} ${icon.top} ${icon.width} ${icon.height}`, + width: width.toString(), + height: height.toString(), + } + ); + + // Generate URL + return svgToURL(svg); +} diff --git a/packages/utils/src/css/icon.ts b/packages/utils/src/css/icon.ts index 7d22f8e..bd58daf 100644 --- a/packages/utils/src/css/icon.ts +++ b/packages/utils/src/css/icon.ts @@ -1,11 +1,15 @@ import type { IconifyIcon } from '@iconify/types'; import { defaultIconProps } from '../icon/defaults'; -import { generateItemCSSRules, getCommonCSSRules } from './common'; +import { + generateItemCSSRules, + generateItemContent, + getCommonCSSRules, +} from './common'; import { formatCSS } from './format'; -import type { IconCSSIconOptions } from './types'; +import type { IconCSSIconOptions, IconContentIconOptions } from './types'; /** - * Get CSS for icon + * Get CSS for icon, rendered as background or mask */ export function getIconCSS( icon: IconifyIcon, @@ -38,10 +42,12 @@ export function getIconCSS( } const rules = { + ...options.rules, ...getCommonCSSRules(newOptions), ...generateItemCSSRules({ ...defaultIconProps, ...icon }, newOptions), }; + // Get selector and format CSS const selector = options.iconSelector || '.icon'; return formatCSS( [ @@ -53,3 +59,32 @@ export function getIconCSS( newOptions.format ); } + +/** + * Get CSS for icon, rendered as content + */ +export function getIconContentCSS( + icon: IconifyIcon, + options: IconContentIconOptions +): string { + // Get content + const content = generateItemContent( + { ...defaultIconProps, ...icon }, + options + ); + + // Get selector and format CSS + const selector = options.iconSelector || '.icon::after'; + return formatCSS( + [ + { + selector, + rules: { + ...options.rules, + content, + }, + }, + ], + options.format + ); +} diff --git a/packages/utils/src/css/icons.ts b/packages/utils/src/css/icons.ts index e939c47..b4a2146 100644 --- a/packages/utils/src/css/icons.ts +++ b/packages/utils/src/css/icons.ts @@ -1,17 +1,23 @@ import type { IconifyJSON } from '@iconify/types'; import { getIconData } from '../icon-set/get-icon'; import { defaultIconProps } from '../icon/defaults'; -import { generateItemCSSRules, getCommonCSSRules } from './common'; +import { + generateItemCSSRules, + generateItemContent, + getCommonCSSRules, +} from './common'; import { formatCSS } from './format'; import type { CSSUnformattedItem, IconCSSIconSetOptions, IconCSSSelectorOptions, + IconContentIconSetOptions, } from './types'; // Default selectors const commonSelector = '.icon--{prefix}'; const iconSelector = '.icon--{prefix}--{name}'; +const contentSelector = '.icon--{prefix}--{name}::after'; const defaultSelectors: IconCSSSelectorOptions = { commonSelector, iconSelector, @@ -86,7 +92,10 @@ export function getIconsCSSData( ); // Get common CSS - const commonRules = getCommonCSSRules(newOptions); + const commonRules = { + ...options.rules, + ...getCommonCSSRules(newOptions), + }; const hasCommonRules = commonSelector && commonSelector !== iconSelector; const commonSelectors: Set = new Set(); if (hasCommonRules) { @@ -106,7 +115,10 @@ export function getIconsCSSData( } const rules = generateItemCSSRules( - { ...defaultIconProps, ...iconData }, + { + ...defaultIconProps, + ...iconData, + }, newOptions ); @@ -155,7 +167,7 @@ export function getIconsCSSData( } /** - * Get CSS for icon + * Get CSS for icons as background/mask */ export function getIconsCSS( iconSet: IconifyJSON, @@ -184,3 +196,48 @@ export function getIconsCSS( (errors.length ? '\n' + errors.join('\n') + '\n' : '') ); } + +/** + * Get CSS for icons as content + */ +export function getIconsContentCSS( + iconSet: IconifyJSON, + names: string[], + options: IconContentIconSetOptions +): string { + const errors: string[] = []; + const css: CSSUnformattedItem[] = []; + const iconSelectorWithPrefix = ( + options.iconSelector ?? contentSelector + ).replace(/{prefix}/g, iconSet.prefix); + + // 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 content = generateItemContent( + { ...defaultIconProps, ...iconData }, + options + ); + const selector = iconSelectorWithPrefix.replace(/{name}/g, name); + + css.push({ + selector, + rules: { + ...options.rules, + content, + }, + }); + } + + // Format + return ( + formatCSS(css, options.format) + + (errors.length ? '\n' + errors.join('\n') + '\n' : '') + ); +} diff --git a/packages/utils/src/css/types.ts b/packages/utils/src/css/types.ts index 9548a0a..a364fd8 100644 --- a/packages/utils/src/css/types.ts +++ b/packages/utils/src/css/types.ts @@ -1,3 +1,9 @@ +/* + * + * Options for rendering icons as mask or background + * + */ + /** * Icon mode */ @@ -43,6 +49,9 @@ export interface IconCSSSharedOptions { // Set color for monotone icons color?: string; + + // Custom rules + rules?: Record; } /** @@ -73,6 +82,48 @@ export interface IconCSSItemOptions // } +/* + * + * Options for rendering icons as content + * + */ + +/** + * Selector for icon + */ +export interface IconContentIconSelectorOptions { + // Selector used for icon + iconSelector?: string; +} + +/** + * Options common for both multiple icons and single icon + */ +export interface IconContentSharedOptions { + // Icon height + height: number; + + // Icon width. If not set, it will be calculated using icon's width/height ratio + width?: number; + + // Set color for monotone icons + color?: string; + + // Custom rules + rules?: Record; +} + +/** + * Options for generating data for one icon + */ +export type IconContentItemOptions = IconContentSharedOptions; + +/* + * + * Options for formatting CSS + * + */ + /** * Formatting modes. Same as in SASS */ @@ -94,8 +145,14 @@ export interface IconCSSFormatOptions { format?: CSSFormatMode; } +/* + * + * Combined options for functions that render and format code + * + */ + /** - * Options for generating data for one icon + * Options for generating data for one icon as background/mask */ export interface IconCSSIconOptions extends IconCSSSharedOptions, @@ -106,7 +163,17 @@ export interface IconCSSIconOptions } /** - * Options for generating multiple icons + * Options for generating data for one icon as content + */ +export interface IconContentIconOptions + extends IconContentSharedOptions, + IconContentIconSelectorOptions, + IconCSSFormatOptions { + // +} + +/** + * Options for generating multiple icons as background/mask */ export interface IconCSSIconSetOptions extends IconCSSSharedOptions, @@ -115,3 +182,13 @@ export interface IconCSSIconSetOptions IconCSSFormatOptions { // } + +/** + * Options for generating multiple icons as content + */ +export interface IconContentIconSetOptions + extends IconContentSharedOptions, + IconContentIconSelectorOptions, + IconCSSFormatOptions { + // +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 72db95c..c291b21 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -71,8 +71,8 @@ export { colorKeywords } from './colors/keywords'; export { stringToColor, compareColors, colorToString } from './colors/index'; // CSS generator -export { getIconCSS } from './css/icon'; -export { getIconsCSS } from './css/icons'; +export { getIconCSS, getIconContentCSS } from './css/icon'; +export { getIconsCSS, getIconsContentCSS } from './css/icons'; // SVG Icon loader export type { diff --git a/packages/utils/tests/icon-to-css-test.ts b/packages/utils/tests/icon-to-css-test.ts index f94f7e6..d4a5a40 100644 --- a/packages/utils/tests/icon-to-css-test.ts +++ b/packages/utils/tests/icon-to-css-test.ts @@ -1,5 +1,5 @@ import { svgToURL } from '../lib/svg/url'; -import { getIconCSS } from '../lib/css/icon'; +import { getIconCSS, getIconContentCSS } from '../lib/css/icon'; import type { IconifyIcon } from '@iconify/types'; describe('Testing CSS for icon', () => { @@ -45,8 +45,12 @@ describe('Testing CSS for icon', () => { varName: 'svg', forceSquare: true, format: 'expanded', + rules: { + visibility: 'visible', + }, }) ).toBe(`.test-icon:after { + visibility: visible; display: inline-block; width: 1em; height: 1em; @@ -142,6 +146,56 @@ describe('Testing CSS for icon', () => { -webkit-mask-image: ${expectedURL}; mask-image: ${expectedURL}; } +`); + }); + + test('Content', () => { + const icon: IconifyIcon = { + body: '', + width: 24, + height: 16, + }; + const expectedURL = svgToURL( + `${icon.body}` + ); + + expect( + getIconContentCSS(icon, { + height: 32, + format: 'expanded', + }) + ).toBe(`.icon::after { + content: ${expectedURL}; +} +`); + }); + + test('Content with options', () => { + const icon: IconifyIcon = { + body: '', + }; + const expectedURL = svgToURL( + `${icon.body.replace( + /currentColor/g, + 'purple' + )}` + ); + + expect( + getIconContentCSS(icon, { + width: 32, + height: 24, + format: 'expanded', + color: 'purple', + iconSelector: '.test-icon::before', + rules: { + visibility: 'visible', + }, + }) + ).toBe(`.test-icon::before { + visibility: visible; + content: ${expectedURL}; +} `); }); }); diff --git a/packages/utils/tests/icons-to-css-test.ts b/packages/utils/tests/icons-to-css-test.ts index dae72b1..39fa4fd 100644 --- a/packages/utils/tests/icons-to-css-test.ts +++ b/packages/utils/tests/icons-to-css-test.ts @@ -1,5 +1,5 @@ import { svgToURL } from '../lib/svg/url'; -import { getIconsCSS } from '../lib/css/icons'; +import { getIconsCSS, getIconsContentCSS } from '../lib/css/icons'; import type { IconifyJSON } from '@iconify/types'; describe('Testing CSS for multiple icons', () => { @@ -33,10 +33,14 @@ describe('Testing CSS for multiple icons', () => { // Detect mode: mask expect( - getIconsCSS(iconSet, ['activity', '123', 'airplane'], { + getIconsCSS(iconSet, ['activity', '123', 'airplane', 'missing'], { format: 'expanded', + rules: { + visibility: 'visible', + }, }) ).toBe(`.icon--test-prefix { + visibility: visible; display: inline-block; width: 1em; height: 1em; @@ -60,6 +64,8 @@ describe('Testing CSS for multiple icons', () => { .icon--test-prefix--airplane { --svg: ${expectedURL('airplane')}; } + +/* Could not find icon: missing */ `); // Force mode: background @@ -219,8 +225,12 @@ describe('Testing CSS for multiple icons', () => { getIconsCSS(iconSet, ['activity', 'airplane-engines'], { format: 'expanded', varName: null, + rules: { + visibility: 'visible', + }, }) ).toBe(`.icon--bi { + visibility: visible; display: inline-block; width: 1em; height: 1em; @@ -293,8 +303,12 @@ describe('Testing CSS for multiple icons', () => { getIconsCSS(iconSet, ['activity', 'airplane-engines'], { format: 'expanded', iconSelector: '.test--{name}', + rules: { + visibility: 'visible', + }, }) ).toBe(`.test--activity, .test--airplane-engines { + visibility: visible; display: inline-block; width: 1em; height: 1em; @@ -381,6 +395,65 @@ describe('Testing CSS for multiple icons', () => { mask-size: 100% 100%; --svg: ${expectedURL('activity')}; } +`); + }); + + test('Content', () => { + const iconSet: IconifyJSON = { + prefix: 'test-prefix', + icons: { + '123': { + body: '', + }, + 'activity': { + body: '', + }, + 'airplane': { + body: '', + }, + 'airplane-engines': { + body: '', + }, + 'empty': { + body: '', + }, + }, + }; + const expectedURL = (name: string, color = 'black') => + svgToURL( + `${iconSet.icons[ + name + ].body.replace(/currentColor/g, color)}` + ); + + expect( + getIconsContentCSS( + iconSet, + ['activity', '123', 'airplane', 'whatever'], + { + height: 16, + format: 'expanded', + rules: { + display: 'inline-block', + }, + } + ) + ).toBe(`.icon--test-prefix--activity::after { + display: inline-block; + content: ${expectedURL('activity')}; +} + +.icon--test-prefix--123::after { + display: inline-block; + content: ${expectedURL('123')}; +} + +.icon--test-prefix--airplane::after { + display: inline-block; + content: ${expectedURL('airplane')}; +} + +/* Could not find icon: whatever */ `); }); });