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:
parent
0e77f9f423
commit
6853b3e29c
@ -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).
|
||||
|
98
plugins/tailwind/README.md
Normal file
98
plugins/tailwind/README.md
Normal 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Ü
|
39
plugins/tailwind/api-extractor.json
Normal file
39
plugins/tailwind/api-extractor.json
Normal 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
98
plugins/tailwind/build.js
Normal 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
422
plugins/tailwind/dist/iconify.js
vendored
Normal 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
71
plugins/tailwind/dist/plugin.d.ts
vendored
Normal 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
439
plugins/tailwind/dist/plugin.js
vendored
Normal 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;
|
7
plugins/tailwind/jest.config.js
Normal file
7
plugins/tailwind/jest.config.js
Normal 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
5
plugins/tailwind/lib/iconify.d.ts
vendored
Normal 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>>;
|
118
plugins/tailwind/lib/iconify.js
Normal file
118
plugins/tailwind/lib/iconify.js
Normal 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
3
plugins/tailwind/lib/options.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import type { IconCSSIconSetOptions } from '@iconify/utils/lib/css/types';
|
||||
export interface IconifyPluginOptions extends IconCSSIconSetOptions {
|
||||
}
|
1
plugins/tailwind/lib/options.js
Normal file
1
plugins/tailwind/lib/options.js
Normal file
@ -0,0 +1 @@
|
||||
export {};
|
13
plugins/tailwind/lib/plugin.d.ts
vendored
Normal file
13
plugins/tailwind/lib/plugin.d.ts
vendored
Normal 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 };
|
15
plugins/tailwind/lib/plugin.js
Normal file
15
plugins/tailwind/lib/plugin.js
Normal 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;
|
21
plugins/tailwind/license.txt
Normal file
21
plugins/tailwind/license.txt
Normal 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.
|
49
plugins/tailwind/package.json
Normal file
49
plugins/tailwind/package.json
Normal 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"
|
||||
}
|
||||
}
|
44
plugins/tailwind/rollup.config.mjs
Normal file
44
plugins/tailwind/rollup.config.mjs
Normal 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;
|
141
plugins/tailwind/src/iconify.ts
Normal file
141
plugins/tailwind/src/iconify.ts
Normal 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;
|
||||
}
|
5
plugins/tailwind/src/options.ts
Normal file
5
plugins/tailwind/src/options.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import type { IconCSSIconSetOptions } from '@iconify/utils/lib/css/types';
|
||||
|
||||
export interface IconifyPluginOptions extends IconCSSIconSetOptions {
|
||||
//
|
||||
}
|
23
plugins/tailwind/src/plugin.ts
Normal file
23
plugins/tailwind/src/plugin.ts
Normal 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 };
|
68
plugins/tailwind/tests/get-css-test.ts
Normal file
68
plugins/tailwind/tests/get-css-test.ts
Normal 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);
|
||||
});
|
||||
});
|
8
plugins/tailwind/tests/tsconfig.json
Normal file
8
plugins/tailwind/tests/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../tsconfig-base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "jest"],
|
||||
"rootDir": ".",
|
||||
"outDir": "../tests-compiled"
|
||||
}
|
||||
}
|
16
plugins/tailwind/tsconfig-base.json
Normal file
16
plugins/tailwind/tsconfig-base.json
Normal 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
|
||||
}
|
||||
}
|
9
plugins/tailwind/tsconfig.json
Normal file
9
plugins/tailwind/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig-base.json",
|
||||
"include": ["src/**/*.ts", ".eslintrc.js"],
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"lib": ["ESNext", "DOM"]
|
||||
}
|
||||
}
|
1
plugins/tailwind/tsconfig.tsbuildinfo
Normal file
1
plugins/tailwind/tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
1196
pnpm-lock.yaml
1196
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -6,6 +6,7 @@ packages:
|
||||
- 'iconify-icon/icon'
|
||||
- 'iconify-icon/*'
|
||||
- 'components/*'
|
||||
- 'plugins/*'
|
||||
- 'components-demo/*'
|
||||
- 'iconify-icon-demo/*'
|
||||
# - 'debug/*'
|
||||
|
Loading…
Reference in New Issue
Block a user