2
0
mirror of https://github.com/iconify/iconify.git synced 2024-12-12 05:37:49 +00:00

feat: plugin for Tailwind CSS

This commit is contained in:
Vjacheslav Trushkin 2023-01-11 17:42:21 +02:00
parent 0e77f9f423
commit 6853b3e29c
27 changed files with 2612 additions and 308 deletions

View File

@ -17,6 +17,7 @@ What is included in this repository?
- Directory `packages` contains main reusable packages: types, utilities, reusable functions used by various components.
- Directory `iconify-icon` contains `iconify-icon` web component that renders icons. It also contains wrappers for various frameworks that cannot handle web components.
- Directory `components` contains older version of icon components that are native to various frameworks, which do not use web component.
- Directory `plugins` contains plugins for various frameworks, which generate icons.
Other repositories you might want to look at:
@ -158,6 +159,14 @@ Directory `components-demo` contains demo packages that show usage of icon compo
- [SvelteKit demo](./components-demo/sveltekit-demo/) - demo for SvelteKit, using Svelte component on the server and in the browser. Run `npm run dev` to start the demo.
- [Ember demo](./components-demo/ember-demo/) - demo for Ember component. Run `npm run build` to build demo and `npm run start` to start it.
### Plugins
Directory `plugins` contains plugins.
| Package | Usage |
| ------------------------------------------ | ------------ |
| [Tailwind CSS plugin](./plugins/tailwind/) | Tailwind CSS |
## Installation, debugging and contributing
See [CONTRIBUTING.md](./CONTRIBUTING.md).

View File

