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