├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc ├── @iconify-demo ├── doc-samples │ ├── .gitignore │ ├── data │ │ └── test.json │ ├── package.json │ ├── src │ │ ├── export │ │ │ ├── all-sets-svg.mjs │ │ │ └── json-to-svg-utils.ts │ │ ├── icon-set │ │ │ ├── chars.ts │ │ │ ├── check-theme.ts │ │ │ ├── count.ts │ │ │ ├── export.ts │ │ │ ├── for-each.ts │ │ │ ├── get-tree.ts │ │ │ ├── list-category.ts │ │ │ ├── list.ts │ │ │ ├── load.ts │ │ │ ├── remove.ts │ │ │ ├── rename.ts │ │ │ ├── resolve.ts │ │ │ ├── set-alias.ts │ │ │ ├── to-string.ts │ │ │ └── to-svg.ts │ │ ├── icon │ │ │ ├── color-default.ts │ │ │ ├── colors.ts │ │ │ ├── icon-set.ts │ │ │ ├── mask-colors.ts │ │ │ ├── mask-demo.ts │ │ │ ├── mask-white.ts │ │ │ ├── paths.ts │ │ │ ├── scale.ts │ │ │ ├── svgo.ts │ │ │ └── svgo2.ts │ │ ├── import │ │ │ ├── dir-sync.ts │ │ │ ├── dir.ts │ │ │ ├── figma-quill.ts │ │ │ ├── figma-solar.mjs │ │ │ ├── json-min.ts │ │ │ └── svg.ts │ │ ├── merge-icon-sets.ts │ │ ├── package │ │ │ ├── api.ts │ │ │ ├── git.ts │ │ │ ├── github.ts │ │ │ ├── gitlab.ts │ │ │ ├── npm-version.ts │ │ │ ├── npm.ts │ │ │ ├── package-version.ts │ │ │ └── version.ts │ │ └── svg │ │ │ └── cleanup.ts │ └── tsconfig.json ├── mdi │ ├── .gitignore │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── unocss │ ├── .gitignore │ ├── assets │ │ ├── svg │ │ │ ├── loading.svg │ │ │ └── loading2.svg │ │ └── test.json │ ├── index.html │ ├── package.json │ ├── src │ │ ├── App.vue │ │ ├── env.d.ts │ │ └── main.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── unocss.config.ts │ └── vite.config.ts └── unplugin │ ├── assets │ ├── svg-animated │ │ ├── loading.svg │ │ └── loading2.svg │ └── svg-ion │ │ ├── accessibility.svg │ │ ├── balloon-outline.svg │ │ └── build.svg │ ├── index.html │ ├── package.json │ ├── src │ ├── App.vue │ └── main.ts │ └── vite.config.ts ├── @iconify └── tools │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── build.config.ts │ ├── jest.config.cjs │ ├── jest.config.mjs │ ├── jest.shared.config.cjs │ ├── license.txt │ ├── package.json │ ├── src │ ├── colors │ │ ├── attribs.ts │ │ ├── detect.ts │ │ ├── parse.ts │ │ └── validate.ts │ ├── css │ │ ├── parse.ts │ │ └── parser │ │ │ ├── error.ts │ │ │ ├── export.ts │ │ │ ├── strings.ts │ │ │ ├── text.ts │ │ │ ├── tokens.ts │ │ │ ├── tree.ts │ │ │ └── types.ts │ ├── download │ │ ├── api │ │ │ ├── cache.ts │ │ │ ├── config.ts │ │ │ ├── download.ts │ │ │ ├── index.ts │ │ │ ├── queue.ts │ │ │ └── types.ts │ │ ├── git │ │ │ ├── branch.ts │ │ │ ├── hash.ts │ │ │ ├── index.ts │ │ │ └── reset.ts │ │ ├── github │ │ │ ├── hash.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── gitlab │ │ │ ├── hash.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── helpers │ │ │ ├── untar.ts │ │ │ └── unzip.ts │ │ ├── index.ts │ │ ├── npm │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ └── version.ts │ │ └── types │ │ │ ├── modified.ts │ │ │ └── sources.ts │ ├── export │ │ ├── directory.ts │ │ ├── helpers │ │ │ ├── custom-files.ts │ │ │ ├── prepare.ts │ │ │ └── types-version.ts │ │ ├── icon-package.ts │ │ └── json-package.ts │ ├── icon-set │ │ ├── index.ts │ │ ├── match.ts │ │ ├── merge.ts │ │ ├── modified.ts │ │ ├── props.ts │ │ ├── tags.ts │ │ └── types.ts │ ├── import │ │ ├── directory.ts │ │ └── figma │ │ │ ├── index.ts │ │ │ ├── nodes.ts │ │ │ ├── query.ts │ │ │ └── types │ │ │ ├── api.ts │ │ │ ├── nodes.ts │ │ │ ├── options.ts │ │ │ └── result.ts │ ├── index.ts │ ├── misc │ │ ├── bump-version.ts │ │ ├── cheerio.ts │ │ ├── compare-dirs.ts │ │ ├── exec.ts │ │ ├── keyword.ts │ │ ├── scan.ts │ │ └── write-json.ts │ ├── optimise │ │ ├── figma.ts │ │ ├── flags.ts │ │ ├── global-style.ts │ │ ├── mask.ts │ │ ├── origin.ts │ │ ├── scale.ts │ │ ├── svgo.ts │ │ └── unwrap.ts │ ├── svg │ │ ├── analyse.ts │ │ ├── analyse │ │ │ ├── error.ts │ │ │ └── types.ts │ │ ├── cleanup.ts │ │ ├── cleanup │ │ │ ├── attribs.ts │ │ │ ├── bad-tags.ts │ │ │ ├── inline-style.ts │ │ │ ├── root-style.ts │ │ │ ├── root-svg.ts │ │ │ └── svgo-style.ts │ │ ├── data │ │ │ ├── attributes.ts │ │ │ └── tags.ts │ │ ├── index.ts │ │ ├── parse-style.ts │ │ └── parse.ts │ └── tests │ │ └── helpers.ts │ ├── tests │ ├── colors │ │ ├── detect-test.ts │ │ ├── parse-test.ts │ │ └── validate-test.ts │ ├── css │ │ ├── quoted-string-test.ts │ │ ├── tokens-test.ts │ │ └── url-string-test.ts │ ├── export │ │ ├── directory-test.ts │ │ ├── icon-package-test.ts │ │ └── json-package-test.ts │ ├── fixtures │ │ ├── 1f3eb.svg │ │ ├── 1st_place_medal_color.svg │ │ ├── amphora_color.svg │ │ ├── animation.svg │ │ ├── apache-original-wordmark.svg │ │ ├── arangodb.svg │ │ ├── arty-animated.json │ │ ├── batch-asterisk.svg │ │ ├── bpmn-default-flow.svg │ │ ├── bpmn-trash.svg │ │ ├── bz.svg │ │ ├── codicon.json │ │ ├── compare1 │ │ │ ├── copy │ │ │ │ ├── collections.json │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.js │ │ │ │ ├── index.mjs │ │ │ │ └── package.json │ │ │ ├── different-version │ │ │ │ ├── collections.json │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.js │ │ │ │ ├── index.mjs │ │ │ │ └── package.json │ │ │ ├── extra-file │ │ │ │ ├── collections.json │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.js │ │ │ │ ├── index.mjs │ │ │ │ ├── package.json │ │ │ │ └── whatever.ts │ │ │ ├── formatted │ │ │ │ ├── collections.json │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.js │ │ │ │ ├── index.mjs │ │ │ │ └── package.json │ │ │ ├── missing-file │ │ │ │ ├── collections.json │ │ │ │ ├── index.js │ │ │ │ ├── index.mjs │ │ │ │ └── package.json │ │ │ └── original │ │ │ │ ├── collections.json │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.js │ │ │ │ ├── index.mjs │ │ │ │ └── package.json │ │ ├── discord.svg │ │ ├── elements │ │ │ ├── animate.svg │ │ │ ├── animateMotion.svg │ │ │ ├── animateTransform.svg │ │ │ ├── bad │ │ │ │ ├── a.svg │ │ │ │ ├── feBlend.svg │ │ │ │ ├── feConvolveMatrix.svg │ │ │ │ ├── fePointLight.svg │ │ │ │ ├── feSpotLight.svg │ │ │ │ ├── feTile.svg │ │ │ │ ├── foreignObject.svg │ │ │ │ ├── script.svg │ │ │ │ └── svg.svg │ │ │ ├── clipPath.svg │ │ │ ├── clipPath2.svg │ │ │ ├── defs.svg │ │ │ ├── desc.svg │ │ │ ├── feColorMatrix.svg │ │ │ ├── feDiffuseLighting.svg │ │ │ ├── feFlood.svg │ │ │ ├── feGaussianBlur.svg │ │ │ ├── feOffset.svg │ │ │ ├── inline-style │ │ │ │ ├── feComponentTransfer.svg │ │ │ │ ├── feDisplacementMap.svg │ │ │ │ ├── feFlood.svg │ │ │ │ ├── feMerge.svg │ │ │ │ ├── feSpecularLighting.svg │ │ │ │ └── feTurbulence.svg │ │ │ ├── linearGradient.svg │ │ │ ├── marker.svg │ │ │ ├── mask.svg │ │ │ ├── mpath.svg │ │ │ ├── pattern.svg │ │ │ ├── stop.svg │ │ │ ├── style │ │ │ │ ├── set.svg │ │ │ │ └── style.svg │ │ │ ├── symbol.svg │ │ │ └── use.svg │ │ ├── entypo-hair-cross.svg │ │ ├── fci-biomass.svg │ │ ├── fluent.new.json │ │ ├── fluent.old.json │ │ ├── folder-with-files.svg │ │ ├── font-face.svg │ │ ├── noto-coin-minified.svg │ │ ├── noto-coin.svg │ │ ├── openmoji-2117.svg │ │ ├── refresh.svg │ │ ├── spin.svg │ │ ├── subdirs │ │ │ ├── outline │ │ │ │ ├── apps.svg │ │ │ │ └── caret-back.svg │ │ │ └── regular │ │ │ │ ├── apps.svg │ │ │ │ └── camera.svg │ │ └── u1F3CC-golfer.svg │ ├── icon-set │ │ ├── aliases-test.ts │ │ ├── loading-test.ts │ │ ├── match-test.ts │ │ ├── merge-test.ts │ │ ├── tags-test.ts │ │ ├── themes-test.ts │ │ └── updating-icons-test.ts │ ├── import │ │ ├── directory-test.ts │ │ ├── git-test.ts │ │ ├── github-test.ts │ │ ├── gitlab-test.ts │ │ └── npm-test.ts │ ├── misc │ │ ├── compare-dirs-test.ts │ │ ├── concurrency-test.ts │ │ ├── keyword-test.ts │ │ └── version-test.ts │ ├── optimise │ │ ├── compressed-arc-test.ts │ │ ├── figma-clip-path-test.ts │ │ ├── mask-test.ts │ │ ├── scale-test.ts │ │ └── svgo-test.ts │ ├── svg │ │ ├── analyse-test.ts │ │ ├── cleanup-global-style-test.ts │ │ ├── cleanup-root-test.ts │ │ ├── cleanup-tags-tree-test.ts │ │ ├── cleanup-test.ts │ │ ├── convert-style-test.ts │ │ ├── parse-style-test.ts │ │ ├── parse-test.ts │ │ └── svg-test.ts │ └── tsconfig.json │ ├── tsconfig-base.json │ └── tsconfig.json ├── CONTRIBUTING.md ├── README.md ├── license.txt ├── package.json ├── pnpm-lock.yaml └── pnpm-workspace.yaml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = tab 7 | indent_size = 4 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | pull_request: 9 | branches: 10 | - main 11 | - next 12 | 13 | jobs: 14 | ci: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - run: corepack enable 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: latest 23 | cache: pnpm 24 | 25 | - name: 📦 Install dependencies 26 | run: pnpm install --frozen-lockfile 27 | 28 | - name: 🚧 Build project 29 | env: 30 | NODE_OPTIONS: '--max_old_space_size=4096' 31 | run: pnpm build:tools 32 | 33 | - name: 🧪 Test project 34 | run: pnpm test:tools 35 | 36 | - name: 📝 Lint 37 | run: pnpm lint:tools 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | node_modules 4 | debug_packages 5 | lerna-debug.log 6 | @debug 7 | dist/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "singleQuote": true, 4 | "useTabs": true, 5 | "semi": true, 6 | "quoteProps": "consistent", 7 | "endOfLine": "lf" 8 | } 9 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | tsconfig.tsbuildinfo 4 | node_modules 5 | lib 6 | cache 7 | output 8 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/data/test.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iconify/tools/c4863a3da547be45486b6f5dd02b0c91dcf69051/@iconify-demo/doc-samples/data/test.json -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@iconify-demo/doc-samples", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Code samples used in documentation", 6 | "license": "MIT", 7 | "scripts": {}, 8 | "type": "module", 9 | "dependencies": { 10 | "@iconify/tools": "workspace:^" 11 | }, 12 | "devDependencies": { 13 | "@types/node": "^20.17.9", 14 | "typescript": "^5.7.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/export/all-sets-svg.mjs: -------------------------------------------------------------------------------- 1 | import { readFile } from 'node:fs/promises'; 2 | import { downloadNPMPackage, IconSet, exportToDirectory } from '@iconify/tools'; 3 | 4 | // Directories 5 | const cacheDir = 'cache'; 6 | const outDir = 'svg'; 7 | 8 | // Download all icon sets 9 | console.log('Downloading latest package'); 10 | const downloaded = await downloadNPMPackage({ 11 | package: '@iconify/json', 12 | target: cacheDir, 13 | }); 14 | console.log('Downloaded version', downloaded.version); 15 | 16 | // Get list of icon sets 17 | const list = JSON.parse( 18 | await readFile(downloaded.contentsDir + '/collections.json', 'utf8') 19 | ); 20 | const prefixes = Object.keys(list); 21 | console.log('Got', prefixes.length, 'icon sets'); 22 | 23 | // Export each icon set 24 | for (let i = 0; i < prefixes.length; i++) { 25 | const prefix = prefixes[i]; 26 | 27 | // Read file 28 | const data = JSON.parse( 29 | await readFile( 30 | downloaded.contentsDir + '/json/' + prefix + '.json', 31 | 'utf8' 32 | ) 33 | ); 34 | 35 | // Create IconSet 36 | const iconSet = new IconSet(data); 37 | 38 | // Export it 39 | console.log('Exporting', iconSet.info.name); 40 | await exportToDirectory(iconSet, { 41 | target: outDir + '/' + prefix, 42 | }); 43 | } 44 | 45 | console.log('Done'); 46 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/export/json-to-svg-utils.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | 3 | // Function to locate JSON file 4 | import { locate } from '@iconify/json'; 5 | 6 | // Various functions from Iconify Utils 7 | import { parseIconSet } from '@iconify/utils/lib/icon-set/parse'; 8 | import { iconToSVG } from '@iconify/utils/lib/svg/build'; 9 | import { defaults } from '@iconify/utils/lib/customisations'; 10 | 11 | (async () => { 12 | // Locate icons 13 | const filename = locate('mdi'); 14 | 15 | // Load icon set 16 | const icons = JSON.parse(await fs.readFile(filename, 'utf8')); 17 | 18 | // Parse all icons 19 | const exportedSVG: Record = Object.create(null); 20 | parseIconSet(icons, (iconName, iconData) => { 21 | if (!iconData) { 22 | // Invalid icon 23 | console.error(`Error parsing icon ${iconName}`); 24 | return; 25 | } 26 | 27 | // Render icon 28 | const renderData = iconToSVG(iconData, { 29 | ...defaults, 30 | height: 'auto', 31 | }); 32 | 33 | // Generate attributes for SVG element 34 | const svgAttributes: Record = { 35 | 'xmlns': 'http://www.w3.org/2000/svg', 36 | 'xmlns:xlink': 'http://www.w3.org/1999/xlink', 37 | ...renderData.attributes, 38 | }; 39 | const svgAttributesStr = Object.keys(svgAttributes) 40 | .map( 41 | (attr) => 42 | // No need to check attributes for special characters, such as quotes, 43 | // they cannot contain anything that needs escaping. 44 | `${attr}="${ 45 | svgAttributes[attr as keyof typeof svgAttributes] 46 | }"` 47 | ) 48 | .join(' '); 49 | 50 | // Generate SVG 51 | const svg = `${renderData.body}`; 52 | exportedSVG[iconName] = svg; 53 | }); 54 | 55 | // Output directory 56 | const outputDir = 'mdi-export'; 57 | try { 58 | await fs.mkdir(outputDir, { 59 | recursive: true, 60 | }); 61 | } catch (err) { 62 | // 63 | } 64 | 65 | // Save all files 66 | const filenames = Object.keys(exportedSVG); 67 | for (let i = 0; i < filenames.length; i++) { 68 | const filename = filenames[i]; 69 | const svg = exportedSVG[filename]; 70 | await fs.writeFile(outputDir + '/' + filename + '.svg', svg, 'utf8'); 71 | } 72 | })(); 73 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/icon-set/chars.ts: -------------------------------------------------------------------------------- 1 | import { blankIconSet } from '@iconify/tools'; 2 | 3 | // Create icon set, add few icons and characters 4 | const iconSet = blankIconSet('test-prefix'); 5 | 6 | iconSet.setIcon('add', { 7 | body: '', 8 | }); 9 | iconSet.toggleCharacter('add', 'f001', true); 10 | 11 | iconSet.setIcon('triangle-left', { 12 | body: '', 13 | }); 14 | iconSet.toggleCharacter('triangle-left', 'f002', true); 15 | 16 | iconSet.setVariation('triangle-right', 'triangle-left', { 17 | hFlip: true, 18 | }); 19 | iconSet.toggleCharacter('triangle-right', 'f003', true); 20 | 21 | // Set character for icon that does not exist (will fail) 22 | iconSet.toggleCharacter('whatever', 'f005', true); 23 | 24 | // Export characters map 25 | console.log(iconSet.chars()); 26 | 27 | // Characters map is also exported in export(): 28 | console.log(iconSet.export()); 29 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/icon-set/check-theme.ts: -------------------------------------------------------------------------------- 1 | import { IconSet } from '@iconify/tools'; 2 | 3 | // Import icon set 4 | const iconSet = new IconSet({ 5 | prefix: 'carbon', 6 | icons: { 7 | 'add': { 8 | body: '', 9 | }, 10 | 'arrow-down-regular': { 11 | body: '', 12 | }, 13 | 'arrow-left-regular': { 14 | body: '', 15 | }, 16 | 'back-to-top-regular': { 17 | body: '', 18 | }, 19 | 'bookmark-filled': { 20 | body: '', 21 | }, 22 | 'caret-down-regular': { 23 | body: '', 24 | }, 25 | 'caret-left-regular': { 26 | body: '', 27 | }, 28 | }, 29 | aliases: { 30 | 'add-regular': { 31 | parent: 'add', 32 | }, 33 | 'arrow-up-regular': { 34 | parent: 'arrow-down-regular', 35 | vFlip: true, 36 | }, 37 | 'arrow-right-regular': { 38 | parent: 'arrow-left-regular', 39 | hFlip: true, 40 | }, 41 | 'caret-up-regular': { 42 | parent: 'caret-down-regular', 43 | vFlip: true, 44 | }, 45 | 'caret-right-regular': { 46 | parent: 'caret-left-regular', 47 | hFlip: true, 48 | }, 49 | }, 50 | width: 32, 51 | height: 32, 52 | prefixes: { 53 | arrow: 'Arrows', 54 | caret: 'Carets', 55 | }, 56 | suffixes: { 57 | 'filled': 'Filled', 58 | 'regular': 'Regular', 59 | '': 'Other', 60 | }, 61 | }); 62 | 63 | // Check all prefixes 64 | console.log(iconSet.checkTheme(true)); 65 | 66 | // Check all suffixes 67 | console.log(iconSet.checkTheme(false)); 68 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/icon-set/count.ts: -------------------------------------------------------------------------------- 1 | import { IconSet } from '@iconify/tools'; 2 | 3 | const iconSet = new IconSet({ 4 | prefix: 'codicon', 5 | icons: { 6 | // Counted 7 | 'add': { 8 | body: '', 9 | }, 10 | // Ignored: hidden 11 | 'debug-pause': { 12 | body: '', 13 | hidden: true, 14 | }, 15 | // Counted 16 | 'triangle-left': { 17 | body: '', 18 | }, 19 | }, 20 | aliases: { 21 | // Ignored: alias 22 | 'plus': { 23 | parent: 'add', 24 | }, 25 | // Counted: variation 26 | 'triangle-right': { 27 | parent: 'triangle-left', 28 | hFlip: true, 29 | }, 30 | }, 31 | }); 32 | 33 | // Count icons: returns 3 34 | console.log(iconSet.count()); 35 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/icon-set/export.ts: -------------------------------------------------------------------------------- 1 | import { blankIconSet } from '@iconify/tools'; 2 | 3 | // Create icon set, add few icons 4 | const iconSet = blankIconSet('test-prefix'); 5 | iconSet.setIcon('add', { 6 | body: '', 7 | }); 8 | iconSet.setIcon('triangle-left', { 9 | body: '', 10 | }); 11 | iconSet.setVariation('triangle-right', 'triangle-left', { 12 | hFlip: true, 13 | }); 14 | 15 | // Set information 16 | iconSet.info = { 17 | name: 'Test', 18 | author: { 19 | name: 'Me', 20 | }, 21 | license: { 22 | title: 'MIT', 23 | }, 24 | }; 25 | 26 | // Export icon set 27 | const data = iconSet.export(); 28 | console.log(JSON.stringify(data, null, '\t')); 29 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/icon-set/for-each.ts: -------------------------------------------------------------------------------- 1 | import { IconSet, cleanupSVG, parseColors, isEmptyColor } from '@iconify/tools'; 2 | 3 | const iconSet = new IconSet({ 4 | prefix: 'codicon', 5 | icons: { 6 | 'add': { 7 | body: '', 8 | }, 9 | 'debug-pause': { 10 | body: '', 11 | hidden: true, 12 | }, 13 | 'triangle-left': { 14 | body: '', 15 | }, 16 | }, 17 | aliases: { 18 | 'plus': { 19 | parent: 'add', 20 | }, 21 | 'triangle-right': { 22 | parent: 'triangle-left', 23 | hFlip: true, 24 | }, 25 | }, 26 | }); 27 | 28 | // Synchronous example: renaming all icons 29 | console.log('Starting synchronous forEach()'); 30 | iconSet.forEach((name) => { 31 | iconSet.rename(name, 'renamed-' + name); 32 | console.log(`Renaming: ${name}`); 33 | }); 34 | console.log('Completed synchronous forEach()'); 35 | 36 | // Async example: cleaning up icons. 37 | // Wrap code in anonymous async function for asynchronous use case. 38 | console.log('Starting async forEach()'); 39 | (async () => { 40 | await iconSet.forEach(async (name, type) => { 41 | if (type !== 'icon') { 42 | // Ignore aliases and variations: they inherit content from parent icon, so there is nothing to change 43 | return; 44 | } 45 | 46 | const svg = iconSet.toSVG(name); 47 | if (svg) { 48 | // Clean up icon 49 | console.log(`Cleaning up: ${name}`); 50 | try { 51 | cleanupSVG(svg); 52 | } catch (err) { 53 | // Something went wrong: remove icon 54 | iconSet.remove(name); 55 | return; 56 | } 57 | 58 | // Change colors to red 59 | parseColors(svg, { 60 | defaultColor: 'red', 61 | callback: (attr, colorStr, color) => { 62 | return !color || isEmptyColor(color) ? colorStr : 'red'; 63 | }, 64 | }); 65 | 66 | // Update code 67 | iconSet.fromSVG(name, svg); 68 | } 69 | }); 70 | 71 | console.log('Completed async forEach()'); 72 | })(); 73 | 74 | console.log( 75 | 'End of code... (this code is executed before icons are cleaned up, this is why async anonymous function is needed)' 76 | ); 77 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/icon-set/get-tree.ts: -------------------------------------------------------------------------------- 1 | import { IconSet } from '@iconify/tools'; 2 | 3 | const iconSet = new IconSet({ 4 | prefix: 'foo', 5 | icons: { 6 | bar: { 7 | body: '', 8 | }, 9 | }, 10 | aliases: { 11 | baz: { 12 | parent: 'bar', 13 | }, 14 | baz2: { 15 | parent: 'baz', 16 | }, 17 | bad: { 18 | parent: 'whatever', 19 | }, 20 | }, 21 | }); 22 | 23 | console.log(iconSet.getTree()); 24 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/icon-set/list-category.ts: -------------------------------------------------------------------------------- 1 | import { IconSet } from '@iconify/tools'; 2 | 3 | // Import icon set 4 | const iconSet = new IconSet({ 5 | prefix: 'carbon', 6 | icons: { 7 | 'add': { 8 | body: '', 9 | }, 10 | 'arrow-down': { 11 | body: '', 12 | }, 13 | 'arrow-left': { 14 | body: '', 15 | }, 16 | 'back-to-top': { 17 | body: '', 18 | }, 19 | 'bookmark-filled': { 20 | body: '', 21 | }, 22 | 'caret-down': { 23 | body: '', 24 | }, 25 | 'caret-left': { 26 | body: '', 27 | }, 28 | }, 29 | aliases: { 30 | 'plus': { 31 | parent: 'add', 32 | }, 33 | 'arrow-up': { 34 | parent: 'arrow-down', 35 | vFlip: true, 36 | }, 37 | 'arrow-right': { 38 | parent: 'arrow-left', 39 | hFlip: true, 40 | }, 41 | 'caret-up': { 42 | parent: 'caret-down', 43 | vFlip: true, 44 | }, 45 | 'caret-right': { 46 | parent: 'caret-left', 47 | hFlip: true, 48 | }, 49 | }, 50 | width: 32, 51 | height: 32, 52 | }); 53 | 54 | // Add few categories 55 | iconSet.toggleCategory('arrow-down', 'Arrows', true); 56 | iconSet.toggleCategory('arrow-left', 'Arrows', true); 57 | iconSet.toggleCategory('caret-down', 'Arrows', true); 58 | iconSet.toggleCategory('caret-left', 'Arrows', true); 59 | iconSet.toggleCategory('bookmark-filled', 'Bookmarks', true); 60 | iconSet.toggleCategory('bookmark-filled', 'Filled', true); 61 | 62 | // List icons in category 63 | // [ 'arrow-down', 'arrow-left', 'caret-down', 'caret-left' ] 64 | console.log(iconSet.listCategory('Arrows')); 65 | 66 | // Rename category using `categories` property 67 | iconSet.categories.forEach((item) => { 68 | if (item.title === 'Arrows') { 69 | item.title = 'Simple Icons'; 70 | } 71 | }); 72 | 73 | // List icons in category (no longer exists) 74 | // null 75 | console.log(iconSet.listCategory('Arrows')); 76 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/icon-set/list.ts: -------------------------------------------------------------------------------- 1 | import { IconSet, cleanupSVG, parseColors, isEmptyColor } from '@iconify/tools'; 2 | 3 | const iconSet = new IconSet({ 4 | prefix: 'codicon', 5 | icons: { 6 | 'add': { 7 | body: '', 8 | }, 9 | 'debug-pause': { 10 | body: '', 11 | hidden: true, 12 | }, 13 | 'triangle-left': { 14 | body: '', 15 | }, 16 | }, 17 | aliases: { 18 | 'plus': { 19 | parent: 'add', 20 | }, 21 | 'triangle-right': { 22 | parent: 'triangle-left', 23 | hFlip: true, 24 | }, 25 | }, 26 | }); 27 | 28 | // List icons and variations 29 | // [ 'add', 'debug-pause', 'triangle-left', 'triangle-right' ] 30 | console.log(iconSet.list()); 31 | 32 | // List everything 33 | // [ 'add', 'debug-pause', 'triangle-left', 'plus', 'triangle-right' ] 34 | console.log(iconSet.list(['icon', 'variation', 'alias'])); 35 | 36 | // Icons only 37 | // [ 'add', 'debug-pause', 'triangle-left' ] 38 | console.log(iconSet.list(['icon'])); 39 | 40 | // Function can also be used to parse all icons in icon set, though `forEach()` is a better choice for this code 41 | const icons = iconSet.list(); 42 | for (let i = 0; i < icons.length; i++) { 43 | const name = icons[i]; 44 | const svg = iconSet.toSVG(name); 45 | if (svg) { 46 | // Clean up icon 47 | try { 48 | cleanupSVG(svg); 49 | } catch (err) { 50 | // Something went wrong: remove icon 51 | iconSet.remove(name); 52 | continue; 53 | } 54 | 55 | // Change colors to red 56 | parseColors(svg, { 57 | defaultColor: 'red', 58 | callback: (attr, colorStr, color) => { 59 | return !color || isEmptyColor(color) ? colorStr : 'red'; 60 | }, 61 | }); 62 | 63 | // Update code 64 | iconSet.fromSVG(name, svg); 65 | } 66 | } 67 | 68 | // Export updated icon set 69 | console.log(iconSet.export()); 70 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/icon-set/load.ts: -------------------------------------------------------------------------------- 1 | iconSet.load({ 2 | prefix: 'codicon', 3 | icons: { 4 | 'add': { 5 | body: '', 6 | }, 7 | 'chrome-maximize': { 8 | body: '', 9 | }, 10 | 'chrome-minimize': { 11 | body: '', 12 | }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/icon-set/remove.ts: -------------------------------------------------------------------------------- 1 | import { IconSet } from '@iconify/tools'; 2 | 3 | const iconSet = new IconSet({ 4 | prefix: 'codicon', 5 | icons: { 6 | 'add': { 7 | body: '', 8 | }, 9 | 'debug-pause': { 10 | body: '', 11 | hidden: true, 12 | }, 13 | 'triangle-left': { 14 | body: '', 15 | }, 16 | }, 17 | aliases: { 18 | 'plus': { 19 | parent: 'add', 20 | }, 21 | 'triangle-right': { 22 | parent: 'triangle-left', 23 | hFlip: true, 24 | }, 25 | }, 26 | }); 27 | 28 | // Removes 'add' and 'plus' icons 29 | iconSet.remove('add'); 30 | 31 | // Removes 'triangle-left' icon. 32 | // Variation 'triangle-right' no longer has valid parent, but still exists in icon set. 33 | iconSet.remove('triangle-left', false); 34 | 35 | // Export icon set. 'triangle-right' will be in result because export() does not validate icons. 36 | console.log(iconSet.export()); 37 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/icon-set/rename.ts: -------------------------------------------------------------------------------- 1 | import { IconSet } from '@iconify/tools'; 2 | 3 | // Import icon set 4 | const iconSet = new IconSet({ 5 | prefix: 'carbon', 6 | icons: { 7 | 'add': { 8 | body: '', 9 | }, 10 | 'arrow-left': { 11 | body: '', 12 | }, 13 | }, 14 | aliases: { 15 | 'plus': { 16 | parent: 'add', 17 | }, 18 | 'arrow-right': { 19 | parent: 'arrow-left', 20 | hFlip: true, 21 | }, 22 | }, 23 | width: 32, 24 | height: 32, 25 | }); 26 | 27 | // Rename 'add' to 'plus' 28 | iconSet.rename('add', 'plus'); 29 | 30 | // Rename 'arrow-left' to 'arrow', also changes 'parent' property in 'arrow-right' 31 | iconSet.rename('arrow-left', 'arrow'); 32 | 33 | // Export 34 | console.log(iconSet.export()); 35 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/icon-set/resolve.ts: -------------------------------------------------------------------------------- 1 | import { IconSet } from '@iconify/tools'; 2 | 3 | const iconSet = new IconSet({ 4 | prefix: 'codicon', 5 | icons: { 6 | 'add': { 7 | body: '', 8 | }, 9 | 'debug-pause': { 10 | body: '', 11 | hidden: true, 12 | }, 13 | 'triangle-left': { 14 | body: '', 15 | }, 16 | }, 17 | aliases: { 18 | 'plus': { 19 | parent: 'add', 20 | }, 21 | 'triangle-right': { 22 | parent: 'triangle-left', 23 | hFlip: true, 24 | }, 25 | }, 26 | }); 27 | 28 | // Resolve icon (partial and full) 29 | console.log(iconSet.resolve('debug-pause')); 30 | console.log(iconSet.resolve('debug-pause', true)); 31 | 32 | // Resolve variation (partial and full) 33 | console.log(iconSet.resolve('triangle-right')); 34 | console.log(iconSet.resolve('triangle-right', true)); 35 | 36 | // Resolve alias (partial and full) 37 | console.log(iconSet.resolve('plus')); 38 | console.log(iconSet.resolve('plus', true)); 39 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/icon-set/set-alias.ts: -------------------------------------------------------------------------------- 1 | import { blankIconSet } from '@iconify/tools'; 2 | 3 | // Create icon set, add few icons 4 | const iconSet = blankIconSet('test-prefix'); 5 | iconSet.setIcon('add', { 6 | body: '', 7 | width: 32, 8 | height: 32, 9 | }); 10 | 11 | iconSet.setIcon('caret-down', { 12 | body: '', 13 | width: 32, 14 | height: 32, 15 | }); 16 | iconSet.setVariation('caret-up', 'caret-down', { 17 | vFlip: true, 18 | }); 19 | 20 | iconSet.setAlias('plus', 'add'); 21 | 22 | // Export icon set 23 | const data = iconSet.export(); 24 | console.log(JSON.stringify(data, null, '\t')); 25 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/icon-set/to-string.ts: -------------------------------------------------------------------------------- 1 | import { blankIconSet } from '@iconify/tools'; 2 | 3 | const iconSet = blankIconSet(''); 4 | iconSet.setIcon('add', { 5 | body: '', 6 | }); 7 | 8 | // Export icon 9 | console.log(iconSet.toString('add')); 10 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/icon-set/to-svg.ts: -------------------------------------------------------------------------------- 1 | import { blankIconSet, parseColors, isEmptyColor } from '@iconify/tools'; 2 | 3 | // Create an icon set, add one icon 4 | const iconSet = blankIconSet(''); 5 | iconSet.setIcon('add', { 6 | body: '', 7 | }); 8 | 9 | // Export icon to SVG class instance 10 | // Note: SVG instance is not attached to icon set, so it is not updated automatically (see code below). 11 | const svg = iconSet.toSVG('add'); 12 | if (!svg) { 13 | throw new Error('Icon is missing'); 14 | } 15 | 16 | // Set fill to 'currentColor' 17 | parseColors(svg, { 18 | // If a shape uses default color (used in this example), change it to 'currentColor'. 19 | defaultColor: 'currentColor', 20 | 21 | // Callback to change colors. Not called in this example because there are no colors in sample icon. 22 | callback: (attr, colorStr, color) => { 23 | // color === null -> color cannot be parsed -> return colorStr 24 | // isEmptyColor() -> checks if color is empty: 'none' or 'transparent' -> return color object 25 | // without changes (though color string can also be returned, but using object is faster) 26 | // for everything else return 'currentColor' 27 | return !color ? colorStr : isEmptyColor(color) ? color : 'currentColor'; 28 | }, 29 | }); 30 | 31 | // Icon instance is not attached to icon set, so it is not updated automatically. 32 | // Update icon in icon set 33 | iconSet.fromSVG('add', svg); 34 | 35 | // Log to show icon (two ways to do it, one from icon set, one from icon instance) 36 | console.log(svg.toString()); 37 | console.log(iconSet.toString('add')); 38 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/icon/color-default.ts: -------------------------------------------------------------------------------- 1 | import { SVG, parseColors } from '@iconify/tools'; 2 | 3 | (async () => { 4 | const svg = new SVG( 5 | '' 6 | ); 7 | 8 | // Add 'currentColor' to shapes that use default color 9 | await parseColors(svg, { 10 | defaultColor: 'currentColor', 11 | }); 12 | 13 | console.log(svg.toMinifiedString()); 14 | })(); 15 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/icon/colors.ts: -------------------------------------------------------------------------------- 1 | import { compareColors, stringToColor } from '@iconify/utils/lib/colors'; 2 | import { IconSet, parseColors, isEmptyColor } from '@iconify/tools'; 3 | 4 | const iconSet = new IconSet({ 5 | prefix: 'codicon', 6 | icons: { 7 | 'add': { 8 | body: '', 9 | }, 10 | 'debug-pause': { 11 | body: '', 12 | hidden: true, 13 | }, 14 | 'triangle-left': { 15 | body: '', 16 | }, 17 | }, 18 | aliases: { 19 | 'plus': { 20 | parent: 'add', 21 | }, 22 | 'triangle-right': { 23 | parent: 'triangle-left', 24 | hFlip: true, 25 | }, 26 | }, 27 | }); 28 | 29 | // Parse all icons in icon set 30 | iconSet.forEach((name, type) => { 31 | if (type !== 'icon') { 32 | // Ignore aliases and variations: they inherit content from parent icon, so there is nothing to change 33 | return; 34 | } 35 | 36 | // Get icon as SVG class instance 37 | const svg = iconSet.toSVG(name); 38 | if (svg) { 39 | // Parse colors in SVG instance 40 | parseColors(svg, { 41 | // Change default color to 'currentColor' 42 | defaultColor: 'currentColor', 43 | 44 | // Callback to parse each color 45 | callback: (attr, colorStr, color) => { 46 | if (!color) { 47 | // color === null, so color cannot be parsed 48 | // Return colorStr to keep old value 49 | return colorStr; 50 | } 51 | 52 | if (isEmptyColor(color)) { 53 | // Color is empty: 'none' or 'transparent' 54 | // Return color object to keep old value 55 | return color; 56 | } 57 | 58 | // Black color: change to 'currentColor' 59 | if (compareColors(color, stringToColor('black'))) { 60 | return 'currentColor'; 61 | } 62 | 63 | // White color: belongs to white background rectangle: remove rectangle 64 | if (compareColors(color, stringToColor('white'))) { 65 | return 'remove'; 66 | } 67 | 68 | // Unexpected color. Add code to check for it 69 | throw new Error( 70 | `Unexpected color "${colorStr}" in attribute ${attr}` 71 | ); 72 | }, 73 | }); 74 | 75 | // Update icon in icon set 76 | iconSet.fromSVG(name, svg); 77 | } 78 | }); 79 | 80 | // Export icon set 81 | console.log(iconSet.export()); 82 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/icon/icon-set.ts: -------------------------------------------------------------------------------- 1 | iconSet.forEach(async (name, type) => { 2 | if (type !== 'icon') { 3 | // Ignore aliases and variations: they inherit content from parent icon, so there is nothing to change 4 | return; 5 | } 6 | 7 | const svg = iconSet.toSVG(name); 8 | if (svg) { 9 | // Change colors to red 10 | parseColors(svg, { 11 | defaultColor: 'red', 12 | callback: (attr, colorStr, color) => { 13 | return !color || isEmptyColor(color) ? colorStr : 'red'; 14 | }, 15 | }); 16 | 17 | // Update icon from SVG instance 18 | iconSet.fromSVG(name, svg); 19 | } 20 | }); 21 | 22 | // The rest of code here 23 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/icon/mask-colors.ts: -------------------------------------------------------------------------------- 1 | import { SVG, convertSVGToMask } from '@iconify/tools'; 2 | 3 | const svg = new SVG( 4 | ` 5 | 6 | 7 | 8 | 9 | 10 | 11 | ` 12 | ); 13 | 14 | // Convert to mask 15 | convertSVGToMask(svg, { 16 | // Treat black as solid 17 | solid: '#000', 18 | // No transparent colors 19 | transparent: [], 20 | // Custom opacity for other colors 21 | custom: (color) => { 22 | switch (color) { 23 | case '#fff': 24 | return 0.75; // same as returning '#bfbfbf' 25 | 26 | case '#2f88ff': 27 | return 0.25; // same as returning '#404040' 28 | } 29 | }, 30 | }); 31 | 32 | // Output result 33 | console.log(svg.toString()); 34 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/icon/mask-demo.ts: -------------------------------------------------------------------------------- 1 | import { SVG, convertSVGToMask } from '@iconify/tools'; 2 | 3 | const svg = new SVG( 4 | ` 5 | 6 | 7 | 8 | 9 | ` 10 | ); 11 | 12 | // Convert to mask without changing any colors, use them as an alpha channel 13 | convertSVGToMask(svg, { 14 | // Set rectangle color to currentColor 15 | color: 'currentColor', 16 | // Use custom option instead of options above, returning color as is 17 | custom: (color) => color, 18 | }); 19 | 20 | // Output result 21 | console.log(svg.toString()); 22 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/icon/mask-white.ts: -------------------------------------------------------------------------------- 1 | import { SVG, convertSVGToMask } from '@iconify/tools'; 2 | 3 | const svg = new SVG( 4 | ` 5 | 6 | 7 | 8 | 9 | ` 10 | ); 11 | 12 | // Convert to mask, converting black color to solid, white to transparent (default options) 13 | convertSVGToMask(svg); 14 | 15 | // Output result 16 | console.log(svg.toString()); 17 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/icon/paths.ts: -------------------------------------------------------------------------------- 1 | import { SVG, deOptimisePaths } from '@iconify/tools'; 2 | 3 | const svg = new SVG( 4 | '' 5 | ); 6 | 7 | // Update path 8 | deOptimisePaths(svg); 9 | 10 | console.log(svg.toMinifiedString()); 11 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/icon/scale.ts: -------------------------------------------------------------------------------- 1 | import { SVG, scaleSVG, runSVGO } from '@iconify/tools'; 2 | 3 | const svg = new SVG( 4 | '' 5 | ); 6 | 7 | // Reduce size by 64 to get 32x32 icon 8 | scaleSVG(svg, 1 / 64); 9 | 10 | // Optimize icon 11 | runSVGO(svg); 12 | 13 | // Output result 14 | console.log(svg.toString()); 15 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/import/dir-sync.ts: -------------------------------------------------------------------------------- 1 | import { 2 | importDirectorySync, 3 | cleanupSVG, 4 | runSVGO, 5 | parseColors, 6 | isEmptyColor, 7 | } from '@iconify/tools'; 8 | 9 | // Import icons 10 | const iconSet = importDirectorySync('files/svg', { 11 | prefix: 'test', 12 | }); 13 | 14 | // Validate, clean up, fix palette and optimise 15 | iconSet.forEachSync((name, type) => { 16 | if (type !== 'icon') { 17 | return; 18 | } 19 | 20 | const svg = iconSet.toSVG(name); 21 | if (!svg) { 22 | // Invalid icon 23 | iconSet.remove(name); 24 | return; 25 | } 26 | 27 | // Clean up and optimise icons 28 | try { 29 | // Clean up icon code 30 | cleanupSVG(svg); 31 | 32 | // Assume icon is monotone: replace color with currentColor, add if missing 33 | // If icon is not monotone, remove this code 34 | parseColors(svg, { 35 | defaultColor: 'currentColor', 36 | callback: (attr, colorStr, color) => { 37 | return !color || isEmptyColor(color) 38 | ? colorStr 39 | : 'currentColor'; 40 | }, 41 | }); 42 | 43 | // Optimise 44 | runSVGO(svg); 45 | } catch (err) { 46 | // Invalid icon 47 | console.error(`Error parsing ${name}:`, err); 48 | iconSet.remove(name); 49 | return; 50 | } 51 | 52 | // Update icon 53 | iconSet.fromSVG(name, svg); 54 | }); 55 | 56 | // Export 57 | console.log(iconSet.export()); 58 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/import/dir.ts: -------------------------------------------------------------------------------- 1 | import { 2 | importDirectory, 3 | cleanupSVG, 4 | runSVGO, 5 | parseColors, 6 | isEmptyColor, 7 | } from '@iconify/tools'; 8 | 9 | (async () => { 10 | // Import icons 11 | const iconSet = await importDirectory('files/svg', { 12 | prefix: 'test', 13 | }); 14 | 15 | // Validate, clean up, fix palette and optimise 16 | iconSet.forEach((name, type) => { 17 | if (type !== 'icon') { 18 | return; 19 | } 20 | 21 | const svg = iconSet.toSVG(name); 22 | if (!svg) { 23 | // Invalid icon 24 | iconSet.remove(name); 25 | return; 26 | } 27 | 28 | // Clean up and optimise icons 29 | try { 30 | // Clean up icon code 31 | cleanupSVG(svg); 32 | 33 | // Assume icon is monotone: replace color with currentColor, add if missing 34 | // If icon is not monotone, remove this code 35 | parseColors(svg, { 36 | defaultColor: 'currentColor', 37 | callback: (attr, colorStr, color) => { 38 | return !color || isEmptyColor(color) 39 | ? colorStr 40 | : 'currentColor'; 41 | }, 42 | }); 43 | 44 | // Optimise 45 | runSVGO(svg); 46 | } catch (err) { 47 | // Invalid icon 48 | console.error(`Error parsing ${name}:`, err); 49 | iconSet.remove(name); 50 | return; 51 | } 52 | 53 | // Update icon 54 | iconSet.fromSVG(name, svg); 55 | }); 56 | 57 | // Export 58 | console.log(iconSet.export()); 59 | })(); 60 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/import/json-min.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import { IconSet, cleanupSVG } from '@iconify/tools'; 3 | import { validateIconSet } from '@iconify/utils'; 4 | 5 | (async () => { 6 | // Read data, parse JSON 7 | const rawData = JSON.parse( 8 | await fs.readFile('files/arty-animated.svg', 'utf8') 9 | ); 10 | 11 | // Validate icon set 12 | const validatedData = validateIconSet(rawData); 13 | 14 | // Create new IconSet instance 15 | const iconSet = new IconSet(validatedData); 16 | 17 | // Clean up icons 18 | iconSet.forEachSync( 19 | (name) => { 20 | const svg = iconSet.toSVG(name); 21 | if (!svg) { 22 | // Bad icon 23 | iconSet.remove(name); 24 | return; 25 | } 26 | 27 | // Wrap in try...catch to catch errors 28 | try { 29 | // Clean up and validate 30 | cleanupSVG(svg); 31 | 32 | // Update icon data in icon set 33 | iconSet.fromSVG(name, svg); 34 | } catch (err) { 35 | console.error(`Error parsing ${name}:`, err); 36 | iconSet.remove(name); 37 | } 38 | }, 39 | ['icon'] 40 | ); 41 | 42 | // Done. Do other stuff... 43 | })(); 44 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/import/svg.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import { 3 | SVG, 4 | blankIconSet, 5 | cleanupSVG, 6 | runSVGO, 7 | parseColors, 8 | isEmptyColor, 9 | } from '@iconify/tools'; 10 | 11 | (async () => { 12 | // Create an empty icon set 13 | const iconSet = blankIconSet('test'); 14 | 15 | // Read icon, create SVG instance 16 | const content = await fs.readFile('files/home.svg', 'utf8'); 17 | const svg = new SVG(content); 18 | 19 | // Clean up icon code 20 | cleanupSVG(svg); 21 | 22 | // Assume icon is monotone: replace color with currentColor, add if missing 23 | // If icons are not monotone, remove this code 24 | parseColors(svg, { 25 | defaultColor: 'currentColor', 26 | callback: (attr, colorStr, color) => { 27 | return !color || isEmptyColor(color) ? colorStr : 'currentColor'; 28 | }, 29 | }); 30 | 31 | // Optimise 32 | runSVGO(svg); 33 | 34 | // Add icon to icon set 35 | iconSet.fromSVG('home', svg); 36 | })(); 37 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/merge-icon-sets.ts: -------------------------------------------------------------------------------- 1 | import { IconSet, mergeIconSets } from '@iconify/tools'; 2 | 3 | // Merge 2 icon sets 4 | const merged = mergeIconSets( 5 | new IconSet({ 6 | // Prefix, info, categories, characters are not copied from old icon set 7 | prefix: 'foo', 8 | icons: { 9 | 'chrome-maximize': { 10 | body: '', 11 | }, 12 | 'chrome-minimize': { 13 | body: '', 14 | }, 15 | }, 16 | width: 24, 17 | height: 24, 18 | }), 19 | new IconSet({ 20 | prefix: 'bar', 21 | icons: { 22 | remove: { 23 | body: '', 24 | }, 25 | }, 26 | }) 27 | ); 28 | 29 | // Log merged icon set 30 | console.log(merged.export()); 31 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/package/api.ts: -------------------------------------------------------------------------------- 1 | import { sendAPIQuery } from '@iconify/tools'; 2 | import type { APICacheOptions } from '@iconify/tools/lib/download/api/types'; 3 | 4 | // 3 days cache 5 | const ttl = 60 * 60 * 24 * 3; 6 | const dir = 'cache/api'; 7 | const options: APICacheOptions = { 8 | dir, 9 | ttl, 10 | }; 11 | 12 | (async () => { 13 | const data = await sendAPIQuery( 14 | { 15 | uri: 'https://api.iconify.design/collections', 16 | }, 17 | options 18 | ); 19 | console.log(typeof data === 'string' ? JSON.parse(data) : data); 20 | })(); 21 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/package/git.ts: -------------------------------------------------------------------------------- 1 | import { downloadGitRepo } from '@iconify/tools'; 2 | 3 | (async () => { 4 | console.log( 5 | await downloadGitRepo({ 6 | target: 'downloads/boxicons-{hash}', 7 | remote: 'git@github.com:atisawd/boxicons.git', 8 | branch: 'master', 9 | ifModifiedSince: true, 10 | log: true, 11 | }) 12 | ); 13 | })(); 14 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/package/github.ts: -------------------------------------------------------------------------------- 1 | import { downloadGitHubRepo } from '@iconify/tools'; 2 | 3 | // GITHUB_TOKEN=ghp_12345 node example.js 4 | const token = process.env.GITHUB_TOKEN || ''; 5 | 6 | (async () => { 7 | console.log( 8 | await downloadGitHubRepo({ 9 | target: 'downloads/jam', 10 | user: 'michaelampr', 11 | repo: 'jam', 12 | branch: 'master', 13 | token, 14 | }) 15 | ); 16 | })(); 17 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/package/gitlab.ts: -------------------------------------------------------------------------------- 1 | import { downloadGitLabRepo } from '@iconify/tools'; 2 | 3 | // GITLAB_TOKEN=qwertyuiop node example.js 4 | const token = process.env.GITLAB_TOKEN || ''; 5 | 6 | (async () => { 7 | console.log( 8 | await downloadGitLabRepo({ 9 | target: 'downloads/eos-icons', 10 | project: '4600360', 11 | branch: 'master', 12 | token, 13 | }) 14 | ); 15 | })(); 16 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/package/npm-version.ts: -------------------------------------------------------------------------------- 1 | import { getNPMVersion } from '@iconify/tools'; 2 | 3 | (async () => { 4 | console.log( 5 | await getNPMVersion({ 6 | package: '@iconify-json/mdi-light', 7 | // tag: 'latest', 8 | }) 9 | ); 10 | })(); 11 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/package/npm.ts: -------------------------------------------------------------------------------- 1 | import { downloadNPMPackage } from '@iconify/tools'; 2 | 3 | (async () => { 4 | console.log( 5 | await downloadNPMPackage({ 6 | target: 'downloads/icon-sets/mdi-light', 7 | package: '@iconify-json/mdi-light', 8 | }) 9 | ); 10 | })(); 11 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/package/package-version.ts: -------------------------------------------------------------------------------- 1 | import { downloadGitHubRepo, getPackageVersion } from '@iconify/tools'; 2 | 3 | // GITHUB_TOKEN=ghp_12345 node example.js 4 | const token = process.env.GITHUB_TOKEN || ''; 5 | 6 | (async () => { 7 | // Download GitHub repository 8 | const result = await downloadGitHubRepo({ 9 | target: 'downloads/bi', 10 | user: 'twbs', 11 | repo: 'icons', 12 | branch: 'main', 13 | token, 14 | }); 15 | 16 | // Get version from downloaded package 17 | const version = await getPackageVersion(result.contentsDir); 18 | 19 | // '1.7.0' 20 | console.log('Version:', version); 21 | })(); 22 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/src/package/version.ts: -------------------------------------------------------------------------------- 1 | import { bumpVersion } from '@iconify/tools'; 2 | 3 | console.log(bumpVersion('1.0.0')); // 1.0.1 4 | console.log(bumpVersion('2.1.3')); // 2.1.4 5 | console.log(bumpVersion('2.0.0-beta.1')); // 2.0.0-beta.2 6 | -------------------------------------------------------------------------------- /@iconify-demo/doc-samples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "./src", 4 | "outDir": "./lib", 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "declaration": true, 8 | "declarationMap": false, 9 | "sourceMap": false, 10 | "composite": true, 11 | "strict": true, 12 | "moduleResolution": "node", 13 | "esModuleInterop": true, 14 | "forceConsistentCasingInFileNames": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /@iconify-demo/mdi/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | tsconfig.tsbuildinfo 4 | node_modules 5 | lib 6 | cache 7 | output 8 | -------------------------------------------------------------------------------- /@iconify-demo/mdi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@iconify-demo/mdi", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Example of importing Material Design using Iconify Tools", 6 | "license": "MIT", 7 | "main": "lib/index.js", 8 | "scripts": { 9 | "build": "tsc -b", 10 | "start": "node lib/index.js" 11 | }, 12 | "type": "module", 13 | "dependencies": { 14 | "@iconify/tools": "workspace:^", 15 | "@iconify/types": "^2.0.0" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^20.17.9", 19 | "typescript": "^5.7.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /@iconify-demo/mdi/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "./src", 4 | "outDir": "./lib", 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "declaration": true, 8 | "declarationMap": false, 9 | "sourceMap": false, 10 | "composite": true, 11 | "strict": true, 12 | "moduleResolution": "node", 13 | "esModuleInterop": true, 14 | "forceConsistentCasingInFileNames": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /@iconify-demo/unocss/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /@iconify-demo/unocss/assets/svg/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /@iconify-demo/unocss/assets/svg/loading2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /@iconify-demo/unocss/assets/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "prefix": "test", 3 | "icons": { 4 | "github": { 5 | "body": "", 6 | "width": 24, 7 | "height": 24 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /@iconify-demo/unocss/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /@iconify-demo/unocss/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@iconify-demo/unocss", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@iconify/tools": "workspace:^", 13 | "@unocss/reset": "^0.61.9", 14 | "vue": "^3.5.13" 15 | }, 16 | "devDependencies": { 17 | "@antfu/ni": "^0.21.12", 18 | "@iconify-json/carbon": "^1.2.4", 19 | "@iconify/types": "^2.0.0", 20 | "@iconify/utils": "^2.2.0", 21 | "@types/node": "^20.17.9", 22 | "@vitejs/plugin-vue": "^4.6.2", 23 | "typescript": "^5.7.2", 24 | "unocss": "^0.61.9", 25 | "vite": "^5.4.11", 26 | "vue-tsc": "^1.8.27" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /@iconify-demo/unocss/src/App.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 43 | -------------------------------------------------------------------------------- /@iconify-demo/unocss/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue'; 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any>; 7 | export default component; 8 | } 9 | -------------------------------------------------------------------------------- /@iconify-demo/unocss/src/main.ts: -------------------------------------------------------------------------------- 1 | import '@unocss/reset/normalize.css'; 2 | import 'uno.css'; 3 | 4 | import { createApp } from 'vue'; 5 | import App from './App.vue'; 6 | 7 | createApp(App).mount('#app'); 8 | -------------------------------------------------------------------------------- /@iconify-demo/unocss/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "lib": ["esnext", "dom"], 14 | "skipLibCheck": true 15 | }, 16 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 17 | "references": [{ "path": "./tsconfig.node.json" }] 18 | } 19 | -------------------------------------------------------------------------------- /@iconify-demo/unocss/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /@iconify-demo/unocss/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import vue from '@vitejs/plugin-vue'; 3 | import unocss from 'unocss/vite'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [vue(), unocss()], 8 | }); 9 | -------------------------------------------------------------------------------- /@iconify-demo/unplugin/assets/svg-animated/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /@iconify-demo/unplugin/assets/svg-animated/loading2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /@iconify-demo/unplugin/assets/svg-ion/accessibility.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /@iconify-demo/unplugin/assets/svg-ion/balloon-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /@iconify-demo/unplugin/assets/svg-ion/build.svg: -------------------------------------------------------------------------------- 1 | ionicons-v5-h -------------------------------------------------------------------------------- /@iconify-demo/unplugin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /@iconify-demo/unplugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@iconify-demo/unplugin", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "private": true, 6 | "scripts": { 7 | "dev": "vite dev", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "vue": "^3.5.13" 13 | }, 14 | "devDependencies": { 15 | "@iconify/utils": "^2.2.0", 16 | "@iconify/tools": "workspace:^", 17 | "@vitejs/plugin-vue": "^5.2.1", 18 | "typescript": "^5.7.2", 19 | "unconfig": "^0.4.5", 20 | "unplugin-icons": "^0.18.5", 21 | "unplugin-vue-components": "^0.27.5", 22 | "vite": "^5.4.11" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /@iconify-demo/unplugin/src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 18 | 19 | 40 | -------------------------------------------------------------------------------- /@iconify-demo/unplugin/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './App.vue'; 3 | 4 | createApp(App).mount('#app'); 5 | -------------------------------------------------------------------------------- /@iconify/tools/.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | tests-compiled 3 | -------------------------------------------------------------------------------- /@iconify/tools/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 11 | 'plugin:prettier/recommended', 12 | ], 13 | globals: { 14 | Atomics: 'readonly', 15 | SharedArrayBuffer: 'readonly', 16 | }, 17 | parser: '@typescript-eslint/parser', 18 | parserOptions: { 19 | tsconfigRootDir: __dirname, 20 | project: ['tsconfig.json', 'tests/tsconfig.json'], 21 | extraFileExtensions: ['.cjs'], 22 | }, 23 | plugins: ['@typescript-eslint'], 24 | rules: { 25 | 'no-mixed-spaces-and-tabs': ['off'], 26 | 'no-unused-vars': ['off'], 27 | // '@typescript-eslint/no-unused-vars-experimental': ['error'], 28 | '@typescript-eslint/restrict-template-expressions': ['off'], 29 | '@typescript-eslint/no-unnecessary-type-assertion': ['off'], 30 | }, 31 | overrides: [ 32 | { 33 | files: ['src/**/*.ts', 'tests/*.ts'], 34 | }, 35 | ], 36 | }; 37 | -------------------------------------------------------------------------------- /@iconify/tools/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | .DS_Store 4 | .env 5 | *.map 6 | node_modules 7 | npm-debug.log 8 | yarn.lock 9 | tsconfig.tsbuildinfo 10 | lib 11 | cache 12 | -------------------------------------------------------------------------------- /@iconify/tools/.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | .DS_Store 4 | .env 5 | .eslintrc.cjs 6 | *.map 7 | node_modules 8 | npm-debug.log 9 | yarn.lock 10 | tsconfig.tsbuildinfo 11 | tsconfig*.json 12 | build.js 13 | build.config.ts 14 | jest.config.* 15 | jest.shared.config.cjs 16 | src 17 | spec 18 | tests 19 | cache 20 | -------------------------------------------------------------------------------- /@iconify/tools/README.md: -------------------------------------------------------------------------------- 1 | # Iconify Tools 2 | 3 | This library is a collection of tools for importing, exporting and processing SVG images. 4 | 5 | Its main purpose is to convert icon sets and fonts to Iconify JSON collections, but it can be used for other purposes. 6 | 7 | ## Installation 8 | 9 | First install it by running this command: 10 | 11 | ``` 12 | npm install @iconify/tools --save 13 | ``` 14 | 15 | ## Example 16 | 17 | The following code example does the following: 18 | 19 | - Imports set of SVG from directory. 20 | - Cleans up all icons. 21 | - Changes colors in all icons to `currentColor`. 22 | - Optimises icons. 23 | - Exports icons as `IconifyJSON` icon set. 24 | 25 | ```js 26 | import { promises as fs } from 'fs'; 27 | import { importDirectory } from '@iconify/tools/lib/import/directory'; 28 | import { cleanupSVG } from '@iconify/tools/lib/svg/cleanup'; 29 | import { runSVGO } from '@iconify/tools/lib/optimise/svgo'; 30 | import { parseColors, isEmptyColor } from '@iconify/tools/lib/colors/parse'; 31 | 32 | (async () => { 33 | // Import icons 34 | const iconSet = await importDirectory('svg/test', { 35 | prefix: 'test', 36 | }); 37 | 38 | // Validate, clean up, fix palette and optimise 39 | await iconSet.forEach(async (name, type) => { 40 | if (type !== 'icon') { 41 | return; 42 | } 43 | 44 | const svg = iconSet.toSVG(name); 45 | if (!svg) { 46 | // Invalid icon 47 | iconSet.remove(name); 48 | return; 49 | } 50 | 51 | // Clean up and optimise icons 52 | try { 53 | cleanupSVG(svg); 54 | await parseColors(svg, { 55 | defaultColor: 'currentColor', 56 | callback: (attr, colorStr, color) => { 57 | return !color || isEmptyColor(color) 58 | ? colorStr 59 | : 'currentColor'; 60 | }, 61 | }); 62 | runSVGO(svg); 63 | } catch (err) { 64 | // Invalid icon 65 | console.error(`Error parsing ${name}:`, err); 66 | iconSet.remove(name); 67 | return; 68 | } 69 | 70 | // Update icon 71 | iconSet.fromSVG(name, svg); 72 | }); 73 | 74 | // Export as IconifyJSON 75 | const exported = JSON.stringify(iconSet.export(), null, '\t') + '\n'; 76 | 77 | // Save to file 78 | await fs.writeFile(`output/${iconSet.prefix}.json`, exported, 'utf8'); 79 | })(); 80 | ``` 81 | 82 | ## Documentation 83 | 84 | Full documentation is too big for simple README file. See [Iconify Tools documentation](https://docs.iconify.design/tools/tools2/) for detailed documentation with code samples. 85 | 86 | ## Synchronous functions 87 | 88 | Most functions in example above are asynchronous. 89 | 90 | If you need to import or parse icons synchronously, such as in config file of package that does not support async configuration files, most functions have synchronous copies, such as `importDirectorySync()`. 91 | 92 | ## License 93 | 94 | Library is released with MIT license. 95 | 96 | © 2021-PRESENT Vjacheslav Trushkin 97 | -------------------------------------------------------------------------------- /@iconify/tools/build.config.ts: -------------------------------------------------------------------------------- 1 | import { BuildEntry, defineBuildConfig } from 'unbuild'; 2 | import packageJSON from './package.json'; 3 | 4 | const entries: BuildEntry[] = []; 5 | const exportsList = packageJSON['exports']; 6 | const match = './lib/'; 7 | 8 | Object.keys(exportsList).forEach((key) => { 9 | if (key.slice(0, match.length) !== match) { 10 | return; 11 | } 12 | 13 | const importValue = exportsList[key]['import']; 14 | if (importValue === key + '.mjs') { 15 | const name = key.slice(match.length); 16 | entries.push({ 17 | input: 'src/' + name, 18 | name, 19 | }); 20 | } 21 | }); 22 | 23 | export default defineBuildConfig({ 24 | outDir: './lib', 25 | entries, 26 | clean: true, 27 | declaration: true, 28 | rollup: { 29 | emitCJS: true, 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /@iconify/tools/jest.config.cjs: -------------------------------------------------------------------------------- 1 | const { buildConfiguration } = require('./jest.shared.config.cjs'); 2 | 3 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 4 | module.exports = buildConfiguration({ 5 | moduleFileExtensions: ['ts', 'cjs', 'js'], 6 | globals: { 7 | 'ts-jest': { 8 | useESM: false, 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /@iconify/tools/jest.config.mjs: -------------------------------------------------------------------------------- 1 | import pkg from './jest.shared.config.cjs'; 2 | 3 | export default pkg.buildConfiguration({ 4 | moduleFileExtensions: ['ts', 'mjs', 'js'], 5 | globals: { 6 | 'ts-jest': { 7 | useESM: true, 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /@iconify/tools/jest.shared.config.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Jest shared configuration: see https://jestjs.io/docs/ecmascript-modules. 3 | * 4 | * @param {import('ts-jest/dist/types').InitialOptionsTsJest} configuration 5 | * @return {import('ts-jest/dist/types').InitialOptionsTsJest} 6 | */ 7 | function buildConfiguration(configuration) { 8 | return Object.assign( 9 | {}, 10 | { 11 | verbose: true, 12 | testEnvironment: 'node', 13 | moduleDirectories: ['node_modules', 'src'], 14 | extensionsToTreatAsEsm: ['.ts'], 15 | transform: { 16 | '^.+\\.ts$': 'ts-jest', 17 | }, 18 | testMatch: ['**/tests/**/*-test.ts'], 19 | }, 20 | configuration 21 | ); 22 | } 23 | 24 | exports.buildConfiguration = buildConfiguration; 25 | module.exports = { buildConfiguration }; 26 | -------------------------------------------------------------------------------- /@iconify/tools/license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-PRESENT Vjacheslav Trushkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /@iconify/tools/src/colors/attribs.ts: -------------------------------------------------------------------------------- 1 | import type { Color } from '@iconify/utils/lib/colors/types'; 2 | 3 | /** 4 | * Color attributes 5 | */ 6 | export type CommonColorAttributes = 'color'; 7 | export const commonColorAttributes: CommonColorAttributes[] = ['color']; 8 | 9 | export type ShapeColorAttributes = 'fill' | 'stroke'; 10 | export const shapeColorAttributes: ShapeColorAttributes[] = ['fill', 'stroke']; 11 | 12 | export type SpecialColorAttributes = 'stop-color' | 'flood-color'; 13 | export const specialColorAttributes: SpecialColorAttributes[] = [ 14 | 'stop-color', 15 | 'flood-color', 16 | ]; 17 | 18 | export type ColorAttributes = 19 | | CommonColorAttributes 20 | | ShapeColorAttributes 21 | | SpecialColorAttributes; 22 | 23 | /** 24 | * Default values 25 | */ 26 | export const defaultBlackColor: Color = { 27 | type: 'rgb', 28 | r: 0, 29 | g: 0, 30 | b: 0, 31 | alpha: 1, 32 | }; 33 | 34 | export const defaultColorValues: Record = { 35 | 'color': { type: 'current' }, 36 | 'fill': defaultBlackColor, 37 | 'stroke': { type: 'none' }, 38 | 'stop-color': defaultBlackColor, 39 | 'flood-color': defaultBlackColor, 40 | }; 41 | 42 | /** 43 | * Ignore default color for some tags: 44 | * - If value is true, allow default color 45 | * - If value is attribute name, allow default color if attribute is set 46 | * 47 | * Parent elements are not checked for these tags! 48 | */ 49 | export const allowDefaultColorValue: Partial< 50 | Record 51 | > = { 52 | 'stop-color': true, 53 | 'flood-color': 'flood-opacity', 54 | }; 55 | -------------------------------------------------------------------------------- /@iconify/tools/src/colors/detect.ts: -------------------------------------------------------------------------------- 1 | import type { IconSet } from '../icon-set'; 2 | import { isEmptyColor, parseColors } from './parse'; 3 | 4 | /** 5 | * Detect palette 6 | * 7 | * Returns null if icon set has mixed colors 8 | */ 9 | export function detectIconSetPalette(iconSet: IconSet): boolean | null { 10 | let palette: boolean | null | undefined; 11 | 12 | iconSet.forEachSync( 13 | (name) => { 14 | if (palette === null) { 15 | return; 16 | } 17 | 18 | const svg = iconSet.toSVG(name); 19 | if (!svg) { 20 | return; 21 | } 22 | 23 | let iconPalette: boolean | null | undefined; 24 | parseColors(svg, { 25 | callback: (attr, colorStr, color) => { 26 | if (!color) { 27 | // Something went wrong 28 | iconPalette = null; 29 | return colorStr; 30 | } 31 | 32 | // Empty color or already failed 33 | if (iconPalette === null || isEmptyColor(color)) { 34 | return color; 35 | } 36 | 37 | // Check color 38 | const isColor = color.type !== 'current'; 39 | if (iconPalette === undefined) { 40 | // First entry: assign it 41 | iconPalette = isColor; 42 | return color; 43 | } 44 | 45 | if (iconPalette !== isColor) { 46 | // Mismatch 47 | iconPalette = null; 48 | } 49 | 50 | return color; 51 | }, 52 | }); 53 | 54 | if (iconPalette === undefined) { 55 | // No colors found 56 | iconPalette = null; 57 | } 58 | 59 | if (palette === undefined) { 60 | // First icon 61 | palette = iconPalette; 62 | } else if (palette !== iconPalette) { 63 | // Different value 64 | palette = null; 65 | } 66 | }, 67 | ['icon'] 68 | ); 69 | 70 | return palette ?? null; 71 | } 72 | -------------------------------------------------------------------------------- /@iconify/tools/src/colors/validate.ts: -------------------------------------------------------------------------------- 1 | import { colorToString } from '@iconify/utils/lib/colors'; 2 | import type { SVG } from '../svg/index'; 3 | import { parseColors, ParseColorsOptions } from './parse'; 4 | import type { FindColorsResult } from './parse'; 5 | 6 | /** 7 | * Validate colors in icon 8 | * 9 | * If icon is monotone, 10 | * 11 | * Throws exception on error 12 | */ 13 | export function validateColors( 14 | svg: SVG, 15 | expectMonotone: boolean, 16 | options?: ParseColorsOptions 17 | ): FindColorsResult { 18 | // Parse colors 19 | const palette = parseColors(svg, options); 20 | 21 | // Check palette 22 | palette.colors.forEach((color) => { 23 | if (typeof color === 'string') { 24 | throw new Error('Unexpected color: ' + color); 25 | } 26 | switch (color.type) { 27 | case 'none': 28 | case 'transparent': 29 | return; 30 | 31 | // Monotone 32 | case 'current': 33 | if (!expectMonotone) { 34 | throw new Error( 35 | 'Unexpected color: ' + colorToString(color) 36 | ); 37 | } 38 | return; 39 | 40 | // Palette 41 | case 'rgb': 42 | case 'hsl': 43 | if (expectMonotone) { 44 | throw new Error( 45 | 'Unexpected color: ' + colorToString(color) 46 | ); 47 | } 48 | return; 49 | 50 | default: 51 | // Allow url() 52 | if (color.type !== 'function' || color.func !== 'url') { 53 | // Do not allow other colors 54 | throw new Error( 55 | 'Unexpected color: ' + colorToString(color) 56 | ); 57 | } 58 | } 59 | }); 60 | 61 | return palette; 62 | } 63 | -------------------------------------------------------------------------------- /@iconify/tools/src/css/parse.ts: -------------------------------------------------------------------------------- 1 | import { getTokens } from './parser/tokens'; 2 | 3 | /** 4 | * Parse inline style 5 | */ 6 | export function parseInlineStyle(style: string): Record | null { 7 | const tokens = getTokens(style); 8 | if (!(tokens instanceof Array)) { 9 | return null; 10 | } 11 | 12 | const results = Object.create(null) as Record; 13 | for (let i = 0; i < tokens.length; i++) { 14 | const token = tokens[i]; 15 | if (token.type !== 'rule') { 16 | return null; 17 | } 18 | 19 | results[token.prop] = token.value; 20 | } 21 | 22 | return results; 23 | } 24 | -------------------------------------------------------------------------------- /@iconify/tools/src/css/parser/error.ts: -------------------------------------------------------------------------------- 1 | export interface StyleParseError { 2 | type: 'style-parse-error'; 3 | message: string; 4 | code: string; 5 | index?: number; 6 | fullMessage: string; 7 | } 8 | 9 | /** 10 | * Create error message 11 | */ 12 | export function styleParseError( 13 | message: string, 14 | code: string, 15 | index?: number 16 | ): StyleParseError { 17 | let fullMessage = message; 18 | if (typeof index === 'number' && index !== -1) { 19 | const start = index; 20 | 21 | // Check for space on left side of remaining code to calculate line start correctly 22 | const remaining = code.slice(index) + '!'; 23 | const trimmed = remaining.trim(); 24 | const end = start + remaining.length - trimmed.length; 25 | 26 | const code2 = code.slice(0, end); 27 | const line = code2.length - code2.replace(/\n/g, '').length + 1; 28 | fullMessage = message + ' on line ' + line.toString(); 29 | } 30 | 31 | return { 32 | type: 'style-parse-error', 33 | message, 34 | code, 35 | index, 36 | fullMessage, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /@iconify/tools/src/css/parser/export.ts: -------------------------------------------------------------------------------- 1 | import type { CSSTreeToken } from './types'; 2 | 3 | const tab = '\t'; 4 | const nl = '\n'; 5 | 6 | /** 7 | * Convert tokens tree to string 8 | */ 9 | export function tokensToString(tree: CSSTreeToken[]): string { 10 | let compact = true; 11 | for (let i = 0; i < tree.length; i++) { 12 | if (tree[i].type !== 'rule') { 13 | compact = false; 14 | break; 15 | } 16 | } 17 | 18 | return tree 19 | .map((token) => { 20 | return parseToken(token, compact, 0); 21 | }) 22 | .join(''); 23 | } 24 | 25 | /** 26 | * Old code 27 | */ 28 | 29 | function parseToken( 30 | token: CSSTreeToken, 31 | compact: boolean, 32 | depth: number 33 | ): string { 34 | let content: string; 35 | switch (token.type) { 36 | case 'rule': { 37 | return ( 38 | (compact ? '' : tab.repeat(depth)) + 39 | token.prop + 40 | (compact ? ':' : ': ') + 41 | token.value + 42 | ';' + 43 | (compact ? '' : nl) 44 | ); 45 | } 46 | 47 | case 'at-rule': { 48 | content = `@${token.rule} ${token.value}`.trim(); 49 | break; 50 | } 51 | 52 | case 'selector': { 53 | content = token.selectors.join(compact ? ',' : ', '); 54 | } 55 | } 56 | 57 | const children = token.children.map((item) => { 58 | return parseToken(item, compact, depth + 1); 59 | }); 60 | 61 | return ( 62 | (compact ? '' : tab.repeat(depth)) + 63 | content + 64 | (compact ? '{' : ' {' + nl) + 65 | children.join('') + 66 | (compact ? '}' : tab.repeat(depth) + '}' + nl) 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /@iconify/tools/src/css/parser/strings.ts: -------------------------------------------------------------------------------- 1 | import { styleParseError, StyleParseError } from './error'; 2 | 3 | /** 4 | * Find end of quoted string 5 | * 6 | * Returns index of character after quote 7 | */ 8 | export function findEndOfQuotedString( 9 | code: string, 10 | quote: string, 11 | start: number 12 | ): number | null { 13 | let nextEscape = code.indexOf('\\', start + 1); 14 | let end = code.indexOf(quote, start + 1); 15 | 16 | if (end === -1) { 17 | // Invalid string 18 | return null; 19 | } 20 | 21 | while (nextEscape !== -1 && nextEscape < end) { 22 | if (end === nextEscape + 1) { 23 | end = code.indexOf(quote, end + 1); 24 | if (end === -1) { 25 | // Invalid string 26 | return null; 27 | } 28 | } 29 | nextEscape = code.indexOf('\\', nextEscape + 2); 30 | } 31 | 32 | return end + 1; 33 | } 34 | 35 | /** 36 | * Find end of url 37 | * 38 | * Returns index of character after end of URL 39 | */ 40 | export function findEndOfURL( 41 | code: string, 42 | start: number 43 | ): number | StyleParseError { 44 | let index = (start || 0) + 4; 45 | const length = code.length; 46 | 47 | // eslint-disable-next-line no-constant-condition 48 | while (true) { 49 | if (index >= length) { 50 | return styleParseError('Cannot find end of URL', code, start); 51 | } 52 | let next = code.charAt(index); 53 | switch (next) { 54 | case '"': 55 | case "'": { 56 | // quoted url 57 | let end = findEndOfQuotedString(code, next, index); 58 | if (end === null) { 59 | return styleParseError('Incomplete string', code, index); 60 | } 61 | end = code.indexOf(')', end); 62 | return end === -1 63 | ? styleParseError('Cannot find end of URL', code, start) 64 | : end + 1; 65 | } 66 | 67 | case ' ': 68 | case '\t': 69 | case '\r': 70 | case '\n': 71 | // skip whitespace 72 | index++; 73 | break; 74 | 75 | default: 76 | // unquoted url 77 | // eslint-disable-next-line no-constant-condition 78 | while (true) { 79 | switch (next) { 80 | case ')': 81 | return index + 1; 82 | 83 | case '"': 84 | case "'": 85 | case '(': 86 | case ' ': 87 | case '\t': 88 | case '\r': 89 | case '\n': 90 | return styleParseError('Invalid URL', code, start); 91 | 92 | default: 93 | if (code.charCodeAt(index) < 32) { 94 | return styleParseError( 95 | 'Invalid URL', 96 | code, 97 | start 98 | ); 99 | } 100 | } 101 | index++; 102 | if (index >= length) { 103 | return styleParseError( 104 | 'Cannot find end of URL', 105 | code, 106 | start 107 | ); 108 | } 109 | next = code.charAt(index); 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /@iconify/tools/src/css/parser/tree.ts: -------------------------------------------------------------------------------- 1 | import type { CSSToken, CSSTreeToken } from './types'; 2 | 3 | /** 4 | * Convert tokens list to tree 5 | */ 6 | export function tokensTree(tokens: CSSToken[]): CSSTreeToken[] { 7 | const result: CSSTreeToken[] = []; 8 | let index = 0; 9 | 10 | function parse(target: CSSTreeToken[]): void { 11 | while (index < tokens.length) { 12 | const token = tokens[index]; 13 | index++; 14 | 15 | switch (token.type) { 16 | case 'close': 17 | return; 18 | 19 | case 'selector': 20 | case 'at-rule': { 21 | const newItem = { 22 | ...token, 23 | children: [], 24 | }; 25 | target.push(newItem); 26 | parse(newItem.children); 27 | 28 | // Remove token without children 29 | if (!newItem.children.length) { 30 | const index = target.indexOf(newItem); 31 | if (index !== -1) { 32 | target.splice(index, 1); 33 | } 34 | } 35 | break; 36 | } 37 | 38 | default: 39 | target.push(token); 40 | } 41 | } 42 | } 43 | 44 | while (index < tokens.length) { 45 | parse(result); 46 | } 47 | 48 | return result; 49 | } 50 | -------------------------------------------------------------------------------- /@iconify/tools/src/css/parser/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Text tokens, to be split combined into correct tokens later 3 | */ 4 | export interface TextToken { 5 | type: 'chunk' | 'url' | 'quoted-string'; 6 | index: number; 7 | text: string; 8 | } 9 | 10 | /** 11 | * Values 12 | */ 13 | export type CSSATValue = string | string[]; 14 | 15 | /** 16 | * Tokens 17 | */ 18 | // Simple rule 19 | export interface CSSRuleToken { 20 | type: 'rule'; 21 | index: number; 22 | prop: string; 23 | value: string; 24 | important?: boolean; 25 | } 26 | 27 | // Selector. Followed by children tokens until close token 28 | export interface CSSSelectorToken { 29 | type: 'selector'; 30 | index: number; 31 | code: string; 32 | selectors: string[]; 33 | } 34 | 35 | // At-rule. Followed by children tokens until close token 36 | export interface CSSAtRuleToken { 37 | type: 'at-rule'; 38 | index: number; 39 | rule: string; 40 | value: string; 41 | } 42 | 43 | // Closes selector or at-rule: '}' 44 | export interface CSSCloseToken { 45 | type: 'close'; 46 | index: number; 47 | } 48 | 49 | export type CSSTokenWithSelector = CSSSelectorToken | CSSAtRuleToken; 50 | 51 | export type CSSToken = 52 | | CSSRuleToken 53 | | CSSSelectorToken 54 | | CSSAtRuleToken 55 | | CSSCloseToken; 56 | 57 | /** 58 | * Tree tokens 59 | */ 60 | export interface CSSSelectorTreeToken extends CSSSelectorToken { 61 | children: CSSTreeToken[]; 62 | } 63 | 64 | export interface CSSAtRuleTreeToken extends CSSAtRuleToken { 65 | children: CSSTreeToken[]; 66 | } 67 | 68 | export type CSSTreeToken = 69 | | CSSRuleToken 70 | | CSSSelectorTreeToken 71 | | CSSAtRuleTreeToken; 72 | -------------------------------------------------------------------------------- /@iconify/tools/src/download/api/config.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig } from 'axios'; 2 | import { APIQueryParams } from './types'; 3 | 4 | /** 5 | * Axios config, customisable 6 | */ 7 | export const axiosConfig: Omit< 8 | AxiosRequestConfig, 9 | 'headers' | 'responseType' | 'url' | 'method' | 'data' 10 | > = { 11 | // Empty by default. Add properties 12 | }; 13 | 14 | interface AxiosCallbacks { 15 | onStart?: (url: string, params: APIQueryParams) => void; 16 | onSuccess?: (url: string, params: APIQueryParams) => void; 17 | onError?: (url: string, params: APIQueryParams, errorCode?: number) => void; 18 | } 19 | 20 | /** 21 | * Customisable callbacks, used for logging 22 | */ 23 | export const fetchCallbacks: AxiosCallbacks = { 24 | onStart: (url) => console.log('Fetching:', url), 25 | }; 26 | -------------------------------------------------------------------------------- /@iconify/tools/src/download/api/download.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { writeFile } from 'fs/promises'; 3 | import type { APIQueryParams } from './types'; 4 | import { axiosConfig, fetchCallbacks } from './config'; 5 | 6 | /** 7 | * Download file 8 | */ 9 | export async function downloadFile( 10 | query: APIQueryParams, 11 | target: string 12 | ): Promise { 13 | const params = query.params ? query.params.toString() : ''; 14 | const url = query.uri + (params ? '?' + params : ''); 15 | const headers = query.headers; 16 | 17 | fetchCallbacks.onStart?.(url, query); 18 | const response = await axios.get(url, { 19 | ...axiosConfig, 20 | headers, 21 | responseType: 'arraybuffer', 22 | }); 23 | 24 | if (response.status !== 200) { 25 | fetchCallbacks.onError?.(url, query, response.status); 26 | throw new Error(`Error downloading ${url}: ${response.status}`); 27 | } 28 | 29 | const data = response.data as ArrayBuffer; 30 | fetchCallbacks.onSuccess?.(url, query); 31 | await writeFile(target, Buffer.from(data)); 32 | } 33 | -------------------------------------------------------------------------------- /@iconify/tools/src/download/api/index.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { apiCacheKey, getAPICache, storeAPICache } from './cache'; 3 | import type { APICacheOptions, APIQueryParams } from './types'; 4 | import { axiosConfig, fetchCallbacks } from './config'; 5 | 6 | /** 7 | * Send API query 8 | */ 9 | export async function sendAPIQuery( 10 | query: APIQueryParams, 11 | cache?: APICacheOptions 12 | ): Promise { 13 | const cacheKey = cache ? apiCacheKey(query) : ''; 14 | if (cache) { 15 | const cached = await getAPICache(cache.dir, cacheKey); 16 | if (cached) { 17 | return cached; 18 | } 19 | } 20 | const result = await sendQuery(query); 21 | if (cache && typeof result !== 'number') { 22 | try { 23 | await storeAPICache(cache, cacheKey, result); 24 | } catch (err) { 25 | console.error('Error writing API cache'); 26 | } 27 | } 28 | return result; 29 | } 30 | 31 | /** 32 | * Send query 33 | */ 34 | async function sendQuery(query: APIQueryParams): Promise { 35 | const params = query.params ? query.params.toString() : ''; 36 | const url = query.uri + (params ? '?' + params : ''); 37 | const headers = query.headers; 38 | 39 | fetchCallbacks.onStart?.(url, query); 40 | 41 | function fail(value?: number) { 42 | fetchCallbacks.onError?.(url, query, value); 43 | return value ?? 404; 44 | } 45 | 46 | try { 47 | const response = await axios.get(url, { 48 | ...axiosConfig, 49 | headers, 50 | responseType: 'text', 51 | }); 52 | 53 | if (response.status !== 200) { 54 | return fail(response.status); 55 | } 56 | if (typeof response.data !== 'string') { 57 | return fail(); 58 | } 59 | 60 | fetchCallbacks.onSuccess?.(url, query); 61 | 62 | return response.data; 63 | } catch (err) { 64 | return fail(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /@iconify/tools/src/download/api/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * API Cache 3 | */ 4 | export interface APICacheOptions { 5 | // Directory where cache should be stored 6 | dir: string; 7 | 8 | // TTL in seconds 9 | ttl: number; 10 | } 11 | 12 | /** 13 | * Params 14 | */ 15 | export interface APIQueryParams { 16 | uri: string; 17 | params?: URLSearchParams; 18 | headers?: Record; 19 | } 20 | -------------------------------------------------------------------------------- /@iconify/tools/src/download/git/branch.ts: -------------------------------------------------------------------------------- 1 | import type { ExportTargetOptions } from '../../export/helpers/prepare'; 2 | import { execAsync } from '../../misc/exec'; 3 | 4 | /** 5 | * Get current branch from cloned git repo 6 | */ 7 | export async function getGitRepoBranch( 8 | options: ExportTargetOptions, 9 | checkout?: string 10 | ): Promise { 11 | const result = await execAsync('git branch --show-current', { 12 | cwd: options.target, 13 | }); 14 | const branch = result.stdout.trim(); 15 | 16 | if (typeof checkout === 'string' && branch !== checkout) { 17 | // Checkout correct branch 18 | await execAsync(`git checkout ${checkout} "${options.target}"`); 19 | return await getGitRepoBranch(options); 20 | } 21 | 22 | return branch; 23 | } 24 | -------------------------------------------------------------------------------- /@iconify/tools/src/download/git/hash.ts: -------------------------------------------------------------------------------- 1 | import type { ExportTargetOptions } from '../../export/helpers/prepare'; 2 | import { execAsync } from '../../misc/exec'; 3 | 4 | /** 5 | * Get latest hash from cloned git repo 6 | */ 7 | export async function getGitRepoHash( 8 | options: ExportTargetOptions 9 | ): Promise { 10 | const result = await execAsync('git rev-parse HEAD', { 11 | cwd: options.target, 12 | }); 13 | return result.stdout.trim(); 14 | } 15 | -------------------------------------------------------------------------------- /@iconify/tools/src/download/git/reset.ts: -------------------------------------------------------------------------------- 1 | import { execAsync } from '../..'; 2 | 3 | /** 4 | * Reset Git repo contents 5 | */ 6 | export async function resetGitRepoContents(target: string) { 7 | await execAsync('git add -A', { 8 | cwd: target, 9 | }); 10 | await execAsync('git reset --hard --quiet', { 11 | cwd: target, 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /@iconify/tools/src/download/github/hash.ts: -------------------------------------------------------------------------------- 1 | import { sendAPIQuery } from '../api'; 2 | import type { GitHubAPIOptions } from './types'; 3 | 4 | /** 5 | * Get latest hash from GitHub using API 6 | */ 7 | export async function getGitHubRepoHash( 8 | options: GitHubAPIOptions 9 | ): Promise { 10 | const uri = `https://api.github.com/repos/${options.user}/${options.repo}/branches/${options.branch}`; 11 | const data = await sendAPIQuery({ 12 | uri, 13 | headers: { 14 | Accept: 'application/vnd.github.v3+json', 15 | Authorization: 'token ' + options.token, 16 | }, 17 | }); 18 | if (typeof data !== 'string') { 19 | throw new Error(`Error downloading data from GitHub API: ${data}`); 20 | } 21 | 22 | interface GitHubAPIResponse { 23 | commit?: { 24 | sha: string; 25 | }; 26 | } 27 | const content = JSON.parse(data) as GitHubAPIResponse; 28 | const hash = content?.commit?.sha; 29 | if (typeof hash !== 'string') { 30 | throw new Error('Error parsing GitHub API response'); 31 | } 32 | return hash; 33 | } 34 | -------------------------------------------------------------------------------- /@iconify/tools/src/download/github/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * API options 3 | */ 4 | export interface GitHubAPIOptions { 5 | // API token 6 | token: string; 7 | 8 | // GitHub user or project name 9 | user: string; 10 | 11 | // Repository name 12 | repo: string; 13 | 14 | // Branch name 15 | branch: string; 16 | } 17 | -------------------------------------------------------------------------------- /@iconify/tools/src/download/gitlab/hash.ts: -------------------------------------------------------------------------------- 1 | import { sendAPIQuery } from '../api'; 2 | import { defaultGitLabBaseURI, GitLabAPIOptions } from './types'; 3 | 4 | /** 5 | * Get latest hash from GitHub using API 6 | */ 7 | export async function getGitLabRepoHash( 8 | options: GitLabAPIOptions 9 | ): Promise { 10 | const uri = `${options.uri || defaultGitLabBaseURI}/${ 11 | options.project 12 | }/repository/branches/${options.branch}/`; 13 | const data = await sendAPIQuery({ 14 | uri, 15 | headers: { 16 | Authorization: 'token ' + options.token, 17 | }, 18 | }); 19 | if (typeof data !== 'string') { 20 | throw new Error(`Error downloading data from GitLab API: ${data}`); 21 | } 22 | 23 | interface GitLabAPIResponse { 24 | name: string; 25 | commit: { 26 | id: string; 27 | }; 28 | } 29 | const content = JSON.parse(data) as GitLabAPIResponse | GitLabAPIResponse[]; 30 | const item = (content instanceof Array ? content : [content]).find( 31 | (item) => 32 | item.name === options.branch && typeof item.commit.id === 'string' 33 | ); 34 | if (!item) { 35 | throw new Error('Error parsing GitLab API response'); 36 | } 37 | 38 | return item.commit.id; 39 | } 40 | -------------------------------------------------------------------------------- /@iconify/tools/src/download/gitlab/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * API options 3 | */ 4 | export interface GitLabAPIOptions { 5 | // Base URI 6 | uri?: string; 7 | 8 | // API token 9 | token: string; 10 | 11 | // Project id 12 | project: string; 13 | 14 | // Branch name 15 | branch: string; 16 | } 17 | 18 | /** 19 | * Default base URI for GitLab API 20 | */ 21 | export const defaultGitLabBaseURI = 'https://gitlab.com/api/v4/projects'; 22 | -------------------------------------------------------------------------------- /@iconify/tools/src/download/helpers/untar.ts: -------------------------------------------------------------------------------- 1 | import { x } from 'tar'; 2 | 3 | /** 4 | * Unpack .tgz archive 5 | */ 6 | export async function untar(file: string, path: string): Promise { 7 | await x({ 8 | file, 9 | C: path, 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /@iconify/tools/src/download/helpers/unzip.ts: -------------------------------------------------------------------------------- 1 | import extract from 'extract-zip'; 2 | import { promises as fs } from 'fs'; 3 | 4 | /** 5 | * Unzip archive 6 | */ 7 | export async function unzip(source: string, path: string): Promise { 8 | const dir = await fs.realpath(path); 9 | await extract(source, { 10 | dir, 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /@iconify/tools/src/download/npm/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Package options 3 | */ 4 | export interface NPMPackageOptions { 5 | // Package 6 | package: string; 7 | 8 | // Tag, default is 'latest' 9 | tag?: string; 10 | } 11 | -------------------------------------------------------------------------------- /@iconify/tools/src/download/npm/version.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import type { NPMPackageOptions } from './types'; 3 | import { execAsync } from '../../misc/exec'; 4 | 5 | export interface GetNPMVersionResult { 6 | // Version 7 | version: string; 8 | 9 | // URL of file 10 | file?: string; 11 | } 12 | 13 | /** 14 | * Get version of package from NPM registry 15 | */ 16 | export async function getNPMVersion( 17 | options: NPMPackageOptions 18 | ): Promise { 19 | const tag = options.tag || 'latest'; 20 | const result = await execAsync(`npm view ${options.package}@${tag} --json`); 21 | 22 | interface NPMViewResponse { 23 | 'name': string; 24 | 'dist-tags': Record; 25 | 'versions': string[]; 26 | 'time': Record; 27 | 'version': string; 28 | 'dist'?: { 29 | integrity: string; 30 | shasum: string; 31 | tarball: string; 32 | fileCount: number; 33 | unpackedSize: number; 34 | }; 35 | } 36 | const data = JSON.parse(result.stdout) as NPMViewResponse; 37 | return { 38 | version: data.version, 39 | file: data.dist?.tarball, 40 | }; 41 | } 42 | 43 | /** 44 | * Get version of package from filename 45 | */ 46 | export async function getPackageVersion(target: string): Promise { 47 | interface PackageContent { 48 | name: string; 49 | version: string; 50 | } 51 | return ( 52 | JSON.parse( 53 | await fs.readFile(target + '/package.json', 'utf8') 54 | ) as PackageContent 55 | ).version; 56 | } 57 | -------------------------------------------------------------------------------- /@iconify/tools/src/download/types/modified.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Document wasn't modified 3 | */ 4 | export type DocumentNotModified = 'not_modified'; 5 | -------------------------------------------------------------------------------- /@iconify/tools/src/download/types/sources.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Download types 3 | */ 4 | export type DownloadSourceType = 'git' | 'github' | 'gitlab' | 'npm'; 5 | 6 | /** 7 | * Type in other objects 8 | */ 9 | export interface DownloadSourceMixin { 10 | downloadType: T; 11 | } 12 | -------------------------------------------------------------------------------- /@iconify/tools/src/export/directory.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import type { IconifyIconCustomisations } from '@iconify/utils/lib/customisations/defaults'; 3 | import type { IconSet } from '../icon-set'; 4 | import type { ExportTargetOptions } from './helpers/prepare'; 5 | import { prepareDirectoryForExport } from './helpers/prepare'; 6 | 7 | /** 8 | * Options 9 | */ 10 | export interface ExportToDirectoryOptions extends ExportTargetOptions { 11 | // Set icon height to 'auto', which results in width and height matching viewBox. 12 | // If false, height will be set to '1em'. 13 | // Default is true 14 | autoHeight?: boolean; 15 | 16 | // Include aliases. Default is true 17 | includeAliases?: boolean; 18 | 19 | // Include characters. Default is false 20 | includeChars?: boolean; 21 | 22 | // Log stored files. Default is false 23 | log?: boolean; 24 | } 25 | 26 | /** 27 | * Export icon set to directory 28 | * 29 | * Returns list of stored files 30 | */ 31 | export async function exportToDirectory( 32 | iconSet: IconSet, 33 | options: ExportToDirectoryOptions 34 | ): Promise { 35 | // Normalise and prepare directory 36 | const dir = await prepareDirectoryForExport(options); 37 | 38 | const storedFiles: Set = new Set(); 39 | const customisations: IconifyIconCustomisations = 40 | options.autoHeight === false 41 | ? { 42 | height: '1em', 43 | } 44 | : { 45 | width: 'auto', 46 | height: 'auto', 47 | }; 48 | 49 | // Function to save icon to file 50 | const store = async (name: string, target: string) => { 51 | const svg = iconSet.toString(name, customisations); 52 | if (!svg) { 53 | return; 54 | } 55 | 56 | await fs.writeFile(target, svg, 'utf8'); 57 | 58 | storedFiles.add(target); 59 | if (options.log) { 60 | console.log(`Saved ${target} (${svg.length} bytes)`); 61 | } 62 | }; 63 | 64 | // Export characters first, in case if icon name conflicts with character name 65 | if (options.includeChars) { 66 | const chars = iconSet.chars(); 67 | for (const char in chars) { 68 | const name = chars[char]; 69 | await store(name, `${dir}/${char}.svg`); 70 | } 71 | } 72 | 73 | // Export all icons 74 | await iconSet.forEach(async (name, type) => { 75 | if (type === 'alias' && options.includeAliases === false) { 76 | return; 77 | } 78 | await store(name, `${dir}/${name}.svg`); 79 | }); 80 | 81 | return Array.from(storedFiles); 82 | } 83 | -------------------------------------------------------------------------------- /@iconify/tools/src/export/helpers/custom-files.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import { writeJSONFile } from '../../misc/write-json'; 3 | 4 | /** 5 | * Options 6 | */ 7 | export interface ExportOptionsWithCustomFiles { 8 | // Custom files. Key of filename, value is content. 9 | // If value is null, file will be deleted. If value is an object, it will be handled as JSON data 10 | customFiles?: Record | null>; 11 | } 12 | 13 | /** 14 | * Write custom files 15 | */ 16 | export async function exportCustomFiles( 17 | dir: string, 18 | options: ExportOptionsWithCustomFiles, 19 | result?: Set 20 | ): Promise { 21 | const customFiles = options.customFiles || {}; 22 | for (const filename in customFiles) { 23 | const content = customFiles[filename]; 24 | if (content === null) { 25 | // Delete file, if exists 26 | try { 27 | await fs.unlink(dir + '/' + filename); 28 | } catch (err) { 29 | // 30 | } 31 | continue; 32 | } 33 | if (typeof content === 'string') { 34 | await fs.writeFile(dir + '/' + filename, content, 'utf8'); 35 | } else if (typeof content === 'object') { 36 | await writeJSONFile(dir + '/' + filename, content); 37 | } 38 | result?.add(filename); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /@iconify/tools/src/export/helpers/prepare.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import { normalize } from 'pathe'; 3 | 4 | /** 5 | * Common options for all functions that export to directory 6 | */ 7 | export interface ExportTargetOptions { 8 | // Target directory 9 | target: string; 10 | 11 | // Remove old files before exporting. Default is false 12 | cleanup?: boolean; 13 | } 14 | 15 | /** 16 | * Normalize directory 17 | */ 18 | export function normalizeDir(dir: string): string { 19 | // Normalise directory 20 | dir = normalize(dir); 21 | if (dir.slice(-1) === '/') { 22 | dir = dir.slice(0, -1); 23 | } 24 | return dir; 25 | } 26 | 27 | /** 28 | * Prepare directory for export 29 | * 30 | * Also normalizes directory and returns normalized value 31 | */ 32 | export async function prepareDirectoryForExport( 33 | options: ExportTargetOptions 34 | ): Promise { 35 | // Normalise directory 36 | const dir = normalizeDir(options.target); 37 | 38 | if (options.cleanup) { 39 | // Remove old files 40 | try { 41 | await fs.rm(dir, { 42 | recursive: true, 43 | force: true, 44 | }); 45 | } catch (err) { 46 | // 47 | } 48 | } 49 | 50 | // Create directory if missing 51 | try { 52 | await fs.mkdir(dir, { 53 | recursive: true, 54 | }); 55 | } catch (err) { 56 | // 57 | } 58 | 59 | return dir; 60 | } 61 | -------------------------------------------------------------------------------- /@iconify/tools/src/export/helpers/types-version.ts: -------------------------------------------------------------------------------- 1 | import { resolveModule } from 'local-pkg'; 2 | import { promises as fs } from 'fs'; 3 | 4 | let cache: string; 5 | 6 | async function getVersion(): Promise { 7 | const packageName = '@iconify/types/package.json'; 8 | const filename = resolveModule(packageName); 9 | if (!filename) { 10 | throw new Error(`Cannot resolve ${packageName}`); 11 | } 12 | const content = JSON.parse(await fs.readFile(filename, 'utf8')) as Record< 13 | string, 14 | unknown 15 | >; 16 | return (cache = content.version as string); 17 | } 18 | 19 | /** 20 | * Get current version of Iconify Types package 21 | */ 22 | export async function getTypesVersion(): Promise { 23 | throw new Error( 24 | `getTypesVersion() is deprecated, use wildcard to make packages work with all versions` 25 | ); 26 | return cache || (await getVersion()); 27 | } 28 | -------------------------------------------------------------------------------- /@iconify/tools/src/icon-set/match.ts: -------------------------------------------------------------------------------- 1 | import { defaultIconProps } from '@iconify/utils/lib/icon/defaults'; 2 | import type { FullIconifyIcon } from '@iconify/utils/lib/icon/defaults'; 3 | import type { IconSet } from '.'; 4 | 5 | // Maximum depth for looking for parent icons 6 | const maxIteration = 5; 7 | 8 | /** 9 | * Find matching icon in icon set 10 | */ 11 | export function findMatchingIcon( 12 | iconSet: IconSet, 13 | icon: FullIconifyIcon 14 | ): string | null { 15 | const body = icon.body; 16 | let hiddenMatch: string | null = null; 17 | 18 | function isMatching(data: FullIconifyIcon): boolean { 19 | for (const key in defaultIconProps) { 20 | const attr = key as keyof typeof defaultIconProps; 21 | if (data[attr] !== icon[attr]) { 22 | return false; 23 | } 24 | } 25 | return true; 26 | } 27 | 28 | /** 29 | * Check if icon matches 30 | */ 31 | function test(name: string, iteration: number): string | null { 32 | const data = iconSet.resolve(name, true); 33 | if (!data) { 34 | return null; 35 | } 36 | if (isMatching(data)) { 37 | if (data.hidden) { 38 | hiddenMatch = name; 39 | } else { 40 | return name; 41 | } 42 | } 43 | if (iteration > maxIteration) { 44 | return null; 45 | } 46 | 47 | // Check aliases 48 | for (const key in iconSet.entries) { 49 | const item = iconSet.entries[key]; 50 | if (item.type === 'variation' && item.parent === name) { 51 | const result = test(key, iteration + 1); 52 | if (typeof result === 'string') { 53 | return result; 54 | } 55 | } 56 | } 57 | return null; 58 | } 59 | 60 | // Find icons that match 61 | for (const key in iconSet.entries) { 62 | const item = iconSet.entries[key]; 63 | if (item.type === 'icon' && item.body === body) { 64 | // Possible match 65 | const result = test(key, 0); 66 | if (typeof result === 'string') { 67 | return result; 68 | } 69 | } 70 | } 71 | 72 | return hiddenMatch; 73 | } 74 | -------------------------------------------------------------------------------- /@iconify/tools/src/icon-set/merge.ts: -------------------------------------------------------------------------------- 1 | import { IconSet } from '.'; 2 | import { findMatchingIcon } from './match'; 3 | import { hasIconDataBeenModified } from './modified'; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 6 | function assertNever(v: never) { 7 | // 8 | } 9 | 10 | /** 11 | * Merge icon sets 12 | */ 13 | export function mergeIconSets(oldIcons: IconSet, newIcons: IconSet): IconSet { 14 | const mergedIcons = new IconSet(newIcons.export()); 15 | const oldEntries = oldIcons.entries; 16 | const entries = mergedIcons.entries; 17 | 18 | function add(name: string): boolean { 19 | if (entries[name]) { 20 | // Already exists 21 | return true; 22 | } 23 | 24 | const item = oldEntries[name]; 25 | switch (item.type) { 26 | case 'icon': { 27 | // Attempt to find matching icon 28 | const fullIcon = oldIcons.resolve(name, true); 29 | const parent = fullIcon 30 | ? findMatchingIcon(mergedIcons, fullIcon) 31 | : null; 32 | if (parent !== null) { 33 | // Add as alias 34 | mergedIcons.setAlias(name, parent); 35 | return true; 36 | } 37 | 38 | // Add as is, duplicating props 39 | const props = item.props; 40 | mergedIcons.setItem(name, { 41 | ...item, 42 | props: { 43 | ...props, 44 | hidden: true, 45 | }, 46 | categories: new Set(), 47 | }); 48 | return true; 49 | } 50 | 51 | case 'variation': 52 | case 'alias': { 53 | // Add parent 54 | let parent = item.parent; 55 | if (!add(parent)) { 56 | return false; 57 | } 58 | const parentItem = entries[parent]; 59 | if (parentItem.type === 'alias') { 60 | // Alias of alias - use parent 61 | parent = parentItem.parent; 62 | } 63 | 64 | if (item.type === 'variation') { 65 | // Hide variation and copy props 66 | const props = item.props; 67 | mergedIcons.setItem(name, { 68 | ...item, 69 | parent, 70 | props: { 71 | ...props, 72 | hidden: true, 73 | }, 74 | }); 75 | } else { 76 | mergedIcons.setItem(name, { 77 | ...item, 78 | parent, 79 | }); 80 | } 81 | return true; 82 | } 83 | 84 | default: 85 | assertNever(item); 86 | return false; 87 | } 88 | } 89 | 90 | // Add old icons 91 | for (const name in oldEntries) { 92 | add(name); 93 | } 94 | 95 | // Keep old lastModified if possible 96 | if ( 97 | oldIcons.lastModified && 98 | !hasIconDataBeenModified(oldIcons, mergedIcons) 99 | ) { 100 | // Old and merged icon sets are identical: set last modification time to old icon set 101 | mergedIcons.updateLastModified(oldIcons.lastModified); 102 | } else if ( 103 | newIcons.lastModified && 104 | !hasIconDataBeenModified(newIcons, mergedIcons) 105 | ) { 106 | // New and merged icon sets are identical: set last modificaiton time to new icon set 107 | mergedIcons.updateLastModified(newIcons.lastModified); 108 | } 109 | 110 | return mergedIcons; 111 | } 112 | -------------------------------------------------------------------------------- /@iconify/tools/src/icon-set/modified.ts: -------------------------------------------------------------------------------- 1 | import type { IconSet } from '.'; 2 | 3 | /** 4 | * Check if icons in an icon set were updated. 5 | * 6 | * This function checks only icons, not metadata. It also ignores icon visibility. 7 | */ 8 | export function hasIconDataBeenModified(set1: IconSet, set2: IconSet): boolean { 9 | const entries1 = set1.entries; 10 | const entries2 = set2.entries; 11 | 12 | const keys1 = Object.keys(entries1); 13 | const keys2 = Object.keys(entries2); 14 | 15 | // Check number of icons first 16 | if (keys1.length !== keys2.length) { 17 | return true; 18 | } 19 | 20 | // Check if icon names are the same 21 | for (let i = 0; i < keys1.length; i++) { 22 | if (!entries2[keys1[i]]) { 23 | return true; 24 | } 25 | } 26 | 27 | // Check all icons 28 | for (let i = 0; i < keys1.length; i++) { 29 | const name = keys1[i]; 30 | if (set1.toString(name) !== set2.toString(name)) { 31 | return true; 32 | } 33 | } 34 | 35 | // Icon sets are identical 36 | return false; 37 | } 38 | -------------------------------------------------------------------------------- /@iconify/tools/src/icon-set/props.ts: -------------------------------------------------------------------------------- 1 | import { 2 | commonObjectProps, 3 | unmergeObjects, 4 | } from '@iconify/utils/lib/misc/objects'; 5 | import type { CommonIconProps } from './types'; 6 | import { defaultIconProps } from '@iconify/utils'; 7 | 8 | /** 9 | * Common properties for icon and alias 10 | */ 11 | export const defaultCommonProps: Required = Object.freeze({ 12 | ...defaultIconProps, 13 | hidden: false, 14 | }); 15 | 16 | /** 17 | * Filter icon props: copies properties, removing undefined and default entries 18 | */ 19 | export function filterProps( 20 | data: CommonIconProps, 21 | reference: CommonIconProps, 22 | compareDefaultValues: boolean 23 | ): CommonIconProps { 24 | const result = commonObjectProps(data, reference); 25 | return compareDefaultValues ? unmergeObjects(result, reference) : result; 26 | } 27 | -------------------------------------------------------------------------------- /@iconify/tools/src/icon-set/tags.ts: -------------------------------------------------------------------------------- 1 | import { defaultIconDimensions } from '@iconify/utils/lib/icon/defaults'; 2 | import type { IconSet } from '.'; 3 | import { detectIconSetPalette } from '../colors/detect'; 4 | 5 | // Palette 6 | export const paletteTags = { 7 | monotone: 'Monotone', 8 | palette: 'Has Colors', 9 | }; 10 | 11 | // Icon size 12 | export const sizeTags = { 13 | square: 'Square', 14 | gridPrefix: 'Grid: ', 15 | heightPrefix: 'Height: ', 16 | }; 17 | 18 | /** 19 | * Add tags to icon set 20 | * 21 | * @deprecated 22 | */ 23 | export function addTagsToIconSet( 24 | iconSet: IconSet, 25 | customTags?: string[] 26 | ): string[] { 27 | const info = iconSet.info; 28 | const tags: string[] = []; 29 | 30 | // Find all icons to check 31 | const iconNames = Object.keys(iconSet.entries).filter((key) => { 32 | const item = iconSet.entries[key]; 33 | if (item.type !== 'icon') { 34 | return false; 35 | } 36 | if (item.props.hidden) { 37 | return false; 38 | } 39 | return true; 40 | }); 41 | 42 | if (iconNames.length) { 43 | // Palette 44 | let hasPalette: boolean | null | undefined = info?.palette; 45 | if (hasPalette === undefined) { 46 | hasPalette = detectIconSetPalette(iconSet); 47 | } 48 | 49 | if (hasPalette === true) { 50 | tags.push(paletteTags.palette); 51 | } 52 | if (hasPalette === false) { 53 | tags.push(paletteTags.monotone); 54 | } 55 | 56 | // Grid / height 57 | let isSquare = true; 58 | let height: number | undefined | null; 59 | 60 | for (let i = 0; i < iconNames.length; i++) { 61 | const icon = iconSet.entries[iconNames[i]]; 62 | if (icon.type !== 'icon') { 63 | continue; 64 | } 65 | const iconProps = icon.props; 66 | const iconWidth = iconProps.width || defaultIconDimensions.width; 67 | const iconHeight = iconProps.height || defaultIconDimensions.height; 68 | 69 | // Check if icon is square 70 | if (isSquare && iconWidth !== iconHeight) { 71 | isSquare = false; 72 | } 73 | 74 | // Check grid 75 | if (height === undefined) { 76 | height = iconHeight; 77 | } else if (height && iconHeight !== height) { 78 | // Failed 79 | height = null; 80 | } 81 | 82 | // Check for failure 83 | if (!height && !isSquare) { 84 | break; 85 | } 86 | } 87 | 88 | if (height && Math.round(height) === height) { 89 | // Grid 90 | tags.push( 91 | (isSquare ? sizeTags.gridPrefix : sizeTags.heightPrefix) + 92 | height.toString() 93 | ); 94 | } 95 | if (isSquare) { 96 | tags.push(sizeTags.square); 97 | } 98 | } 99 | 100 | // Merge custom tags, assign to info 101 | const result = tags.concat(customTags || []); 102 | if (info) { 103 | info.tags = result; 104 | } 105 | return result; 106 | } 107 | -------------------------------------------------------------------------------- /@iconify/tools/src/import/figma/types/api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Basic document structure 3 | */ 4 | // Various types for items 5 | // interface FigmaColor { 6 | // r: number; 7 | // g: number; 8 | // b: number; 9 | // a: number; 10 | // } 11 | 12 | interface FigmaBoundingBox { 13 | x: number; 14 | y: number; 15 | width: number; 16 | height: number; 17 | } 18 | 19 | // Base node: common elements for all nodes 20 | interface BaseFigmaNode { 21 | id: string; 22 | name: string; 23 | } 24 | 25 | // Generic node for irrelevant node types, might contain children 26 | interface GenericFigmaNode extends BaseFigmaNode { 27 | type: string; 28 | children?: FigmaNode[]; 29 | } 30 | 31 | // Frame or component: node that contains icon 32 | export interface IconFigmaNode extends BaseFigmaNode { 33 | type: 'FRAME' | 'COMPONENT' | 'INSTANCE'; 34 | clipsContent?: boolean; 35 | absoluteBoundingBox?: FigmaBoundingBox; 36 | children: FigmaNode[]; 37 | } 38 | 39 | // Document node 40 | export interface FigmaDocumentNode extends BaseFigmaNode { 41 | type: 'DOCUMENT'; 42 | children: FigmaNode[]; 43 | } 44 | 45 | // Node that can be a child of document node 46 | export type FigmaNode = GenericFigmaNode | IconFigmaNode; 47 | 48 | /** 49 | * Document response from API 50 | */ 51 | export interface FigmaDocument { 52 | document: FigmaDocumentNode; 53 | name: string; 54 | version: string; 55 | lastModified: string; 56 | thumbnailUrl: string; 57 | role: string; 58 | editorType: 'figma' | 'figjam'; 59 | } 60 | 61 | export interface FigmaAPIError { 62 | status: number; 63 | err: string; 64 | } 65 | 66 | /** 67 | * Result for retrieved icons 68 | */ 69 | export interface FigmaAPIImagesResponse { 70 | err?: string | null; 71 | images: Record; 72 | } 73 | -------------------------------------------------------------------------------- /@iconify/tools/src/import/figma/types/nodes.ts: -------------------------------------------------------------------------------- 1 | import type { FigmaDocument, IconFigmaNode } from './api'; 2 | import type { FigmaIconNode, FigmaNodesImportResult } from './result'; 3 | 4 | // Node types that can be parent nodes 5 | // 'CANVAS' in API is equal to 'PAGE' in plugins 6 | export type FigmaImportParentNodeType = 7 | | 'CANVAS' 8 | | 'FRAME' 9 | | 'GROUP' 10 | | 'SECTION' 11 | | 'COMPONENT_SET'; 12 | 13 | // Node types that can be icons 14 | export type FigmaImportIconNodeType = IconFigmaNode['type']; 15 | 16 | /** 17 | * Node information passed to callback 18 | */ 19 | export interface FigmaParentNodeData { 20 | id: string; 21 | type: FigmaImportParentNodeType; 22 | name: string; 23 | } 24 | 25 | export interface FigmaImportNodeData { 26 | id: string; 27 | type: FigmaImportIconNodeType; 28 | name: string; 29 | width: number; 30 | height: number; 31 | // First item is document 32 | parents: FigmaParentNodeData[]; 33 | } 34 | 35 | /** 36 | * Callback to check if node needs to be checked for icons 37 | * 38 | * Used to speed up processing by eleminating pages, frames and groups that do not need processing 39 | */ 40 | export type FigmaImportParentNodeFilter = ( 41 | // Nodes tree, first item is page, last item is node being checked 42 | node: FigmaParentNodeData[], 43 | // Figma document, raw response from Figma API 44 | document: FigmaDocument 45 | ) => boolean; 46 | 47 | /** 48 | * Check if node is an icon. 49 | * 50 | * Returns icon name on success, null or undefined if not should be ignored. 51 | * Function can also return FigmaIconNode object, where it can put extra properties that can be used later 52 | */ 53 | // FigmaIconNode with 'keyword' property being mandatory, other properties being optional 54 | type FigmaIconNodeWithKeyword = Partial & 55 | Pick; 56 | 57 | export type FigmaImportNodeFilter = ( 58 | // Node to check 59 | node: FigmaImportNodeData, 60 | // Other nodes that were already found (can be used to check for duplicate keywords) 61 | nodes: FigmaNodesImportResult, 62 | // Figma document, raw response from Figma API 63 | document: FigmaDocument 64 | ) => string | FigmaIconNodeWithKeyword | null | undefined; 65 | -------------------------------------------------------------------------------- /@iconify/tools/src/import/figma/types/result.ts: -------------------------------------------------------------------------------- 1 | import type { IconSet } from '../../../icon-set'; 2 | 3 | /** 4 | * Result for found icons 5 | */ 6 | export interface FigmaIconNode { 7 | // Node id 8 | id: string; 9 | // Node name 10 | name: string; 11 | // Keyword 12 | keyword: string; 13 | // URL to download from 14 | url?: string; 15 | // Downloaded SVG 16 | content?: string; 17 | } 18 | 19 | /** 20 | * Nodes count 21 | */ 22 | export interface FigmaNodesCount { 23 | // Number of nodes marked as icons 24 | nodesCount: number; 25 | // Number of generated icons 26 | generatedIconsCount: number; 27 | // Number of downloaded icons 28 | downloadedIconsCount: number; 29 | } 30 | 31 | /** 32 | * Import result for icons 33 | */ 34 | export interface FigmaNodesImportResult extends Partial { 35 | // Icons 36 | icons: Record; 37 | } 38 | 39 | /** 40 | * Import result 41 | */ 42 | export interface FigmaImportResult extends FigmaNodesCount { 43 | // Document 44 | name: string; 45 | version: string; 46 | lastModified: string; 47 | 48 | // Icon set 49 | iconSet: IconSet; 50 | missing: FigmaIconNode[]; 51 | } 52 | -------------------------------------------------------------------------------- /@iconify/tools/src/misc/bump-version.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Bump version number 3 | */ 4 | export function bumpVersion(version: string): string { 5 | const versionParts = version.split('.'); 6 | const lastPart = versionParts.pop() as string; 7 | const num = parseInt(lastPart); 8 | if (isNaN(num) || num.toString() !== lastPart) { 9 | versionParts.push(lastPart + '.1'); 10 | } else { 11 | versionParts.push((num + 1).toString()); 12 | } 13 | return versionParts.join('.'); 14 | } 15 | -------------------------------------------------------------------------------- /@iconify/tools/src/misc/cheerio.ts: -------------------------------------------------------------------------------- 1 | import type { Element } from 'domhandler'; 2 | import type { Cheerio } from 'cheerio'; 3 | 4 | /** 5 | * Shortcuts for Cheerio elements 6 | */ 7 | export type CheerioElement = Element; 8 | export type WrappedCheerioElement = Cheerio; 9 | -------------------------------------------------------------------------------- /@iconify/tools/src/misc/exec.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'pathe'; 2 | import { exec } from 'child_process'; 3 | import type { ExecOptions } from 'child_process'; 4 | 5 | export interface ExecResult { 6 | stdout: string; 7 | stderr: string; 8 | } 9 | 10 | /** 11 | * Exec as Promise 12 | */ 13 | export function execAsync( 14 | cmd: string, 15 | options?: ExecOptions 16 | ): Promise { 17 | return new Promise((fulfill, reject) => { 18 | if (typeof options?.cwd === 'string') { 19 | // Relative directories sometimes do not work, so resolve directory first 20 | options = { 21 | ...options, 22 | cwd: resolve(options.cwd), 23 | }; 24 | } 25 | exec(cmd, options, (error, stdout, stderr) => { 26 | if (error) { 27 | reject(error); 28 | } else { 29 | fulfill({ 30 | stdout, 31 | stderr, 32 | } as ExecResult); 33 | } 34 | }); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /@iconify/tools/src/misc/keyword.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Clean up keyword 3 | */ 4 | export function cleanupIconKeyword( 5 | keyword: string, 6 | convertCamelCase = false 7 | ): string { 8 | // Convert camelCase to dash-case 9 | if (convertCamelCase) { 10 | keyword = keyword.replace( 11 | /[A-Z]+/g, 12 | (chars) => '_' + chars.toLowerCase() 13 | ); 14 | } 15 | 16 | // Replace stuff 17 | keyword = keyword 18 | .toLowerCase() 19 | .trim() 20 | // Replace few characters with dash 21 | .replace(/[\s_.:/\\]/g, '-') 22 | // Remove bad characters 23 | .replace(/[^a-z0-9-]/g, '') 24 | // Replace repeating dash 25 | .replace(/[-]+/g, '-'); 26 | 27 | // Remove '-' at start and end 28 | if (keyword.slice(0, 1) === '-') { 29 | keyword = keyword.slice(1); 30 | } 31 | if (keyword.slice(-1) === '-') { 32 | keyword = keyword.slice(0, -1); 33 | } 34 | 35 | return keyword; 36 | } 37 | -------------------------------------------------------------------------------- /@iconify/tools/src/misc/write-json.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | 3 | /** 4 | * Write JSON file 5 | */ 6 | export async function writeJSONFile( 7 | filename: string, 8 | data: unknown 9 | ): Promise { 10 | return fs.writeFile(filename, JSON.stringify(data, null, '\t') + '\n'); 11 | } 12 | -------------------------------------------------------------------------------- /@iconify/tools/src/optimise/origin.ts: -------------------------------------------------------------------------------- 1 | import type { SVG } from '../svg'; 2 | import { runSVGO } from './svgo'; 3 | 4 | /** 5 | * Reset origin to 0 0 6 | */ 7 | export function resetSVGOrigin(svg: SVG) { 8 | const viewBox = svg.viewBox; 9 | if (viewBox.left !== 0 || viewBox.top !== 0) { 10 | // Shift content 11 | const content = `${svg.getBody()}`; 18 | svg.load(content); 19 | 20 | runSVGO(svg, { 21 | plugins: [ 22 | 'collapseGroups', 23 | 'convertTransform', 24 | 'convertPathData', 25 | 'sortAttrs', 26 | ], 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /@iconify/tools/src/optimise/scale.ts: -------------------------------------------------------------------------------- 1 | import type { SVG } from '../svg'; 2 | import { resetSVGOrigin } from './origin'; 3 | import { runSVGO } from './svgo'; 4 | 5 | /** 6 | * Scale icon 7 | */ 8 | export function scaleSVG(svg: SVG, scale: number) { 9 | // Reset origin first 10 | resetSVGOrigin(svg); 11 | 12 | // Scale 13 | if (scale !== 1) { 14 | const viewBox = svg.viewBox; 15 | const width = viewBox.width * scale; 16 | const height = viewBox.height * scale; 17 | 18 | const content = `${svg.getBody()}`; 19 | svg.load(content); 20 | 21 | runSVGO(svg, { 22 | plugins: [ 23 | 'collapseGroups', 24 | 'convertTransform', 25 | 'convertPathData', 26 | 'sortAttrs', 27 | ], 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /@iconify/tools/src/optimise/unwrap.ts: -------------------------------------------------------------------------------- 1 | import type { SVG } from '../svg'; 2 | 3 | /** 4 | * Removes empty group from SVG root element 5 | */ 6 | export function unwrapEmptyGroup(svg: SVG) { 7 | const cheerio = svg.$svg; 8 | const $root = svg.$svg(':root'); 9 | const children = $root.children(); 10 | 11 | if (children.length !== 1 || children[0].tagName !== 'g') { 12 | return; 13 | } 14 | const groupNode = children[0]; 15 | const html = cheerio(groupNode).html(); 16 | if (!html) { 17 | return; 18 | } 19 | 20 | // Check attributes 21 | for (const attr in groupNode.attribs) { 22 | const value = groupNode.attribs[attr]; 23 | switch (attr) { 24 | case 'id': { 25 | // Check if ID is used 26 | if (html?.includes(value)) { 27 | // ID is used 28 | return; 29 | } 30 | break; 31 | } 32 | 33 | default: 34 | // Unknown attribute: do not mess with it 35 | return; 36 | } 37 | } 38 | 39 | // Unwrap group 40 | $root.html(html); 41 | } 42 | -------------------------------------------------------------------------------- /@iconify/tools/src/svg/analyse/error.ts: -------------------------------------------------------------------------------- 1 | import type { ExtendedTagElement } from './types'; 2 | 3 | /** 4 | * Get tag for error message 5 | */ 6 | export function analyseTagError(element: ExtendedTagElement): string { 7 | let result = '<' + element.tagName; 8 | if (element._id) { 9 | result += ' id="' + element._id + '"'; 10 | } 11 | const attribs = element.attribs; 12 | if (attribs['d']) { 13 | const value = attribs['d']; 14 | result += 15 | ' d="' + 16 | (value.length > 16 ? value.slice(0, 12) + '...' : value) + 17 | '"'; 18 | } 19 | return result + '>'; 20 | } 21 | -------------------------------------------------------------------------------- /@iconify/tools/src/svg/cleanup.ts: -------------------------------------------------------------------------------- 1 | import type { SVG } from '.'; 2 | import { removeBadAttributes } from './cleanup/attribs'; 3 | import { CheckBadTagsOptions, checkBadTags } from './cleanup/bad-tags'; 4 | import { cleanupInlineStyle } from './cleanup/inline-style'; 5 | import { cleanupRootStyle } from './cleanup/root-style'; 6 | import { cleanupSVGRoot } from './cleanup/root-svg'; 7 | import { convertStyleToAttrs } from './cleanup/svgo-style'; 8 | 9 | /** 10 | * Options 11 | */ 12 | export type CleanupSVGOptions = CheckBadTagsOptions; 13 | 14 | /** 15 | * Clean up SVG before parsing/optimising it 16 | */ 17 | export function cleanupSVG(svg: SVG, options?: CleanupSVGOptions): void { 18 | // Remove junk from style 19 | cleanupInlineStyle(svg); 20 | 21 | // Expand style 22 | convertStyleToAttrs(svg); 23 | 24 | // Cleanup element 25 | cleanupSVGRoot(svg); 26 | 27 | // Check for bad tags 28 | checkBadTags(svg, options); 29 | 30 | // Remove attributes 31 | removeBadAttributes(svg); 32 | 33 | // Clean up root style 34 | cleanupRootStyle(svg); 35 | } 36 | -------------------------------------------------------------------------------- /@iconify/tools/src/svg/cleanup/attribs.ts: -------------------------------------------------------------------------------- 1 | import type { SVG } from '../../svg'; 2 | import { 3 | badAttributes, 4 | badAttributePrefixes, 5 | badSoftwareAttributes, 6 | tagSpecificPresentationalAttributes, 7 | } from '../data/attributes'; 8 | import { defsTag } from '../data/tags'; 9 | import { parseSVG } from '../parse'; 10 | 11 | /** 12 | * Remove useless attributes 13 | */ 14 | export function removeBadAttributes(svg: SVG): void { 15 | parseSVG(svg, (item) => { 16 | const tagName = item.tagName; 17 | const attribs = item.element.attribs; 18 | const $element = item.$element; 19 | 20 | // Common tags 21 | Object.keys(attribs).forEach((attr) => { 22 | // Bad attributes, events 23 | if ( 24 | attr.slice(0, 2) === 'on' || 25 | badAttributes.has(attr) || 26 | badSoftwareAttributes.has(attr) || 27 | badAttributePrefixes.has(attr.split('-').shift() as string) 28 | ) { 29 | $element.removeAttr(attr); 30 | return; 31 | } 32 | 33 | // Attributes on aren't passed to child nodes, so remove everything) 34 | if ( 35 | defsTag.has(tagName) && 36 | !tagSpecificPresentationalAttributes[tagName].has(attr) 37 | ) { 38 | $element.removeAttr(attr); 39 | return; 40 | } 41 | 42 | // Check for namespace 43 | const nsParts = attr.split(':'); 44 | if (nsParts.length > 1) { 45 | const namespace = nsParts.shift(); 46 | const newAttr = nsParts.join(':'); 47 | switch (namespace) { 48 | case 'xlink': { 49 | // Deprecated: use without namespace 50 | if (attribs[newAttr] === undefined) { 51 | $element.attr(newAttr, attribs[attr]); 52 | } 53 | break; 54 | } 55 | } 56 | 57 | // Remove all namespace attributes 58 | $element.removeAttr(attr); 59 | } 60 | }); 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /@iconify/tools/src/svg/cleanup/root-style.ts: -------------------------------------------------------------------------------- 1 | import type { SVG } from '..'; 2 | import { parseSVGStyle } from '../parse-style'; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 5 | function assertNever(v: never) { 6 | // 7 | } 8 | 9 | interface CleanupRootStyleResult { 10 | animations?: Set; 11 | removedAtRules?: Set; 12 | } 13 | 14 | /** 15 | * Clean up root style 16 | * 17 | * This function removes all at-rule tokens, such as `@font-face`, `@media` 18 | */ 19 | export function cleanupRootStyle(svg: SVG): CleanupRootStyleResult { 20 | const result: CleanupRootStyleResult = {}; 21 | 22 | parseSVGStyle(svg, (item) => { 23 | switch (item.type) { 24 | case 'inline': 25 | // Keep it 26 | return item.value; 27 | 28 | case 'global': 29 | // Keep it 30 | return item.value; 31 | 32 | case 'at-rule': 33 | // at-rule: remove it 34 | ( 35 | result.removedAtRules || (result.removedAtRules = new Set()) 36 | ).add(item.prop); 37 | return; 38 | 39 | case 'keyframes': 40 | // Keep it 41 | (result.animations || (result.animations = new Set())).add( 42 | item.value 43 | ); 44 | return item.value; 45 | 46 | default: 47 | assertNever(item); 48 | } 49 | }); 50 | 51 | return result; 52 | } 53 | -------------------------------------------------------------------------------- /@iconify/tools/src/svg/cleanup/svgo-style.ts: -------------------------------------------------------------------------------- 1 | import type { SVG } from '../../svg'; 2 | import { 3 | badAttributes, 4 | badAttributePrefixes, 5 | badSoftwareAttributes, 6 | } from '../data/attributes'; 7 | import { parseSVGStyle } from '../parse-style'; 8 | import { runSVGO } from '../../optimise/svgo'; 9 | 10 | /** 11 | * Expand inline style 12 | */ 13 | export function convertStyleToAttrs(svg: SVG): void { 14 | let hasStyle = false; 15 | 16 | // Clean up style, removing useless junk 17 | parseSVGStyle(svg, (item) => { 18 | if (item.type !== 'inline' && item.type !== 'global') { 19 | return item.value; 20 | } 21 | 22 | // Inline or global 23 | const prop = item.prop; 24 | if ( 25 | // Attributes / properties now allowed 26 | badAttributes.has(prop) || 27 | badSoftwareAttributes.has(prop) || 28 | badAttributePrefixes.has(prop.split('-').shift() as string) 29 | ) { 30 | return; 31 | } 32 | 33 | hasStyle = true; 34 | return item.value; 35 | }); 36 | 37 | // Nothing to check? 38 | if (!hasStyle) { 39 | return; 40 | } 41 | 42 | // Run SVGO 43 | runSVGO(svg, { 44 | plugins: ['convertStyleToAttrs', 'inlineStyles'], 45 | multipass: true, 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /@iconify/tools/src/svg/parse.ts: -------------------------------------------------------------------------------- 1 | import type { CheerioElement, WrappedCheerioElement } from '../misc/cheerio'; 2 | import type { SVG } from './'; 3 | /** 4 | * Item in callback 5 | */ 6 | export interface ParseSVGCallbackItem { 7 | tagName: string; 8 | element: CheerioElement; 9 | $element: WrappedCheerioElement; 10 | svg: SVG; 11 | // Parent elements, first item is direct parent, last item is 'svg' 12 | parents: ParseSVGCallbackItem[]; 13 | // Set to false to stop parsing 14 | testChildren: boolean; 15 | // Set to true to remove node 16 | removeNode: boolean; 17 | } 18 | 19 | /** 20 | * Callback function 21 | */ 22 | export type ParseSVGCallback = (item: ParseSVGCallbackItem) => void; 23 | 24 | /** 25 | * Parse SVG 26 | * 27 | * This function finds all elements in SVG and calls callback for each element. 28 | */ 29 | export function parseSVG(svg: SVG, callback: ParseSVGCallback): void { 30 | const cheerio = svg.$svg; 31 | const $root = svg.$svg(':root'); 32 | 33 | function checkNode( 34 | element: CheerioElement, 35 | parents: ParseSVGCallbackItem[] 36 | ) { 37 | // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison 38 | if (element.type !== 'tag') { 39 | return; 40 | } 41 | 42 | const $element = cheerio(element); 43 | const tagName = element.tagName; 44 | const item: ParseSVGCallbackItem = { 45 | tagName, 46 | element, 47 | $element, 48 | svg, 49 | parents, 50 | testChildren: true, 51 | removeNode: false, 52 | }; 53 | 54 | // Run callback 55 | const callbackResult = callback(item); 56 | if ((callbackResult as unknown) instanceof Promise) { 57 | // Old code 58 | throw new Error('parseSVG does not support async callbacks'); 59 | } 60 | 61 | // Test child nodes 62 | const newParents = parents.slice(0); 63 | newParents.unshift(item); 64 | 65 | let queue: CheerioElement[] = []; 66 | if (tagName !== 'style' && item.testChildren && !item.removeNode) { 67 | const children = $element.children().toArray(); 68 | queue = children.slice(0); 69 | } 70 | 71 | while (queue.length) { 72 | const queueItem = queue.shift(); 73 | if (!queueItem || item.removeNode) { 74 | // Do not parse child items if item is marked for removal 75 | break; 76 | } 77 | 78 | checkNode(queueItem, newParents); 79 | } 80 | 81 | // Remove node 82 | if (item.removeNode) { 83 | $element.remove(); 84 | } 85 | } 86 | 87 | checkNode($root.get(0) as CheerioElement, []); 88 | } 89 | -------------------------------------------------------------------------------- /@iconify/tools/src/tests/helpers.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | 3 | /** 4 | * Test helper: load fixture 5 | */ 6 | export async function loadFixture(file: string): Promise { 7 | return await fs.readFile('tests/fixtures/' + file, 'utf8'); 8 | } 9 | 10 | /** 11 | * Checks if running tests that download stuff 12 | * 13 | * These tests are disabled for quick testing and CI 14 | */ 15 | export function isTestingRemote(): boolean { 16 | return process.env['TEST_REMOTE'] !== 'false'; 17 | } 18 | -------------------------------------------------------------------------------- /@iconify/tools/tests/colors/detect-test.ts: -------------------------------------------------------------------------------- 1 | import type { IconifyJSON } from '@iconify/types'; 2 | import { IconSet } from '../../lib/icon-set'; 3 | import { detectIconSetPalette } from '../../lib/colors/detect'; 4 | import { loadFixture } from '../../lib/tests/helpers'; 5 | 6 | describe('Detecting palette', () => { 7 | test('Empty icon set', () => { 8 | const iconSetData: IconifyJSON = { 9 | prefix: 'foo', 10 | icons: {}, 11 | }; 12 | const iconSet = new IconSet(iconSetData); 13 | 14 | expect(detectIconSetPalette(iconSet)).toBe(null); 15 | }); 16 | 17 | test('Icons with palette', () => { 18 | const iconSetData: IconifyJSON = { 19 | prefix: 'foo', 20 | icons: { 21 | foo: { 22 | body: '', 23 | }, 24 | bar: { 25 | body: '', 26 | }, 27 | }, 28 | }; 29 | const iconSet = new IconSet(iconSetData); 30 | 31 | expect(detectIconSetPalette(iconSet)).toBe(true); 32 | }); 33 | 34 | test('Icons without palette', () => { 35 | const iconSetData: IconifyJSON = { 36 | prefix: 'foo', 37 | icons: { 38 | foo: { 39 | body: '', 40 | }, 41 | bar: { 42 | // lower case 43 | body: '', 44 | }, 45 | }, 46 | }; 47 | const iconSet = new IconSet(iconSetData); 48 | 49 | expect(detectIconSetPalette(iconSet)).toBe(false); 50 | }); 51 | 52 | test('Mixed', () => { 53 | const iconSetData: IconifyJSON = { 54 | prefix: 'foo', 55 | icons: { 56 | foo: { 57 | body: '', 58 | }, 59 | bar: { 60 | body: '', 61 | }, 62 | }, 63 | }; 64 | const iconSet = new IconSet(iconSetData); 65 | 66 | expect(detectIconSetPalette(iconSet)).toBe(null); 67 | }); 68 | 69 | test('No colors', () => { 70 | const iconSetData: IconifyJSON = { 71 | prefix: 'foo', 72 | icons: { 73 | foo: { 74 | body: '', 75 | }, 76 | }, 77 | }; 78 | const iconSet = new IconSet(iconSetData); 79 | 80 | expect(detectIconSetPalette(iconSet)).toBe(null); 81 | }); 82 | 83 | test('arty-animated.json', async () => { 84 | const iconSetData = JSON.parse( 85 | await loadFixture('arty-animated.json') 86 | ) as IconifyJSON; 87 | const iconSet = new IconSet(iconSetData); 88 | 89 | expect(detectIconSetPalette(iconSet)).toBe(false); 90 | }); 91 | 92 | test('codicon.json', async () => { 93 | const iconSetData = JSON.parse( 94 | await loadFixture('codicon.json') 95 | ) as IconifyJSON; 96 | const iconSet = new IconSet(iconSetData); 97 | 98 | expect(detectIconSetPalette(iconSet)).toBe(false); 99 | }); 100 | 101 | test('fluent.json', async () => { 102 | const iconSetData = JSON.parse( 103 | await loadFixture('fluent.new.json') 104 | ) as IconifyJSON; 105 | const iconSet = new IconSet(iconSetData); 106 | 107 | expect(detectIconSetPalette(iconSet)).toBe(false); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /@iconify/tools/tests/css/quoted-string-test.ts: -------------------------------------------------------------------------------- 1 | import { findEndOfQuotedString } from '../../lib/css/parser/strings'; 2 | 3 | describe('findEndOfQuotedString()', () => { 4 | test('Simple code', () => { 5 | const testStr = 'div { background: url("test;}{url"); color: blue; }'; 6 | const testChar = '"'; 7 | const testStartIndex = testStr.indexOf(testChar); 8 | const expectedIndex = 9 | testStr.slice(testStartIndex + 1).indexOf(testChar) + 10 | testStartIndex + 11 | 2; 12 | expect(findEndOfQuotedString(testStr, testChar, testStartIndex)).toBe( 13 | expectedIndex 14 | ); 15 | }); 16 | 17 | test('Simple code 2', () => { 18 | const testStr = '.foo { color: red; @import "bar.css"; opacity: 0 }'; 19 | const testChar = '"'; 20 | const testStartIndex = testStr.indexOf(testChar); 21 | const expectedIndex = 22 | testStr.slice(testStartIndex + 1).indexOf(testChar) + 23 | testStartIndex + 24 | 2; 25 | expect(findEndOfQuotedString(testStr, testChar, testStartIndex)).toBe( 26 | expectedIndex 27 | ); 28 | }); 29 | 30 | test('No end quote', () => { 31 | const testStr = "This is a test 'with quote"; 32 | const testChar = "'"; 33 | const testStartIndex = testStr.indexOf(testChar); 34 | expect( 35 | findEndOfQuotedString(testStr, testChar, testStartIndex) 36 | ).toBeNull(); 37 | }); 38 | 39 | test('Escaped character', () => { 40 | const testStr = "span[data-foo='test\"\\'str']"; 41 | const testChar = "'"; 42 | const testStartIndex = testStr.indexOf(testChar); 43 | const expectedIndex = 44 | testStr.slice(testStartIndex + 1).indexOf(testChar) + 45 | testStartIndex + 46 | 2 + 47 | // 4 characters after next match 48 | 4; 49 | expect(findEndOfQuotedString(testStr, testChar, testStartIndex)).toBe( 50 | expectedIndex 51 | ); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /@iconify/tools/tests/css/url-string-test.ts: -------------------------------------------------------------------------------- 1 | import { findEndOfURL } from '../../lib/css/parser/strings'; 2 | 3 | describe('findEndOfURL()', () => { 4 | test('Simple code', () => { 5 | const testStr = 6 | 'div { background: url(data:image/png;base64,whatever/*}{&); color: blue; }'; 7 | const testStartIndex = testStr.indexOf('url'); 8 | const expectedIndex = testStr.indexOf(')') + 1; 9 | expect(findEndOfURL(testStr, testStartIndex)).toBe(expectedIndex); 10 | }); 11 | 12 | test('Quoted URL', () => { 13 | const testStr = 'div { background: url("test;}{url"); color: blue; }'; 14 | const testStartIndex = testStr.indexOf('url'); 15 | const expectedIndex = testStr.indexOf(')') + 1; 16 | expect(findEndOfURL(testStr, testStartIndex)).toBe(expectedIndex); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /@iconify/tools/tests/export/json-package-test.ts: -------------------------------------------------------------------------------- 1 | import type { IconifyJSON } from '@iconify/types'; 2 | import { promises as fs } from 'fs'; 3 | import { exportJSONPackage } from '../../lib/export/json-package'; 4 | import { IconSet } from '../../lib/icon-set'; 5 | import { scanDirectory } from '../../lib/misc/scan'; 6 | 7 | // Check if file or directory exists 8 | async function exists(filename: string): Promise { 9 | try { 10 | const stat = await fs.lstat(filename); 11 | return stat.isFile() || stat.isDirectory(); 12 | } catch (err) { 13 | return false; 14 | } 15 | } 16 | 17 | describe('Exporting to JSON package', () => { 18 | test('Few icons', async () => { 19 | const lastModified = 12345; 20 | const targetDir = 'cache/export-json-package-test'; 21 | const iconSet = new IconSet({ 22 | prefix: 'foo', 23 | lastModified, 24 | icons: { 25 | maximize: { 26 | body: '', 27 | }, 28 | minimize: { 29 | body: '', 30 | }, 31 | }, 32 | aliases: { 33 | test: { 34 | parent: 'maximize', 35 | }, 36 | }, 37 | width: 24, 38 | height: 24, 39 | }); 40 | 41 | // Clean directory 42 | try { 43 | await fs.rm(targetDir, { 44 | recursive: true, 45 | force: true, 46 | }); 47 | } catch (err) { 48 | // 49 | } 50 | expect(await exists(targetDir)).toBe(false); 51 | 52 | // Export icon set 53 | await exportJSONPackage(iconSet, { 54 | target: targetDir, 55 | }); 56 | 57 | // Make sure directory exists and list files 58 | expect(await exists(targetDir)).toBe(true); 59 | const files = await scanDirectory(targetDir); 60 | files.sort((a, b) => a.localeCompare(b)); 61 | expect(files).toEqual([ 62 | 'chars.json', 63 | 'icons.json', 64 | 'index.d.ts', 65 | 'index.js', 66 | 'index.mjs', 67 | 'info.json', 68 | 'metadata.json', 69 | 'package.json', 70 | ]); 71 | 72 | // Check contents of icons.json 73 | // No metadata or characters to check 74 | const actualData = JSON.parse( 75 | await fs.readFile(`${targetDir}/icons.json`, 'utf8') 76 | ) as IconifyJSON; 77 | const expectedData = iconSet.export(); 78 | expect(actualData).toEqual(expectedData); 79 | 80 | // Check package.json to make sure it uses wildcard 81 | const packageContent = JSON.parse( 82 | await fs.readFile(`${targetDir}/package.json`, 'utf8') 83 | ) as Record; 84 | expect(packageContent['dependencies']).toEqual({ 85 | '@iconify/types': '*', 86 | }); 87 | 88 | // Clean up 89 | await fs.rm(targetDir, { 90 | recursive: true, 91 | force: true, 92 | }); 93 | expect(await exists(targetDir)).toBe(false); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/batch-asterisk.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/compare1/copy/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { IconifyInfo } from '@iconify/types'; 2 | 3 | export { IconifyInfo }; 4 | 5 | export declare const collections: Record; 6 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/compare1/copy/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {Record} collections 3 | */ 4 | const collections = require('./collections.json'); 5 | 6 | exports.collections = collections; 7 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/compare1/copy/index.mjs: -------------------------------------------------------------------------------- 1 | import importedCollections from './collections.json'; 2 | 3 | /** 4 | * @type {Record} collections 5 | */ 6 | const collections = importedCollections; 7 | 8 | export { collections }; 9 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/compare1/copy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@iconify/collections", 3 | "description": "Iconify icon sets list", 4 | "version": "1.0.18", 5 | "main": "index.js", 6 | "module": "index.mjs", 7 | "types": "index.d.ts", 8 | "exports": { 9 | "./*": "./*", 10 | ".": { 11 | "require": "./index.js", 12 | "import": "./index.mjs" 13 | }, 14 | "./collections.json": "./collections.json" 15 | }, 16 | "dependencies": { 17 | "@iconify/types": "^1.0.10" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/compare1/different-version/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { IconifyInfo } from '@iconify/types'; 2 | 3 | export { IconifyInfo }; 4 | 5 | export declare const collections: Record; 6 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/compare1/different-version/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {Record} collections 3 | */ 4 | const collections = require('./collections.json'); 5 | 6 | exports.collections = collections; 7 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/compare1/different-version/index.mjs: -------------------------------------------------------------------------------- 1 | import importedCollections from './collections.json'; 2 | 3 | /** 4 | * @type {Record} collections 5 | */ 6 | const collections = importedCollections; 7 | 8 | export { collections }; 9 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/compare1/different-version/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@iconify/collections", 3 | "description": "Iconify icon sets list", 4 | "version": "1.0.17", 5 | "main": "index.js", 6 | "module": "index.mjs", 7 | "types": "index.d.ts", 8 | "exports": { 9 | "./*": "./*", 10 | ".": { 11 | "require": "./index.js", 12 | "import": "./index.mjs" 13 | }, 14 | "./collections.json": "./collections.json" 15 | }, 16 | "dependencies": { 17 | "@iconify/types": "^1.0.10" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/compare1/extra-file/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { IconifyInfo } from '@iconify/types'; 2 | 3 | export { IconifyInfo }; 4 | 5 | export declare const collections: Record; 6 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/compare1/extra-file/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {Record} collections 3 | */ 4 | const collections = require('./collections.json'); 5 | 6 | exports.collections = collections; 7 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/compare1/extra-file/index.mjs: -------------------------------------------------------------------------------- 1 | import importedCollections from './collections.json'; 2 | 3 | /** 4 | * @type {Record} collections 5 | */ 6 | const collections = importedCollections; 7 | 8 | export { collections }; 9 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/compare1/extra-file/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@iconify/collections", 3 | "description": "Iconify icon sets list", 4 | "version": "1.0.18", 5 | "main": "index.js", 6 | "module": "index.mjs", 7 | "types": "index.d.ts", 8 | "exports": { 9 | "./*": "./*", 10 | ".": { 11 | "require": "./index.js", 12 | "import": "./index.mjs" 13 | }, 14 | "./collections.json": "./collections.json" 15 | }, 16 | "dependencies": { 17 | "@iconify/types": "^1.0.10" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/compare1/extra-file/whatever.ts: -------------------------------------------------------------------------------- 1 | import type { IconifyInfo } from '@iconify/types'; 2 | 3 | export { IconifyInfo }; 4 | 5 | export declare const collections: Record; 6 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/compare1/formatted/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { IconifyInfo } from '@iconify/types'; 2 | 3 | export { IconifyInfo }; 4 | 5 | export declare const collections: Record; 6 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/compare1/formatted/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {Record} collections 3 | */ 4 | const collections = require('./collections.json'); 5 | 6 | exports.collections = collections; 7 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/compare1/formatted/index.mjs: -------------------------------------------------------------------------------- 1 | import importedCollections from './collections.json'; 2 | 3 | /** 4 | * @type {Record} collections 5 | */ 6 | const collections = importedCollections; 7 | 8 | export { collections }; 9 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/compare1/formatted/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@iconify/collections", 3 | "description": "Iconify icon sets list", 4 | "version": "1.0.18", 5 | "main": "index.js", 6 | "module": "index.mjs", 7 | "types": "index.d.ts", 8 | "exports": { 9 | "./*": "./*", 10 | ".": { 11 | "require": "./index.js", 12 | "import": "./index.mjs" 13 | }, 14 | "./collections.json": "./collections.json" 15 | }, 16 | "dependencies": { 17 | "@iconify/types": "^1.0.10" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/compare1/missing-file/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {Record} collections 3 | */ 4 | const collections = require('./collections.json'); 5 | 6 | exports.collections = collections; 7 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/compare1/missing-file/index.mjs: -------------------------------------------------------------------------------- 1 | import importedCollections from './collections.json'; 2 | 3 | /** 4 | * @type {Record} collections 5 | */ 6 | const collections = importedCollections; 7 | 8 | export { collections }; 9 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/compare1/missing-file/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@iconify/collections", 3 | "description": "Iconify icon sets list", 4 | "version": "1.0.18", 5 | "main": "index.js", 6 | "module": "index.mjs", 7 | "types": "index.d.ts", 8 | "exports": { 9 | "./*": "./*", 10 | ".": { 11 | "require": "./index.js", 12 | "import": "./index.mjs" 13 | }, 14 | "./collections.json": "./collections.json" 15 | }, 16 | "dependencies": { 17 | "@iconify/types": "^1.0.10" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/compare1/original/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { IconifyInfo } from '@iconify/types'; 2 | 3 | export { IconifyInfo }; 4 | 5 | export declare const collections: Record; 6 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/compare1/original/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {Record} collections 3 | */ 4 | const collections = require('./collections.json'); 5 | 6 | exports.collections = collections; 7 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/compare1/original/index.mjs: -------------------------------------------------------------------------------- 1 | import importedCollections from './collections.json'; 2 | 3 | /** 4 | * @type {Record} collections 5 | */ 6 | const collections = importedCollections; 7 | 8 | export { collections }; 9 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/compare1/original/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@iconify/collections", 3 | "description": "Iconify icon sets list", 4 | "version": "1.0.18", 5 | "main": "index.js", 6 | "module": "index.mjs", 7 | "types": "index.d.ts", 8 | "exports": { 9 | "./*": "./*", 10 | ".": { 11 | "require": "./index.js", 12 | "import": "./index.mjs" 13 | }, 14 | "./collections.json": "./collections.json" 15 | }, 16 | "dependencies": { 17 | "@iconify/types": "^1.0.10" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/discord.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/animate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/animateMotion.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/animateTransform.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/bad/a.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <circle> 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/bad/feBlend.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/bad/feConvolveMatrix.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/bad/fePointLight.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 16 | 17 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/bad/feSpotLight.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 16 | 17 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/bad/feTile.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/bad/foreignObject.svg: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 20 |
21 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 22 | Sed mollis mollis mi ut ultricies. Nullam magna ipsum, 23 | porta vel dui convallis, rutrum imperdiet eros. Aliquam 24 | erat volutpat. 25 |
26 |
27 |
28 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/bad/script.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/bad/svg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/clipPath.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/clipPath2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 13 | 15 | 16 | 17 | 19 | 20 | 23 | 25 | 26 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/defs.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/desc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | I'm a circle and that description is here to 5 | demonstrate how I can be described, but is it 6 | really necessary to describe a simple circle 7 | like me? 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/feColorMatrix.svg: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 35 | 36 | 37 | 38 | 39 | 40 | 43 | 44 | 45 | 46 | 47 | 48 | 51 | 52 | 53 | 54 | 55 | 56 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/feDiffuseLighting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 24 | 25 | 26 | 27 | 29 | 30 | 31 | 33 | 34 | 35 | 36 | 38 | 40 | 41 | 42 | 44 | 45 | 46 | 48 | 49 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/feFlood.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/feGaussianBlur.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/feOffset.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/inline-style/feComponentTransfer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/inline-style/feDisplacementMap.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 8 | 9 | 10 | 12 | 13 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/inline-style/feFlood.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/inline-style/feMerge.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/inline-style/feSpecularLighting.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/inline-style/feTurbulence.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 8 | 9 | 10 | 12 | 13 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/linearGradient.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/marker.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 24 | 25 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/mask.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/mpath.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 7 | 8 | 10 | 12 | 13 | 14 | 15 | 16 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/pattern.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/stop.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/style/set.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/style/style.svg: -------------------------------------------------------------------------------- 1 | 2 | Circle 3 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/symbol.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/elements/use.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/entypo-hair-cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/fci-biomass.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/folder-with-files.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/font-face.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/openmoji-2117.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/spin.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/subdirs/outline/apps.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/subdirs/outline/caret-back.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/subdirs/regular/apps.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /@iconify/tools/tests/fixtures/subdirs/regular/camera.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /@iconify/tools/tests/misc/compare-dirs-test.ts: -------------------------------------------------------------------------------- 1 | import { compareDirectories } from '../../lib/misc/compare-dirs'; 2 | 3 | describe('Comparing directories', () => { 4 | const rootDir = 'tests/fixtures/compare1'; 5 | 6 | test('Identical directories', async () => { 7 | expect( 8 | await compareDirectories(`${rootDir}/original`, `${rootDir}/copy`) 9 | ).toBe(true); 10 | 11 | // Different versions 12 | expect( 13 | await compareDirectories( 14 | `${rootDir}/original`, 15 | `${rootDir}/different-version` 16 | ) 17 | ).toBe(true); 18 | }); 19 | 20 | test('Different directories', async () => { 21 | expect( 22 | await compareDirectories( 23 | `${rootDir}/original`, 24 | `${rootDir}/extra-file` 25 | ) 26 | ).toBe(false); 27 | expect( 28 | await compareDirectories( 29 | `${rootDir}/original`, 30 | `${rootDir}/missing-file` 31 | ) 32 | ).toBe(false); 33 | 34 | // Different spacing 35 | expect( 36 | await compareDirectories( 37 | `${rootDir}/original`, 38 | `${rootDir}/formatted`, 39 | { 40 | ignoreNewLine: false, 41 | } 42 | ) 43 | ).toBe(false); 44 | 45 | // Different versions 46 | expect( 47 | await compareDirectories( 48 | `${rootDir}/original`, 49 | `${rootDir}/different-version`, 50 | { 51 | ignoreVersions: false, 52 | } 53 | ) 54 | ).toBe(false); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /@iconify/tools/tests/misc/concurrency-test.ts: -------------------------------------------------------------------------------- 1 | import { runConcurrentQueries } from '../../lib/download/api/queue'; 2 | 3 | describe('Testing concurrency', () => { 4 | test('Simple queue', async () => { 5 | const tests: Record = { 6 | test1: 2, 7 | test2: 100, 8 | test3: 50, 9 | test4: 1, 10 | }; 11 | 12 | const keys = Object.keys(tests); 13 | const callbacks: Set = new Set(); 14 | const resolved: Set = new Set(); 15 | 16 | const result = await runConcurrentQueries({ 17 | total: keys.length, 18 | 19 | callback: (index) => { 20 | expect(callbacks.has(index)).toBeFalsy(); 21 | expect(resolved.has(index)).toBeFalsy(); 22 | 23 | if (index < 3) { 24 | // When first 3 items are called, nothing should be resolved 25 | expect(Array.from(resolved)).toEqual([]); 26 | } 27 | if (index === 3) { 28 | // When 4th item is called, first one should be resolved 29 | expect(Array.from(resolved)).toEqual([0]); 30 | } 31 | 32 | const key = keys[index]; 33 | const delay = tests[key]; 34 | return new Promise((resolve) => { 35 | setTimeout(() => { 36 | resolved.add(index); 37 | resolve(delay); 38 | }, delay); 39 | }); 40 | }, 41 | limit: 3, 42 | }); 43 | 44 | expect(result).toEqual(Object.values(tests)); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /@iconify/tools/tests/misc/keyword-test.ts: -------------------------------------------------------------------------------- 1 | import { cleanupIconKeyword } from '../../lib/misc/keyword'; 2 | 3 | describe('Icon keywords', () => { 4 | test('Converting strings', () => { 5 | expect(cleanupIconKeyword('foo')).toBe('foo'); 6 | expect(cleanupIconKeyword('1f1e7-1f1fe')).toBe('1f1e7-1f1fe'); 7 | expect(cleanupIconKeyword('some-icon.svg')).toBe('some-icon-svg'); 8 | expect(cleanupIconKeyword('u1F30A')).toBe('u1f30a'); 9 | expect(cleanupIconKeyword('_icon_#')).toBe('icon'); 10 | }); 11 | 12 | test('camelCase strings', () => { 13 | expect(cleanupIconKeyword('someIcon', true)).toBe('some-icon'); 14 | expect(cleanupIconKeyword('E1F30A', true)).toBe('e1-f30-a'); 15 | }); 16 | 17 | test('Empty strings', () => { 18 | expect(cleanupIconKeyword('`', true)).toBe(''); 19 | expect(cleanupIconKeyword('#', true)).toBe(''); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /@iconify/tools/tests/misc/version-test.ts: -------------------------------------------------------------------------------- 1 | import { bumpVersion } from '../../lib/misc/bump-version'; 2 | 3 | describe('Version number', () => { 4 | test('Bumping version', () => { 5 | // Simple numbers 6 | expect(bumpVersion('1.0.0')).toBe('1.0.1'); 7 | expect(bumpVersion('1.2.3.4')).toBe('1.2.3.5'); 8 | expect(bumpVersion('1.0.0-beta.1')).toBe('1.0.0-beta.2'); 9 | 10 | // No numbers at the end 11 | expect(bumpVersion('2.0.0-dev')).toBe('2.0.0-dev.1'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /@iconify/tools/tests/optimise/compressed-arc-test.ts: -------------------------------------------------------------------------------- 1 | import { SVG } from '../../lib/svg'; 2 | import { deOptimisePaths } from '../../lib/optimise/flags'; 3 | 4 | describe('De-optimise paths', () => { 5 | test('Should de-compress arcs', () => { 6 | const svg = new SVG( 7 | '' 8 | ); 9 | deOptimisePaths(svg); 10 | expect(svg.toMinifiedString()).toBe( 11 | '' 12 | ); 13 | }); 14 | 15 | test('Should de-compress lines', () => { 16 | const svg = new SVG( 17 | '' 18 | ); 19 | deOptimisePaths(svg); 20 | expect(svg.toMinifiedString()).toBe( 21 | '' 22 | ); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /@iconify/tools/tests/optimise/scale-test.ts: -------------------------------------------------------------------------------- 1 | import { SVG } from '../../lib/svg'; 2 | import { scaleSVG } from '../../lib/optimise/scale'; 3 | 4 | describe('Scaling icon', () => { 5 | test('Scale by 20', () => { 6 | const svg = new SVG( 7 | '' 8 | ); 9 | scaleSVG(svg, 1 / 20); 10 | expect(svg.toMinifiedString()).toBe( 11 | '' 12 | ); 13 | }); 14 | 15 | test('Scale and shift', () => { 16 | const svg = new SVG( 17 | '' 18 | ); 19 | expect(svg.viewBox).toEqual({ 20 | left: 0, 21 | top: 96, 22 | width: 960, 23 | height: 960, 24 | }); 25 | 26 | scaleSVG(svg, 1 / 40); 27 | expect(svg.toMinifiedString()).toBe( 28 | '' 29 | ); 30 | expect(svg.viewBox).toEqual({ 31 | left: 0, 32 | top: 0, 33 | width: 24, 34 | height: 24, 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /@iconify/tools/tests/svg/cleanup-root-test.ts: -------------------------------------------------------------------------------- 1 | import { SVG } from '../../lib/svg'; 2 | import { cleanupSVGRoot } from '../../lib/svg/cleanup/root-svg'; 3 | 4 | describe('Cleaning up SVG root element', () => { 5 | test('Moving fill to content', () => { 6 | const svg = new SVG( 7 | '' 8 | ); 9 | cleanupSVGRoot(svg); 10 | expect(svg.toMinifiedString()).toBe( 11 | '' 12 | ); 13 | }); 14 | 15 | test('With style', () => { 16 | const svg = 17 | new SVG(` 18 | 22 | 23 | 24 | 25 | 26 | 27 | `); 28 | cleanupSVGRoot(svg); 29 | expect(svg.toMinifiedString()).toBe( 30 | '' 31 | ); 32 | }); 33 | 34 | test('Style with keyframes', () => { 35 | const svg = 36 | new SVG(` 37 | 51 | 52 | 53 | `); 54 | cleanupSVGRoot(svg); 55 | expect(svg.toMinifiedString()).toBe( 56 | '' 57 | ); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /@iconify/tools/tests/svg/parse-test.ts: -------------------------------------------------------------------------------- 1 | import { SVG } from '../../lib/svg'; 2 | import { cleanupSVG } from '../../lib/svg/cleanup'; 3 | import { parseSVG } from '../../lib/svg/parse'; 4 | import { loadFixture } from '../../lib/tests/helpers'; 5 | 6 | describe('Parsing SVG', () => { 7 | test('Removing grid', async () => { 8 | const svg = new SVG(await loadFixture('openmoji-2117.svg')); 9 | 10 | // Clean up 11 | cleanupSVG(svg); 12 | 13 | // Parse 14 | parseSVG(svg, (item) => { 15 | if (item.tagName === 'g') { 16 | // Check for grid 17 | const attribs = item.element.attribs; 18 | if (attribs.id === 'grid') { 19 | // Remove element, do not parse child elements 20 | item.$element.remove(); 21 | item.testChildren = false; 22 | } 23 | } 24 | }); 25 | 26 | expect(svg.toMinifiedString()).toBe( 27 | '' 28 | ); 29 | }); 30 | 31 | test('Async callback', async () => { 32 | const source = await loadFixture('refresh.svg'); 33 | const svg = new SVG(source); 34 | 35 | let threw = false; 36 | try { 37 | parseSVG(svg, () => { 38 | return new Promise((resolve) => { 39 | setTimeout(() => { 40 | resolve(undefined); 41 | }, 0); 42 | }) as unknown as void; 43 | }); 44 | } catch { 45 | threw = true; 46 | } 47 | expect(threw).toBeTruthy(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /@iconify/tools/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig-base.json", 3 | "compilerOptions": { 4 | "types": ["node", "jest", "cheerio"], 5 | "rootDir": ".", 6 | "sourceMap": true, 7 | "mapRoot": "tests/" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /@iconify/tools/tsconfig-base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "strict": true, 6 | "skipLibCheck": true, 7 | "moduleResolution": "node", 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "resolveJsonModule": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /@iconify/tools/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig-base.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./lib", 6 | "lib": ["ESNext", "DOM"] 7 | }, 8 | "include": ["src/**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | ## Reporting a bug 4 | 5 | If you have found a bug, please [create an issue on GitHub](https://github.com/iconify/tools/issues). 6 | 7 | Please include the following: 8 | 9 | - A concise description of the bug. 10 | - Version of Iconify Tools, Node.js 11 | - Is possible, reduced test case. It can be a small piece of code or link to a repository. 12 | - Any other information that you think might help fix that bug. 13 | 14 | ## Development 15 | 16 | This repository uses `pnpm` to manage dependencies and workspaces. 17 | 18 | It is recommended that you use [@antfu/ni](https://github.com/antfu/ni), then you don't need to worry if project you are working on uses `npm`, `pnpm`, `yarn` or something else: all commands will be identical regardless of package manager. 19 | 20 | ### Branches 21 | 22 | There are two main branches: 23 | 24 | - `main` that contains latest stable code. 25 | - `next` that contains development code. 26 | 27 | If you want to create a pull request, base your new branch off `next` branch and create a pull request for `next` branch. 28 | 29 | If you are having issues with `next` branch, using `main` branch is also fine, though `next` is preferred. 30 | 31 | ### Installation 32 | 33 | Clone repository, run `ni` to install all dependencies. 34 | 35 | ### Directory structure 36 | 37 | This repository contains several packages: 38 | 39 | - `@iconify/tools` directory contains main package. 40 | - `@iconify-demo` directory contains various packages used for demo. 41 | 42 | #### Tools directory 43 | 44 | For most use cases, everything you need is in directory `@iconify/tools`: 45 | 46 | - Source code is in sub-directory `src`. 47 | - Unit tests are in directory `tests`. 48 | 49 | ### Building and testing 50 | 51 | To build Iconify Tools, run `nr build`. 52 | 53 | To test code, run `nr test`. Make sure you build package before testing. 54 | 55 | You can run these commands from either root directory or from `@iconify/tools` sub-directory. If you run them from root directory, it will be ran for all packages. If you run them from `@iconify/tools` sub-directory, it will be ran only for Iconify Tools. 56 | 57 | ### Making a pull request 58 | 59 | To create a pull request, please following these steps: 60 | 61 | 1. Fork this repository. 62 | 2. In your forked repository, create a new branch based on `next` branch, such as `git checkout -b dev/my-fix next`. 63 | 3. Install dependencies: `ni`. 64 | 4. Update code. 65 | 5. Build it: `nr build`. 66 | 6. Test it: `nr test`. 67 | 7. Commit changes: `git add -A`, `git commit -m "chore: short description"` (change commit message). 68 | 8. Push changes: `git push origin dev/my-fix` (change branch name). 69 | 9. On GitHub, send a pull request from your branch to `next` branch. 70 | 71 | If you have never contributed to a project before, do not worry about making mistakes. 72 | You can ask for help in an issue on GitHub or on Iconify Discord. 73 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-PRESENT Vjacheslav Trushkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iconify-tools", 3 | "description": "Collection of functions for cleaning up and parsing SVG for Iconify project", 4 | "author": "Vjacheslav Trushkin", 5 | "license": "MIT", 6 | "private": true, 7 | "bugs": "https://github.com/iconify/tools/issues", 8 | "homepage": "https://github.com/iconify/tools", 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/iconify/tools.git" 12 | }, 13 | "workspaces": [ 14 | "@iconify/*", 15 | "@debug/*", 16 | "@iconify-demo/*" 17 | ], 18 | "packageManager": "pnpm@9.15.0", 19 | "scripts": { 20 | "build": "pnpm recursive run build", 21 | "test": "pnpm recursive run test", 22 | "lint": "pnpm recursive run lint", 23 | "build:tools": "pnpm recursive --filter \"./@iconify/tools\" run build", 24 | "test:tools": "pnpm recursive --filter \"./@iconify/tools\" run test:ci", 25 | "lint:tools": "pnpm recursive --filter \"./@iconify/tools\" run lint" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - '@iconify/tools' 3 | - '@iconify-demo/*' 4 | --------------------------------------------------------------------------------