From f243624fbd175a93cf24fcdf970b72425afa246d Mon Sep 17 00:00:00 2001 From: Cole Bemis <cole@ifixit.com> Date: Sat, 18 Nov 2017 20:00:16 -0800 Subject: [PATCH] feat: Update API BREAKING CHANGE: Each icon in the `feather.icons` object is now an `Icon` object with a `name`, `contents`, `tags` and `attrs` property. ```js /* BEFORE */ feather.icons.x // '<line ... /><line ... />' /* AFTER */ feather.icons.x // { // name: 'x', // contents: '<line ... /><line ... />`, // tags: ['cancel', 'close', 'delete', 'remove'], // attrs: { // class: 'feather feather-x', // xmlns: 'http://www.w3.org/2000/svg', // width: 24, // height: 24, // viewBox: '0 0 24 24', // fill: 'none', // stroke: 'currentColor', // 'stroke-width': 2, // 'stroke-linecap': 'round', // 'stroke-linejoin': 'round', // } // } ``` `feather.toSvg()` has been deprecated in favor of `feather.icons[name].toSvg()`: ```js /* BEFORE */ feather.toSvg('x') /* AFTER */ feather.icons.x.toSvg() ``` `feather.replace()` now copies all attributes on the placeholder element (i.e. `<i>`) to the `<svg>` tag instead of just `class` and `id`: ```html <i data-feather="circle" id="my-circle" class="foo bar" stroke-width="1"></i> <!-- <i> will be replaced with: <svg id="my-circle" class="feather feather-circle foo bar" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle></svg> --> ``` --- .babelrc | 5 +- .eslintrc.json | 9 +- .travis.yml | 2 +- Makefile | 34 --- README.md | 151 +++++++---- .../build-icons-object.test.js.snap | 8 + bin/__tests__/build-icons-object.test.js | 17 ++ bin/build-icons-json.js | 19 ++ bin/build-icons-object.js | 34 +++ bin/build-json.js | 71 ------ bin/build-svgs.js | 16 +- bin/build.sh | 14 + bin/process-svg.js | 10 +- bin/process-svgs.js | 13 +- package-lock.json | 240 ++++++++++++++++++ package.json | 16 +- src/__tests__/__snapshots__/icon.test.js.snap | 54 ++++ .../__snapshots__/icons.test.js.snap | 45 ++++ .../__snapshots__/replace.test.js.snap | 13 + .../__snapshots__/to-svg.test.js.snap | 7 + src/__tests__/icon.test.js | 28 ++ src/__tests__/icons.test.js | 16 ++ src/__tests__/index.test.js | 8 + src/__tests__/replace.test.js | 32 +++ src/__tests__/to-svg.test.js | 21 ++ ...ult-attributes.json => default-attrs.json} | 0 src/icon.js | 55 ++++ src/icons.js | 10 + src/index.js | 6 +- src/replace.js | 72 +++--- src/tags.json | 7 + src/to-svg.js | 72 ++---- 32 files changed, 828 insertions(+), 277 deletions(-) delete mode 100644 Makefile create mode 100644 bin/__tests__/__snapshots__/build-icons-object.test.js.snap create mode 100644 bin/__tests__/build-icons-object.test.js create mode 100644 bin/build-icons-json.js create mode 100644 bin/build-icons-object.js delete mode 100755 bin/build-json.js create mode 100755 bin/build.sh create mode 100644 src/__tests__/__snapshots__/icon.test.js.snap create mode 100644 src/__tests__/__snapshots__/icons.test.js.snap create mode 100644 src/__tests__/__snapshots__/replace.test.js.snap create mode 100644 src/__tests__/__snapshots__/to-svg.test.js.snap create mode 100644 src/__tests__/icon.test.js create mode 100644 src/__tests__/icons.test.js create mode 100644 src/__tests__/index.test.js create mode 100644 src/__tests__/replace.test.js create mode 100644 src/__tests__/to-svg.test.js rename src/{default-attributes.json => default-attrs.json} (100%) create mode 100644 src/icon.js create mode 100644 src/icons.js create mode 100644 src/tags.json diff --git a/.babelrc b/.babelrc index 3c078e9..831f20a 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,4 @@ { - "presets": [ - "es2015" - ] + "presets": ["es2015"], + "plugins": ["transform-object-rest-spread"] } diff --git a/.eslintrc.json b/.eslintrc.json index f020c3c..4317b2d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,12 +1,11 @@ { "extends": "airbnb-base", - "plugins": [ - "import" - ], + "plugins": ["import"], "rules": { - "no-use-before-define": "off", "arrow-parens": ["error", "as-needed"], + "no-console": ["error", { "allow": ["warn", "error"] }], + "no-param-reassign": "off", "no-shadow": "off", - "no-console": ["error", { "allow": ["warn", "error"] }] + "no-use-before-define": "off" } } diff --git a/.travis.yml b/.travis.yml index 0c28266..4ba7f6c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,6 @@ node_js: 6 before_script: - npm prune script: - - npm run all + - npm start after_success: - npm run semantic-release diff --git a/Makefile b/Makefile deleted file mode 100644 index 7a28afc..0000000 --- a/Makefile +++ /dev/null @@ -1,34 +0,0 @@ -src_files := src/*.js -src_dir := src - -.PHONY: all lint test build - -all: lint test build - -lint: dist/icons.json - ./node_modules/.bin/eslint . - -test: - ./node_modules/.bin/jest - -build: dist/feather.js dist/feather.min.js dist/icons - -node_modules: - npm install - -dist: - mkdir dist - -dist/icons.json: node_modules dist icons icons/*.svg - ./node_modules/.bin/babel-node bin/build-json.js - -dist/feather.js: dist/icons.json $(src_dir) $(src_files) - ./node_modules/.bin/webpack --output-filename feather.js - -dist/feather.min.js: dist/icons.json $(src_dir) $(src_files) - ./node_modules/.bin/webpack --output-filename feather.min.js -p - -dist/icons: dist/icons.json - rm -rf dist/icons - mkdir -p dist/icons - ./node_modules/.bin/babel-node bin/build-svgs.js diff --git a/README.md b/README.md index e6afc75..bdc7920 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,12 @@ [![Travis branch](https://img.shields.io/travis/colebemis/feather/master.svg?style=flat-square)](https://travis-ci.org/colebemis/feather) [![npm](https://img.shields.io/npm/v/feather-icons.svg?style=flat-square)](https://www.npmjs.com/package/feather-icons) [![npm](https://img.shields.io/npm/dm/feather-icons.svg?style=flat-square)](https://npm-stat.com/charts.html?package=feather-icons&from=2017-06-01) -[![Code Climate](https://img.shields.io/codeclimate/github/colebemis/feather.svg?style=flat-square)](https://codeclimate.com/github/colebemis/feather) [![CDNJS version](https://img.shields.io/cdnjs/v/feather-icons.svg?style=flat-square)](https://cdnjs.com/libraries/feather-icons) +[![Code Climate](https://img.shields.io/codeclimate/github/colebemis/feather.svg?style=flat-square)](https://codeclimate.com/github/colebemis/feather) ## What is Feather? -Feather is a collection of **simply beautiful open source icons**. Each icon is designed on a 24x24 grid with an emphasis on simplicity, consistency and readability. +Feather is a collection of simply beautiful open source icons. Each icon is designed on a 24x24 grid with an emphasis on simplicity, consistency and readability. **[feathericons.com](https://feathericons.com)** @@ -20,12 +20,13 @@ npm install feather-icons * [Quick Start](#quick-start) * [Usage](#usage) - * [Client-side JavaScript](#client-side-javascript) + * [Client-side](#client-side) * [Node](#node) * [API Reference](#api-reference) * [`feather.icons`](#feathericons) - * [`feather.toSvg()`](#feathertosvgkey-options) - * [`feather.replace()`](#featherreplaceoptions) + * [`feather.icons[name].toSvg()`](#feathericonsnametosvgattrs) + * [`feather.replace()`](#featherreplaceattrs) + * [[DEPRECATED] `feather.toSvg()`](#deprecated-feathertosvgname-attrs) * [Roadmap](#roadmap) * [Contributing](#contributing) * [Related Projects](#related-projects) @@ -60,7 +61,7 @@ At its core, Feather is a collection of [SVG](https://svgontheweb.com/#svg) file The following are additional ways you can use Feather. -### Client-side JavaScript +### Client-side #### 1. Install @@ -79,7 +80,7 @@ Or just copy [`feather.js`](https://unpkg.com/feather-icons/dist/feather.js) or Include `feather.js` or `feather.min.js` with a `<script>` tag. These files are located in the `dist` directory of the npm package. ```html -<script src="path/to/dist/feather.min.js"></script> +<script src="path/to/dist/feather.js"></script> ``` Or load the script from a CDN provider. @@ -104,7 +105,7 @@ See the complete list of icons at [feathericons.com](https://feathericons.com). #### 4. Replace -Call the `feather.replace` method. +Call the `feather.replace()` method. ```html <script> @@ -126,44 +127,75 @@ npm install feather-icons --save #### 2. Require ```javascript -var feather = require('feather-icons') +const feather = require('feather-icons') ``` #### 3. Use -```javascript -feather.icons.circle -// <circle cx="12" cy="12" r="10"></circle> - -feather.toSvg('circle') -// '<svg class="feather feather-circle" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle></svg>' -feather.toSvg('circle', { class: 'my-class', 'stroke-width': 1 }) -// '<svg class="feather feather-circle my-class" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle></svg>' +```js +feather.icons.x +// { +// name: 'x', +// contents: '<line ... /><line ... />`, +// tags: ['cancel', 'close', 'delete', 'remove'], +// attrs: { +// class: 'feather feather-x', +// xmlns: 'http://www.w3.org/2000/svg', +// width: 24, +// height: 24, +// viewBox: '0 0 24 24', +// fill: 'none', +// stroke: 'currentColor', +// 'stroke-width': 2, +// 'stroke-linecap': 'round', +// 'stroke-linejoin': 'round', +// } +// } + +feather.icons.x.toSvg() +// <svg class="feather feather-x" ...><line ... /><line ... /></svg> + +feather.icons.x.toSvg({ class: 'foo bar', 'stroke-width': 1, color: 'red' }) +// <svg class="feather feather-x foo bar" stroke-width="1" color="red" ...><line ... /><line ... /></svg> ``` See the [API Reference](#api-reference) for more information about the available properties and methods of the `feather` object. -### Sprite - -*Coming soon* - ## API Reference ### `feather.icons` -An object with SVG path information for every icon. +An object with information about every icon. #### Usage -```javascript -feather.icons.circle -// <circle cx="12" cy="12" r="10"></circle> - -feather.icons.clock -// '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 15 15"/>' +```js +feather.icons.x +// { +// name: 'x', +// contents: '<line ... /><line ... />`, +// tags: ['cancel', 'close', 'delete', 'remove'], +// attrs: { +// class: 'feather feather-x', +// xmlns: 'http://www.w3.org/2000/svg', +// width: 24, +// height: 24, +// viewBox: '0 0 24 24', +// fill: 'none', +// stroke: 'currentColor', +// 'stroke-width': 2, +// 'stroke-linecap': 'round', +// 'stroke-linejoin': 'round', +// } +// } + +feather.icons.x.toString() +// '<line ... /><line ... />` ``` -### `feather.toSvg(key, [options])` +[View Source](https://github.com/colebemis/feather/blob/master/src/icons.js) + +### `feather.icons[name].toSvg([attrs])` Returns an SVG string. @@ -171,25 +203,24 @@ Returns an SVG string. | Name | Type | Description | | --------- | ------ | ----------- | -| `key` | string | Icon name | -| `options` (optional) | Object | Key-value pairs in the `options` object will be mapped to HTML attributes on the `<svg>` tag (e.g. `{ foo: 'bar' }` maps to `foo="bar"`). All default attributes on the `<svg>` tag can be overridden with the `options` object. | +| `attrs` (optional) | Object | Key-value pairs in the `attrs` object will be mapped to HTML attributes on the `<svg>` tag (e.g. `{ foo: 'bar' }` maps to `foo="bar"`). All default attributes on the `<svg>` tag can be overridden with the `attrs` object. | #### Usage ```javascript -feather.toSvg('circle') +feather.icons.circle.toSvg() // '<svg class="feather feather-circle" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle></svg>' -feather.toSvg('circle', { 'stroke-width': 1 }) +feather.icons.circle.toSvg({ 'stroke-width': 1 }) // '<svg class="feather feather-circle" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle></svg>' -feather.toSvg('circle', { class: 'foo bar' }) +feather.icons.circle.toSvg({ class: 'foo bar' }) // '<svg class="feather feather-circle foo bar" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle></svg>' ``` -[View Source](https://github.com/colebemis/feather/blob/master/src/to-svg.js) +[View Source](https://github.com/colebemis/feather/blob/master/src/icons.js) -### `feather.replace([options])` +### `feather.replace([attrs])` Replaces all elements that have a `data-feather` attribute with SVG markup corresponding to the element's `data-feather` attribute value. @@ -197,7 +228,7 @@ Replaces all elements that have a `data-feather` attribute with SVG markup corre | Name | Type | Description | | ---------- | ------ | ----------- | -| `options` (optional) | Object | Key-value pairs in the `options` object will be mapped to HTML attributes on the `<svg>` tag (e.g. `{ foo: 'bar' }` maps to `foo="bar"`). All default attributes on the `<svg>` tag can be overridden with the `options` object. | +| `attrs` (optional) | Object | Key-value pairs in the `attrs` object will be mapped to HTML attributes on the `<svg>` tag (e.g. `{ foo: 'bar' }` maps to `foo="bar"`). All default attributes on the `<svg>` tag can be overridden with the `attrs` object. | #### Usage @@ -216,7 +247,7 @@ Simple usage: </script> ``` -You can pass `feather.replace()` an `options` object: +You can pass `feather.replace()` an `attrs` object: ```html <i data-feather="circle"></i> <!-- @@ -229,13 +260,13 @@ You can pass `feather.replace()` an `options` object: </script> ``` -The id and classes on a placeholder element (i.e. `<i>`) will be copied to the `<svg>` tag: +All attributes on the placeholder element (i.e. `<i>`) will be copied to the `<svg>` tag: ```html -<i id="my-circle-icon" class="foo bar" data-feather="circle"></i> +<i data-feather="circle" id="my-circle" class="foo bar" stroke-width="1"></i> <!-- <i> will be replaced with: -<svg id="my-circle-icon" class="feather feather-circle foo bar" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle></svg> +<svg id="my-circle" class="feather feather-circle foo bar" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle></svg> --> <script> @@ -245,19 +276,43 @@ The id and classes on a placeholder element (i.e. `<i>`) will be copied to the ` [View Source](https://github.com/colebemis/feather/blob/master/src/replace.js) +### [DEPRECATED] `feather.toSvg(name, [attrs])` + +> **Note:** `feather.toSvg()` is deprecated. Please use `feather.icons[name].toSvg()` instead. + +Returns an SVG string. + +#### Parameters + +| Name | Type | Description | +| --------- | ------ | ----------- | +| `name` | string | Icon name | +| `attrs` (optional) | Object | Key-value pairs in the `attrs` object will be mapped to HTML attributes on the `<svg>` tag (e.g. `{ foo: 'bar' }` maps to `foo="bar"`). All default attributes on the `<svg>` tag can be overridden with the `attrs` object. | + +#### Usage + +```javascript +feather.toSvg('circle') +// '<svg class="feather feather-circle" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle></svg>' + +feather.toSvg('circle', { 'stroke-width': 1 }) +// '<svg class="feather feather-circle" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle></svg>' + +feather.toSvg('circle', { class: 'foo bar' }) +// '<svg class="feather feather-circle foo bar" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle></svg>' +``` + +[View Source](https://github.com/colebemis/feather/blob/master/src/to-svg.js) + ## Roadmap -- [x] Write contributing guidelines - [ ] Write icon design guidelines -- [ ] Add usage examples -- [ ] Add SVG sprite -- [ ] Add tests - [ ] Track code coverage - [ ] Use Prettier to enforce consistent code style -- [ ] Add search/filter functionality to project website -- [ ] Handle icon aliases -- [ ] Handle usage of custom icons - [ ] Improve SVG accessibility +- [ ] Handle usage of custom icons +- [ ] Add usage examples +- [ ] Make `<feather-icon>` web component ## Contributing diff --git a/bin/__tests__/__snapshots__/build-icons-object.test.js.snap b/bin/__tests__/__snapshots__/build-icons-object.test.js.snap new file mode 100644 index 0000000..5881706 --- /dev/null +++ b/bin/__tests__/__snapshots__/build-icons-object.test.js.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`builds object correctly 1`] = ` +Object { + "icon1": "<line x1=\\"23\\" y1=\\"1\\" x2=\\"1\\" y2=\\"23\\"></line><line x1=\\"1\\" y1=\\"1\\" x2=\\"23\\" y2=\\"23\\"></line>", + "icon2": "<circle cx=\\"12\\" cy=\\"12\\" r=\\"11\\"></circle>", +} +`; diff --git a/bin/__tests__/build-icons-object.test.js b/bin/__tests__/build-icons-object.test.js new file mode 100644 index 0000000..0a422f3 --- /dev/null +++ b/bin/__tests__/build-icons-object.test.js @@ -0,0 +1,17 @@ +/* eslint-env jest */ +import buildIconsObject from '../build-icons-object'; + +const SVG_FILES = { + 'icon1.svg': + '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="23" y1="1" x2="1" y2="23" /><line x1="1" y1="1" x2="23" y2="23" /></svg>', + 'icon2.svg': + '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="11" /></svg>', +}; + +function getSvg(svgFile) { + return SVG_FILES[svgFile]; +} + +test('builds object correctly', () => { + expect(buildIconsObject(Object.keys(SVG_FILES), getSvg)).toMatchSnapshot(); +}); diff --git a/bin/build-icons-json.js b/bin/build-icons-json.js new file mode 100644 index 0000000..8a549b8 --- /dev/null +++ b/bin/build-icons-json.js @@ -0,0 +1,19 @@ +import fs from 'fs'; +import path from 'path'; + +import buildIconsObject from './build-icons-object'; + +const IN_DIR = path.resolve(__dirname, '../icons'); +const OUT_FILE = path.resolve(__dirname, '../dist/icons.json'); + +console.log(`Building ${OUT_FILE}`); // eslint-disable-line no-console + +const svgFiles = fs + .readdirSync(IN_DIR) + .filter(file => path.extname(file) === '.svg'); + +const getSvg = svgFile => fs.readFileSync(path.join(IN_DIR, svgFile)); + +const icons = buildIconsObject(svgFiles, getSvg); + +fs.writeFileSync(OUT_FILE, JSON.stringify(icons)); diff --git a/bin/build-icons-object.js b/bin/build-icons-object.js new file mode 100644 index 0000000..920b0a3 --- /dev/null +++ b/bin/build-icons-object.js @@ -0,0 +1,34 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import path from 'path'; +import cheerio from 'cheerio'; + +/** + * Build an object in the format: `{ <name>: <contents> }`. + * @param {string[]} svgFiles - A list of file names. + * @param {Function} getSvg - A function that returns the contents of an SVG file. + * @returns {Object} + */ +function buildIconsObject(svgFiles, getSvg) { + return svgFiles + .map(svgFile => { + const name = path.basename(svgFile, '.svg'); + const svg = getSvg(svgFile); + const contents = getSvgContents(svg); + return { name, contents }; + }) + .reduce((icons, icon) => { + icons[icon.name] = icon.contents; + return icons; + }, {}); +} + +/** + * Get contents between opening and closing `<svg>` tags. + * @param {string} svg + */ +function getSvgContents(svg) { + const $ = cheerio.load(svg); + return $('svg').html(); +} + +export default buildIconsObject; diff --git a/bin/build-json.js b/bin/build-json.js deleted file mode 100755 index 0280c81..0000000 --- a/bin/build-json.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * @file Builds `icons.json` from `icons` directory. - */ - -/* eslint-disable import/no-extraneous-dependencies */ -import fs from 'fs'; -import path from 'path'; -import RSVP from 'rsvp'; -import Svgo from 'svgo'; -import parse5 from 'parse5'; - -const svgFiles = fs.readdirSync(path.resolve(__dirname, '../icons')) - .filter(file => path.extname(file) === '.svg'); - -buildIconsObject(svgFiles) - .then(icons => { - fs.writeFileSync( - path.resolve(__dirname, '../dist/icons.json'), - JSON.stringify(icons), - ); - }); - -/** - * Build an icons object in the format: `{ <icon name>: <svg content> }`. - * @param {string[]} svgFiles - A list of file names. - * @returns {RSVP.Promise<Object>} - */ -function buildIconsObject(svgFiles) { - const icons = {}; - - svgFiles.forEach(svgFile => { - const svg = fs.readFileSync(path.resolve(__dirname, '../icons', svgFile), 'utf8'); - const key = path.basename(svgFile, '.svg'); - - icons[key] = optimizeSvg(svg) - .then(optimizedSvg => getSvgContent(optimizedSvg)); - }); - - return RSVP.hash(icons); -} - -/** - * Optimize SVG with `svgo`. - * @param {string} svg - An SVG string. - * @returns {RSVP.Promise<string>} - */ -function optimizeSvg(svg) { - // configure svgo - const svgo = new Svgo({ - plugins: [ - { convertShapeToPath: false }, - { mergePaths: false }, - { removeAttrs: { attrs: '(fill|stroke.*)' } }, - ], - }); - - return new RSVP.Promise(resolve => { - svgo.optimize(svg, ({ data }) => resolve(data)); - }); -} - -/** - * Get content between opening and closing `<svg>` tags. - * @param {string} svg - An SVG string. - * @returns {string} - */ -function getSvgContent(svg) { - const fragment = parse5.parseFragment(svg); - const content = parse5.serialize(fragment.childNodes[0]); - return content; -} diff --git a/bin/build-svgs.js b/bin/build-svgs.js index c69eb85..1ae31d0 100644 --- a/bin/build-svgs.js +++ b/bin/build-svgs.js @@ -1,13 +1,13 @@ -/** - * @file Builds `dist/icons` directory. - */ - import fs from 'fs'; import path from 'path'; -import { icons, toSvg } from '../src'; +import icons from '../src/icons'; + +const OUT_DIR = path.resolve(__dirname, '../dist/icons'); + +console.log(`Building SVGs in ${OUT_DIR}`); // eslint-disable-line no-console -Object.keys(icons).forEach(icon => { - const svg = toSvg(icon); +Object.keys(icons).forEach(name => { + const svg = icons[name].toSvg(); - fs.writeFileSync(path.resolve(__dirname, `../dist/icons/${icon}.svg`), svg); + fs.writeFileSync(path.join(OUT_DIR, `${name}.svg`), svg); }); diff --git a/bin/build.sh b/bin/build.sh new file mode 100755 index 0000000..a151ba0 --- /dev/null +++ b/bin/build.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +./node_modules/.bin/babel-node bin/process-svgs.js + +./node_modules/.bin/rimraf dist +mkdir dist +./node_modules/.bin/babel-node bin/build-icons-json.js + +./node_modules/.bin/rimraf dist/icons +mkdir dist/icons +./node_modules/.bin/babel-node bin/build-svgs.js + +./node_modules/.bin/webpack --output-filename feather.js +./node_modules/.bin/webpack --output-filename feather.min.js -p diff --git a/bin/process-svg.js b/bin/process-svg.js index 501c897..a85e92e 100644 --- a/bin/process-svg.js +++ b/bin/process-svg.js @@ -3,7 +3,7 @@ import Svgo from 'svgo'; import cheerio from 'cheerio'; import { format } from 'prettier'; -import DEFAULT_ATTRIBUTES from '../src/default-attributes.json'; +import DEFAULT_ATTRS from '../src/default-attrs.json'; /** * Process SVG string. @@ -13,7 +13,7 @@ import DEFAULT_ATTRIBUTES from '../src/default-attributes.json'; function processSvg(svg) { return ( optimize(svg) - .then(setAttributes) + .then(setAttrs) .then(format) // remove semicolon inserted by prettier // because prettier thinks it's formatting JSX not HTML @@ -46,11 +46,11 @@ function optimize(svg) { * @param {string} svg - An SVG string. * @returns {string} */ -function setAttributes(svg) { +function setAttrs(svg) { const $ = cheerio.load(svg); - Object.keys(DEFAULT_ATTRIBUTES).forEach(key => - $('svg').attr(key, DEFAULT_ATTRIBUTES[key]), + Object.keys(DEFAULT_ATTRS).forEach(key => + $('svg').attr(key, DEFAULT_ATTRS[key]), ); return $('body').html(); diff --git a/bin/process-svgs.js b/bin/process-svgs.js index 3bd4981..721d2a0 100644 --- a/bin/process-svgs.js +++ b/bin/process-svgs.js @@ -1,15 +1,18 @@ -/* eslint-disable import/no-extraneous-dependencies */ import fs from 'fs'; import path from 'path'; import processSvg from './process-svg'; -const ICONS_DIR = path.resolve(__dirname, '../icons'); +const IN_DIR = path.resolve(__dirname, '../icons'); + +console.log(`Processing SVGs in ${IN_DIR}`); // eslint-disable-line no-console fs - .readdirSync(ICONS_DIR) + .readdirSync(IN_DIR) .filter(file => path.extname(file) === '.svg') .forEach(svgFile => { - const svg = fs.readFileSync(path.join(ICONS_DIR, svgFile)); - processSvg(svg).then(svg => fs.writeFileSync(path.join(ICONS_DIR, svgFile), svg)); + const svg = fs.readFileSync(path.join(IN_DIR, svgFile)); + processSvg(svg).then(svg => + fs.writeFileSync(path.join(IN_DIR, svgFile), svg), + ); }); diff --git a/package-lock.json b/package-lock.json index f84de5a..2b0dfb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -304,12 +304,30 @@ "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", "dev": true }, + "array-filter": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", + "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=", + "dev": true + }, "array-find-index": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", "dev": true }, + "array-map": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", + "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=", + "dev": true + }, + "array-reduce": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", + "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=", + "dev": true + }, "array-union": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", @@ -889,6 +907,16 @@ "regexpu-core": "2.0.0" } }, + "babel-plugin-transform-object-rest-spread": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", + "integrity": "sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=", + "dev": true, + "requires": { + "babel-plugin-syntax-object-rest-spread": "6.13.0", + "babel-runtime": "6.26.0" + } + }, "babel-plugin-transform-regenerator": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", @@ -1417,6 +1445,11 @@ "chalk": "1.1.3" } }, + "classnames": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.5.tgz", + "integrity": "sha1-+zgB1FNGdknvNgPH1hoCvRKb3m0=" + }, "cli-cursor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", @@ -1962,6 +1995,16 @@ } } }, + "define-properties": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", + "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", + "dev": true, + "requires": { + "foreach": "2.0.5", + "object-keys": "1.0.11" + } + }, "del": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", @@ -2184,6 +2227,30 @@ "is-arrayish": "0.2.1" } }, + "es-abstract": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.9.0.tgz", + "integrity": "sha512-kk3IJoKo7A3pWJc0OV8yZ/VEX2oSUytfekrJiqoxBlKJMFAJVJVpGdHClCCTdv+Fn2zHfpDHHIelMFhZVfef3Q==", + "dev": true, + "requires": { + "es-to-primitive": "1.1.1", + "function-bind": "1.1.1", + "has": "1.0.1", + "is-callable": "1.1.3", + "is-regex": "1.0.4" + } + }, + "es-to-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", + "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", + "dev": true, + "requires": { + "is-callable": "1.1.3", + "is-date-object": "1.0.1", + "is-symbol": "1.0.1" + } + }, "es5-ext": { "version": "0.10.35", "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.35.tgz", @@ -2998,6 +3065,12 @@ "for-in": "1.0.2" } }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", + "dev": true + }, "foreachasync": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/foreachasync/-/foreachasync-3.0.0.tgz", @@ -4619,6 +4692,12 @@ "builtin-modules": "1.1.1" } }, + "is-callable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", + "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=", + "dev": true + }, "is-ci": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.0.10.tgz", @@ -4628,6 +4707,12 @@ "ci-info": "1.1.1" } }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, "is-dotfile": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", @@ -4751,6 +4836,15 @@ "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", "dev": true }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, + "requires": { + "has": "1.0.1" + } + }, "is-resolvable": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz", @@ -4766,6 +4860,12 @@ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", "dev": true }, + "is-symbol": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", + "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", + "dev": true + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -5827,6 +5927,12 @@ "integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==", "dev": true }, + "json-parse-better-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.1.tgz", + "integrity": "sha512-xyQpxeWWMKyJps9CuGJYeng6ssI5bpqS9ltQpdVQ90t4ql6NdnxFKh95JcRt2cun/DjMVNrdjniLPuMA69xmCw==", + "dev": true + }, "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", @@ -6207,6 +6313,12 @@ "readable-stream": "2.3.3" } }, + "memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=", + "dev": true + }, "meow": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", @@ -6565,6 +6677,96 @@ "slide": "1.1.6" } }, + "npm-run-all": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.2.tgz", + "integrity": "sha512-Z2aRlajMK4SQ8u19ZA75NZZu7wupfCNQWdYosIi8S6FgBdGf/8Y6Hgyjdc8zU2cYmIRVCx1nM80tJPkdEd+UYg==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "chalk": "2.3.0", + "cross-spawn": "5.1.0", + "memorystream": "0.3.1", + "minimatch": "3.0.4", + "ps-tree": "1.1.0", + "read-pkg": "3.0.0", + "shell-quote": "1.6.1", + "string.prototype.padend": "3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "4.0.0", + "pify": "3.0.0", + "strip-bom": "3.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "1.3.1", + "json-parse-better-errors": "1.0.1" + } + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "3.0.0" + } + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "4.0.0", + "normalize-package-data": "2.4.0", + "path-type": "3.0.0" + } + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -6662,6 +6864,12 @@ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true }, + "object-keys": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", + "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=", + "dev": true + }, "object.omit": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", @@ -7080,6 +7288,15 @@ "integrity": "sha1-GoS4WQgyVQFBGFPQCB7j+obikmo=", "dev": true }, + "ps-tree": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.1.0.tgz", + "integrity": "sha1-tCGyQUDWID8e08dplrRCewjowBQ=", + "dev": true, + "requires": { + "event-stream": "3.3.4" + } + }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -7671,6 +7888,18 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, + "shell-quote": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", + "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", + "dev": true, + "requires": { + "array-filter": "0.0.1", + "array-map": "0.0.0", + "array-reduce": "0.0.0", + "jsonify": "0.0.0" + } + }, "shelljs": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.6.tgz", @@ -7897,6 +8126,17 @@ "strip-ansi": "3.0.1" } }, + "string.prototype.padend": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz", + "integrity": "sha1-86rvfBcZ8XDF6rHDK/eA2W4h8vA=", + "dev": true, + "requires": { + "define-properties": "1.1.2", + "es-abstract": "1.9.0", + "function-bind": "1.1.1" + } + }, "string_decoder": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", diff --git a/package.json b/package.json index e26ab7c..9220ad0 100644 --- a/package.json +++ b/package.json @@ -7,30 +7,36 @@ "dist" ], "scripts": { - "all": "make", - "lint": "make lint", - "test": "make test", - "build": "make build", + "start": "npm-run-all --sequential build lint test", + "build": "./bin/build.sh", + "lint": "eslint .", + "test": "jest", "commitmsg": "validate-commit-msg", "cm": "git-cz", "semantic-release": "semantic-release pre && npm publish && semantic-release post" }, + "dependencies": { + "classnames": "^2.2.5" + }, "devDependencies": { "babel-cli": "^6.24.1", "babel-loader": "^7.1.1", + "babel-plugin-transform-object-rest-spread": "^6.26.0", "babel-preset-es2015": "^6.24.1", "babel-register": "^6.24.1", "cheerio": "^1.0.0-rc.2", "commitizen": "^2.9.6", "core-js": "^2.4.1", - "cz-conventional-changelog": "^2.0.0", + "cz-conventional-changelog": "^2.1.0", "eslint": "^4.0.0", "eslint-config-airbnb-base": "^11.2.0", "eslint-plugin-import": "^2.5.0", "husky": "^0.13.4", "jest": "^21.2.1", + "npm-run-all": "^4.1.2", "parse5": "^3.0.2", "prettier": "^1.8.2", + "rimraf": "^2.6.2", "rsvp": "^3.6.0", "semantic-release": "^6.3.6", "svgo": "^0.7.2", diff --git a/src/__tests__/__snapshots__/icon.test.js.snap b/src/__tests__/__snapshots__/icon.test.js.snap new file mode 100644 index 0000000..2e7f2df --- /dev/null +++ b/src/__tests__/__snapshots__/icon.test.js.snap @@ -0,0 +1,54 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`constructs icon object correctly 1`] = ` +Icon { + "attrs": Object { + "class": "feather feather-test", + "fill": "none", + "height": 24, + "stroke": "currentColor", + "stroke-linecap": "round", + "stroke-linejoin": "round", + "stroke-width": 2, + "viewBox": "0 0 24 24", + "width": 24, + "xmlns": "http://www.w3.org/2000/svg", + }, + "contents": "<line x1=\\"23\\" y1=\\"1\\" x2=\\"1\\" y2=\\"23\\" /><line x1=\\"1\\" y1=\\"1\\" x2=\\"23\\" y2=\\"23\\" />", + "name": "test", + "tags": Array [ + "hello", + "world", + "foo", + "bar", + ], +} +`; + +exports[`constructs icon object correctly 2`] = ` +Icon { + "attrs": Object { + "class": "feather feather-test", + "fill": "none", + "height": 24, + "stroke": "currentColor", + "stroke-linecap": "round", + "stroke-linejoin": "round", + "stroke-width": 2, + "viewBox": "0 0 24 24", + "width": 24, + "xmlns": "http://www.w3.org/2000/svg", + }, + "contents": "<line x1=\\"23\\" y1=\\"1\\" x2=\\"1\\" y2=\\"23\\" /><line x1=\\"1\\" y1=\\"1\\" x2=\\"23\\" y2=\\"23\\" />", + "name": "test", + "tags": Array [], +} +`; + +exports[`toString() returns correct string 1`] = `"<line x1=\\"23\\" y1=\\"1\\" x2=\\"1\\" y2=\\"23\\" /><line x1=\\"1\\" y1=\\"1\\" x2=\\"23\\" y2=\\"23\\" />"`; + +exports[`toSvg() returns correct string 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" width=\\"24\\" height=\\"24\\" viewBox=\\"0 0 24 24\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-width=\\"2\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" class=\\"feather feather-test\\"><line x1=\\"23\\" y1=\\"1\\" x2=\\"1\\" y2=\\"23\\" /><line x1=\\"1\\" y1=\\"1\\" x2=\\"23\\" y2=\\"23\\" /></svg>"`; + +exports[`toSvg() returns correct string 2`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" width=\\"24\\" height=\\"24\\" viewBox=\\"0 0 24 24\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-width=\\"1\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" class=\\"feather feather-test\\" color=\\"red\\"><line x1=\\"23\\" y1=\\"1\\" x2=\\"1\\" y2=\\"23\\" /><line x1=\\"1\\" y1=\\"1\\" x2=\\"23\\" y2=\\"23\\" /></svg>"`; + +exports[`toSvg() returns correct string 3`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" width=\\"24\\" height=\\"24\\" viewBox=\\"0 0 24 24\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-width=\\"2\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" class=\\"feather feather-test foo bar\\" color=\\"red\\"><line x1=\\"23\\" y1=\\"1\\" x2=\\"1\\" y2=\\"23\\" /><line x1=\\"1\\" y1=\\"1\\" x2=\\"23\\" y2=\\"23\\" /></svg>"`; diff --git a/src/__tests__/__snapshots__/icons.test.js.snap b/src/__tests__/__snapshots__/icons.test.js.snap new file mode 100644 index 0000000..2e47a3c --- /dev/null +++ b/src/__tests__/__snapshots__/icons.test.js.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`exports correct object 1`] = ` +Object { + "icon1": Icon { + "attrs": Object { + "class": "feather feather-icon1", + "fill": "none", + "height": 24, + "stroke": "currentColor", + "stroke-linecap": "round", + "stroke-linejoin": "round", + "stroke-width": 2, + "viewBox": "0 0 24 24", + "width": 24, + "xmlns": "http://www.w3.org/2000/svg", + }, + "contents": "<line x1=\\"23\\" y1=\\"1\\" x2=\\"1\\" y2=\\"23\\" /><line x1=\\"1\\" y1=\\"1\\" x2=\\"23\\" y2=\\"23\\" />", + "name": "icon1", + "tags": Array [ + "foo", + "bar", + "hello", + "world", + ], + }, + "icon2": Icon { + "attrs": Object { + "class": "feather feather-icon2", + "fill": "none", + "height": 24, + "stroke": "currentColor", + "stroke-linecap": "round", + "stroke-linejoin": "round", + "stroke-width": 2, + "viewBox": "0 0 24 24", + "width": 24, + "xmlns": "http://www.w3.org/2000/svg", + }, + "contents": "<circle cx=\\"12\\" cy=\\"12\\" r=\\"11\\" />", + "name": "icon2", + "tags": Array [], + }, +} +`; diff --git a/src/__tests__/__snapshots__/replace.test.js.snap b/src/__tests__/__snapshots__/replace.test.js.snap new file mode 100644 index 0000000..74903ed --- /dev/null +++ b/src/__tests__/__snapshots__/replace.test.js.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`copies placeholder element attributes to <svg> tag 1`] = `"<i data-feather=\\"icon1\\" id=\\"test\\" class=\\"foo bar\\" stroke-width=\\"1\\"></i>"`; + +exports[`copies placeholder element attributes to <svg> tag 2`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" width=\\"24\\" height=\\"24\\" viewBox=\\"0 0 24 24\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-width=\\"1\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" class=\\"feather feather-icon1 foo bar\\" id=\\"test\\"><line x1=\\"23\\" y1=\\"1\\" x2=\\"1\\" y2=\\"23\\"></line><line x1=\\"1\\" y1=\\"1\\" x2=\\"23\\" y2=\\"23\\"></line></svg>"`; + +exports[`replaces [data-feather] elements with SVG markup 1`] = `"<i data-feather=\\"icon1\\"></i><span data-feather=\\"icon2\\"></span>"`; + +exports[`replaces [data-feather] elements with SVG markup 2`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" width=\\"24\\" height=\\"24\\" viewBox=\\"0 0 24 24\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-width=\\"2\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" class=\\"feather feather-icon1\\"><line x1=\\"23\\" y1=\\"1\\" x2=\\"1\\" y2=\\"23\\"></line><line x1=\\"1\\" y1=\\"1\\" x2=\\"23\\" y2=\\"23\\"></line></svg><svg xmlns=\\"http://www.w3.org/2000/svg\\" width=\\"24\\" height=\\"24\\" viewBox=\\"0 0 24 24\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-width=\\"2\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" class=\\"feather feather-icon2\\"><circle cx=\\"12\\" cy=\\"12\\" r=\\"11\\"></circle></svg>"`; + +exports[`sets attributes passed as parameters 1`] = `"<i data-feather=\\"icon1\\" id=\\"test\\" class=\\"foo bar\\" stroke-width=\\"1\\"></i>"`; + +exports[`sets attributes passed as parameters 2`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" width=\\"24\\" height=\\"24\\" viewBox=\\"0 0 24 24\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-width=\\"1\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" class=\\"feather feather-icon1 foo bar hello\\" color=\\"salmon\\" id=\\"test\\"><line x1=\\"23\\" y1=\\"1\\" x2=\\"1\\" y2=\\"23\\"></line><line x1=\\"1\\" y1=\\"1\\" x2=\\"23\\" y2=\\"23\\"></line></svg>"`; diff --git a/src/__tests__/__snapshots__/to-svg.test.js.snap b/src/__tests__/__snapshots__/to-svg.test.js.snap new file mode 100644 index 0000000..2394a42 --- /dev/null +++ b/src/__tests__/__snapshots__/to-svg.test.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`returns correct string 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" width=\\"24\\" height=\\"24\\" viewBox=\\"0 0 24 24\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-width=\\"2\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" class=\\"feather feather-icon1\\"><line x1=\\"23\\" y1=\\"1\\" x2=\\"1\\" y2=\\"23\\" /><line x1=\\"1\\" y1=\\"1\\" x2=\\"23\\" y2=\\"23\\" /></svg>"`; + +exports[`returns correct string 2`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" width=\\"24\\" height=\\"24\\" viewBox=\\"0 0 24 24\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-width=\\"1\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" class=\\"feather feather-icon1\\" color=\\"red\\"><line x1=\\"23\\" y1=\\"1\\" x2=\\"1\\" y2=\\"23\\" /><line x1=\\"1\\" y1=\\"1\\" x2=\\"23\\" y2=\\"23\\" /></svg>"`; + +exports[`returns correct string 3`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" width=\\"24\\" height=\\"24\\" viewBox=\\"0 0 24 24\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-width=\\"2\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" class=\\"feather feather-icon1 foo bar\\" color=\\"red\\"><line x1=\\"23\\" y1=\\"1\\" x2=\\"1\\" y2=\\"23\\" /><line x1=\\"1\\" y1=\\"1\\" x2=\\"23\\" y2=\\"23\\" /></svg>"`; diff --git a/src/__tests__/icon.test.js b/src/__tests__/icon.test.js new file mode 100644 index 0000000..173ed7b --- /dev/null +++ b/src/__tests__/icon.test.js @@ -0,0 +1,28 @@ +/* eslint-env jest */ +import Icon from '../icon'; + +const icon1 = new Icon( + 'test', + '<line x1="23" y1="1" x2="1" y2="23" /><line x1="1" y1="1" x2="23" y2="23" />', + ['hello', 'world', 'foo', 'bar'], +); + +const icon2 = new Icon( + 'test', + '<line x1="23" y1="1" x2="1" y2="23" /><line x1="1" y1="1" x2="23" y2="23" />', +); + +test('constructs icon object correctly', () => { + expect(icon1).toMatchSnapshot(); + expect(icon2).toMatchSnapshot(); +}); + +test('toSvg() returns correct string', () => { + expect(icon1.toSvg()).toMatchSnapshot(); + expect(icon1.toSvg({ 'stroke-width': 1, color: 'red' })).toMatchSnapshot(); + expect(icon1.toSvg({ class: 'foo bar', color: 'red' })).toMatchSnapshot(); +}); + +test('toString() returns correct string', () => { + expect(icon1.toString()).toMatchSnapshot(); +}); diff --git a/src/__tests__/icons.test.js b/src/__tests__/icons.test.js new file mode 100644 index 0000000..528055d --- /dev/null +++ b/src/__tests__/icons.test.js @@ -0,0 +1,16 @@ +/* eslint-env jest */ +import icons from '../icons'; + +jest.mock('../../dist/icons.json', () => ({ + icon1: + '<line x1="23" y1="1" x2="1" y2="23" /><line x1="1" y1="1" x2="23" y2="23" />', + icon2: '<circle cx="12" cy="12" r="11" />', +})); + +jest.mock('../tags.json', () => ({ + icon1: ['foo', 'bar', 'hello', 'world'], +})); + +test('exports correct object', () => { + expect(icons).toMatchSnapshot(); +}); diff --git a/src/__tests__/index.test.js b/src/__tests__/index.test.js new file mode 100644 index 0000000..dd49716 --- /dev/null +++ b/src/__tests__/index.test.js @@ -0,0 +1,8 @@ +/* eslint-env jest */ +import feather from '../..'; + +test('has correct properties', () => { + expect(feather).toHaveProperty('icons'); + expect(feather).toHaveProperty('toSvg'); + expect(feather).toHaveProperty('replace'); +}); diff --git a/src/__tests__/replace.test.js b/src/__tests__/replace.test.js new file mode 100644 index 0000000..47008ed --- /dev/null +++ b/src/__tests__/replace.test.js @@ -0,0 +1,32 @@ +/* eslint-env jest, browser */ +import replace from '../replace'; + +jest.mock('../../dist/icons.json', () => ({ + icon1: + '<line x1="23" y1="1" x2="1" y2="23" /><line x1="1" y1="1" x2="23" y2="23" />', + icon2: '<circle cx="12" cy="12" r="11" />', +})); + +test('replaces [data-feather] elements with SVG markup', () => { + document.body.innerHTML = + '<i data-feather="icon1"></i><span data-feather="icon2"></i>'; + expect(document.body.innerHTML).toMatchSnapshot(); + replace(); + expect(document.body.innerHTML).toMatchSnapshot(); +}); + +test('copies placeholder element attributes to <svg> tag', () => { + document.body.innerHTML = + '<i data-feather="icon1" id="test" class="foo bar" stroke-width="1"></i>'; + expect(document.body.innerHTML).toMatchSnapshot(); + replace(); + expect(document.body.innerHTML).toMatchSnapshot(); +}); + +test('sets attributes passed as parameters', () => { + document.body.innerHTML = + '<i data-feather="icon1" id="test" class="foo bar" stroke-width="1"></i>'; + expect(document.body.innerHTML).toMatchSnapshot(); + replace({ class: 'foo bar hello', 'stroke-width': 1.5, color: 'salmon' }); + expect(document.body.innerHTML).toMatchSnapshot(); +}); diff --git a/src/__tests__/to-svg.test.js b/src/__tests__/to-svg.test.js new file mode 100644 index 0000000..5feec75 --- /dev/null +++ b/src/__tests__/to-svg.test.js @@ -0,0 +1,21 @@ +/* eslint-env jest */ +import toSvg from '../to-svg'; + +jest.mock('../../dist/icons.json', () => ({ + icon1: + '<line x1="23" y1="1" x2="1" y2="23" /><line x1="1" y1="1" x2="23" y2="23" />', +})); + +test('returns correct string', () => { + expect(toSvg('icon1')).toMatchSnapshot(); + expect(toSvg('icon1', { 'stroke-width': 1, color: 'red' })).toMatchSnapshot(); + expect(toSvg('icon1', { class: 'foo bar', color: 'red' })).toMatchSnapshot(); +}); + +test('throws error when `name` parameter is undefined', () => { + expect(() => toSvg()).toThrow(); +}); + +test('throws error when passed unknown icon name', () => { + expect(() => toSvg('foo')).toThrow(); +}); diff --git a/src/default-attributes.json b/src/default-attrs.json similarity index 100% rename from src/default-attributes.json rename to src/default-attrs.json diff --git a/src/icon.js b/src/icon.js new file mode 100644 index 0000000..161b644 --- /dev/null +++ b/src/icon.js @@ -0,0 +1,55 @@ +import classnames from 'classnames/dedupe'; + +import DEFAULT_ATTRS from './default-attrs.json'; + +class Icon { + constructor(name, contents, tags = []) { + this.name = name; + this.contents = contents; + this.tags = tags; + this.attrs = { + ...DEFAULT_ATTRS, + ...{ class: `feather feather-${name}` }, + }; + } + + /** + * Create an SVG string. + * @param {Object} attrs + * @returns {string} + */ + toSvg(attrs = {}) { + const combinedAttrs = { + ...this.attrs, + ...attrs, + ...{ class: classnames(this.attrs.class, attrs.class) }, + }; + + return `<svg ${attrsToString(combinedAttrs)}>${this.contents}</svg>`; + } + + /** + * Return string representation of an `Icon`. + * + * Added for backward compatibility. If old code expects `feather.icons.<name>` + * to be a string, `toString()` will get implicitly called. + * + * @returns {string} + */ + toString() { + return this.contents; + } +} + +/** + * Convert attributes object to string of HTML attributes. + * @param {Object} attrs + * @returns {string} + */ +function attrsToString(attrs) { + return Object.keys(attrs) + .map(key => `${key}="${attrs[key]}"`) + .join(' '); +} + +export default Icon; diff --git a/src/icons.js b/src/icons.js new file mode 100644 index 0000000..afe5144 --- /dev/null +++ b/src/icons.js @@ -0,0 +1,10 @@ +import Icon from './icon'; +import icons from '../dist/icons.json'; +import tags from './tags.json'; + +export default Object.keys(icons) + .map(key => new Icon(key, icons[key], tags[key])) + .reduce((object, icon) => { + object[icon.name] = icon; + return object; + }, {}); diff --git a/src/index.js b/src/index.js index 354f5a0..3136297 100644 --- a/src/index.js +++ b/src/index.js @@ -1,8 +1,4 @@ -/** - * @file Exposes `feather` object. - */ - -import icons from '../dist/icons.json'; +import icons from './icons'; import toSvg from './to-svg'; import replace from './replace'; diff --git a/src/replace.js b/src/replace.js index d07b860..61475da 100644 --- a/src/replace.js +++ b/src/replace.js @@ -1,54 +1,60 @@ -/** - * @file Implements `replace` function. - */ +/* eslint-env browser */ +import classnames from 'classnames/dedupe'; -/* global document, DOMParser */ - -import icons from '../dist/icons.json'; -import toSvg from './to-svg'; +import icons from './icons'; /** - * Replace all elements that have a `data-feather` attribute with SVG markup + * Replace all HTML elements that have a `data-feather` attribute with SVG markup * corresponding to the element's `data-feather` attribute value. - * @param {Object} options + * @param {Object} attrs */ -export default function replace(options = {}) { +function replace(attrs = {}) { if (typeof document === 'undefined') { throw new Error('`feather.replace()` only works in a browser environment.'); } const elementsToReplace = document.querySelectorAll('[data-feather]'); - Array.from(elementsToReplace).forEach(element => replaceElement(element, options)); + Array.from(elementsToReplace).forEach(element => + replaceElement(element, attrs), + ); } /** - * Replace single element with SVG markup + * Replace a single HTML element with SVG markup * corresponding to the element's `data-feather` attribute value. - * @param {Element} element - * @param {Object} options + * @param {HTMLElement} element + * @param {Object} attrs */ -function replaceElement(element, options) { - const key = element.getAttribute('data-feather'); - - if (!key) { - throw new Error('The required `data-feather` attribute has no value.'); - } - - if (!icons[key]) { - throw new Error(`No icon matching '${key}'. See the complete list of icons at https://feathericons.com`); - } - - const elementClassAttr = element.getAttribute('class') || ''; - const elementIdAttr = element.getAttribute('id'); - const classNames = ( - options.class ? `${options.class} ${elementClassAttr}` : elementClassAttr +function replaceElement(element, attrs = {}) { + const elementAttrs = getAttrs(element); + const name = elementAttrs['data-feather']; + delete elementAttrs['data-feather']; + + const svgString = icons[name].toSvg({ + ...attrs, + ...elementAttrs, + ...{ class: classnames(attrs.class, elementAttrs.class) }, + }); + const svgDocument = new DOMParser().parseFromString( + svgString, + 'image/svg+xml', ); - - const svgOptions = Object.assign({}, options, { class: classNames, id: elementIdAttr }); - const svgString = toSvg(key, svgOptions); - const svgDocument = new DOMParser().parseFromString(svgString, 'image/svg+xml'); const svgElement = svgDocument.querySelector('svg'); element.parentNode.replaceChild(svgElement, element); } + +/** + * Get the attributes of an HTML element. + * @param {HTMLElement} element + * @returns {Object} + */ +function getAttrs(element) { + return Array.from(element.attributes).reduce((attrs, attr) => { + attrs[attr.name] = attr.value; + return attrs; + }, {}); +} + +export default replace; diff --git a/src/tags.json b/src/tags.json new file mode 100644 index 0000000..37e98ec --- /dev/null +++ b/src/tags.json @@ -0,0 +1,7 @@ +{ + "airplay": ["stream"], + "bell": ["alarm", "notification"], + "settings": ["cog", "edit", "gear", "preferences"], + "star": ["bookmark"], + "x": ["cancel", "close", "delete", "remove"] +} diff --git a/src/to-svg.js b/src/to-svg.js index 682791c..c73237c 100644 --- a/src/to-svg.js +++ b/src/to-svg.js @@ -1,66 +1,30 @@ -/** - * @file Implements `toSvg` function. - */ - -import icons from '../dist/icons.json'; -import DEFAULT_ATTRIBUTES from './default-attributes.json'; +import icons from './icons'; /** * Create an SVG string. - * @param {string} key - Icon name. - * @param {Object} options + * @deprecated + * @param {string} name + * @param {Object} attrs * @returns {string} */ -export default function toSvg(key, options = {}) { - if (!key) { +function toSvg(name, attrs = {}) { + console.warn( + 'feather.toSvg() is deprecated. Please use feather.icons[name].toSvg() instead.', + ); + + if (!name) { throw new Error('The required `key` (icon name) parameter is missing.'); } - if (!icons[key]) { - throw new Error(`No icon matching '${key}'. See the complete list of icons at https://feathericons.com`); + if (!icons[name]) { + throw new Error( + `No icon matching '${ + name + }'. See the complete list of icons at https://feathericons.com`, + ); } - const combinedOptions = Object.assign({}, DEFAULT_ATTRIBUTES, options); - - combinedOptions.class = addDefaultClassNames(combinedOptions.class, key); - - const attributes = optionsToAttributes(combinedOptions); - - return `<svg ${attributes}>${icons[key]}</svg>`; + return icons[name].toSvg(attrs); } -/** - * Add default class names. - * @param {string} classNames - One or more class names seperated by spaces. - * @param {string} key - Icon name. - * @returns {string} - */ -function addDefaultClassNames(classNames, key) { - // convert class names string into an array - const classNamesArray = classNames ? classNames.trim().split(/\s+/) : []; - - // use Set to avoid duplicate class names - const classNamesSet = new Set(classNamesArray); - - // add default class names - classNamesSet.add('feather').add(`feather-${key}`); - - return Array.from(classNamesSet).join(' '); -} - -/** - * Convert options object to string of html attributes. - * @param {Object} options - * @returns {string} - */ -function optionsToAttributes(options) { - const attributes = []; - - Object.keys(options).forEach(key => { - if (options[key]) { - attributes.push(`${key}="${options[key]}"`); - } - }); - - return attributes.join(' '); -} +export default toSvg; -- 2.21.0