@ -0,0 +1,98 @@
# Iconify for Tailwind CSS
This plugin creates CSS for over 100k open source icons.
[Browse icons at Iconify](https://icon-sets.iconify.design/) to see all icons.
## Usage
1. Install packages icon sets.
2. In `tailwind.config.js` import plugin and specify list of icons you want to load.
## HTML
To use icon in HTML, it is as easy as adding 2 class names:
- Class name for icon set.
- Class name for icon.
```html
<span class="icon--mdi icon--mdi--home"></span>
```
Why 2 class names? It reduces duplication and makes it easy to change all icons from one icon set.
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).
### Color, size, alignment
To change icon size or color, change font size or text color, like you would with any text.
Icon color cannot be changed for icons with hardcoded palette, such as most emoji sets or flag icons.
To align icon below baseline, add negative vertical alignment, like this (you can also use Tailwind class for that):
```html
<span class="icon--mdi icon--mdi--home" style="vertical-align: -0.125em"></span>
```
## Installing icon sets
Plugin does not include icon sets. You need to install icon sets separately.
To install all 100k+ icons, install `@iconify/json` as a dev dependency.
If you do not want to install big package, install `@iconify-json/` packages for icon sets that you use.
See [Iconify icon sets](https://icon-sets.iconify.design/) for list of available icon sets and icons.
See [Iconify documentation](https://docs.iconify.design/icons/json.html) for list of packages.
## Tailwind config
Then you need to add and configure plugin.
Add this to `tailwind.config.js`:
```js
const iconifyPlugin = require('@iconify/tailwind');
```
Then in plugins section add `iconifyPlugin` with list of icons you want to load.
Example:
```js
module.exports = {
content: ['./src/*.html'],
theme: {
extend: {},
},
plugins: [
// Iconify plugin with list of icons you need
iconifyPlugin(['mdi:home', 'mdi-light:account']),
],
presets: [],
};
```
### Icon names
Unfortunately Tailwind CSS cannot dynamically find all icon names. You need to specify list of icons you want to use.
### Options
Plugin accepts options as a second parameter. You can use it to change selectors.
See [documentation for function used by plugin](https://docs.iconify.design/tools/utils/get-icons-css.html) for list of options.
## License
This package is licensed under MIT license.
`SPDX-License-Identifier: MIT`
This license does not apply to icons. Icons are released under different licenses, see each icon set for details.
Icons available by default are all licensed under some kind of open-source or free license.
© 2023 Vjacheslav Trushkin / Iconify OÜ

View File

@ -0,0 +1,39 @@
{
"$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
plugins/tailwind/build.js Normal file
View File

@ -0,0 +1,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
plugins/tailwind/dist/iconify.js vendored Normal file
View File

@ -0,0 +1,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;

71
plugins/tailwind/dist/plugin.d.ts vendored Normal file
View File

@ -0,0 +1,71 @@
import { Config } from 'tailwindcss/types/config';
import { PluginCreator } from 'tailwindcss/types/config';
/**
* Formatting modes. Same as in SASS
*/
declare type CSSFormatMode = 'expanded' | 'compact' | 'compressed';
/**
* Formatting options
*/
declare interface IconCSSFormatOptions {
format?: CSSFormatMode;
}
/**
* Selector for icon
*/
declare interface IconCSSIconSelectorOptions {
pseudoSelector?: boolean;
iconSelector?: string;
}
/**
* Options for generating multiple icons
*/
declare interface IconCSSIconSetOptions extends IconCSSSharedOptions, IconCSSSelectorOptions, IconCSSModeOptions, IconCSSFormatOptions {
}
/**
* Icon mode
*/
declare type IconCSSMode = 'mask' | 'background';
/**
* Mode
*/
declare interface IconCSSModeOptions {
mode?: IconCSSMode;
}
/**
* Selector for icon when generating data from icon set
*/
declare interface IconCSSSelectorOptions extends IconCSSIconSelectorOptions {
commonSelector?: string;
overrideSelector?: string;
}
/**
* Options common for both multiple icons and single icon
*/
declare interface IconCSSSharedOptions {
varName?: string | null;
forceSquare?: boolean;
color?: string;
}
/**
* Iconify plugin
*/
declare function iconifyPlugin(icons: string[] | string, options?: IconifyPluginOptions): {
handler: PluginCreator;
config?: Partial<Config>;
};
export default iconifyPlugin;
export declare interface IconifyPluginOptions extends IconCSSIconSetOptions {
}
export { }

439
plugins/tailwind/dist/plugin.js vendored Normal file
View File

@ -0,0 +1,439 @@
/**
* (c) Iconify for Tailwind CSS
*
* 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
*/
'use strict';
var plugin = require('tailwindcss/plugin');
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) => {
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
*/
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;
}
/**
* Iconify plugin
*/
function iconifyPlugin(icons, options = {}) {
return plugin(({ addUtilities }) => {
const rules = getCSSRules(icons, options);
addUtilities(rules);
});
}
module.exports = iconifyPlugin;

View File

@ -0,0 +1,7 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
verbose: true,
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/tests/*-test.ts'],
};

5
plugins/tailwind/lib/iconify.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
import type { IconifyPluginOptions } from './options';
/**
* Get CSS rules for icon
*/
export declare function getCSSRules(icons: string[] | string, options?: IconifyPluginOptions): Record<string, Record<string, string>>;

View File

@ -0,0 +1,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;
}

3
plugins/tailwind/lib/options.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
import type { IconCSSIconSetOptions } from '@iconify/utils/lib/css/types';
export interface IconifyPluginOptions extends IconCSSIconSetOptions {
}

View File

@ -0,0 +1 @@
export {};

13
plugins/tailwind/lib/plugin.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
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 };

View File

@ -0,0 +1,15 @@
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;

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Vjacheslav Trushkin / Iconify OÜ
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,49 @@
{
"name": "@iconify/tailwind",
"description": "Iconify plugin for Tailwind CSS",
"author": "Vjacheslav Trushkin <cyberalien@gmail.com> (https://iconify.design)",
"version": "0.0.1",
"license": "MIT",
"main": "./dist/plugin.js",
"types": "./dist/plugin.d.ts",
"bugs": "https://github.com/iconify/iconify/issues",
"homepage": "https://iconify.design/",
"funding": "https://github.com/sponsors/cyberalien",
"repository": {
"type": "git",
"url": "https://github.com/iconify/iconify.git",
"directory": "plugins/tailwind"
},
"scripts": {
"clean": "rimraf lib dist tsconfig.tsbuildinfo",
"lint": "eslint src/**/*.ts",
"prebuild": "pnpm run lint && pnpm run clean",
"build": "node build",
"build:api": "api-extractor run --local --verbose",
"build:lib": "tsc -b",
"build:dist": "rollup -c rollup.config.mjs",
"test": "jest --runInBand"
},
"dependencies": {
"@iconify/types": "workspace:^"
},
"devDependencies": {
"@iconify-json/line-md": "^1.1.22",
"@iconify-json/mdi-light": "^1.1.5",
"@iconify/utils": "workspace:^",
"@microsoft/api-extractor": "^7.33.7",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-replace": "^5.0.2",
"@types/jest": "^29.2.4",
"@types/jsdom": "^20.0.1",
"@types/node": "^18.11.17",
"@typescript-eslint/eslint-plugin": "^5.47.0",
"eslint": "^8.30.0",
"jest": "^29.3.1",
"rimraf": "^3.0.2",
"rollup": "^3.8.1",
"tailwindcss": "^3.2.4",
"ts-jest": "^29.0.3",
"typescript": "^4.9.4"
}
}

View File

@ -0,0 +1,44 @@
import { readFileSync, writeFileSync } from 'fs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
// Header
const header = `/**
* (c) Iconify for Tailwind CSS
*
* 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 __iconify_version__
*/`;
// Get replacements
const replacements = {
preventAssignment: true,
};
const packageJSON = JSON.parse(readFileSync('package.json', 'utf8'));
replacements['__iconify_version__'] = packageJSON.version;
// Export configuration
const config = {
input: 'lib/plugin.js',
output: [
{
file: 'dist/plugin.js',
format: 'cjs',
banner: header,
},
],
external: ['tailwindcss/plugin'],
plugins: [
resolve({
browser: true,
}),
replace(replacements),
],
};
export default config;

View File

@ -0,0 +1,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;
}

View File

@ -0,0 +1,5 @@
import type { IconCSSIconSetOptions } from '@iconify/utils/lib/css/types';
export interface IconifyPluginOptions extends IconCSSIconSetOptions {
//
}

View File

@ -0,0 +1,23 @@
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 };

View File

@ -0,0 +1,68 @@
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);
});
});

View File

@ -0,0 +1,8 @@
{
"extends": "../tsconfig-base.json",
"compilerOptions": {
"types": ["node", "jest"],
"rootDir": ".",
"outDir": "../tests-compiled"
}
}

View File

@ -0,0 +1,16 @@
{
"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
}
}

View File

@ -0,0 +1,9 @@
{
"extends": "./tsconfig-base.json",
"include": ["src/**/*.ts", ".eslintrc.js"],
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"lib": ["ESNext", "DOM"]
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,7 @@ packages:
- 'iconify-icon/icon'
- 'iconify-icon/*'
- 'components/*'
- 'plugins/*'
- 'components-demo/*'
- 'iconify-icon-demo/*'
# - 'debug/*'