@iconify/tailwind
Advanced tools
+11
-1
@@ -68,5 +68,15 @@ import { Config } from 'tailwindcss/types/config'; | ||
| export declare interface IconifyPluginOptions extends IconCSSIconSetOptions { | ||
| /** | ||
| * Options for locating icon sets | ||
| */ | ||
| declare interface IconifyPluginFileOptions { | ||
| files?: Record<string, string>; | ||
| } | ||
| /** | ||
| * All options | ||
| */ | ||
| export declare interface IconifyPluginOptions extends IconCSSIconSetOptions, IconifyPluginFileOptions { | ||
| } | ||
| export { } |
+9
-6
@@ -10,3 +10,3 @@ /** | ||
| * @license MIT | ||
| * @version 0.0.1 | ||
| * @version 0.0.2 | ||
| */ | ||
@@ -318,3 +318,6 @@ 'use strict'; | ||
| */ | ||
| function locateIconSet(prefix) { | ||
| function locateIconSet(prefix, options) { | ||
| if (options.files?.[prefix]) { | ||
| return options.files?.[prefix]; | ||
| } | ||
| try { | ||
@@ -332,4 +335,4 @@ return require.resolve(`@iconify-json/${prefix}/icons.json`); | ||
| */ | ||
| function loadIconSet(prefix) { | ||
| const filename = locateIconSet(prefix); | ||
| function loadIconSet(prefix, options) { | ||
| const filename = locateIconSet(prefix, options); | ||
| if (filename) { | ||
@@ -414,3 +417,3 @@ try { | ||
| for (const prefix in prefixes) { | ||
| const iconSet = loadIconSet(prefix); | ||
| const iconSet = loadIconSet(prefix, options); | ||
| if (!iconSet) { | ||
@@ -437,4 +440,4 @@ throw new Error(`Cannot load icon set for "${prefix}"`); | ||
| function iconifyPlugin(icons, options = {}) { | ||
| const rules = getCSSRules(icons, options); | ||
| return plugin(({ addUtilities }) => { | ||
| const rules = getCSSRules(icons, options); | ||
| addUtilities(rules); | ||
@@ -441,0 +444,0 @@ }); |
+1
-1
@@ -5,3 +5,3 @@ { | ||
| "author": "Vjacheslav Trushkin <cyberalien@gmail.com> (https://iconify.design)", | ||
| "version": "0.0.1", | ||
| "version": "0.0.2", | ||
| "license": "MIT", | ||
@@ -8,0 +8,0 @@ "main": "./dist/plugin.js", |
+5
-5
@@ -16,4 +16,4 @@ # Iconify for Tailwind CSS | ||
| - Class name for icon set. | ||
| - Class name for icon. | ||
| - Class name for icon set: `icon--{prefix}`. | ||
| - Class name for icon: `icon--{prefix}--{name}`. | ||
@@ -24,3 +24,3 @@ ```html | ||
| Why 2 class names? It reduces duplication and makes it easy to change all icons from one icon set. | ||
| Why 2 class names? It reduces duplication and makes it easy to target all icons from one icon set. | ||
@@ -31,2 +31,4 @@ You can change that with options: you can change class names format, you can disable common selector. See [options for function used by plugin](https://docs.iconify.design/tools/utils/get-icons-css.html). | ||
| Monoton icons can change color! See [Iconify documentation](https://docs.iconify.design/icon-components/css.html#mask) for longer explanation. | ||
| To change icon size or color, change font size or text color, like you would with any text. | ||
@@ -56,4 +58,2 @@ | ||
| Then you need to add and configure plugin. | ||
| Add this to `tailwind.config.js`: | ||
@@ -60,0 +60,0 @@ |
| { | ||
| "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", | ||
| "mainEntryPointFilePath": "lib/plugin.d.ts", | ||
| "bundledPackages": ["@iconify/utils"], | ||
| "compiler": {}, | ||
| "apiReport": { | ||
| "enabled": false | ||
| }, | ||
| "docModel": { | ||
| "enabled": false | ||
| }, | ||
| "dtsRollup": { | ||
| "enabled": true, | ||
| "untrimmedFilePath": "<projectFolder>/dist/plugin.d.ts" | ||
| }, | ||
| "tsdocMetadata": { | ||
| "enabled": false | ||
| }, | ||
| "messages": { | ||
| "compilerMessageReporting": { | ||
| "default": { | ||
| "logLevel": "warning" | ||
| } | ||
| }, | ||
| "extractorMessageReporting": { | ||
| "default": { | ||
| "logLevel": "warning" | ||
| }, | ||
| "ae-missing-release-tag": { | ||
| "logLevel": "none" | ||
| } | ||
| }, | ||
| "tsdocMessageReporting": { | ||
| "default": { | ||
| "logLevel": "warning" | ||
| } | ||
| } | ||
| } | ||
| } |
-98
| /* eslint-disable */ | ||
| const fs = require('fs'); | ||
| const child_process = require('child_process'); | ||
| // List of commands to run | ||
| const commands = []; | ||
| // Parse command line | ||
| const compile = { | ||
| lib: true, | ||
| dist: true, | ||
| api: true, | ||
| }; | ||
| process.argv.slice(2).forEach((cmd) => { | ||
| if (cmd.slice(0, 2) !== '--') { | ||
| return; | ||
| } | ||
| const parts = cmd.slice(2).split('-'); | ||
| if (parts.length === 2) { | ||
| // Parse 2 part commands like --with-lib | ||
| const key = parts.pop(); | ||
| if (compile[key] === void 0) { | ||
| return; | ||
| } | ||
| switch (parts.shift()) { | ||
| case 'with': | ||
| // enable module | ||
| compile[key] = true; | ||
| break; | ||
| case 'without': | ||
| // disable module | ||
| compile[key] = false; | ||
| break; | ||
| case 'only': | ||
| // disable other modules | ||
| Object.keys(compile).forEach((key2) => { | ||
| compile[key2] = key2 === key; | ||
| }); | ||
| break; | ||
| } | ||
| } | ||
| }); | ||
| // Check if required modules in same monorepo are available | ||
| const fileExists = (file) => { | ||
| try { | ||
| fs.statSync(file); | ||
| } catch (e) { | ||
| return false; | ||
| } | ||
| return true; | ||
| }; | ||
| if (compile.dist && !fileExists('./lib/index.js')) { | ||
| compile.lib = true; | ||
| } | ||
| if (compile.api && !fileExists('./lib/index.d.ts')) { | ||
| compile.lib = true; | ||
| } | ||
| // Compile packages | ||
| Object.keys(compile).forEach((key) => { | ||
| if (compile[key]) { | ||
| commands.push({ | ||
| cmd: 'npm', | ||
| args: ['run', 'build:' + key], | ||
| }); | ||
| } | ||
| }); | ||
| /** | ||
| * Run next command | ||
| */ | ||
| const next = () => { | ||
| const item = commands.shift(); | ||
| if (item === void 0) { | ||
| process.exit(0); | ||
| } | ||
| if (item.cwd === void 0) { | ||
| item.cwd = __dirname; | ||
| } | ||
| const result = child_process.spawnSync(item.cmd, item.args, { | ||
| cwd: item.cwd, | ||
| stdio: 'inherit', | ||
| }); | ||
| if (result.status === 0) { | ||
| process.nextTick(next); | ||
| } else { | ||
| process.exit(result.status); | ||
| } | ||
| }; | ||
| next(); |
-422
| /** | ||
| * (c) Iconify | ||
| * | ||
| * For the full copyright and license information, please view the license.txt | ||
| * files at https://github.com/iconify/iconify | ||
| * | ||
| * Licensed under MIT. | ||
| * | ||
| * @license MIT | ||
| * @version 0.0.1-dev | ||
| */ | ||
| 'use strict'; | ||
| var fs = require('fs'); | ||
| const defaultIconDimensions = Object.freeze( | ||
| { | ||
| left: 0, | ||
| top: 0, | ||
| width: 16, | ||
| height: 16 | ||
| } | ||
| ); | ||
| const defaultIconTransformations = Object.freeze({ | ||
| rotate: 0, | ||
| vFlip: false, | ||
| hFlip: false | ||
| }); | ||
| const defaultIconProps = Object.freeze({ | ||
| ...defaultIconDimensions, | ||
| ...defaultIconTransformations | ||
| }); | ||
| const defaultExtendedIconProps = Object.freeze({ | ||
| ...defaultIconProps, | ||
| body: "", | ||
| hidden: false | ||
| }); | ||
| function mergeIconTransformations(obj1, obj2) { | ||
| const result = {}; | ||
| if (!obj1.hFlip !== !obj2.hFlip) { | ||
| result.hFlip = true; | ||
| } | ||
| if (!obj1.vFlip !== !obj2.vFlip) { | ||
| result.vFlip = true; | ||
| } | ||
| const rotate = ((obj1.rotate || 0) + (obj2.rotate || 0)) % 4; | ||
| if (rotate) { | ||
| result.rotate = rotate; | ||
| } | ||
| return result; | ||
| } | ||
| function mergeIconData(parent, child) { | ||
| const result = mergeIconTransformations(parent, child); | ||
| for (const key in defaultExtendedIconProps) { | ||
| if (key in defaultIconTransformations) { | ||
| if (key in parent && !(key in result)) { | ||
| result[key] = defaultIconTransformations[key]; | ||
| } | ||
| } else if (key in child) { | ||
| result[key] = child[key]; | ||
| } else if (key in parent) { | ||
| result[key] = parent[key]; | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| function getIconsTree(data, names) { | ||
| const icons = data.icons; | ||
| const aliases = data.aliases || /* @__PURE__ */ Object.create(null); | ||
| const resolved = /* @__PURE__ */ Object.create(null); | ||
| function resolve(name) { | ||
| if (icons[name]) { | ||
| return resolved[name] = []; | ||
| } | ||
| if (!(name in resolved)) { | ||
| resolved[name] = null; | ||
| const parent = aliases[name] && aliases[name].parent; | ||
| const value = parent && resolve(parent); | ||
| if (value) { | ||
| resolved[name] = [parent].concat(value); | ||
| } | ||
| } | ||
| return resolved[name]; | ||
| } | ||
| (names || Object.keys(icons).concat(Object.keys(aliases))).forEach(resolve); | ||
| return resolved; | ||
| } | ||
| function internalGetIconData(data, name, tree) { | ||
| const icons = data.icons; | ||
| const aliases = data.aliases || /* @__PURE__ */ Object.create(null); | ||
| let currentProps = {}; | ||
| function parse(name2) { | ||
| currentProps = mergeIconData( | ||
| icons[name2] || aliases[name2], | ||
| currentProps | ||
| ); | ||
| } | ||
| parse(name); | ||
| tree.forEach(parse); | ||
| return mergeIconData(data, currentProps); | ||
| } | ||
| function getIconData(data, name) { | ||
| if (data.icons[name]) { | ||
| return internalGetIconData(data, name, []); | ||
| } | ||
| const tree = getIconsTree(data, [name])[name]; | ||
| return tree ? internalGetIconData(data, name, tree) : null; | ||
| } | ||
| function iconToHTML(body, attributes) { | ||
| let renderAttribsHTML = body.indexOf("xlink:") === -1 ? "" : ' xmlns:xlink="http://www.w3.org/1999/xlink"'; | ||
| for (const attr in attributes) { | ||
| renderAttribsHTML += " " + attr + '="' + attributes[attr] + '"'; | ||
| } | ||
| return '<svg xmlns="http://www.w3.org/2000/svg"' + renderAttribsHTML + ">" + body + "</svg>"; | ||
| } | ||
| const unitsSplit = /(-?[0-9.]*[0-9]+[0-9.]*)/g; | ||
| const unitsTest = /^-?[0-9.]*[0-9]+[0-9.]*$/g; | ||
| function calculateSize(size, ratio, precision) { | ||
| if (ratio === 1) { | ||
| return size; | ||
| } | ||
| precision = precision || 100; | ||
| if (typeof size === "number") { | ||
| return Math.ceil(size * ratio * precision) / precision; | ||
| } | ||
| if (typeof size !== "string") { | ||
| return size; | ||
| } | ||
| const oldParts = size.split(unitsSplit); | ||
| if (oldParts === null || !oldParts.length) { | ||
| return size; | ||
| } | ||
| const newParts = []; | ||
| let code = oldParts.shift(); | ||
| let isNumber = unitsTest.test(code); | ||
| while (true) { | ||
| if (isNumber) { | ||
| const num = parseFloat(code); | ||
| if (isNaN(num)) { | ||
| newParts.push(code); | ||
| } else { | ||
| newParts.push(Math.ceil(num * ratio * precision) / precision); | ||
| } | ||
| } else { | ||
| newParts.push(code); | ||
| } | ||
| code = oldParts.shift(); | ||
| if (code === void 0) { | ||
| return newParts.join(""); | ||
| } | ||
| isNumber = !isNumber; | ||
| } | ||
| } | ||
| function encodeSVGforURL(svg) { | ||
| return svg.replace(/"/g, "'").replace(/%/g, "%25").replace(/#/g, "%23").replace(/</g, "%3C").replace(/>/g, "%3E").replace(/\s+/g, " "); | ||
| } | ||
| function svgToURL(svg) { | ||
| return 'url("data:image/svg+xml,' + encodeSVGforURL(svg) + '")'; | ||
| } | ||
| function getCommonCSSRules(options) { | ||
| const result = { | ||
| display: "inline-block", | ||
| width: "1em", | ||
| height: "1em" | ||
| }; | ||
| 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; | ||
| } | ||
| function generateItemCSSRules(icon, options) { | ||
| const result = {}; | ||
| const varName = options.varName; | ||
| if (!options.forceSquare && icon.width !== icon.height) { | ||
| result["width"] = calculateSize("1em", icon.width / icon.height); | ||
| } | ||
| const svg = iconToHTML( | ||
| icon.body.replace(/currentColor/g, options.color || "black"), | ||
| { | ||
| viewBox: `${icon.left} ${icon.top} ${icon.width} ${icon.height}`, | ||
| width: icon.width.toString(), | ||
| height: icon.height.toString() | ||
| } | ||
| ); | ||
| const url = svgToURL(svg); | ||
| 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; | ||
| } | ||
| const commonSelector = ".icon--{prefix}"; | ||
| const iconSelector = ".icon--{prefix}--{name}"; | ||
| const defaultSelectors = { | ||
| commonSelector, | ||
| iconSelector, | ||
| overrideSelector: commonSelector + iconSelector | ||
| }; | ||
| function getIconsCSSData(iconSet, names, options = {}) { | ||
| const css = []; | ||
| const errors = []; | ||
| const palette = options.color ? true : iconSet.info?.palette; | ||
| let mode = options.mode || typeof palette === "boolean" && (palette ? "background" : "mask"); | ||
| if (!mode) { | ||
| mode = "mask"; | ||
| errors.push( | ||
| "/* cannot detect icon mode: not set in options and icon set is missing info, rendering as " + mode + " */" | ||
| ); | ||
| } | ||
| let varName = options.varName; | ||
| if (varName === void 0 && mode === "mask") { | ||
| varName = "svg"; | ||
| } | ||
| const newOptions = { | ||
| ...options, | ||
| mode, | ||
| varName | ||
| }; | ||
| const { commonSelector: commonSelector2, iconSelector: iconSelector2, overrideSelector } = newOptions.iconSelector ? newOptions : defaultSelectors; | ||
| const iconSelectorWithPrefix = iconSelector2.replace( | ||
| /{prefix}/g, | ||
| iconSet.prefix | ||
| ); | ||
| const commonRules = getCommonCSSRules(newOptions); | ||
| const hasCommonRules = commonSelector2 && commonSelector2 !== iconSelector2; | ||
| const commonSelectors = /* @__PURE__ */ new Set(); | ||
| if (hasCommonRules) { | ||
| css.push({ | ||
| selector: commonSelector2.replace(/{prefix}/g, iconSet.prefix), | ||
| rules: commonRules | ||
| }); | ||
| } | ||
| 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); | ||
| } | ||
| } | ||
| const result = { | ||
| css, | ||
| errors | ||
| }; | ||
| if (!hasCommonRules && commonSelectors.size) { | ||
| const selector = Array.from(commonSelectors).join( | ||
| newOptions.format === "compressed" ? "," : ", " | ||
| ); | ||
| result.common = { | ||
| selector, | ||
| rules: commonRules | ||
| }; | ||
| } | ||
| return result; | ||
| } | ||
| const matchIconName = /^[a-z0-9]+(-[a-z0-9]+)*$/; | ||
| const missingIconsListError = 'TailwindCSS cannot dynamically find all used icons. Need to pass list of used icons to Iconify plugin.'; | ||
| /** | ||
| * Locate icon set | ||
| */ | ||
| function locateIconSet(prefix) { | ||
| try { | ||
| return require.resolve(`@iconify-json/${prefix}/icons.json`); | ||
| } | ||
| catch { } | ||
| try { | ||
| return require.resolve(`@iconify/json/json/${prefix}.json`); | ||
| } | ||
| catch { } | ||
| } | ||
| /** | ||
| * Load icon set | ||
| */ | ||
| function loadIconSet(prefix) { | ||
| const filename = locateIconSet(prefix); | ||
| if (filename) { | ||
| try { | ||
| return JSON.parse(fs.readFileSync(filename, 'utf8')); | ||
| } | ||
| catch { } | ||
| } | ||
| } | ||
| /** | ||
| * Get icon names from list | ||
| */ | ||
| function getIconNames(icons) { | ||
| const prefixes = Object.create(null); | ||
| // Add entry | ||
| const add = (prefix, name) => { | ||
| if (typeof prefix === 'string' && | ||
| prefix.match(matchIconName) && | ||
| typeof name === 'string' && | ||
| name.match(matchIconName)) { | ||
| (prefixes[prefix] || (prefixes[prefix] = new Set())).add(name); | ||
| } | ||
| }; | ||
| // Comma or space separated string | ||
| let iconNames; | ||
| if (typeof icons === 'string') { | ||
| iconNames = icons.split(/[\s,]/); | ||
| } | ||
| else if (icons instanceof Array) { | ||
| iconNames = []; | ||
| // Split each array entry | ||
| icons.forEach((item) => { | ||
| item.split(/[\s,]/).forEach((name) => iconNames.push(name)); | ||
| }); | ||
| } | ||
| else { | ||
| throw new Error(missingIconsListError); | ||
| } | ||
| // Parse array | ||
| if (iconNames?.length) { | ||
| iconNames.forEach((icon) => { | ||
| // Attempt prefix:name split | ||
| const nameParts = icon.split(':'); | ||
| if (nameParts.length === 2) { | ||
| add(nameParts[0], nameParts[1]); | ||
| return; | ||
| } | ||
| // Attempt icon class: .icon--{prefix}--{name} | ||
| // with or without dot | ||
| const classParts = icon.split('--'); | ||
| if (classParts[0].match(/^\.?icon$/)) { | ||
| if (classParts.length === 3) { | ||
| add(classParts[1], classParts[2]); | ||
| return; | ||
| } | ||
| if (classParts.length === 2) { | ||
| // Partial match | ||
| return; | ||
| } | ||
| } | ||
| // Throw error | ||
| throw new Error(`Cannot resolve icon: "${icon}"`); | ||
| }); | ||
| } | ||
| else { | ||
| throw new Error(missingIconsListError); | ||
| } | ||
| return prefixes; | ||
| } | ||
| /** | ||
| * Get CSS rules for icon | ||
| */ | ||
| function getCSSRules(icons, options = {}) { | ||
| const rules = Object.create(null); | ||
| // Get all icons | ||
| const prefixes = getIconNames(icons); | ||
| // Parse all icon sets | ||
| for (const prefix in prefixes) { | ||
| const iconSet = loadIconSet(prefix); | ||
| const generated = getIconsCSSData(iconSet, Array.from(prefixes[prefix]), options); | ||
| const result = generated.common | ||
| ? [generated.common, ...generated.css] | ||
| : generated.css; | ||
| result.forEach((item) => { | ||
| const selector = item.selector instanceof Array | ||
| ? item.selector.join(', ') | ||
| : item.selector; | ||
| rules[selector] = item.rules; | ||
| }); | ||
| } | ||
| return rules; | ||
| } | ||
| exports.getCSSRules = getCSSRules; |
| /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ | ||
| module.exports = { | ||
| verbose: true, | ||
| preset: 'ts-jest', | ||
| testEnvironment: 'node', | ||
| testMatch: ['**/tests/*-test.ts'], | ||
| }; |
| import type { IconifyPluginOptions } from './options'; | ||
| /** | ||
| * Get CSS rules for icon | ||
| */ | ||
| export declare function getCSSRules(icons: string[] | string, options?: IconifyPluginOptions): Record<string, Record<string, string>>; |
-118
| import { readFileSync } from 'fs'; | ||
| import { getIconsCSSData } from '@iconify/utils/lib/css/icons'; | ||
| import { matchIconName } from '@iconify/utils/lib/icon/name'; | ||
| const missingIconsListError = 'TailwindCSS cannot dynamically find all used icons. Need to pass list of used icons to Iconify plugin.'; | ||
| /** | ||
| * Locate icon set | ||
| */ | ||
| function locateIconSet(prefix) { | ||
| try { | ||
| return require.resolve(`@iconify-json/${prefix}/icons.json`); | ||
| } | ||
| catch { } | ||
| try { | ||
| return require.resolve(`@iconify/json/json/${prefix}.json`); | ||
| } | ||
| catch { } | ||
| } | ||
| /** | ||
| * Load icon set | ||
| */ | ||
| function loadIconSet(prefix) { | ||
| const filename = locateIconSet(prefix); | ||
| if (filename) { | ||
| try { | ||
| return JSON.parse(readFileSync(filename, 'utf8')); | ||
| } | ||
| catch { } | ||
| } | ||
| } | ||
| /** | ||
| * Get icon names from list | ||
| */ | ||
| function getIconNames(icons) { | ||
| const prefixes = Object.create(null); | ||
| // Add entry | ||
| const add = (prefix, name) => { | ||
| if (typeof prefix === 'string' && | ||
| prefix.match(matchIconName) && | ||
| typeof name === 'string' && | ||
| name.match(matchIconName)) { | ||
| (prefixes[prefix] || (prefixes[prefix] = new Set())).add(name); | ||
| } | ||
| }; | ||
| // Comma or space separated string | ||
| let iconNames; | ||
| if (typeof icons === 'string') { | ||
| iconNames = icons.split(/[\s,.]/); | ||
| } | ||
| else if (icons instanceof Array) { | ||
| iconNames = []; | ||
| // Split each array entry | ||
| icons.forEach((item) => { | ||
| item.split(/[\s,.]/).forEach((name) => iconNames.push(name)); | ||
| }); | ||
| } | ||
| else { | ||
| throw new Error(missingIconsListError); | ||
| } | ||
| // Parse array | ||
| if (iconNames?.length) { | ||
| iconNames.forEach((icon) => { | ||
| if (!icon.trim()) { | ||
| return; | ||
| } | ||
| // Attempt prefix:name split | ||
| const nameParts = icon.split(':'); | ||
| if (nameParts.length === 2) { | ||
| add(nameParts[0], nameParts[1]); | ||
| return; | ||
| } | ||
| // Attempt icon class: .icon--{prefix}--{name} | ||
| // with or without dot | ||
| const classParts = icon.split('--'); | ||
| if (classParts[0].match(/^\.?icon$/)) { | ||
| if (classParts.length === 3) { | ||
| add(classParts[1], classParts[2]); | ||
| return; | ||
| } | ||
| if (classParts.length === 2) { | ||
| // Partial match | ||
| return; | ||
| } | ||
| } | ||
| // Throw error | ||
| throw new Error(`Cannot resolve icon: "${icon}"`); | ||
| }); | ||
| } | ||
| else { | ||
| throw new Error(missingIconsListError); | ||
| } | ||
| return prefixes; | ||
| } | ||
| /** | ||
| * Get CSS rules for icon | ||
| */ | ||
| export function getCSSRules(icons, options = {}) { | ||
| const rules = Object.create(null); | ||
| // Get all icons | ||
| const prefixes = getIconNames(icons); | ||
| // Parse all icon sets | ||
| for (const prefix in prefixes) { | ||
| const iconSet = loadIconSet(prefix); | ||
| if (!iconSet) { | ||
| throw new Error(`Cannot load icon set for "${prefix}"`); | ||
| } | ||
| const generated = getIconsCSSData(iconSet, Array.from(prefixes[prefix]), options); | ||
| const result = generated.common | ||
| ? [generated.common, ...generated.css] | ||
| : generated.css; | ||
| result.forEach((item) => { | ||
| const selector = item.selector instanceof Array | ||
| ? item.selector.join(', ') | ||
| : item.selector; | ||
| rules[selector] = item.rules; | ||
| }); | ||
| } | ||
| return rules; | ||
| } |
| import type { IconCSSIconSetOptions } from '@iconify/utils/lib/css/types'; | ||
| export interface IconifyPluginOptions extends IconCSSIconSetOptions { | ||
| } |
| export {}; |
| import type { IconifyPluginOptions } from './options'; | ||
| /** | ||
| * Iconify plugin | ||
| */ | ||
| declare function iconifyPlugin(icons: string[] | string, options?: IconifyPluginOptions): { | ||
| handler: import("tailwindcss/types/config").PluginCreator; | ||
| config?: Partial<import("tailwindcss/types/config").Config>; | ||
| }; | ||
| /** | ||
| * Export stuff | ||
| */ | ||
| export default iconifyPlugin; | ||
| export type { IconifyPluginOptions }; |
| import plugin from 'tailwindcss/plugin'; | ||
| import { getCSSRules } from './iconify'; | ||
| /** | ||
| * Iconify plugin | ||
| */ | ||
| function iconifyPlugin(icons, options = {}) { | ||
| return plugin(({ addUtilities }) => { | ||
| const rules = getCSSRules(icons, options); | ||
| addUtilities(rules); | ||
| }); | ||
| } | ||
| /** | ||
| * Export stuff | ||
| */ | ||
| export default iconifyPlugin; |
-141
| import { readFileSync } from 'fs'; | ||
| import type { IconifyJSON } from '@iconify/types'; | ||
| import { getIconsCSSData } from '@iconify/utils/lib/css/icons'; | ||
| import { matchIconName } from '@iconify/utils/lib/icon/name'; | ||
| import type { IconifyPluginOptions } from './options'; | ||
| const missingIconsListError = | ||
| 'TailwindCSS cannot dynamically find all used icons. Need to pass list of used icons to Iconify plugin.'; | ||
| /** | ||
| * Locate icon set | ||
| */ | ||
| function locateIconSet(prefix: string): string | undefined { | ||
| try { | ||
| return require.resolve(`@iconify-json/${prefix}/icons.json`); | ||
| } catch {} | ||
| try { | ||
| return require.resolve(`@iconify/json/json/${prefix}.json`); | ||
| } catch {} | ||
| } | ||
| /** | ||
| * Load icon set | ||
| */ | ||
| function loadIconSet(prefix: string): IconifyJSON | undefined { | ||
| const filename = locateIconSet(prefix); | ||
| if (filename) { | ||
| try { | ||
| return JSON.parse(readFileSync(filename, 'utf8')); | ||
| } catch {} | ||
| } | ||
| } | ||
| /** | ||
| * Get icon names from list | ||
| */ | ||
| function getIconNames(icons: string[] | string): Record<string, Set<string>> { | ||
| const prefixes = Object.create(null) as Record<string, Set<string>>; | ||
| // Add entry | ||
| const add = (prefix: string, name: string) => { | ||
| if ( | ||
| typeof prefix === 'string' && | ||
| prefix.match(matchIconName) && | ||
| typeof name === 'string' && | ||
| name.match(matchIconName) | ||
| ) { | ||
| (prefixes[prefix] || (prefixes[prefix] = new Set())).add(name); | ||
| } | ||
| }; | ||
| // Comma or space separated string | ||
| let iconNames: string[] | undefined; | ||
| if (typeof icons === 'string') { | ||
| iconNames = icons.split(/[\s,.]/); | ||
| } else if (icons instanceof Array) { | ||
| iconNames = []; | ||
| // Split each array entry | ||
| icons.forEach((item) => { | ||
| item.split(/[\s,.]/).forEach((name) => iconNames.push(name)); | ||
| }); | ||
| } else { | ||
| throw new Error(missingIconsListError); | ||
| } | ||
| // Parse array | ||
| if (iconNames?.length) { | ||
| iconNames.forEach((icon) => { | ||
| if (!icon.trim()) { | ||
| return; | ||
| } | ||
| // Attempt prefix:name split | ||
| const nameParts = icon.split(':'); | ||
| if (nameParts.length === 2) { | ||
| add(nameParts[0], nameParts[1]); | ||
| return; | ||
| } | ||
| // Attempt icon class: .icon--{prefix}--{name} | ||
| // with or without dot | ||
| const classParts = icon.split('--'); | ||
| if (classParts[0].match(/^\.?icon$/)) { | ||
| if (classParts.length === 3) { | ||
| add(classParts[1], classParts[2]); | ||
| return; | ||
| } | ||
| if (classParts.length === 2) { | ||
| // Partial match | ||
| return; | ||
| } | ||
| } | ||
| // Throw error | ||
| throw new Error(`Cannot resolve icon: "${icon}"`); | ||
| }); | ||
| } else { | ||
| throw new Error(missingIconsListError); | ||
| } | ||
| return prefixes; | ||
| } | ||
| /** | ||
| * Get CSS rules for icon | ||
| */ | ||
| export function getCSSRules( | ||
| icons: string[] | string, | ||
| options: IconifyPluginOptions = {} | ||
| ): Record<string, Record<string, string>> { | ||
| const rules = Object.create(null) as Record<string, Record<string, string>>; | ||
| // Get all icons | ||
| const prefixes = getIconNames(icons); | ||
| // Parse all icon sets | ||
| for (const prefix in prefixes) { | ||
| const iconSet = loadIconSet(prefix); | ||
| if (!iconSet) { | ||
| throw new Error(`Cannot load icon set for "${prefix}"`); | ||
| } | ||
| const generated = getIconsCSSData( | ||
| iconSet, | ||
| Array.from(prefixes[prefix]), | ||
| options | ||
| ); | ||
| const result = generated.common | ||
| ? [generated.common, ...generated.css] | ||
| : generated.css; | ||
| result.forEach((item) => { | ||
| const selector = | ||
| item.selector instanceof Array | ||
| ? item.selector.join(', ') | ||
| : item.selector; | ||
| rules[selector] = item.rules; | ||
| }); | ||
| } | ||
| return rules; | ||
| } |
| import type { IconCSSIconSetOptions } from '@iconify/utils/lib/css/types'; | ||
| export interface IconifyPluginOptions extends IconCSSIconSetOptions { | ||
| // | ||
| } |
| import plugin from 'tailwindcss/plugin'; | ||
| import { getCSSRules } from './iconify'; | ||
| import type { IconifyPluginOptions } from './options'; | ||
| /** | ||
| * Iconify plugin | ||
| */ | ||
| function iconifyPlugin( | ||
| icons: string[] | string, | ||
| options: IconifyPluginOptions = {} | ||
| ) { | ||
| return plugin(({ addUtilities }) => { | ||
| const rules = getCSSRules(icons, options); | ||
| addUtilities(rules); | ||
| }); | ||
| } | ||
| /** | ||
| * Export stuff | ||
| */ | ||
| export default iconifyPlugin; | ||
| export type { IconifyPluginOptions }; |
| import { getCSSRules } from '../src/iconify'; | ||
| describe('Testing CSS rules', () => { | ||
| it('One icon', () => { | ||
| const data = getCSSRules('mdi-light:home'); | ||
| expect(Object.keys(data)).toEqual([ | ||
| '.icon--mdi-light', | ||
| '.icon--mdi-light--home', | ||
| ]); | ||
| expect(Object.keys(data['.icon--mdi-light--home'])).toEqual(['--svg']); | ||
| }); | ||
| it('Multiple icons from same icon set', () => { | ||
| const data = getCSSRules([ | ||
| // By name | ||
| 'mdi-light:home', | ||
| // By selector | ||
| '.icon--mdi-light--arrow-left', | ||
| '.icon--mdi-light.icon--mdi-light--arrow-down', | ||
| // By class name | ||
| 'icon--mdi-light--file', | ||
| 'icon--mdi-light icon--mdi-light--format-clear', | ||
| ]); | ||
| expect(Object.keys(data)).toEqual([ | ||
| '.icon--mdi-light', | ||
| '.icon--mdi-light--home', | ||
| '.icon--mdi-light--arrow-left', | ||
| '.icon--mdi-light--arrow-down', | ||
| '.icon--mdi-light--file', | ||
| '.icon--mdi-light--format-clear', | ||
| ]); | ||
| }); | ||
| it('Multiple icon sets', () => { | ||
| const data = getCSSRules([ | ||
| // MDI Light | ||
| 'mdi-light:home', | ||
| // Line MD | ||
| 'line-md:home', | ||
| ]); | ||
| expect(Object.keys(data)).toEqual([ | ||
| '.icon--mdi-light', | ||
| '.icon--mdi-light--home', | ||
| '.icon--line-md', | ||
| '.icon--line-md--home', | ||
| ]); | ||
| }); | ||
| it('Bad class name', () => { | ||
| let threw = false; | ||
| try { | ||
| getCSSRules(['icon--mdi-light--home test']); | ||
| } catch { | ||
| threw = true; | ||
| } | ||
| expect(threw).toBe(true); | ||
| }); | ||
| it('Bad icon set', () => { | ||
| let threw = false; | ||
| try { | ||
| getCSSRules('test123:home'); | ||
| } catch { | ||
| threw = true; | ||
| } | ||
| expect(threw).toBe(true); | ||
| }); | ||
| }); |
| { | ||
| "extends": "../tsconfig-base.json", | ||
| "compilerOptions": { | ||
| "types": ["node", "jest"], | ||
| "rootDir": ".", | ||
| "outDir": "../tests-compiled" | ||
| } | ||
| } |
| { | ||
| "compilerOptions": { | ||
| "target": "ESNext", | ||
| "module": "ESNext", | ||
| "declaration": true, | ||
| "declarationMap": false, | ||
| "sourceMap": false, | ||
| "composite": true, | ||
| "strict": false, | ||
| "moduleResolution": "node", | ||
| "esModuleInterop": true, | ||
| "forceConsistentCasingInFileNames": true, | ||
| "importsNotUsedAsValues": "error", | ||
| "skipLibCheck": true | ||
| } | ||
| } |
| { | ||
| "extends": "./tsconfig-base.json", | ||
| "include": ["src/**/*.ts", ".eslintrc.js"], | ||
| "compilerOptions": { | ||
| "rootDir": "./src", | ||
| "outDir": "./lib", | ||
| "lib": ["ESNext", "DOM"] | ||
| } | ||
| } |
Sorry, the diff of this file is not supported yet
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
6
-53.85%0
-100%20580
-73.29%6
-75%536
-63.39%