[Pkg-javascript-commits] [underscore.string] 01/14: Imported Upstream version 3.3.4

Praveen Arimbrathodiyil praveen at moszumanska.debian.org
Thu Jul 28 06:23:27 UTC 2016


This is an automated email from the git hooks/post-receive script.

praveen pushed a commit to branch master
in repository underscore.string.

commit 63cfe4e3a16b979ab8380463bbbd7f2535d7b53c
Author: Praveen Arimbrathodiyil <praveen at debian.org>
Date:   Mon Jul 11 13:10:03 2016 +0530

    Imported Upstream version 3.3.4
---
 .editorconfig                 |    9 +
 .eslintignore                 |    8 +
 .eslintrc                     |   26 +
 .gitignore                    |    4 +-
 .npmignore                    |    4 +
 .travis.yml                   |   12 +-
 CHANGELOG.markdown            |  200 ++++++
 CONTRIBUTING.markdown         |   32 +
 Gemfile                       |    4 -
 Gemfile.lock                  |   17 -
 README.markdown               |  878 ++++++++++++++------------
 Rakefile                      |   23 -
 bench/chop.js                 |    5 +
 bench/count.js                |    5 +
 bench/endsWith.js             |    5 +
 bench/escapeHTML.js           |    5 +
 bench/insert.js               |    5 +
 bench/isBlank.js              |    5 +
 bench/join.js                 |    5 +
 bench/levenshtein.js          |    8 +
 bench/pad.js                  |   26 +
 bench/prune.js                |    5 +
 bench/reverse.js              |    5 +
 bench/slugify.js              |    5 +
 bench/splice.js               |    5 +
 bench/startsWith.js           |    5 +
 bench/strLeft.js              |    5 +
 bench/strLeftBack.js          |    5 +
 bench/strRight.js             |    5 +
 bench/strRightBack.js         |    5 +
 bench/succ.js                 |   12 +
 bench/titleize.js             |    5 +
 bench/toNumber.js             |    5 +
 bench/trim.js                 |   18 +
 bench/truncate.js             |    5 +
 bench/unescapeHTML.js         |    5 +
 package.json => bower.json    |   13 +-
 camelize.js                   |   14 +
 capitalize.js                 |    8 +
 chars.js                      |    5 +
 chop.js                       |    6 +
 classify.js                   |    8 +
 clean.js                      |    5 +
 cleanDiacritics.js            |   22 +
 component.json                |   13 +-
 count.js                      |   10 +
 dasherize.js                  |    5 +
 decapitalize.js               |    6 +
 dedent.js                     |   28 +
 dist/underscore.string.js     | 1369 +++++++++++++++++++++++++++++++++++++++++
 dist/underscore.string.min.js |   19 +
 endsWith.js                   |   13 +
 escapeHTML.js                 |   17 +
 exports.js                    |   10 +
 helper/adjacent.js            |    9 +
 helper/defaultToWhiteSpace.js |   10 +
 helper/escapeChars.js         |   19 +
 helper/escapeRegExp.js        |    5 +
 helper/htmlEntities.js        |   19 +
 helper/makeString.js          |    7 +
 helper/strRepeat.js           |    9 +
 helper/toPositive.js          |    3 +
 humanize.js                   |    7 +
 include.js                    |    6 +
 index.js                      |  143 +++++
 insert.js                     |    5 +
 isBlank.js                    |    5 +
 join.js                       |    9 +
 levenshtein.js                |   52 ++
 lib/underscore.string.js      |  673 --------------------
 lines.js                      |    4 +
 lpad.js                       |    5 +
 lrpad.js                      |    5 +
 ltrim.js                      |   10 +
 map.js                        |    9 +
 meteor-post.js                |    2 +
 meteor-pre.js                 |    6 +
 naturalCmp.js                 |   29 +
 numberFormat.js               |   12 +
 package.js                    |   16 +
 package.json                  |   51 +-
 pad.js                        |   26 +
 pred.js                       |    5 +
 prune.js                      |   27 +
 quote.js                      |    5 +
 repeat.js                     |   16 +
 replaceAll.js                 |    8 +
 reverse.js                    |    5 +
 rpad.js                       |    5 +
 rtrim.js                      |   10 +
 scripts/bump-version.js       |   19 +
 scripts/push-tags.js          |    4 +
 slugify.js                    |    7 +
 splice.js                     |    7 +
 sprintf.js                    |    4 +
 startsWith.js                 |    9 +
 strLeft.js                    |    8 +
 strLeftBack.js                |    8 +
 strRight.js                   |    8 +
 strRightBack.js               |    8 +
 stripTags.js                  |    5 +
 succ.js                       |    5 +
 surround.js                   |    3 +
 swapCase.js                   |    7 +
 tests/camelize.js             |   35 ++
 tests/capitalize.js           |   29 +
 tests/chars.js                |   12 +
 tests/chop.js                 |   12 +
 tests/classify.js             |   17 +
 tests/clean.js                |   11 +
 tests/cleanDiacritics.js      |   24 +
 tests/count.js                |   22 +
 tests/dasherize.js            |   22 +
 tests/decapitalize.js         |   13 +
 tests/dedent.js               |   35 ++
 tests/endsWith.js             |   42 ++
 tests/escapeHTML.js           |   15 +
 tests/escapeRegExp.js         |    9 +
 tests/exports.js              |    9 +
 tests/humanize.js             |   18 +
 tests/include.js              |   15 +
 tests/insert.js               |   15 +
 tests/isBlank.js              |   17 +
 tests/join.js                 |   18 +
 tests/levenshtein.js          |   22 +
 tests/lines.js                |   20 +
 tests/lpad.js                 |   14 +
 tests/lrpad.js                |   16 +
 tests/ltrim.js                |   21 +
 tests/map.js                  |   31 +
 tests/naturalCmp.js           |   40 ++
 tests/naturalSort.js          |    8 +
 tests/numberFormat.js         |   25 +
 tests/pad.js                  |   19 +
 tests/pred.js                 |   20 +
 tests/prune.js                |   25 +
 tests/quote.js                |   18 +
 tests/repeat.js               |   16 +
 tests/replaceAll.js           |   20 +
 tests/reverse.js              |   16 +
 tests/rpad.js                 |   15 +
 tests/rtrim.js                |   22 +
 tests/slugify.js              |   16 +
 tests/splice.js               |   10 +
 tests/sprintf.js              |   15 +
 tests/standalone.js           |   97 +++
 tests/startsWith.js           |   39 ++
 tests/strLeft.js              |   16 +
 tests/strLeftBack.js          |   16 +
 tests/strRight.js             |   17 +
 tests/strRightBack.js         |   16 +
 tests/stripTags.js            |   14 +
 tests/succ.js                 |   20 +
 tests/surround.js             |   15 +
 tests/swapCase.js             |   12 +
 tests/titleize.js             |   16 +
 tests/toBoolean.js            |   29 +
 tests/toNumber.js             |   45 ++
 tests/toSentence.js           |   12 +
 tests/toSentenceSerial.js     |   10 +
 tests/trim.js                 |   29 +
 tests/truncate.js             |   14 +
 tests/underscored.js          |   15 +
 tests/unescapeHTML.js         |   31 +
 tests/unquote.js              |   11 +
 tests/vsprintf.js             |   13 +
 tests/words.js                |   17 +
 tests/wrap.js                 |   35 ++
 titleize.js                   |    7 +
 toBoolean.js                  |   20 +
 toNumber.js                   |    5 +
 toSentence.js                 |   12 +
 toSentenceSerial.js           |    5 +
 trim.js                       |   10 +
 truncate.js                   |    8 +
 underscored.js                |    5 +
 unescapeHTML.js               |   20 +
 unquote.js                    |    6 +
 vsprintf.js                   |    4 +
 words.js                      |    7 +
 wrap.js                       |  102 +++
 181 files changed, 4690 insertions(+), 1150 deletions(-)

diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..84b480f
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,9 @@
+# EditorConfig is awesome: http://EditorConfig.org
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 2
+charset = utf-8
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..519bcdb
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,8 @@
+.eslintrc.js
+gulpfile.js
+meteor-*.js
+package.js
+dist/**
+scripts/**
+coverage/**
+node_modules/**
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..5bc7d1a
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,26 @@
+{
+    "rules": {
+        "indent": [
+            2,
+            2
+        ],
+        "quotes": [
+            2,
+            "single"
+        ],
+        "linebreak-style": [
+            2,
+            "unix"
+        ],
+        "semi": [
+            2,
+            "always"
+        ]
+    },
+    "env": {
+        "mocha": true,
+        "node": true,
+        "browser": true
+    },
+    "extends": "eslint:recommended"
+}
diff --git a/.gitignore b/.gitignore
index b4b44e2..0e53191 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,6 @@
 .gitignore
 .DS_Store
 .project
-.tmp_*
\ No newline at end of file
+.tmp_*
+node_modules
+coverage
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000..d6d0555
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,4 @@
+tests
+bench
+coverage
+scripts
diff --git a/.travis.yml b/.travis.yml
index ab27b29..42f90e8 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,4 @@
-language: ruby
-rvm:
-  - 1.9.3
-
-before_script:
-  - "export DISPLAY=:99.0"
-  - "sh -e /etc/init.d/xvfb start"
-  - sleep 2
\ No newline at end of file
+language: node_js
+node_js:
+  - "0.12"
+  - "stable"
diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown
new file mode 100644
index 0000000..24e7fd3
--- /dev/null
+++ b/CHANGELOG.markdown
@@ -0,0 +1,200 @@
+
+# Changelog
+
+### 3.3.4
+* set standalone in browserify `s`
+
+### 3.3.1 / 3.3.2 / 3.3.3
+* fix release script
+
+### 3.3.0
+
+* `sprintf` and `vsprintf` is now marked as deprecated [#479](https://github.com/epeli/underscore.string/pull/479)
+* `wrap` is added to `exports` [#489](https://github.com/epeli/underscore.string/pull/489)
+* new build chain without gulp
+* [Full changelog](https://github.com/epeli/underscore.string/compare/3.2.3...3.3.0)
+
+### 3.2.3
+
+* Add romanian characters to `cleanDiacritics` [#470](https://github.com/epeli/underscore.string/pull/470)
+* Fix global leaks
+* [Full changelog](https://github.com/epeli/underscore.string/compare/3.2.2...3.2.3)
+
+### 3.2.2
+
+* Fix `slugify`regression [#448](https://github.com/epeli/underscore.string/pull/448)
+* [Full changelog](https://github.com/epeli/underscore.string/compare/3.2.1...3.2.2)
+
+### 3.2.1
+
+* Export `cleanDiacritics` in index.js
+* [Full changelog](https://github.com/epeli/underscore.string/compare/3.2.0...3.2.1)
+
+### 3.2.0
+
+* Add `cleanDiacritics` [#444](https://github.com/epeli/underscore.string/pull/444)
+* Add `wrap` [#410](https://github.com/epeli/underscore.string/pull/410)
+* `lines`: add support to CR ending lines [#440](https://github.com/epeli/underscore.string/pull/440)
+* Documentation improvements
+* Small performance improvements
+* [Full changelog](https://github.com/epeli/underscore.string/compare/3.1.1...3.2.0)
+
+
+### 3.1.1
+
+* Add coverage folder to npmignore
+* [Full changelog](https://github.com/epeli/underscore.string/compare/3.1.0...3.1.1)
+
+### 3.1.0
+
+* Meteor integration [baeb0da](https://github.com/epeli/underscore.string/commit/baeb0da0053549e5346184630a7e0c5007b8be4f)
+* Add flag to capitalize to lowercase remaining characters [#408](https://github.com/epeli/underscore.string/pull/408)
+* Move to mocha [#409](https://github.com/epeli/underscore.string/pull/409)
+* Add support for more htmlEntites in escapeHTML and unescapeHTML [#417](https://github.com/epeli/underscore.string/pull/417)
+* Performance improvement in levenshtein [#427](https://github.com/epeli/underscore.string/pull/427)
+* [Full changelog](https://github.com/epeli/underscore.string/compare/3.0.3...3.1.0)
+
+### 3.0.3
+
+* Provide `dist` in npm package [#402](https://github.com/epeli/underscore.string/pull/402)
+* [Full changelog](https://github.com/epeli/underscore.string/compare/3.0.2...3.0.3)
+
+### 3.0.2
+
+* Fix .gitignore for bower [#400](https://github.com/epeli/underscore.string/issues/400)
+* Some docs cleanup
+* [Full changelog](https://github.com/epeli/underscore.string/compare/3.0.1...3.0.2)
+
+### 3.0.1
+
+* Minor fixes in the documentation [#390](https://github.com/epeli/underscore.string/pull/390) and [5135cb9](https://github.com/epeli/underscore.string/commit/5135cb9026034e9ea206c2ed8588db1eeb3ce95a)
+* Fix bower warnings [#393](https://github.com/epeli/underscore.string/pull/393)
+* `humanize` now uses `trim` [#392](https://github.com/epeli/underscore.string/pull/392)
+* [Full changelog](https://github.com/epeli/underscore.string/compare/3.0.0...3.0.1)
+
+### 3.0.0
+
+* Each function is now extracted to individual CommonJS modules
+  * Browserify users can now load only the functions they actually use
+* Usage as Underscore.js or Lo-Dash mixin is now discouraged as there is too many colliding methods
+* The prebuild library now exports a `s` global instead of `_s` and trying to
+  stick itself to existing underscore instances
+* New gh-pages with documentation
+* Implement chaining without Underscore.js
+* String.prototype methods can be chained with underscore.string functions [#383](https://github.com/epeli/underscore.string/pull/383)
+* Don't compare lowercase versions of strings in naturalCmp [#326](https://github.com/epeli/underscore.string/issues/326)
+* Always return +-1 or 0 in naturalCmp [#324](https://github.com/epeli/underscore.string/pull/324)
+* Align [starts|ends]With with the ES6 spec [#345](https://github.com/epeli/underscore.string/pull/345)
+* New functions `decapitalize`, `pred`, `dedent` and `replaceAll`
+* `slugify` now actually replaces all special chars with a dash
+* `slugify` supports Easter E languages [#340](https://github.com/epeli/underscore.string/pull/340)
+* `join` is now a conflicting function [#320](https://github.com/epeli/underscore.string/pull/320)
+* New decapitalize flag for `camelize` [#370](https://github.com/epeli/underscore.string/pull/370)
+* `toNumber` allows negative decimal precision [#332](https://github.com/epeli/underscore.string/pull/332)
+* [Full changelog](https://github.com/epeli/underscore.string/compare/2.4.0...3.0.0)
+
+## 2.4.0
+
+* Move from rake to gulp
+* Add support form classify camelcase strings
+* Fix bower.json
+* [Full changelog](https://github.com/epeli/underscore.string/compare/v2.3.3...2.4.0)
+
+## 2.3.3
+
+* Add `toBoolean`
+* Add `unquote`
+* Add quote char option to `quote`
+* Support dash-separated words in `titleize`
+* [Full changelog](https://github.com/epeli/underscore.string/compare/v2.3.2...2.3.3)
+
+## 2.3.2
+
+* Add `naturalCmp`
+* Bug fix to `camelize`
+* Add ă, ș, ț and ś to `slugify`
+* Doc updates
+* Add support for [component](http://component.io/)
+* [Full changelog](https://github.com/epeli/underscore.string/compare/v2.3.1...v2.3.2)
+
+## 2.3.1
+
+* Bug fixes to `escapeHTML`, `classify`, `substr`
+* Faster `count`
+* Documentation fixes
+* [Full changelog](https://github.com/epeli/underscore.string/compare/v2.3.0...v2.3.1)
+
+## 2.3.0
+
+* Added `numberformat` method
+* Added `levenshtein` method (Levenshtein distance calculation)
+* Added `swapCase` method
+* Changed default behavior of `words` method
+* Added `toSentenceSerial` method
+* Added `surround` and `quote` methods
+
+## 2.2.1
+
+* Same as 2.2.0 (2.2.0rc on npm) to fix some npm drama
+
+## 2.2.0
+
+* Capitalize method behavior changed
+* Various performance tweaks
+
+## 2.1.1
+
+* Fixed words method bug
+* Added classify method
+
+## 2.1.0
+
+* AMD support
+* Added toSentence method
+* Added slugify method
+* Lots of speed optimizations
+
+## 2.0.0
+
+* Added prune, humanize functions
+* Added _.string (_.str) namespace for Underscore.string library
+* Removed includes function
+
+For upgrading to this version you need to mix in Underscore.string library to Underscore object:
+
+```javascript
+_.mixin(_.string.exports());
+```
+
+and all non-conflict Underscore.string functions will be available through Underscore object.
+Also function `includes` has been removed, you should replace this function by `_.str.include`
+or create alias `_.includes = _.str.include` and all your code will work fine.
+
+## 1.1.6
+
+* Fixed reverse and truncate
+* Added isBlank, stripTags, inlude(alias for includes)
+* Added uglifier compression
+
+## 1.1.5
+
+* Added strRight, strRightBack, strLeft, strLeftBack
+
+## 1.1.4
+
+* Added pad, lpad, rpad, lrpad methods and aliases center, ljust, rjust
+* Integration with Underscore 1.1.6
+
+## 1.1.3
+
+* Added methods: underscored, camelize, dasherize
+* Support newer version of npm
+
+## 1.1.2
+
+* Created functions: lines, chars, words functions
+
+## 1.0.2
+
+* Created integration test suite with underscore.js 1.1.4 (now it's absolutely compatible)
+* Removed 'reverse' function, because this function override underscore.js 'reverse'
diff --git a/CONTRIBUTING.markdown b/CONTRIBUTING.markdown
new file mode 100644
index 0000000..b97eae0
--- /dev/null
+++ b/CONTRIBUTING.markdown
@@ -0,0 +1,32 @@
+
+# Contributing
+
+- Always add tests
+- Update documentation if needed
+- Do not commit build artifacts in the `dist` directory
+
+## Bug fixes
+
+Always add a test for the bug in a separate commit so we can easily cherry pick
+it for verification.
+
+## New features
+
+It's recommended to open an issue before sending a pull request to avoid
+unnecessary work. There are quite few areas we consider to be out of scope for
+this library. Idea is to add few generic string helpers for Javascript. For
+example anything related to internationalization or is too language specific
+is out of scope.
+
+## Release checklist
+
+(for maintainers)
+
+  - Write a changelog entry to `CHANGELOG.markdown`
+    - Use Github compare to see what has changed from previous tag. Ex https://github.com/epeli/underscore.string/compare/3.0.0...master 
+  - Update the version in the `package.json`
+  - Publish a new version of _.string `npm run release`
+  - Update the [gh-pages][ghp] branch `npm run bump`
+
+[d]: https://github.com/epeli/underscore.string/releases
+[ghp]: https://github.com/epeli/underscore.string/tree/gh-pages
diff --git a/Gemfile b/Gemfile
deleted file mode 100644
index aed29c3..0000000
--- a/Gemfile
+++ /dev/null
@@ -1,4 +0,0 @@
-source "https://rubygems.org"
-
-gem 'uglifier'
-gem 'rake'
diff --git a/Gemfile.lock b/Gemfile.lock
deleted file mode 100644
index 2c52be4..0000000
--- a/Gemfile.lock
+++ /dev/null
@@ -1,17 +0,0 @@
-GEM
-  remote: https://rubygems.org/
-  specs:
-    execjs (1.4.0)
-      multi_json (~> 1.0)
-    multi_json (1.3.6)
-    rake (0.9.2.2)
-    uglifier (1.3.0)
-      execjs (>= 0.3.0)
-      multi_json (~> 1.0, >= 1.0.2)
-
-PLATFORMS
-  ruby
-
-DEPENDENCIES
-  rake
-  uglifier
diff --git a/README.markdown b/README.markdown
index 1a39ad9..4882548 100644
--- a/README.markdown
+++ b/README.markdown
@@ -1,793 +1,864 @@
-# Underscore.string [![Build Status](https://secure.travis-ci.org/epeli/underscore.string.png?branch=master)](http://travis-ci.org/epeli/underscore.string) #
+<span class="github-only">
+
+The stable release documentation can be found here https://epeli.github.io/underscore.string/
 
+</span>
 
+# Underscore.string [![Build Status](https://secure.travis-ci.org/epeli/underscore.string.png?branch=master)](http://travis-ci.org/epeli/underscore.string) #
 
 Javascript lacks complete string manipulation operations.
-This an attempt to fill that gap. List of build-in methods can be found
+This is an attempt to fill that gap. List of build-in methods can be found
 for example from [Dive Into JavaScript][d].
+Originally started as an Underscore.js extension but is a full standalone
+library nowadays.
 
-[d]: http://www.diveintojavascript.com/core-javascript-reference/the-string-object
+Upgrading from 2.x to 3.x? Please read the [changelog][c].
+
+[c]: https://github.com/epeli/underscore.string/blob/master/CHANGELOG.markdown#300
+
+## Usage
+
+### For Node.js, Browserify and Webpack
 
+Install from npm
 
-As name states this an extension for [Underscore.js][u], but it can be used
-independently from **_s**-global variable. But with Underscore.js you can
-use Object-Oriented style and chaining:
+    npm install underscore.string
 
-[u]: http://documentcloud.github.com/underscore/
+Require individual functions
 
 ```javascript
-_("   epeli  ").chain().trim().capitalize().value()
-=> "Epeli"
+var slugify = require("underscore.string/slugify");
+
+slugify("Hello world!");
+// => hello-world
 ```
 
-## Download ##
+or load the full library to enable chaining
 
-  * [Development version](https://raw.github.com/epeli/underscore.string/master/lib/underscore.string.js) *Uncompressed with Comments 18kb*
-  * [Production version](https://github.com/epeli/underscore.string/raw/master/dist/underscore.string.min.js) *Minified 7kb*
+```javascript
+var s = require("underscore.string");
 
+s("   epeli  ").trim().capitalize().value();
+// => "Epeli"
+```
 
-## Node.js installation ##
+but especially when using with [Browserify][] the individual function approach
+is recommended because using it you only add those functions to your bundle you
+use.
 
-**npm package**
+[Browserify]: http://browserify.org/
 
-    npm install underscore.string
+### In Meteor
 
-**Standalone usage**:
+From your [Meteor][] project folder
 
-```javascript
-var _s = require('underscore.string');
+```shell
+    meteor add underscorestring:underscore.string
 ```
 
-**Integrate with Underscore.js**:
+and you'll be able to access the library with the ***s*** global from both the server and the client.
 
 ```javascript
-var _  = require('underscore');
+s.slugify("Hello world!");
+// => hello-world
+
+s("   epeli  ").trim().capitalize().value();
+// => "Epeli"
+```
 
-// Import Underscore.string to separate object, because there are conflict functions (include, reverse, contains)
-_.str = require('underscore.string');
+[Meteor]: http://www.meteor.com/
 
-// Mix in non-conflict functions to Underscore namespace if you want
-_.mixin(_.str.exports());
+### Others
 
-// All functions, include conflict, will be available through _.str object
-_.str.include('Underscore.string', 'string'); // => true
-```
+The `dist/underscore.string.js` file is an [UMD][] build. You can load it using
+an AMD loader such as [RequireJS][] or just stick it to a web page and access
+the library from the ***s*** global.
+
+[UMD]: https://github.com/umdjs/umd
+[RequireJS]: http://requirejs.org/
+
+### Underscore.js/Lo-Dash integration
 
-**Or Integrate with Underscore.js without module loading**
+It is still possible use as Underscore.js/Lo-Dash extension
 
-Run the following expression after Underscore.js and Underscore.string are loaded
 ```javascript
-// _.str becomes a global variable if no module loading is detected
-// Mix in non-conflict functions to Underscore namespace
-_.mixin(_.str.exports());
+_.mixin(s.exports());
 ```
+But it's not recommended since `include`, `contains`, `reverse` and `join`
+are dropped because they collide with the functions already defined by Underscore.js.
 
-## String Functions ##
+### Lo-Dash-FP/Ramda integration
 
-For availability of functions in this way you need to mix in Underscore.string functions:
+If you want to use underscore.string with [ramdajs](http://ramdajs.com/) or [Lo-Dash-FP](https://github.com/lodash/lodash-fp) you can use [underscore.string.fp](https://github.com/stoeffel/underscore.string.fp).
+
+    npm install underscore.string.fp
 
 ```javascript
-_.mixin(_.string.exports());
+var S = require('underscore.string.fp');
+var filter = require('lodash-fp').filter;
+var filter = require('ramda').filter;
+
+filter(S.startsWith('.'), [
+  '.vimrc',
+  'foo.md',
+  '.zshrc'
+]);
+// => ['.vimrc', '.zshrc']
 ```
 
-otherwise functions from examples will be available through _.string or _.str objects:
+## Download
+  
+  * [Development version](https://npmcdn.com/underscore.string/dist/underscore.string.js) *Uncompressed with Comments*
+  * [Production version](https://npmcdn.com/underscore.string/dist/underscore.string.min.js) *Minified*
 
-```javascript
-_.str.capitalize('epeli')
-=> "Epeli"
-```
+## API
+
+### Individual functions
 
-**numberFormat** _.numberFormat(number, [ decimals=0, decimalSeparator='.', orderSeparator=','])
+#### numberFormat(number, [ decimals=0, decimalSeparator='.', orderSeparator=',']) => string
 
 Formats the numbers.
 
 ```javascript
-_.numberFormat(1000, 2)
-=> "1,000.00"
+numberFormat(1000, 2);
+// => "1,000.00"
 
-_.numberFormat(123456789.123, 5, '.', ',')
-=> "123,456,789.12300"
+numberFormat(123456789.123, 5, ".", ",");
+// => "123,456,789.12300"
 ```
 
 
-**levenshtein** _.levenshtein(string1, string2)
+#### levenshtein(string1, string2) => number
 
 Calculates [Levenshtein distance][ld] between two strings.
 [ld]: http://en.wikipedia.org/wiki/Levenshtein_distance
 
 ```javascript
-_.levenshtein('kitten', 'kittah')
-=> 2
+levenshtein("kitten", "kittah");
+// => 2
 ```
 
-**capitalize** _.capitalize(string)
+#### capitalize(string, [lowercaseRest=false]) => string
 
-Converts first letter of the string to uppercase.
+Converts first letter of the string to uppercase. If `true` is passed as second argument the rest
+of the string will be converted to lower case.
 
 ```javascript
-_.capitalize("foo Bar")
-=> "Foo Bar"
+capitalize("foo Bar");
+// => "Foo Bar"
+
+capitalize("FOO Bar", true);
+// => "Foo bar"
 ```
 
-**chop** _.chop(string, step)
+#### decapitalize(string) => string
+
+Converts first letter of the string to lowercase.
 
 ```javascript
-_.chop('whitespace', 3)
-=> ['whi','tes','pac','e']
+decapitalize("Foo Bar");
+// => "foo Bar"
 ```
 
-**clean** _.clean(str)
-
-Compress some whitespaces to one.
+#### chop(string, step) => array
 
 ```javascript
-_.clean(" foo    bar   ")
-=> 'foo bar'
+chop("whitespace", 3);
+// => ["whi", "tes", "pac", "e"]
 ```
 
-**chars** _.chars(str)
+#### clean(string) => string
+
+Trim and replace multiple spaces with a single space.
 
 ```javascript
-_.chars('Hello')
-=> ['H','e','l','l','o']
+clean(" foo    bar   ");
+// => "foo bar"
 ```
 
-**swapCase** _.swapCase(str)
+#### cleanDiacritics(string) => string
 
-Returns a copy of the string in which all the case-based characters have had their case swapped.
+Replace [diacritic][dc] characters with closest ASCII equivalents. Check the
+[source][s] for supported characters. [Pull requests][p] welcome for missing
+characters!
+
+[dc]: https://en.wikipedia.org/wiki/Diacritic
+[s]: https://github.com/epeli/underscore.string/blob/master/cleanDiacritics.js
+[p]: https://github.com/epeli/underscore.string/blob/master/CONTRIBUTING.markdown
 
 ```javascript
-_.swapCase('hELLO')
-=> 'Hello'
+cleanDiacritics("ääkkönen");
+// => "aakkonen"
 ```
 
-**include** available only through _.str object, because Underscore has function with the same name.
+#### chars(string) => array
 
 ```javascript
-_.str.include("foobar", "ob")
-=> true
+chars("Hello");
+// => ["H", "e", "l", "l", "o"]
 ```
 
-(removed) **includes** _.includes(string, substring)
+#### swapCase(string) => string
 
-Tests if string contains a substring.
+Returns a copy of the string in which all the case-based characters have had their case swapped.
 
 ```javascript
-_.includes("foobar", "ob")
-=> true
+swapCase("hELLO");
+// => "Hello"
 ```
 
-**includes** function was removed
+#### include(string, substring) => boolean
 
-But you can create it in this way, for compatibility with previous versions:
+Tests if string contains a substring.
 
 ```javascript
-_.includes = _.str.include
+include("foobar", "ob");
+// => true
 ```
 
-**count** _.count(string, substring)
+#### count(string, substring) => number
+
+Returns number of occurrences of substring in string.
 
 ```javascript
-_('Hello world').count('l')
-=> 3
+count("Hello world", "l");
+// => 3
 ```
 
-**escapeHTML** _.escapeHTML(string)
+#### escapeHTML(string) => string
 
 Converts HTML special characters to their entity equivalents.
+This function supports cent, yen, euro, pound, lt, gt, copy, reg, quote, amp, apos.
 
 ```javascript
-_('<div>Blah blah blah</div>').escapeHTML();
-=> '<div>Blah blah blah</div>'
+escapeHTML("<div>Blah blah blah</div>");
+// => "<div>Blah blah blah</div>"
 ```
 
-**unescapeHTML** _.unescapeHTML(string)
+#### unescapeHTML(string) => string
 
 Converts entity characters to HTML equivalents.
+This function supports cent, yen, euro, pound, lt, gt, copy, reg, quote, amp, apos, nbsp.
+
+```javascript
+unescapeHTML("<div>Blah blah blah</div>");
+// => "<div>Blah blah blah</div>"
+```
+
+#### insert(string, index, substring) => string
 
 ```javascript
-_('<div>Blah blah blah</div>').unescapeHTML();
-=> '<div>Blah blah blah</div>'
+insert("Hellworld", 4, "o ");
+// => "Hello world"
 ```
 
-**insert** _.insert(string, index, substing)
+#### replaceAll(string, find, replace, [ignorecase=false]) => string
 
 ```javascript
-_('Hello ').insert(6, 'world')
-=> 'Hello world'
+replaceAll("foo", "o", "a");
+// => "faa"
 ```
 
-**isBlank** _.isBlank(string)
+#### isBlank(string) => boolean
 
 ```javascript
-_('').isBlank(); // => true
-_('\n').isBlank(); // => true
-_(' ').isBlank(); // => true
-_('a').isBlank(); // => false
+isBlank(""); // => true
+isBlank("\n"); // => true
+isBlank(" "); // => true
+isBlank("a"); // => false
 ```
 
-**join** _.join(separator, *strings)
+#### join(separator, ...strings) => string
 
 Joins strings together with given separator
 
 ```javascript
-_.join(" ", "foo", "bar")
-=> "foo bar"
+join(" ", "foo", "bar");
+// => "foo bar"
 ```
 
-**lines** _.lines(str)
+#### lines(str) => array
+
+Split lines to an array
 
 ```javascript
-_.lines("Hello\nWorld")
-=> ["Hello", "World"]
+lines("Hello\nWorld");
+// => ["Hello", "World"]
 ```
 
-**reverse** available only through _.str object, because Underscore has function with the same name.
+#### wrap(str, options) => string
 
-Return reversed string:
+Splits a line `str` (default '') into several lines of size `options.width` (default 75) using a `options.seperator` (default '\n'). If `options.trailingSpaces` is true, make each line at least `width` long using trailing spaces. If `options.cut` is true, create new lines in the middle of words. If `options.preserveSpaces` is true, preserve the space that should be there at the end of a line (only works if options.cut is false).
 
 ```javascript
-_.str.reverse("foobar")
-=> 'raboof'
+wrap("Hello World", { width:5 })
+// => "Hello\nWorld"
+
+wrap("Hello World", { width:6, seperator:'.', trailingSpaces: true })
+// => "Hello .World "
+
+wrap("Hello World", { width:5, seperator:'.', cut:true, trailingSpaces: true })
+// => "Hello. Worl.d    "
+
+wrap("Hello World", { width:5, seperator:'.', preserveSpaces: true })
+// => "Hello .World"
+
 ```
 
-**splice**  _.splice(string, index, howmany, substring)
+#### dedent(str, [pattern]) => string
+
+Dedent unnecessary indentation or dedent by a pattern.
 
-Like a array splice.
+Credits go to @sindresorhus.
+This implementation is similar to https://github.com/sindresorhus/strip-indent
 
 ```javascript
-_('https://edtsech@bitbucket.org/edtsech/underscore.strings').splice(30, 7, 'epeli')
-=> 'https://edtsech@bitbucket.org/epeli/underscore.strings'
+dedent("  Hello\n    World");
+// => "Hello\n  World"
+
+dedent("\t\tHello\n\t\t\t\tWorld");
+// => "Hello\n\t\tWorld"
+
+dedent("    Hello\n    World", "  "); // Dedent by 2 spaces
+// => "  Hello\n  World"
 ```
 
-**startsWith** _.startsWith(string, starts)
+#### reverse(string) => string
 
-This method checks whether string starts with starts.
+Return reversed string:
 
 ```javascript
-_("image.gif").startsWith("image")
-=> true
+reverse("foobar");
+// => "raboof"
 ```
 
-**endsWith** _.endsWith(string, ends)
+#### splice(string, index, howmany, substring) => string
 
-This method checks whether string ends with ends.
+Like an array splice.
 
 ```javascript
-_("image.gif").endsWith("gif")
-=> true
+splice("https://edtsech@bitbucket.org/edtsech/underscore.strings", 30, 7, "epeli");
+// => "https://edtsech@bitbucket.org/epeli/underscore.strings"
 ```
 
-**succ**  _.succ(str)
+#### startsWith(string, starts, [position]) => boolean
 
-Returns the successor to str.
+This method checks whether the string begins with `starts` at `position` (default: 0).
 
 ```javascript
-_('a').succ()
-=> 'b'
+startsWith("image.gif", "image");
+// => true
 
-_('A').succ()
-=> 'B'
+startsWith(".vimrc", "vim", 1);
+// => true
 ```
 
-**supplant**
+#### endsWith(string, ends, [position]) => boolean
 
-Supplant function was removed, use Underscore.js [template function][p].
+This method checks whether the string ends with `ends` at `position` (default: string.length).
 
-[p]: http://documentcloud.github.com/underscore/#template
+```javascript
+endsWith("image.gif", "gif");
+// => true
 
-**strip** alias for *trim*
+endsWith("image.old.gif", "old", 9);
+// => true
+```
+
+#### pred(string) => string
+
+Returns the predecessor to str.
 
-**lstrip** alias for *ltrim*
+```javascript
+pred("b");
+// => "a"
 
-**rstrip** alias for *rtrim*
+pred("B");
+// => "A"
+```
 
-**titleize** _.titleize(string)
+#### succ(string) => string
+
+Returns the successor to str.
+
+```javascript
+succ("a");
+// => "b"
+
+succ("A");
+// => "B"
+```
+
+
+#### titleize(string) => string
 
 ```javascript
-_('my name is epeli').titleize()
-=> 'My Name Is Epeli'
+titleize("my name is epeli");
+// => "My Name Is Epeli"
 ```
 
-**camelize** _.camelize(string)
+#### camelize(string, [decapitalize=false]) => string
 
-Converts underscored or dasherized string to a camelized one
+Converts underscored or dasherized string to a camelized one. Begins with
+a lower case letter unless it starts with an underscore, dash or an upper case letter.
 
 ```javascript
-_('-moz-transform').camelize()
-=> 'MozTransform'
+camelize("moz-transform");
+// => "mozTransform"
+
+camelize("-moz-transform");
+// => "MozTransform"
+
+camelize("_moz_transform");
+// => "MozTransform"
+
+camelize("Moz-transform");
+// => "MozTransform"
+
+camelize("-moz-transform", true);
+// => "mozTransform"
 ```
 
-**classify** _.classify(string)
+#### classify(string) => string
 
-Converts string to camelized class name
+Converts string to camelized class name. First letter is always upper case
 
 ```javascript
-_('some_class_name').classify()
-=> 'SomeClassName'
+classify("some_class_name");
+// => "SomeClassName"
 ```
 
-**underscored** _.underscored(string)
+#### underscored(string) => string
 
 Converts a camelized or dasherized string into an underscored one
 
 ```javascript
-_('MozTransform').underscored()
-=> 'moz_transform'
+underscored("MozTransform");
+// => "moz_transform"
 ```
 
-**dasherize** _.dasherize(string)
+#### dasherize(string) => string
 
 Converts a underscored or camelized string into an dasherized one
 
 ```javascript
-_('MozTransform').dasherize()
-=> '-moz-transform'
+dasherize("MozTransform");
+// => "-moz-transform"
 ```
 
-**humanize** _.humanize(string)
+#### humanize(string) => string
 
 Converts an underscored, camelized, or dasherized string into a humanized one.
 Also removes beginning and ending whitespace, and removes the postfix '_id'.
 
 ```javascript
-_('  capitalize dash-CamelCase_underscore trim  ').humanize()
-=> 'Capitalize dash camel case underscore trim'
+humanize("  capitalize dash-CamelCase_underscore trim  ");
+// => "Capitalize dash camel case underscore trim"
 ```
 
-**trim** _.trim(string, [characters])
+#### trim(string, [characters]) => string
 
-trims defined characters from begining and ending of the string.
+Trims defined characters from begining and ending of the string.
 Defaults to whitespace characters.
 
 ```javascript
-_.trim("  foobar   ")
-=> "foobar"
+trim("  foobar   ");
+// => "foobar"
 
-_.trim("_-foobar-_", "_-")
-=> "foobar"
+trim("_-foobar-_", "_-");
+// => "foobar"
 ```
 
 
-**ltrim** _.ltrim(string, [characters])
+#### ltrim(string, [characters]) => string
 
 Left trim. Similar to trim, but only for left side.
 
-
-**rtrim** _.rtrim(string, [characters])
+#### rtrim(string, [characters]) => string
 
 Right trim. Similar to trim, but only for right side.
 
-**truncate** _.truncate(string, length, truncateString)
+#### truncate(string, length, [truncateString = '...']) => string
 
 ```javascript
-_('Hello world').truncate(5)
-=> 'Hello...'
+truncate("Hello world", 5);
+// => "Hello..."
 
-_('Hello').truncate(10)
-=> 'Hello'
+truncate("Hello", 10);
+// => "Hello"
 ```
 
-**prune** _.prune(string, length, pruneString)
+#### prune(string, length, pruneString) => string
 
-Elegant version of truncate.
-Makes sure the pruned string does not exceed the original length.
-Avoid half-chopped words when truncating.
+Elegant version of truncate.  Makes sure the pruned string does not exceed the
+original length.  Avoid half-chopped words when truncating.
 
 ```javascript
-_('Hello, world').prune(5)
-=> 'Hello...'
+prune("Hello, world", 5);
+// => "Hello..."
 
-_('Hello, world').prune(8)
-=> 'Hello...'
+prune("Hello, world", 8);
+// => "Hello..."
 
-_('Hello, world').prune(5, ' (read a lot more)')
-=> 'Hello, world' (as adding "(read a lot more)" would be longer than the original string)
+prune("Hello, world", 5, " (read a lot more)");
+// => "Hello, world" (as adding "(read a lot more)" would be longer than the original string)
 
-_('Hello, cruel world').prune(15)
-=> 'Hello, cruel...'
+prune("Hello, cruel world", 15);
+// => "Hello, cruel..."
 
-_('Hello').prune(10)
-=> 'Hello'
+prune("Hello", 10);
+// => "Hello"
 ```
 
-**words** _.words(str, delimiter=/\s+/)
+#### words(str, delimiter=/\s+/) => array
 
 Split string by delimiter (String or RegExp), /\s+/ by default.
 
 ```javascript
-_.words("   I   love   you   ")
-=> ["I","love","you"]
+words("   I   love   you   ");
+// => ["I", "love", "you"]
 
-_.words("I_love_you", "_")
-=> ["I","love","you"]
+words("I_love_you", "_");
+// => ["I", "love", "you"]
 
-_.words("I-love-you", /-/)
-=> ["I","love","you"]
+words("I-love-you", /-/);
+// => ["I", "love", "you"]
 
-_.words("   ")
-=> []
+words("   ")
+// => []
 ```
 
-**sprintf** _.sprintf(string format, *arguments)
+#### sprintf(string format, ...arguments) => string
 
-C like string formatting.
-Credits goes to [Alexandru Marasteanu][o].
-For more detailed documentation, see the [original page][o].
+C like string formatting. Makes use of the [sprintf-js](https://npmjs.org/package/sprintf-js) package.
 
-[o]: http://www.diveintojavascript.com/projects/sprintf-for-javascript
+**This function will be removed in the next major release, use the [sprintf-js](https://npmjs.org/package/sprintf-js) package instead.**
 
 ```javascript
-_.sprintf("%.1f", 1.17)
-"1.2"
+sprintf("%.1f", 1.17);
+// => "1.2"
 ```
 
-**pad** _.pad(str, length, [padStr, type])
+#### pad(str, length, [padStr, type]) => string
 
 pads the `str` with characters until the total string length is equal to the passed `length` parameter. By default, pads on the **left** with the space char (`" "`). `padStr` is truncated to a single character if necessary.
 
 ```javascript
-_.pad("1", 8)
--> "       1";
+pad("1", 8);
+// => "       1"
 
-_.pad("1", 8, '0')
--> "00000001";
+pad("1", 8, "0");
+// => "00000001"
 
-_.pad("1", 8, '0', 'right')
--> "10000000";
+pad("1", 8, "0", "right");
+// => "10000000"
 
-_.pad("1", 8, '0', 'both')
--> "00001000";
+pad("1", 8, "0", "both");
+// => "00001000"
 
-_.pad("1", 8, 'bleepblorp', 'both')
--> "bbbb1bbb";
+pad("1", 8, "bleepblorp", "both");
+// => "bbbb1bbb"
 ```
 
-**lpad** _.lpad(str, length, [padStr])
+#### lpad(str, length, [padStr]) => string
 
-left-pad a string. Alias for `pad(str, length, padStr, 'left')`
+left-pad a string. Alias for `pad(str, length, padStr, "left")`
 
 ```javascript
-_.lpad("1", 8, '0')
--> "00000001";
+lpad("1", 8, "0");
+// => "00000001"
 ```
 
-**rpad** _.rpad(str, length, [padStr])
+#### rpad(str, length, [padStr]) => string
 
-right-pad a string. Alias for `pad(str, length, padStr, 'right')`
+right-pad a string. Alias for `pad(str, length, padStr, "right")`
 
 ```javascript
-_.rpad("1", 8, '0')
--> "10000000";
+rpad("1", 8, "0");
+// => "10000000"
 ```
 
-**lrpad** _.lrpad(str, length, [padStr])
+#### lrpad(str, length, [padStr]) => string
 
-left/right-pad a string. Alias for `pad(str, length, padStr, 'both')`
+left/right-pad a string. Alias for `pad(str, length, padStr, "both")`
 
 ```javascript
-_.lrpad("1", 8, '0')
--> "00001000";
+lrpad("1", 8, '0');
+// => "00001000"
 ```
 
-**center** alias for **lrpad**
-
-**ljust** alias for *rpad*
-
-**rjust** alias for *lpad*
 
-**toNumber**  _.toNumber(string, [decimals])
+#### toNumber(string, [decimals]) => number
 
 Parse string to number. Returns NaN if string can't be parsed to number.
 
 ```javascript
-_('2.556').toNumber()
-=> 3
+toNumber("2.556");
+// => 3
 
-_('2.556').toNumber(1)
-=> 2.6
+toNumber("2.556", 1);
+// => 2.6
+
+toNumber("999.999", -1);
+// => 990
 ```
 
-**strRight**  _.strRight(string, pattern)
+#### strRight(string, pattern) => string
 
 Searches a string from left to right for a pattern and returns a substring consisting of the characters in the string that are to the right of the pattern or all string if no match found.
 
 ```javascript
-_('This_is_a_test_string').strRight('_')
-=> "is_a_test_string";
+strRight("This_is_a_test_string", "_");
+// => "is_a_test_string"
 ```
 
-**strRightBack**  _.strRightBack(string, pattern)
+#### strRightBack(string, pattern) => string
 
 Searches a string from right to left for a pattern and returns a substring consisting of the characters in the string that are to the right of the pattern or all string if no match found.
 
 ```javascript
-_('This_is_a_test_string').strRightBack('_')
-=> "string";
+strRightBack("This_is_a_test_string", "_");
+// => "string"
 ```
 
-**strLeft**  _.strLeft(string, pattern)
+#### strLeft(string, pattern) => string
 
 Searches a string from left to right for a pattern and returns a substring consisting of the characters in the string that are to the left of the pattern or all string if no match found.
 
 ```javascript
-_('This_is_a_test_string').strLeft('_')
-=> "This";
+strLeft("This_is_a_test_string", "_");
+// => "This";
 ```
 
-**strLeftBack**  _.strLeftBack(string, pattern)
+#### strLeftBack(string, pattern) => string
 
 Searches a string from right to left for a pattern and returns a substring consisting of the characters in the string that are to the left of the pattern or all string if no match found.
 
 ```javascript
-_('This_is_a_test_string').strLeftBack('_')
-=> "This_is_a_test";
+strLeftBack("This_is_a_test_string", "_");
+// => "This_is_a_test";
 ```
 
-**stripTags**
+#### stripTags(string) => string
 
 Removes all html tags from string.
 
 ```javascript
-_('a <a href="#">link</a>').stripTags()
-=> 'a link'
+stripTags("a <a href=\"#\">link</a>");
+// => "a link"
 
-_('a <a href="#">link</a><script>alert("hello world!")</script>').stripTags()
-=> 'a linkalert("hello world!")'
+stripTags("a <a href=\"#\">link</a><script>alert(\"hello world!\")</script>");
+// => "a linkalert("hello world!")"
 ```
 
-**toSentence**  _.toSentence(array, [delimiter, lastDelimiter])
+#### toSentence(array, [delimiter, lastDelimiter]) => string
 
 Join an array into a human readable sentence.
 
 ```javascript
-_.toSentence(['jQuery', 'Mootools', 'Prototype'])
-=> 'jQuery, Mootools and Prototype';
+toSentence(["jQuery", "Mootools", "Prototype"]);
+// => "jQuery, Mootools and Prototype";
 
-_.toSentence(['jQuery', 'Mootools', 'Prototype'], ', ', ' unt ')
-=> 'jQuery, Mootools unt Prototype';
+toSentence(["jQuery", "Mootools", "Prototype"], ", ", " unt ");
+// => "jQuery, Mootools unt Prototype";
 ```
 
-**toSentenceSerial**  _.toSentenceSerial(array, [delimiter, lastDelimiter])
+#### toSentenceSerial(array, [delimiter, lastDelimiter]) => string
 
 The same as `toSentence`, but adjusts delimeters to use [Serial comma](http://en.wikipedia.org/wiki/Serial_comma).
 
 ```javascript
-_.toSentenceSerial(['jQuery', 'Mootools'])
-=> 'jQuery and Mootools';
+toSentenceSerial(["jQuery", "Mootools"]);
+// => "jQuery and Mootools"
 
-_.toSentenceSerial(['jQuery', 'Mootools', 'Prototype'])
-=> 'jQuery, Mootools, and Prototype'
+toSentenceSerial(["jQuery", "Mootools", "Prototype"]);
+// => "jQuery, Mootools, and Prototype"
 
-_.toSentenceSerial(['jQuery', 'Mootools', 'Prototype'], ', ', ' unt ');
-=> 'jQuery, Mootools, unt Prototype';
+toSentenceSerial(["jQuery", "Mootools", "Prototype"], ", ", " unt ");
+// => "jQuery, Mootools, unt Prototype"
 ```
 
-**repeat** _.repeat(string, count, [separator])
+#### repeat(string, count, [separator]) => string
 
 Repeats a string count times.
 
 ```javascript
-_.repeat("foo", 3)
-=> 'foofoofoo';
+repeat("foo", 3);
+// => "foofoofoo"
 
-_.repeat("foo", 3, "bar")
-=> 'foobarfoobarfoo'
+repeat("foo", 3, "bar");
+// => "foobarfoobarfoo"
 ```
 
-**surround** _.surround(string, wrap)
+#### surround(string, wrap) => string
 
 Surround a string with another string.
 
 ```javascript
-_.surround("foo", "ab")
-=> 'abfooab';
+surround("foo", "ab");
+// => "abfooab"
 ```
 
-**quote** _.quote(string, quoteChar) or _.q(string, quoteChar)
+#### quote(string, quoteChar) or q(string, quoteChar) => string
 
 Quotes a string. `quoteChar` defaults to `"`.
 
 ```javascript
-_.quote('foo', quoteChar)
-=> '"foo"';
+quote("foo", '"');
+// => '"foo"';
 ```
-**unquote** _.unquote(string, quoteChar)
+#### unquote(string, quoteChar) => string
 
 Unquotes a string. `quoteChar` defaults to `"`.
 
 ```javascript
-_.unquote('"foo"')
-=> 'foo';
-_.unquote("'foo'", "'")
-=> 'foo';
+unquote('"foo"');
+// => "foo"
+
+unquote("'foo'", "'");
+// => "foo"
 ```
 
 
-**slugify** _.slugify(string)
+#### slugify(string) => string
 
-Transform text into a URL slug. Replaces whitespaces, accentuated, and special characters with a dash.
+Transform text into an ascii slug which can be used in safely in URLs. Replaces whitespaces, accentuated, and special characters with a dash. Limited set of non-ascii characters are transformed to similar versions in the ascii character set such as `ä` to `a`.
 
 ```javascript
-_.slugify("Un éléphant à l'orée du bois")
-=> 'un-elephant-a-loree-du-bois';
+slugify("Un éléphant à l\'orée du bois");
+// => "un-elephant-a-l-oree-du-bois"
 ```
 
 ***Caution: this function is charset dependent***
 
-**naturalCmp** array.sort(_.naturalCmp)
+#### naturalCmp(string1, string2) => number
 
-Naturally sort strings like humans would do.
+Naturally sort strings like humans would do. None numbers are compared by their [ASCII values](http://www.asciitable.com/). Note: this means "a" > "A". Use `.toLowerCase` if this isn't to be desired.
+
+Just past it to `Array#sort`.
 
 ```javascript
-['foo20', 'foo5'].sort(_.naturalCmp)
-=> [ 'foo5', 'foo20' ]
+["foo20", "foo5"].sort(naturalCmp);
+// => ["foo5", "foo20"]
 ```
 
-**toBoolean** _.toBoolean(string) or _.toBool(string)
+#### toBoolean(string) => boolean
 
 Turn strings that can be commonly considered as booleas to real booleans. Such as "true", "false", "1" and "0". This function is case insensitive.
 
 ```javascript
-_.toBoolean("true")
-=> true
-_.toBoolean("FALSE")
-=> false
-_.toBoolean("random")
-=> undefined
-```
+toBoolean("true");
+// => true
 
-It can be customized by giving arrays of truth and falsy value matcher as parameters. Matchers can be also RegExp objects.
+toBoolean("FALSE");
+// => false
 
-```javascript
-_.toBoolean("truthy", ["truthy"], ["falsy"])
-=> true
-_.toBoolean("true only at start", [/^true/])
-=> true
+toBoolean("random");
+// => undefined
 ```
 
-## Roadmap ##
-
-Any suggestions or bug reports are welcome. Just email me or more preferably open an issue.
-
-#### Problems
-
-We lose two things for `include` and `reverse` methods from `_.string`:
-
-* Calls like `_('foobar').include('bar')` aren't available;
-* Chaining isn't available too.
-
-But if you need this functionality you can create aliases for conflict functions which will be convenient for you:
+It can be customized by giving arrays of truth and falsy value matcher as parameters. Matchers can be also RegExp objects.
 
 ```javascript
-_.mixin({
-    includeString: _.str.include,
-    reverseString: _.str.reverse
-})
+toBoolean("truthy", ["truthy"], ["falsy"]);
+// => true
 
-// Now wrapper calls and chaining are available.
-_('foobar').chain().reverseString().includeString('rab').value()
+toBoolean("true only at start", [/^true/]);
+// => true
 ```
 
-#### Standalone Usage
+#### map(string, function) => string
 
-If you are using Underscore.string without Underscore. You also have `_.string` namespace for it and `_.str` alias
-But of course you can just reassign `_` variable with `_.string`
+Creates a new string with the results of calling a provided function on every character of the given string.
 
 ```javascript
-_ = _.string
-```
+map("Hello world", function(x) {
+  return x;
+});
+// => "Hello world"
 
-## Changelog ##
+map(12345, function(x) {
+  return x;
+});
+// => "12345"
 
-### 2.3.3 ###
-
-* Add `toBoolean`
-* Add `unquote`
-* Add quote char option to `quote`
-* Support dash-separated words in `titleize`
-
-### 2.3.2 ###
-
-* Add `naturalCmp`
-* Bug fix to `camelize`
-* Add ă, ș, ț and ś to `slugify`
-* Doc updates
-* Add support for [component](http://component.io/)
-* [Full changelog](https://github.com/epeli/underscore.string/compare/v2.3.1...v2.3.2)
-
-### 2.3.1 ###
-
-* Bug fixes to `escapeHTML`, `classify`, `substr`
-* Faster `count`
-* Documentation fixes
-* [Full changelog](https://github.com/epeli/underscore.string/compare/v2.3.0...v2.3.1)
-
-### 2.3.0 ###
-
-* Added `numberformat` method
-* Added `levenshtein` method (Levenshtein distance calculation)
-* Added `swapCase` method
-* Changed default behavior of `words` method
-* Added `toSentenceSerial` method
-* Added `surround` and `quote` methods
-
-### 2.2.1 ###
-
-* Same as 2.2.0 (2.2.0rc on npm) to fix some npm drama
-
-### 2.2.0 ###
+map("Hello world", function(x) {
+  if (x === 'o') x = 'O';
+  return x;
+});
+// => "HellO wOrld"
+```
 
-* Capitalize method behavior changed
-* Various perfomance tweaks
+### Library functions
 
-### 2.1.1###
+If you require the full library you can use chaining and aliases
 
-* Fixed words method bug
-* Added classify method
+#### s(string) => chain
 
-### 2.1.0 ###
+Start a chain. Returns an immutable chain object with the string functions as
+methods which return a new chain object instead of the plain string value.
 
-* AMD support
-* Added toSentence method
-* Added slugify method
-* Lots of speed optimizations
+The chain object includes also following native Javascript string methods:
 
-### 2.0.0 ###
+  - [toUpperCase](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase)
+  - [toLowerCase](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLowerCase)
+  - [split](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split)
+  - [replace](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace)
+  - [slice](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/slice)
+  - [substring](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/String/substring)
+  - [substr](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substr)
+  - [concat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/concat)
 
-* Added prune, humanize functions
-* Added _.string (_.str) namespace for Underscore.string library
-* Removed includes function
+#### chain.value()
 
-For upgrading to this version you need to mix in Underscore.string library to Underscore object:
+Return the string value from the chain
 
 ```javascript
-_.mixin(_.string.exports());
+s("  foo  ").trim().capitalize().value();
+// => "Foo"
 ```
 
-and all non-conflict Underscore.string functions will be available through Underscore object.
-Also function `includes` has been removed, you should replace this function by `_.str.include`
-or create alias `_.includes = _.str.include` and all your code will work fine.
-
-### 1.1.6 ###
-
-* Fixed reverse and truncate
-* Added isBlank, stripTags, inlude(alias for includes)
-* Added uglifier compression
-
-### 1.1.5 ###
+When calling a method which does not return a string the resulting value is
+immediately returned
 
-* Added strRight, strRightBack, strLeft, strLeftBack
-
-### 1.1.4 ###
-
-* Added pad, lpad, rpad, lrpad methods and aliases center, ljust, rjust
-* Integration with Underscore 1.1.6
-
-### 1.1.3 ###
-
-* Added methods: underscored, camelize, dasherize
-* Support newer version of npm
+```javascript
+s(" foobar ").trim().startsWith("foo");
+// => true
+```
 
-### 1.1.2 ###
+#### chain.tap(function) => chain
 
-* Created functions: lines, chars, words functions
+Tap into the chain with a custom function
 
-### 1.0.2 ###
+```javascript
+s("foo").tap(function(value){
+  return value + "bar";
+}).value();
+// => "foobar"
+```
 
-* Created integration test suite with underscore.js 1.1.4 (now it's absolutely compatible)
-* Removed 'reverse' function, because this function override underscore.js 'reverse'
 
-## Contribute ##
+#### Aliases
 
-* Fork & pull request. Don't forget about tests.
-* If you planning add some feature please create issue before.
+```javascript
+strip     = trim
+lstrip    = ltrim
+rstrip    = rtrim
+center    = lrpad
+rjust     = lpad
+ljust     = rpad
+contains  = include
+q         = quote
+toBool    = toBoolean
+camelcase = camelize
+```
 
-Otherwise changes will be rejected.
+## Maintainers ##
 
-## Contributors list ##
-[Can be found here](https://github.com/epeli/underscore.string/graphs/contributors).
+This library is maintained by
 
+  - Esa-Matti Suuronen – ***[@epeli](https://github.com/epeli)***
+  - Christoph Hermann – ***[@stoeffel](https://github.com/stoeffel)***
 
 ## Licence ##
 
@@ -812,3 +883,6 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
+
+
+[d]: http://www.diveintojavascript.com/core-javascript-reference/the-string-object
diff --git a/Rakefile b/Rakefile
deleted file mode 100644
index 2cd9eed..0000000
--- a/Rakefile
+++ /dev/null
@@ -1,23 +0,0 @@
-# encoding: utf-8
-task default: :test
-
-desc 'Use UglifyJS to compress Underscore.string'
-task :build do
-  require 'uglifier'
-  source = File.read('lib/underscore.string.js', :encoding => 'utf-8')
-  compressed = Uglifier.compile(source, copyright: false)
-  File.open('dist/underscore.string.min.js', 'w'){ |f| f.write compressed }
-  compression_rate = compressed.length.to_f/source.length
-  puts "compressed dist/underscore.string.min.js: #{compressed.length}/#{source.length} #{(compression_rate * 100).round}%"
-end
-
-desc 'Run tests'
-task :test do
-  puts "Running underscore.string test suite."
-  result1 = system %{phantomjs ./test/run-qunit.js "test/test.html"}
-
-  puts "Running Underscore test suite."
-  result2 = system %{phantomjs ./test/run-qunit.js "test/test_underscore/index.html"}
-
-  exit(result1 && result2 ? 0 : 1)
-end
diff --git a/bench/chop.js b/bench/chop.js
new file mode 100644
index 0000000..c267d14
--- /dev/null
+++ b/bench/chop.js
@@ -0,0 +1,5 @@
+var chop = require('../chop');
+
+module.exports = function() {
+  chop('whitespace', 2);
+};
diff --git a/bench/count.js b/bench/count.js
new file mode 100644
index 0000000..f7ee59b
--- /dev/null
+++ b/bench/count.js
@@ -0,0 +1,5 @@
+var count = require('../count');
+
+module.exports = function() {
+  count('Hello worls', 'l');
+};
diff --git a/bench/endsWith.js b/bench/endsWith.js
new file mode 100644
index 0000000..39b49f5
--- /dev/null
+++ b/bench/endsWith.js
@@ -0,0 +1,5 @@
+var endsWith = require('../endsWith');
+
+module.exports = function() {
+  endsWith('foobar', 'xx');
+};
diff --git a/bench/escapeHTML.js b/bench/escapeHTML.js
new file mode 100644
index 0000000..fa96e31
--- /dev/null
+++ b/bench/escapeHTML.js
@@ -0,0 +1,5 @@
+var escapeHTML = require('../escapeHTML');
+
+module.exports = function() {
+  escapeHTML('<div>Blah blah blah</div>');
+};
diff --git a/bench/insert.js b/bench/insert.js
new file mode 100644
index 0000000..8a39c67
--- /dev/null
+++ b/bench/insert.js
@@ -0,0 +1,5 @@
+var insert = require('../insert');
+
+module.exports = function() {
+  insert('Hello ', 6, 'world');
+};
diff --git a/bench/isBlank.js b/bench/isBlank.js
new file mode 100644
index 0000000..877e430
--- /dev/null
+++ b/bench/isBlank.js
@@ -0,0 +1,5 @@
+var isBlank = require('../isBlank');
+
+module.exports = function() {
+  isBlank('');
+};
diff --git a/bench/join.js b/bench/join.js
new file mode 100644
index 0000000..d658c44
--- /dev/null
+++ b/bench/join.js
@@ -0,0 +1,5 @@
+var join = require('../join');
+
+module.exports = function() {
+  join('separator', 1, 2, 3, 4, 5, 6, 7, 8, 'foo', 'bar', 'lol', 'wut');
+};
diff --git a/bench/levenshtein.js b/bench/levenshtein.js
new file mode 100644
index 0000000..89ca91e
--- /dev/null
+++ b/bench/levenshtein.js
@@ -0,0 +1,8 @@
+var levenshtein = require('../levenshtein');
+
+module.exports = function() {
+  levenshtein('pineapple', 'potato');
+  levenshtein('seven', 'eight');
+  levenshtein('the very same string', 'the very same string');
+  levenshtein('very very very long string', 'something completely different');
+};
diff --git a/bench/pad.js b/bench/pad.js
new file mode 100644
index 0000000..0a2c680
--- /dev/null
+++ b/bench/pad.js
@@ -0,0 +1,26 @@
+var pad = require('../pad');
+var tests = {};
+
+tests['pad default'] = function(){
+  pad('foo', 12);
+};
+
+tests['pad hash left'] = function(){
+  pad('foo', 12, '#');
+};
+
+tests['pad hash right'] = function(){
+  pad('foo', 12, '#', 'right');
+};
+
+tests['pad hash both'] = function(){
+  pad('foo', 12, '#', 'both');
+};
+
+tests['pad hash both longPad'] = function(){
+  pad('foo', 12, 'f00f00f00', 'both');
+};
+
+module.exports = {
+  tests: tests
+};
diff --git a/bench/prune.js b/bench/prune.js
new file mode 100644
index 0000000..7877903
--- /dev/null
+++ b/bench/prune.js
@@ -0,0 +1,5 @@
+var prune = require('../prune');
+
+module.exports = function() {
+  prune('Hello world', 5);
+};
diff --git a/bench/reverse.js b/bench/reverse.js
new file mode 100644
index 0000000..b8694c5
--- /dev/null
+++ b/bench/reverse.js
@@ -0,0 +1,5 @@
+var reverse = require('../reverse');
+
+module.exports = function() {
+  reverse('Hello World');
+};
diff --git a/bench/slugify.js b/bench/slugify.js
new file mode 100644
index 0000000..f65c1e6
--- /dev/null
+++ b/bench/slugify.js
@@ -0,0 +1,5 @@
+var slugify = require('../slugify');
+
+module.exports = function() {
+  slugify('Un éléphant à l\'orée du bois');
+};
diff --git a/bench/splice.js b/bench/splice.js
new file mode 100644
index 0000000..a24be5d
--- /dev/null
+++ b/bench/splice.js
@@ -0,0 +1,5 @@
+var splice = require('../splice');
+
+module.exports = function() {
+  splice('https://edtsech@bitbucket.org/edtsech/underscore.strings', 30, 7, 'epeli');
+};
diff --git a/bench/startsWith.js b/bench/startsWith.js
new file mode 100644
index 0000000..1b283d4
--- /dev/null
+++ b/bench/startsWith.js
@@ -0,0 +1,5 @@
+var startsWith = require('../startsWith');
+
+module.exports = function() {
+  startsWith('foobar', 'foo');
+};
diff --git a/bench/strLeft.js b/bench/strLeft.js
new file mode 100644
index 0000000..2aca9b1
--- /dev/null
+++ b/bench/strLeft.js
@@ -0,0 +1,5 @@
+var strLeft = require('../strLeft');
+
+module.exports = function() {
+  strLeft('aaa_bbb_ccc', '_');
+};
diff --git a/bench/strLeftBack.js b/bench/strLeftBack.js
new file mode 100644
index 0000000..873c4be
--- /dev/null
+++ b/bench/strLeftBack.js
@@ -0,0 +1,5 @@
+var strLeftBack = require('../strLeftBack');
+
+module.exports = function() {
+  strLeftBack('aaa_bbb_ccc', '_');
+};
diff --git a/bench/strRight.js b/bench/strRight.js
new file mode 100644
index 0000000..24e0ac8
--- /dev/null
+++ b/bench/strRight.js
@@ -0,0 +1,5 @@
+var strRight = require('../strRight');
+
+module.exports = function() {
+  strRight('aaa_bbb_ccc', '_');
+};
diff --git a/bench/strRightBack.js b/bench/strRightBack.js
new file mode 100644
index 0000000..dcf3e03
--- /dev/null
+++ b/bench/strRightBack.js
@@ -0,0 +1,5 @@
+var strRightBack = require('../strRightBack');
+
+module.exports = function() {
+  strRightBack('aaa_bbb_ccc', '_');
+};
diff --git a/bench/succ.js b/bench/succ.js
new file mode 100644
index 0000000..b7e700a
--- /dev/null
+++ b/bench/succ.js
@@ -0,0 +1,12 @@
+var succ = require('../succ');
+
+module.exports = function() {
+  var letter = 'a', alphabet = [];
+
+  for (var i=0; i < 26; i++) {
+    alphabet.push(letter);
+    letter = succ(letter);
+  }
+
+  return alphabet;
+};
diff --git a/bench/titleize.js b/bench/titleize.js
new file mode 100644
index 0000000..08ee91e
--- /dev/null
+++ b/bench/titleize.js
@@ -0,0 +1,5 @@
+var titleize = require('../titleize');
+
+module.exports = function() {
+  titleize('the titleize string method');
+};
diff --git a/bench/toNumber.js b/bench/toNumber.js
new file mode 100644
index 0000000..fb4f01a
--- /dev/null
+++ b/bench/toNumber.js
@@ -0,0 +1,5 @@
+var toNumber = require('../toNumber');
+
+module.exports = function() {
+  toNumber('10.232323', 2);
+};
diff --git a/bench/trim.js b/bench/trim.js
new file mode 100644
index 0000000..cabd156
--- /dev/null
+++ b/bench/trim.js
@@ -0,0 +1,18 @@
+var s = require('../');
+var tests = {};
+
+tests['trimNoNative'] = function() {
+  return s.trim('  foobar  ', ' ');
+};
+
+tests['trim'] = function() {
+  return s.trim('  foobar  ');
+};
+
+tests['trim object-oriented'] = function() {
+  return s('  foobar  ').trim().value();
+};
+
+module.exports = {
+  tests: tests
+};
diff --git a/bench/truncate.js b/bench/truncate.js
new file mode 100644
index 0000000..3747743
--- /dev/null
+++ b/bench/truncate.js
@@ -0,0 +1,5 @@
+var truncate = require('../truncate');
+
+module.exports = function() {
+  truncate('Hello world', 5);
+};
diff --git a/bench/unescapeHTML.js b/bench/unescapeHTML.js
new file mode 100644
index 0000000..ad21466
--- /dev/null
+++ b/bench/unescapeHTML.js
@@ -0,0 +1,5 @@
+var unescapeHTML = require('../unescapeHTML');
+
+module.exports = function() {
+  unescapeHTML('<div>Blah blah blah</div>');
+};
diff --git a/package.json b/bower.json
similarity index 87%
copy from package.json
copy to bower.json
index 34538ca..3c7c510 100644
--- a/package.json
+++ b/bower.json
@@ -1,6 +1,6 @@
 {
   "name": "underscore.string",
-  "version": "2.3.3",
+  "version": "3.3.4",
   "description": "String manipulation extensions for Underscore.js javascript library.",
   "homepage": "http://epeli.github.com/underscore.string/",
   "contributors": [
@@ -17,13 +17,8 @@
     "underscore",
     "string"
   ],
-  "main": "./lib/underscore.string",
-  "directories": {
-    "lib": "./lib"
-  },
-  "engines": {
-    "node": "*"
-  },
+  "main": "./dist/underscore.string.js",
+  "ignore": [],
   "repository": {
     "type": "git",
     "url": "https://github.com/epeli/underscore.string.git"
@@ -36,4 +31,4 @@
       "type": "MIT"
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/camelize.js b/camelize.js
new file mode 100644
index 0000000..f7c40e2
--- /dev/null
+++ b/camelize.js
@@ -0,0 +1,14 @@
+var trim = require('./trim');
+var decap = require('./decapitalize');
+
+module.exports = function camelize(str, decapitalize) {
+  str = trim(str).replace(/[-_\s]+(.)?/g, function(match, c) {
+    return c ? c.toUpperCase() : '';
+  });
+
+  if (decapitalize === true) {
+    return decap(str);
+  } else {
+    return str;
+  }
+};
diff --git a/capitalize.js b/capitalize.js
new file mode 100644
index 0000000..2693376
--- /dev/null
+++ b/capitalize.js
@@ -0,0 +1,8 @@
+var makeString = require('./helper/makeString');
+
+module.exports = function capitalize(str, lowercaseRest) {
+  str = makeString(str);
+  var remainingChars = !lowercaseRest ? str.slice(1) : str.slice(1).toLowerCase();
+
+  return str.charAt(0).toUpperCase() + remainingChars;
+};
diff --git a/chars.js b/chars.js
new file mode 100644
index 0000000..d94a901
--- /dev/null
+++ b/chars.js
@@ -0,0 +1,5 @@
+var makeString = require('./helper/makeString');
+
+module.exports = function chars(str) {
+  return makeString(str).split('');
+};
diff --git a/chop.js b/chop.js
new file mode 100644
index 0000000..73e17eb
--- /dev/null
+++ b/chop.js
@@ -0,0 +1,6 @@
+module.exports = function chop(str, step) {
+  if (str == null) return [];
+  str = String(str);
+  step = ~~step;
+  return step > 0 ? str.match(new RegExp('.{1,' + step + '}', 'g')) : [str];
+};
diff --git a/classify.js b/classify.js
new file mode 100644
index 0000000..08547e0
--- /dev/null
+++ b/classify.js
@@ -0,0 +1,8 @@
+var capitalize = require('./capitalize');
+var camelize = require('./camelize');
+var makeString = require('./helper/makeString');
+
+module.exports = function classify(str) {
+  str = makeString(str);
+  return capitalize(camelize(str.replace(/[\W_]/g, ' ')).replace(/\s/g, ''));
+};
diff --git a/clean.js b/clean.js
new file mode 100644
index 0000000..16a09d0
--- /dev/null
+++ b/clean.js
@@ -0,0 +1,5 @@
+var trim = require('./trim');
+
+module.exports = function clean(str) {
+  return trim(str).replace(/\s\s+/g, ' ');
+};
diff --git a/cleanDiacritics.js b/cleanDiacritics.js
new file mode 100644
index 0000000..d877006
--- /dev/null
+++ b/cleanDiacritics.js
@@ -0,0 +1,22 @@
+
+var makeString = require('./helper/makeString');
+
+var from  = 'ąàáäâãåæăćčĉęèéëêĝĥìíïîĵłľńňòóöőôõðøśșşšŝťțţŭùúüűûñÿýçżźž',
+  to    = 'aaaaaaaaaccceeeeeghiiiijllnnoooooooossssstttuuuuuunyyczzz';
+
+from += from.toUpperCase();
+to += to.toUpperCase();
+
+to = to.split('');
+
+// for tokens requireing multitoken output
+from += 'ß';
+to.push('ss');
+
+
+module.exports = function cleanDiacritics(str) {
+  return makeString(str).replace(/.{1}/g, function(c){
+    var index = from.indexOf(c);
+    return index === -1 ? c : to[index];
+  });
+};
diff --git a/component.json b/component.json
index ae91b65..badca5b 100644
--- a/component.json
+++ b/component.json
@@ -2,10 +2,15 @@
   "name": "underscore.string",
   "repo": "epeli/underscore.string",
   "description": "String manipulation extensions for Underscore.js javascript library",
-  "version": "2.3.3",
-  "keywords": ["underscore", "string"],
+  "version": "3.3.4",
+  "keywords": [
+    "underscore",
+    "string"
+  ],
   "dependencies": {},
   "development": {},
-  "main": "lib/underscore.string.js",
-  "scripts": ["lib/underscore.string.js"]
+  "main": "index.js",
+  "scripts": [
+    "*.js"
+  ]
 }
diff --git a/count.js b/count.js
new file mode 100644
index 0000000..2207d70
--- /dev/null
+++ b/count.js
@@ -0,0 +1,10 @@
+var makeString = require('./helper/makeString');
+
+module.exports = function(str, substr) {
+  str = makeString(str);
+  substr = makeString(substr);
+
+  if (str.length === 0 || substr.length === 0) return 0;
+  
+  return str.split(substr).length - 1;
+};
diff --git a/dasherize.js b/dasherize.js
new file mode 100644
index 0000000..544ae0c
--- /dev/null
+++ b/dasherize.js
@@ -0,0 +1,5 @@
+var trim = require('./trim');
+
+module.exports = function dasherize(str) {
+  return trim(str).replace(/([A-Z])/g, '-$1').replace(/[-_\s]+/g, '-').toLowerCase();
+};
diff --git a/decapitalize.js b/decapitalize.js
new file mode 100644
index 0000000..6aa2673
--- /dev/null
+++ b/decapitalize.js
@@ -0,0 +1,6 @@
+var makeString = require('./helper/makeString');
+
+module.exports = function decapitalize(str) {
+  str = makeString(str);
+  return str.charAt(0).toLowerCase() + str.slice(1);
+};
diff --git a/dedent.js b/dedent.js
new file mode 100644
index 0000000..41b4f07
--- /dev/null
+++ b/dedent.js
@@ -0,0 +1,28 @@
+var makeString = require('./helper/makeString');
+
+function getIndent(str) {
+  var matches = str.match(/^[\s\\t]*/gm);
+  var indent = matches[0].length;
+  
+  for (var i = 1; i < matches.length; i++) {
+    indent = Math.min(matches[i].length, indent);
+  }
+
+  return indent;
+}
+
+module.exports = function dedent(str, pattern) {
+  str = makeString(str);
+  var indent = getIndent(str);
+  var reg;
+
+  if (indent === 0) return str;
+
+  if (typeof pattern === 'string') {
+    reg = new RegExp('^' + pattern, 'gm');
+  } else {
+    reg = new RegExp('^[ \\t]{' + indent + '}', 'gm');
+  }
+
+  return str.replace(reg, '');
+};
diff --git a/dist/underscore.string.js b/dist/underscore.string.js
new file mode 100644
index 0000000..884ccc3
--- /dev/null
+++ b/dist/underscore.string.js
@@ -0,0 +1,1369 @@
+/*
+* Underscore.string
+* (c) 2010 Esa-Matti Suuronen <esa-matti aet suuronen dot org>
+* Underscore.string is freely distributable under the terms of the MIT license.
+* Documentation: https://github.com/epeli/underscore.string
+* Some code is borrowed from MooTools and Alexandru Marasteanu.
+* Version '3.3.4'
+* @preserve
+*/
+
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.s = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)retur [...]
+var trim = require('./trim');
+var decap = require('./decapitalize');
+
+module.exports = function camelize(str, decapitalize) {
+  str = trim(str).replace(/[-_\s]+(.)?/g, function(match, c) {
+    return c ? c.toUpperCase() : '';
+  });
+
+  if (decapitalize === true) {
+    return decap(str);
+  } else {
+    return str;
+  }
+};
+
+},{"./decapitalize":10,"./trim":65}],2:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+
+module.exports = function capitalize(str, lowercaseRest) {
+  str = makeString(str);
+  var remainingChars = !lowercaseRest ? str.slice(1) : str.slice(1).toLowerCase();
+
+  return str.charAt(0).toUpperCase() + remainingChars;
+};
+
+},{"./helper/makeString":20}],3:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+
+module.exports = function chars(str) {
+  return makeString(str).split('');
+};
+
+},{"./helper/makeString":20}],4:[function(require,module,exports){
+module.exports = function chop(str, step) {
+  if (str == null) return [];
+  str = String(str);
+  step = ~~step;
+  return step > 0 ? str.match(new RegExp('.{1,' + step + '}', 'g')) : [str];
+};
+
+},{}],5:[function(require,module,exports){
+var capitalize = require('./capitalize');
+var camelize = require('./camelize');
+var makeString = require('./helper/makeString');
+
+module.exports = function classify(str) {
+  str = makeString(str);
+  return capitalize(camelize(str.replace(/[\W_]/g, ' ')).replace(/\s/g, ''));
+};
+
+},{"./camelize":1,"./capitalize":2,"./helper/makeString":20}],6:[function(require,module,exports){
+var trim = require('./trim');
+
+module.exports = function clean(str) {
+  return trim(str).replace(/\s\s+/g, ' ');
+};
+
+},{"./trim":65}],7:[function(require,module,exports){
+
+var makeString = require('./helper/makeString');
+
+var from  = 'ąàáäâãåæăćčĉęèéëêĝĥìíïîĵłľńňòóöőôõðøśșşšŝťțţŭùúüűûñÿýçżźž',
+  to    = 'aaaaaaaaaccceeeeeghiiiijllnnoooooooossssstttuuuuuunyyczzz';
+
+from += from.toUpperCase();
+to += to.toUpperCase();
+
+to = to.split('');
+
+// for tokens requireing multitoken output
+from += 'ß';
+to.push('ss');
+
+
+module.exports = function cleanDiacritics(str) {
+  return makeString(str).replace(/.{1}/g, function(c){
+    var index = from.indexOf(c);
+    return index === -1 ? c : to[index];
+  });
+};
+
+},{"./helper/makeString":20}],8:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+
+module.exports = function(str, substr) {
+  str = makeString(str);
+  substr = makeString(substr);
+
+  if (str.length === 0 || substr.length === 0) return 0;
+  
+  return str.split(substr).length - 1;
+};
+
+},{"./helper/makeString":20}],9:[function(require,module,exports){
+var trim = require('./trim');
+
+module.exports = function dasherize(str) {
+  return trim(str).replace(/([A-Z])/g, '-$1').replace(/[-_\s]+/g, '-').toLowerCase();
+};
+
+},{"./trim":65}],10:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+
+module.exports = function decapitalize(str) {
+  str = makeString(str);
+  return str.charAt(0).toLowerCase() + str.slice(1);
+};
+
+},{"./helper/makeString":20}],11:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+
+function getIndent(str) {
+  var matches = str.match(/^[\s\\t]*/gm);
+  var indent = matches[0].length;
+  
+  for (var i = 1; i < matches.length; i++) {
+    indent = Math.min(matches[i].length, indent);
+  }
+
+  return indent;
+}
+
+module.exports = function dedent(str, pattern) {
+  str = makeString(str);
+  var indent = getIndent(str);
+  var reg;
+
+  if (indent === 0) return str;
+
+  if (typeof pattern === 'string') {
+    reg = new RegExp('^' + pattern, 'gm');
+  } else {
+    reg = new RegExp('^[ \\t]{' + indent + '}', 'gm');
+  }
+
+  return str.replace(reg, '');
+};
+
+},{"./helper/makeString":20}],12:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+var toPositive = require('./helper/toPositive');
+
+module.exports = function endsWith(str, ends, position) {
+  str = makeString(str);
+  ends = '' + ends;
+  if (typeof position == 'undefined') {
+    position = str.length - ends.length;
+  } else {
+    position = Math.min(toPositive(position), str.length) - ends.length;
+  }
+  return position >= 0 && str.indexOf(ends, position) === position;
+};
+
+},{"./helper/makeString":20,"./helper/toPositive":22}],13:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+var escapeChars = require('./helper/escapeChars');
+
+var regexString = '[';
+for(var key in escapeChars) {
+  regexString += key;
+}
+regexString += ']';
+
+var regex = new RegExp( regexString, 'g');
+
+module.exports = function escapeHTML(str) {
+
+  return makeString(str).replace(regex, function(m) {
+    return '&' + escapeChars[m] + ';';
+  });
+};
+
+},{"./helper/escapeChars":17,"./helper/makeString":20}],14:[function(require,module,exports){
+module.exports = function() {
+  var result = {};
+
+  for (var prop in this) {
+    if (!this.hasOwnProperty(prop) || prop.match(/^(?:include|contains|reverse|join|map|wrap)$/)) continue;
+    result[prop] = this[prop];
+  }
+
+  return result;
+};
+
+},{}],15:[function(require,module,exports){
+var makeString = require('./makeString');
+
+module.exports = function adjacent(str, direction) {
+  str = makeString(str);
+  if (str.length === 0) {
+    return '';
+  }
+  return str.slice(0, -1) + String.fromCharCode(str.charCodeAt(str.length - 1) + direction);
+};
+
+},{"./makeString":20}],16:[function(require,module,exports){
+var escapeRegExp = require('./escapeRegExp');
+
+module.exports = function defaultToWhiteSpace(characters) {
+  if (characters == null)
+    return '\\s';
+  else if (characters.source)
+    return characters.source;
+  else
+    return '[' + escapeRegExp(characters) + ']';
+};
+
+},{"./escapeRegExp":18}],17:[function(require,module,exports){
+/* We're explicitly defining the list of entities we want to escape.
+nbsp is an HTML entity, but we don't want to escape all space characters in a string, hence its omission in this map.
+
+*/
+var escapeChars = {
+  '¢' : 'cent',
+  '£' : 'pound',
+  '¥' : 'yen',
+  '€': 'euro',
+  '©' :'copy',
+  '®' : 'reg',
+  '<' : 'lt',
+  '>' : 'gt',
+  '"' : 'quot',
+  '&' : 'amp',
+  '\'' : '#39'
+};
+
+module.exports = escapeChars;
+
+},{}],18:[function(require,module,exports){
+var makeString = require('./makeString');
+
+module.exports = function escapeRegExp(str) {
+  return makeString(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
+};
+
+},{"./makeString":20}],19:[function(require,module,exports){
+/*
+We're explicitly defining the list of entities that might see in escape HTML strings
+*/
+var htmlEntities = {
+  nbsp: ' ',
+  cent: '¢',
+  pound: '£',
+  yen: '¥',
+  euro: '€',
+  copy: '©',
+  reg: '®',
+  lt: '<',
+  gt: '>',
+  quot: '"',
+  amp: '&',
+  apos: '\''
+};
+
+module.exports = htmlEntities;
+
+},{}],20:[function(require,module,exports){
+/**
+ * Ensure some object is a coerced to a string
+ **/
+module.exports = function makeString(object) {
+  if (object == null) return '';
+  return '' + object;
+};
+
+},{}],21:[function(require,module,exports){
+module.exports = function strRepeat(str, qty){
+  if (qty < 1) return '';
+  var result = '';
+  while (qty > 0) {
+    if (qty & 1) result += str;
+    qty >>= 1, str += str;
+  }
+  return result;
+};
+
+},{}],22:[function(require,module,exports){
+module.exports = function toPositive(number) {
+  return number < 0 ? 0 : (+number || 0);
+};
+
+},{}],23:[function(require,module,exports){
+var capitalize = require('./capitalize');
+var underscored = require('./underscored');
+var trim = require('./trim');
+
+module.exports = function humanize(str) {
+  return capitalize(trim(underscored(str).replace(/_id$/, '').replace(/_/g, ' ')));
+};
+
+},{"./capitalize":2,"./trim":65,"./underscored":67}],24:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+
+module.exports = function include(str, needle) {
+  if (needle === '') return true;
+  return makeString(str).indexOf(needle) !== -1;
+};
+
+},{"./helper/makeString":20}],25:[function(require,module,exports){
+/*
+* Underscore.string
+* (c) 2010 Esa-Matti Suuronen <esa-matti aet suuronen dot org>
+* Underscore.string is freely distributable under the terms of the MIT license.
+* Documentation: https://github.com/epeli/underscore.string
+* Some code is borrowed from MooTools and Alexandru Marasteanu.
+* Version '3.3.4'
+* @preserve
+*/
+
+'use strict';
+
+function s(value) {
+  /* jshint validthis: true */
+  if (!(this instanceof s)) return new s(value);
+  this._wrapped = value;
+}
+
+s.VERSION = '3.3.4';
+
+s.isBlank          = require('./isBlank');
+s.stripTags        = require('./stripTags');
+s.capitalize       = require('./capitalize');
+s.decapitalize     = require('./decapitalize');
+s.chop             = require('./chop');
+s.trim             = require('./trim');
+s.clean            = require('./clean');
+s.cleanDiacritics  = require('./cleanDiacritics');
+s.count            = require('./count');
+s.chars            = require('./chars');
+s.swapCase         = require('./swapCase');
+s.escapeHTML       = require('./escapeHTML');
+s.unescapeHTML     = require('./unescapeHTML');
+s.splice           = require('./splice');
+s.insert           = require('./insert');
+s.replaceAll       = require('./replaceAll');
+s.include          = require('./include');
+s.join             = require('./join');
+s.lines            = require('./lines');
+s.dedent           = require('./dedent');
+s.reverse          = require('./reverse');
+s.startsWith       = require('./startsWith');
+s.endsWith         = require('./endsWith');
+s.pred             = require('./pred');
+s.succ             = require('./succ');
+s.titleize         = require('./titleize');
+s.camelize         = require('./camelize');
+s.underscored      = require('./underscored');
+s.dasherize        = require('./dasherize');
+s.classify         = require('./classify');
+s.humanize         = require('./humanize');
+s.ltrim            = require('./ltrim');
+s.rtrim            = require('./rtrim');
+s.truncate         = require('./truncate');
+s.prune            = require('./prune');
+s.words            = require('./words');
+s.pad              = require('./pad');
+s.lpad             = require('./lpad');
+s.rpad             = require('./rpad');
+s.lrpad            = require('./lrpad');
+s.sprintf          = require('./sprintf');
+s.vsprintf         = require('./vsprintf');
+s.toNumber         = require('./toNumber');
+s.numberFormat     = require('./numberFormat');
+s.strRight         = require('./strRight');
+s.strRightBack     = require('./strRightBack');
+s.strLeft          = require('./strLeft');
+s.strLeftBack      = require('./strLeftBack');
+s.toSentence       = require('./toSentence');
+s.toSentenceSerial = require('./toSentenceSerial');
+s.slugify          = require('./slugify');
+s.surround         = require('./surround');
+s.quote            = require('./quote');
+s.unquote          = require('./unquote');
+s.repeat           = require('./repeat');
+s.naturalCmp       = require('./naturalCmp');
+s.levenshtein      = require('./levenshtein');
+s.toBoolean        = require('./toBoolean');
+s.exports          = require('./exports');
+s.escapeRegExp     = require('./helper/escapeRegExp');
+s.wrap             = require('./wrap');
+s.map              = require('./map');
+
+// Aliases
+s.strip     = s.trim;
+s.lstrip    = s.ltrim;
+s.rstrip    = s.rtrim;
+s.center    = s.lrpad;
+s.rjust     = s.lpad;
+s.ljust     = s.rpad;
+s.contains  = s.include;
+s.q         = s.quote;
+s.toBool    = s.toBoolean;
+s.camelcase = s.camelize;
+s.mapChars  = s.map;
+
+
+// Implement chaining
+s.prototype = {
+  value: function value() {
+    return this._wrapped;
+  }
+};
+
+function fn2method(key, fn) {
+  if (typeof fn !== 'function') return;
+  s.prototype[key] = function() {
+    var args = [this._wrapped].concat(Array.prototype.slice.call(arguments));
+    var res = fn.apply(null, args);
+    // if the result is non-string stop the chain and return the value
+    return typeof res === 'string' ? new s(res) : res;
+  };
+}
+
+// Copy functions to instance methods for chaining
+for (var key in s) fn2method(key, s[key]);
+
+fn2method('tap', function tap(string, fn) {
+  return fn(string);
+});
+
+function prototype2method(methodName) {
+  fn2method(methodName, function(context) {
+    var args = Array.prototype.slice.call(arguments, 1);
+    return String.prototype[methodName].apply(context, args);
+  });
+}
+
+var prototypeMethods = [
+  'toUpperCase',
+  'toLowerCase',
+  'split',
+  'replace',
+  'slice',
+  'substring',
+  'substr',
+  'concat'
+];
+
+for (var method in prototypeMethods) prototype2method(prototypeMethods[method]);
+
+
+module.exports = s;
+
+},{"./camelize":1,"./capitalize":2,"./chars":3,"./chop":4,"./classify":5,"./clean":6,"./cleanDiacritics":7,"./count":8,"./dasherize":9,"./decapitalize":10,"./dedent":11,"./endsWith":12,"./escapeHTML":13,"./exports":14,"./helper/escapeRegExp":18,"./humanize":23,"./include":24,"./insert":26,"./isBlank":27,"./join":28,"./levenshtein":29,"./lines":30,"./lpad":31,"./lrpad":32,"./ltrim":33,"./map":34,"./naturalCmp":35,"./numberFormat":38,"./pad":39,"./pred":40,"./prune":41,"./quote":42,"./repe [...]
+var splice = require('./splice');
+
+module.exports = function insert(str, i, substr) {
+  return splice(str, i, 0, substr);
+};
+
+},{"./splice":49}],27:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+
+module.exports = function isBlank(str) {
+  return (/^\s*$/).test(makeString(str));
+};
+
+},{"./helper/makeString":20}],28:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+var slice = [].slice;
+
+module.exports = function join() {
+  var args = slice.call(arguments),
+    separator = args.shift();
+
+  return args.join(makeString(separator));
+};
+
+},{"./helper/makeString":20}],29:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+
+/**
+ * Based on the implementation here: https://github.com/hiddentao/fast-levenshtein
+ */
+module.exports = function levenshtein(str1, str2) {
+  'use strict';
+  str1 = makeString(str1);
+  str2 = makeString(str2);
+
+  // Short cut cases  
+  if (str1 === str2) return 0;
+  if (!str1 || !str2) return Math.max(str1.length, str2.length);
+
+  // two rows
+  var prevRow = new Array(str2.length + 1);
+
+  // initialise previous row
+  for (var i = 0; i < prevRow.length; ++i) {
+    prevRow[i] = i;
+  }
+
+  // calculate current row distance from previous row
+  for (i = 0; i < str1.length; ++i) {
+    var nextCol = i + 1;
+
+    for (var j = 0; j < str2.length; ++j) {
+      var curCol = nextCol;
+
+      // substution
+      nextCol = prevRow[j] + ( (str1.charAt(i) === str2.charAt(j)) ? 0 : 1 );
+      // insertion
+      var tmp = curCol + 1;
+      if (nextCol > tmp) {
+        nextCol = tmp;
+      }
+      // deletion
+      tmp = prevRow[j + 1] + 1;
+      if (nextCol > tmp) {
+        nextCol = tmp;
+      }
+
+      // copy current col value into previous (in preparation for next iteration)
+      prevRow[j] = curCol;
+    }
+
+    // copy last col value into previous (in preparation for next iteration)
+    prevRow[j] = nextCol;
+  }
+
+  return nextCol;
+};
+
+},{"./helper/makeString":20}],30:[function(require,module,exports){
+module.exports = function lines(str) {
+  if (str == null) return [];
+  return String(str).split(/\r\n?|\n/);
+};
+
+},{}],31:[function(require,module,exports){
+var pad = require('./pad');
+
+module.exports = function lpad(str, length, padStr) {
+  return pad(str, length, padStr);
+};
+
+},{"./pad":39}],32:[function(require,module,exports){
+var pad = require('./pad');
+
+module.exports = function lrpad(str, length, padStr) {
+  return pad(str, length, padStr, 'both');
+};
+
+},{"./pad":39}],33:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+var defaultToWhiteSpace = require('./helper/defaultToWhiteSpace');
+var nativeTrimLeft = String.prototype.trimLeft;
+
+module.exports = function ltrim(str, characters) {
+  str = makeString(str);
+  if (!characters && nativeTrimLeft) return nativeTrimLeft.call(str);
+  characters = defaultToWhiteSpace(characters);
+  return str.replace(new RegExp('^' + characters + '+'), '');
+};
+
+},{"./helper/defaultToWhiteSpace":16,"./helper/makeString":20}],34:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+
+module.exports = function(str, callback) {
+  str = makeString(str);
+
+  if (str.length === 0 || typeof callback !== 'function') return str;
+
+  return str.replace(/./g, callback);
+};
+
+},{"./helper/makeString":20}],35:[function(require,module,exports){
+module.exports = function naturalCmp(str1, str2) {
+  if (str1 == str2) return 0;
+  if (!str1) return -1;
+  if (!str2) return 1;
+
+  var cmpRegex = /(\.\d+|\d+|\D+)/g,
+    tokens1 = String(str1).match(cmpRegex),
+    tokens2 = String(str2).match(cmpRegex),
+    count = Math.min(tokens1.length, tokens2.length);
+
+  for (var i = 0; i < count; i++) {
+    var a = tokens1[i],
+      b = tokens2[i];
+
+    if (a !== b) {
+      var num1 = +a;
+      var num2 = +b;
+      if (num1 === num1 && num2 === num2) {
+        return num1 > num2 ? 1 : -1;
+      }
+      return a < b ? -1 : 1;
+    }
+  }
+
+  if (tokens1.length != tokens2.length)
+    return tokens1.length - tokens2.length;
+
+  return str1 < str2 ? -1 : 1;
+};
+
+},{}],36:[function(require,module,exports){
+(function(window) {
+    var re = {
+        not_string: /[^s]/,
+        number: /[diefg]/,
+        json: /[j]/,
+        not_json: /[^j]/,
+        text: /^[^\x25]+/,
+        modulo: /^\x25{2}/,
+        placeholder: /^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-gijosuxX])/,
+        key: /^([a-z_][a-z_\d]*)/i,
+        key_access: /^\.([a-z_][a-z_\d]*)/i,
+        index_access: /^\[(\d+)\]/,
+        sign: /^[\+\-]/
+    }
+
+    function sprintf() {
+        var key = arguments[0], cache = sprintf.cache
+        if (!(cache[key] && cache.hasOwnProperty(key))) {
+            cache[key] = sprintf.parse(key)
+        }
+        return sprintf.format.call(null, cache[key], arguments)
+    }
+
+    sprintf.format = function(parse_tree, argv) {
+        var cursor = 1, tree_length = parse_tree.length, node_type = "", arg, output = [], i, k, match, pad, pad_character, pad_length, is_positive = true, sign = ""
+        for (i = 0; i < tree_length; i++) {
+            node_type = get_type(parse_tree[i])
+            if (node_type === "string") {
+                output[output.length] = parse_tree[i]
+            }
+            else if (node_type === "array") {
+                match = parse_tree[i] // convenience purposes only
+                if (match[2]) { // keyword argument
+                    arg = argv[cursor]
+                    for (k = 0; k < match[2].length; k++) {
+                        if (!arg.hasOwnProperty(match[2][k])) {
+                            throw new Error(sprintf("[sprintf] property '%s' does not exist", match[2][k]))
+                        }
+                        arg = arg[match[2][k]]
+                    }
+                }
+                else if (match[1]) { // positional argument (explicit)
+                    arg = argv[match[1]]
+                }
+                else { // positional argument (implicit)
+                    arg = argv[cursor++]
+                }
+
+                if (get_type(arg) == "function") {
+                    arg = arg()
+                }
+
+                if (re.not_string.test(match[8]) && re.not_json.test(match[8]) && (get_type(arg) != "number" && isNaN(arg))) {
+                    throw new TypeError(sprintf("[sprintf] expecting number but found %s", get_type(arg)))
+                }
+
+                if (re.number.test(match[8])) {
+                    is_positive = arg >= 0
+                }
+
+                switch (match[8]) {
+                    case "b":
+                        arg = arg.toString(2)
+                    break
+                    case "c":
+                        arg = String.fromCharCode(arg)
+                    break
+                    case "d":
+                    case "i":
+                        arg = parseInt(arg, 10)
+                    break
+                    case "j":
+                        arg = JSON.stringify(arg, null, match[6] ? parseInt(match[6]) : 0)
+                    break
+                    case "e":
+                        arg = match[7] ? arg.toExponential(match[7]) : arg.toExponential()
+                    break
+                    case "f":
+                        arg = match[7] ? parseFloat(arg).toFixed(match[7]) : parseFloat(arg)
+                    break
+                    case "g":
+                        arg = match[7] ? parseFloat(arg).toPrecision(match[7]) : parseFloat(arg)
+                    break
+                    case "o":
+                        arg = arg.toString(8)
+                    break
+                    case "s":
+                        arg = ((arg = String(arg)) && match[7] ? arg.substring(0, match[7]) : arg)
+                    break
+                    case "u":
+                        arg = arg >>> 0
+                    break
+                    case "x":
+                        arg = arg.toString(16)
+                    break
+                    case "X":
+                        arg = arg.toString(16).toUpperCase()
+                    break
+                }
+                if (re.json.test(match[8])) {
+                    output[output.length] = arg
+                }
+                else {
+                    if (re.number.test(match[8]) && (!is_positive || match[3])) {
+                        sign = is_positive ? "+" : "-"
+                        arg = arg.toString().replace(re.sign, "")
+                    }
+                    else {
+                        sign = ""
+                    }
+                    pad_character = match[4] ? match[4] === "0" ? "0" : match[4].charAt(1) : " "
+                    pad_length = match[6] - (sign + arg).length
+                    pad = match[6] ? (pad_length > 0 ? str_repeat(pad_character, pad_length) : "") : ""
+                    output[output.length] = match[5] ? sign + arg + pad : (pad_character === "0" ? sign + pad + arg : pad + sign + arg)
+                }
+            }
+        }
+        return output.join("")
+    }
+
+    sprintf.cache = {}
+
+    sprintf.parse = function(fmt) {
+        var _fmt = fmt, match = [], parse_tree = [], arg_names = 0
+        while (_fmt) {
+            if ((match = re.text.exec(_fmt)) !== null) {
+                parse_tree[parse_tree.length] = match[0]
+            }
+            else if ((match = re.modulo.exec(_fmt)) !== null) {
+                parse_tree[parse_tree.length] = "%"
+            }
+            else if ((match = re.placeholder.exec(_fmt)) !== null) {
+                if (match[2]) {
+                    arg_names |= 1
+                    var field_list = [], replacement_field = match[2], field_match = []
+                    if ((field_match = re.key.exec(replacement_field)) !== null) {
+                        field_list[field_list.length] = field_match[1]
+                        while ((replacement_field = replacement_field.substring(field_match[0].length)) !== "") {
+                            if ((field_match = re.key_access.exec(replacement_field)) !== null) {
+                                field_list[field_list.length] = field_match[1]
+                            }
+                            else if ((field_match = re.index_access.exec(replacement_field)) !== null) {
+                                field_list[field_list.length] = field_match[1]
+                            }
+                            else {
+                                throw new SyntaxError("[sprintf] failed to parse named argument key")
+                            }
+                        }
+                    }
+                    else {
+                        throw new SyntaxError("[sprintf] failed to parse named argument key")
+                    }
+                    match[2] = field_list
+                }
+                else {
+                    arg_names |= 2
+                }
+                if (arg_names === 3) {
+                    throw new Error("[sprintf] mixing positional and named placeholders is not (yet) supported")
+                }
+                parse_tree[parse_tree.length] = match
+            }
+            else {
+                throw new SyntaxError("[sprintf] unexpected placeholder")
+            }
+            _fmt = _fmt.substring(match[0].length)
+        }
+        return parse_tree
+    }
+
+    var vsprintf = function(fmt, argv, _argv) {
+        _argv = (argv || []).slice(0)
+        _argv.splice(0, 0, fmt)
+        return sprintf.apply(null, _argv)
+    }
+
+    /**
+     * helpers
+     */
+    function get_type(variable) {
+        return Object.prototype.toString.call(variable).slice(8, -1).toLowerCase()
+    }
+
+    function str_repeat(input, multiplier) {
+        return Array(multiplier + 1).join(input)
+    }
+
+    /**
+     * export to either browser or node.js
+     */
+    if (typeof exports !== "undefined") {
+        exports.sprintf = sprintf
+        exports.vsprintf = vsprintf
+    }
+    else {
+        window.sprintf = sprintf
+        window.vsprintf = vsprintf
+
+        if (typeof define === "function" && define.amd) {
+            define(function() {
+                return {
+                    sprintf: sprintf,
+                    vsprintf: vsprintf
+                }
+            })
+        }
+    }
+})(typeof window === "undefined" ? this : window);
+
+},{}],37:[function(require,module,exports){
+(function (global){
+
+/**
+ * Module exports.
+ */
+
+module.exports = deprecate;
+
+/**
+ * Mark that a method should not be used.
+ * Returns a modified function which warns once by default.
+ *
+ * If `localStorage.noDeprecation = true` is set, then it is a no-op.
+ *
+ * If `localStorage.throwDeprecation = true` is set, then deprecated functions
+ * will throw an Error when invoked.
+ *
+ * If `localStorage.traceDeprecation = true` is set, then deprecated functions
+ * will invoke `console.trace()` instead of `console.error()`.
+ *
+ * @param {Function} fn - the function to deprecate
+ * @param {String} msg - the string to print to the console when `fn` is invoked
+ * @returns {Function} a new "deprecated" version of `fn`
+ * @api public
+ */
+
+function deprecate (fn, msg) {
+  if (config('noDeprecation')) {
+    return fn;
+  }
+
+  var warned = false;
+  function deprecated() {
+    if (!warned) {
+      if (config('throwDeprecation')) {
+        throw new Error(msg);
+      } else if (config('traceDeprecation')) {
+        console.trace(msg);
+      } else {
+        console.warn(msg);
+      }
+      warned = true;
+    }
+    return fn.apply(this, arguments);
+  }
+
+  return deprecated;
+}
+
+/**
+ * Checks `localStorage` for boolean values for the given `name`.
+ *
+ * @param {String} name
+ * @returns {Boolean}
+ * @api private
+ */
+
+function config (name) {
+  // accessing global.localStorage can trigger a DOMException in sandboxed iframes
+  try {
+    if (!global.localStorage) return false;
+  } catch (_) {
+    return false;
+  }
+  var val = global.localStorage[name];
+  if (null == val) return false;
+  return String(val).toLowerCase() === 'true';
+}
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{}],38:[function(require,module,exports){
+module.exports = function numberFormat(number, dec, dsep, tsep) {
+  if (isNaN(number) || number == null) return '';
+
+  number = number.toFixed(~~dec);
+  tsep = typeof tsep == 'string' ? tsep : ',';
+
+  var parts = number.split('.'),
+    fnums = parts[0],
+    decimals = parts[1] ? (dsep || '.') + parts[1] : '';
+
+  return fnums.replace(/(\d)(?=(?:\d{3})+$)/g, '$1' + tsep) + decimals;
+};
+
+},{}],39:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+var strRepeat = require('./helper/strRepeat');
+
+module.exports = function pad(str, length, padStr, type) {
+  str = makeString(str);
+  length = ~~length;
+
+  var padlen = 0;
+
+  if (!padStr)
+    padStr = ' ';
+  else if (padStr.length > 1)
+    padStr = padStr.charAt(0);
+
+  switch (type) {
+  case 'right':
+    padlen = length - str.length;
+    return str + strRepeat(padStr, padlen);
+  case 'both':
+    padlen = length - str.length;
+    return strRepeat(padStr, Math.ceil(padlen / 2)) + str + strRepeat(padStr, Math.floor(padlen / 2));
+  default: // 'left'
+    padlen = length - str.length;
+    return strRepeat(padStr, padlen) + str;
+  }
+};
+
+},{"./helper/makeString":20,"./helper/strRepeat":21}],40:[function(require,module,exports){
+var adjacent = require('./helper/adjacent');
+
+module.exports = function succ(str) {
+  return adjacent(str, -1);
+};
+
+},{"./helper/adjacent":15}],41:[function(require,module,exports){
+/**
+ * _s.prune: a more elegant version of truncate
+ * prune extra chars, never leaving a half-chopped word.
+ * @author github.com/rwz
+ */
+var makeString = require('./helper/makeString');
+var rtrim = require('./rtrim');
+
+module.exports = function prune(str, length, pruneStr) {
+  str = makeString(str);
+  length = ~~length;
+  pruneStr = pruneStr != null ? String(pruneStr) : '...';
+
+  if (str.length <= length) return str;
+
+  var tmpl = function(c) {
+      return c.toUpperCase() !== c.toLowerCase() ? 'A' : ' ';
+    },
+    template = str.slice(0, length + 1).replace(/.(?=\W*\w*$)/g, tmpl); // 'Hello, world' -> 'HellAA AAAAA'
+
+  if (template.slice(template.length - 2).match(/\w\w/))
+    template = template.replace(/\s*\S+$/, '');
+  else
+    template = rtrim(template.slice(0, template.length - 1));
+
+  return (template + pruneStr).length > str.length ? str : str.slice(0, template.length) + pruneStr;
+};
+
+},{"./helper/makeString":20,"./rtrim":47}],42:[function(require,module,exports){
+var surround = require('./surround');
+
+module.exports = function quote(str, quoteChar) {
+  return surround(str, quoteChar || '"');
+};
+
+},{"./surround":58}],43:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+var strRepeat = require('./helper/strRepeat');
+
+module.exports = function repeat(str, qty, separator) {
+  str = makeString(str);
+
+  qty = ~~qty;
+
+  // using faster implementation if separator is not needed;
+  if (separator == null) return strRepeat(str, qty);
+
+  // this one is about 300x slower in Google Chrome
+  /*eslint no-empty: 0*/
+  for (var repeat = []; qty > 0; repeat[--qty] = str) {}
+  return repeat.join(separator);
+};
+
+},{"./helper/makeString":20,"./helper/strRepeat":21}],44:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+
+module.exports = function replaceAll(str, find, replace, ignorecase) {
+  var flags = (ignorecase === true)?'gi':'g';
+  var reg = new RegExp(find, flags);
+
+  return makeString(str).replace(reg, replace);
+};
+
+},{"./helper/makeString":20}],45:[function(require,module,exports){
+var chars = require('./chars');
+
+module.exports = function reverse(str) {
+  return chars(str).reverse().join('');
+};
+
+},{"./chars":3}],46:[function(require,module,exports){
+var pad = require('./pad');
+
+module.exports = function rpad(str, length, padStr) {
+  return pad(str, length, padStr, 'right');
+};
+
+},{"./pad":39}],47:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+var defaultToWhiteSpace = require('./helper/defaultToWhiteSpace');
+var nativeTrimRight = String.prototype.trimRight;
+
+module.exports = function rtrim(str, characters) {
+  str = makeString(str);
+  if (!characters && nativeTrimRight) return nativeTrimRight.call(str);
+  characters = defaultToWhiteSpace(characters);
+  return str.replace(new RegExp(characters + '+$'), '');
+};
+
+},{"./helper/defaultToWhiteSpace":16,"./helper/makeString":20}],48:[function(require,module,exports){
+var trim = require('./trim');
+var dasherize = require('./dasherize');
+var cleanDiacritics = require('./cleanDiacritics');
+
+module.exports = function slugify(str) {
+  return trim(dasherize(cleanDiacritics(str).replace(/[^\w\s-]/g, '-').toLowerCase()), '-');
+};
+
+},{"./cleanDiacritics":7,"./dasherize":9,"./trim":65}],49:[function(require,module,exports){
+var chars = require('./chars');
+
+module.exports = function splice(str, i, howmany, substr) {
+  var arr = chars(str);
+  arr.splice(~~i, ~~howmany, substr);
+  return arr.join('');
+};
+
+},{"./chars":3}],50:[function(require,module,exports){
+var deprecate = require('util-deprecate');
+
+module.exports = deprecate(require('sprintf-js').sprintf,
+  'sprintf() will be removed in the next major release, use the sprintf-js package instead.');
+
+},{"sprintf-js":36,"util-deprecate":37}],51:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+var toPositive = require('./helper/toPositive');
+
+module.exports = function startsWith(str, starts, position) {
+  str = makeString(str);
+  starts = '' + starts;
+  position = position == null ? 0 : Math.min(toPositive(position), str.length);
+  return str.lastIndexOf(starts, position) === position;
+};
+
+},{"./helper/makeString":20,"./helper/toPositive":22}],52:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+
+module.exports = function strLeft(str, sep) {
+  str = makeString(str);
+  sep = makeString(sep);
+  var pos = !sep ? -1 : str.indexOf(sep);
+  return~ pos ? str.slice(0, pos) : str;
+};
+
+},{"./helper/makeString":20}],53:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+
+module.exports = function strLeftBack(str, sep) {
+  str = makeString(str);
+  sep = makeString(sep);
+  var pos = str.lastIndexOf(sep);
+  return~ pos ? str.slice(0, pos) : str;
+};
+
+},{"./helper/makeString":20}],54:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+
+module.exports = function strRight(str, sep) {
+  str = makeString(str);
+  sep = makeString(sep);
+  var pos = !sep ? -1 : str.indexOf(sep);
+  return~ pos ? str.slice(pos + sep.length, str.length) : str;
+};
+
+},{"./helper/makeString":20}],55:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+
+module.exports = function strRightBack(str, sep) {
+  str = makeString(str);
+  sep = makeString(sep);
+  var pos = !sep ? -1 : str.lastIndexOf(sep);
+  return~ pos ? str.slice(pos + sep.length, str.length) : str;
+};
+
+},{"./helper/makeString":20}],56:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+
+module.exports = function stripTags(str) {
+  return makeString(str).replace(/<\/?[^>]+>/g, '');
+};
+
+},{"./helper/makeString":20}],57:[function(require,module,exports){
+var adjacent = require('./helper/adjacent');
+
+module.exports = function succ(str) {
+  return adjacent(str, 1);
+};
+
+},{"./helper/adjacent":15}],58:[function(require,module,exports){
+module.exports = function surround(str, wrapper) {
+  return [wrapper, str, wrapper].join('');
+};
+
+},{}],59:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+
+module.exports = function swapCase(str) {
+  return makeString(str).replace(/\S/g, function(c) {
+    return c === c.toUpperCase() ? c.toLowerCase() : c.toUpperCase();
+  });
+};
+
+},{"./helper/makeString":20}],60:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+
+module.exports = function titleize(str) {
+  return makeString(str).toLowerCase().replace(/(?:^|\s|-)\S/g, function(c) {
+    return c.toUpperCase();
+  });
+};
+
+},{"./helper/makeString":20}],61:[function(require,module,exports){
+var trim = require('./trim');
+
+function boolMatch(s, matchers) {
+  var i, matcher, down = s.toLowerCase();
+  matchers = [].concat(matchers);
+  for (i = 0; i < matchers.length; i += 1) {
+    matcher = matchers[i];
+    if (!matcher) continue;
+    if (matcher.test && matcher.test(s)) return true;
+    if (matcher.toLowerCase() === down) return true;
+  }
+}
+
+module.exports = function toBoolean(str, trueValues, falseValues) {
+  if (typeof str === 'number') str = '' + str;
+  if (typeof str !== 'string') return !!str;
+  str = trim(str);
+  if (boolMatch(str, trueValues || ['true', '1'])) return true;
+  if (boolMatch(str, falseValues || ['false', '0'])) return false;
+};
+
+},{"./trim":65}],62:[function(require,module,exports){
+module.exports = function toNumber(num, precision) {
+  if (num == null) return 0;
+  var factor = Math.pow(10, isFinite(precision) ? precision : 0);
+  return Math.round(num * factor) / factor;
+};
+
+},{}],63:[function(require,module,exports){
+var rtrim = require('./rtrim');
+
+module.exports = function toSentence(array, separator, lastSeparator, serial) {
+  separator = separator || ', ';
+  lastSeparator = lastSeparator || ' and ';
+  var a = array.slice(),
+    lastMember = a.pop();
+
+  if (array.length > 2 && serial) lastSeparator = rtrim(separator) + lastSeparator;
+
+  return a.length ? a.join(separator) + lastSeparator + lastMember : lastMember;
+};
+
+},{"./rtrim":47}],64:[function(require,module,exports){
+var toSentence = require('./toSentence');
+
+module.exports = function toSentenceSerial(array, sep, lastSep) {
+  return toSentence(array, sep, lastSep, true);
+};
+
+},{"./toSentence":63}],65:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+var defaultToWhiteSpace = require('./helper/defaultToWhiteSpace');
+var nativeTrim = String.prototype.trim;
+
+module.exports = function trim(str, characters) {
+  str = makeString(str);
+  if (!characters && nativeTrim) return nativeTrim.call(str);
+  characters = defaultToWhiteSpace(characters);
+  return str.replace(new RegExp('^' + characters + '+|' + characters + '+$', 'g'), '');
+};
+
+},{"./helper/defaultToWhiteSpace":16,"./helper/makeString":20}],66:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+
+module.exports = function truncate(str, length, truncateStr) {
+  str = makeString(str);
+  truncateStr = truncateStr || '...';
+  length = ~~length;
+  return str.length > length ? str.slice(0, length) + truncateStr : str;
+};
+
+},{"./helper/makeString":20}],67:[function(require,module,exports){
+var trim = require('./trim');
+
+module.exports = function underscored(str) {
+  return trim(str).replace(/([a-z\d])([A-Z]+)/g, '$1_$2').replace(/[-\s]+/g, '_').toLowerCase();
+};
+
+},{"./trim":65}],68:[function(require,module,exports){
+var makeString = require('./helper/makeString');
+var htmlEntities = require('./helper/htmlEntities');
+
+module.exports = function unescapeHTML(str) {
+  return makeString(str).replace(/\&([^;]+);/g, function(entity, entityCode) {
+    var match;
+
+    if (entityCode in htmlEntities) {
+      return htmlEntities[entityCode];
+    /*eslint no-cond-assign: 0*/
+    } else if (match = entityCode.match(/^#x([\da-fA-F]+)$/)) {
+      return String.fromCharCode(parseInt(match[1], 16));
+    /*eslint no-cond-assign: 0*/
+    } else if (match = entityCode.match(/^#(\d+)$/)) {
+      return String.fromCharCode(~~match[1]);
+    } else {
+      return entity;
+    }
+  });
+};
+
+},{"./helper/htmlEntities":19,"./helper/makeString":20}],69:[function(require,module,exports){
+module.exports = function unquote(str, quoteChar) {
+  quoteChar = quoteChar || '"';
+  if (str[0] === quoteChar && str[str.length - 1] === quoteChar)
+    return str.slice(1, str.length - 1);
+  else return str;
+};
+
+},{}],70:[function(require,module,exports){
+var deprecate = require('util-deprecate');
+
+module.exports = deprecate(require('sprintf-js').vsprintf,
+  'vsprintf() will be removed in the next major release, use the sprintf-js package instead.');
+
+},{"sprintf-js":36,"util-deprecate":37}],71:[function(require,module,exports){
+var isBlank = require('./isBlank');
+var trim = require('./trim');
+
+module.exports = function words(str, delimiter) {
+  if (isBlank(str)) return [];
+  return trim(str, delimiter).split(delimiter || /\s+/);
+};
+
+},{"./isBlank":27,"./trim":65}],72:[function(require,module,exports){
+// Wrap
+// wraps a string by a certain width
+
+var makeString = require('./helper/makeString');
+
+module.exports = function wrap(str, options){
+  str = makeString(str);
+  
+  options = options || {};
+  
+  var width = options.width || 75;
+  var seperator = options.seperator || '\n';
+  var cut = options.cut || false;
+  var preserveSpaces = options.preserveSpaces || false;
+  var trailingSpaces = options.trailingSpaces || false;
+  
+  var result;
+  
+  if(width <= 0){
+    return str;
+  }
+  
+  else if(!cut){
+  
+    var words = str.split(' ');
+    var current_column = 0;
+    result = '';
+  
+    while(words.length > 0){
+      
+      // if adding a space and the next word would cause this line to be longer than width...
+      if(1 + words[0].length + current_column > width){
+        //start a new line if this line is not already empty
+        if(current_column > 0){
+          // add a space at the end of the line is preserveSpaces is true
+          if (preserveSpaces){
+            result += ' ';
+            current_column++;
+          }
+          // fill the rest of the line with spaces if trailingSpaces option is true
+          else if(trailingSpaces){
+            while(current_column < width){
+              result += ' ';
+              current_column++;
+            }            
+          }
+          //start new line
+          result += seperator;
+          current_column = 0;
+        }
+      }
+  
+      // if not at the begining of the line, add a space in front of the word
+      if(current_column > 0){
+        result += ' ';
+        current_column++;
+      }
+  
+      // tack on the next word, update current column, a pop words array
+      result += words[0];
+      current_column += words[0].length;
+      words.shift();
+  
+    }
+  
+    // fill the rest of the line with spaces if trailingSpaces option is true
+    if(trailingSpaces){
+      while(current_column < width){
+        result += ' ';
+        current_column++;
+      }            
+    }
+  
+    return result;
+  
+  }
+  
+  else {
+  
+    var index = 0;
+    result = '';
+  
+    // walk through each character and add seperators where appropriate
+    while(index < str.length){
+      if(index % width == 0 && index > 0){
+        result += seperator;
+      }
+      result += str.charAt(index);
+      index++;
+    }
+  
+    // fill the rest of the line with spaces if trailingSpaces option is true
+    if(trailingSpaces){
+      while(index % width > 0){
+        result += ' ';
+        index++;
+      }            
+    }
+    
+    return result;
+  }
+};
+
+},{"./helper/makeString":20}]},{},[25])(25)
+});
\ No newline at end of file
diff --git a/dist/underscore.string.min.js b/dist/underscore.string.min.js
new file mode 100644
index 0000000..785c03d
--- /dev/null
+++ b/dist/underscore.string.min.js
@@ -0,0 +1,19 @@
+/*
+* Underscore.string
+* (c) 2010 Esa-Matti Suuronen <esa-matti aet suuronen dot org>
+* Underscore.string is freely distributable under the terms of the MIT license.
+* Documentation: https://github.com/epeli/underscore.string
+* Some code is borrowed from MooTools and Alexandru Marasteanu.
+* Version '3.3.4'
+* @preserve
+*/
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.s=f()}})(function(){var define,module,exports;return function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i [...]
+* Underscore.string
+* (c) 2010 Esa-Matti Suuronen <esa-matti aet suuronen dot org>
+* Underscore.string is freely distributable under the terms of the MIT license.
+* Documentation: https://github.com/epeli/underscore.string
+* Some code is borrowed from MooTools and Alexandru Marasteanu.
+* Version '3.3.4'
+* @preserve
+*/
+"use strict";function s(value){if(!(this instanceof s))return new s(value);this._wrapped=value}s.VERSION="3.3.4";s.isBlank=require("./isBlank");s.stripTags=require("./stripTags");s.capitalize=require("./capitalize");s.decapitalize=require("./decapitalize");s.chop=require("./chop");s.trim=require("./trim");s.clean=require("./clean");s.cleanDiacritics=require("./cleanDiacritics");s.count=require("./count");s.chars=require("./chars");s.swapCase=require("./swapCase");s.escapeHTML=require("./ [...]
\ No newline at end of file
diff --git a/endsWith.js b/endsWith.js
new file mode 100644
index 0000000..c452603
--- /dev/null
+++ b/endsWith.js
@@ -0,0 +1,13 @@
+var makeString = require('./helper/makeString');
+var toPositive = require('./helper/toPositive');
+
+module.exports = function endsWith(str, ends, position) {
+  str = makeString(str);
+  ends = '' + ends;
+  if (typeof position == 'undefined') {
+    position = str.length - ends.length;
+  } else {
+    position = Math.min(toPositive(position), str.length) - ends.length;
+  }
+  return position >= 0 && str.indexOf(ends, position) === position;
+};
diff --git a/escapeHTML.js b/escapeHTML.js
new file mode 100644
index 0000000..808e2f0
--- /dev/null
+++ b/escapeHTML.js
@@ -0,0 +1,17 @@
+var makeString = require('./helper/makeString');
+var escapeChars = require('./helper/escapeChars');
+
+var regexString = '[';
+for(var key in escapeChars) {
+  regexString += key;
+}
+regexString += ']';
+
+var regex = new RegExp( regexString, 'g');
+
+module.exports = function escapeHTML(str) {
+
+  return makeString(str).replace(regex, function(m) {
+    return '&' + escapeChars[m] + ';';
+  });
+};
diff --git a/exports.js b/exports.js
new file mode 100644
index 0000000..62e0732
--- /dev/null
+++ b/exports.js
@@ -0,0 +1,10 @@
+module.exports = function() {
+  var result = {};
+
+  for (var prop in this) {
+    if (!this.hasOwnProperty(prop) || prop.match(/^(?:include|contains|reverse|join|map|wrap)$/)) continue;
+    result[prop] = this[prop];
+  }
+
+  return result;
+};
diff --git a/helper/adjacent.js b/helper/adjacent.js
new file mode 100644
index 0000000..bd26013
--- /dev/null
+++ b/helper/adjacent.js
@@ -0,0 +1,9 @@
+var makeString = require('./makeString');
+
+module.exports = function adjacent(str, direction) {
+  str = makeString(str);
+  if (str.length === 0) {
+    return '';
+  }
+  return str.slice(0, -1) + String.fromCharCode(str.charCodeAt(str.length - 1) + direction);
+};
diff --git a/helper/defaultToWhiteSpace.js b/helper/defaultToWhiteSpace.js
new file mode 100644
index 0000000..0cd9f06
--- /dev/null
+++ b/helper/defaultToWhiteSpace.js
@@ -0,0 +1,10 @@
+var escapeRegExp = require('./escapeRegExp');
+
+module.exports = function defaultToWhiteSpace(characters) {
+  if (characters == null)
+    return '\\s';
+  else if (characters.source)
+    return characters.source;
+  else
+    return '[' + escapeRegExp(characters) + ']';
+};
diff --git a/helper/escapeChars.js b/helper/escapeChars.js
new file mode 100644
index 0000000..862fb44
--- /dev/null
+++ b/helper/escapeChars.js
@@ -0,0 +1,19 @@
+/* We're explicitly defining the list of entities we want to escape.
+nbsp is an HTML entity, but we don't want to escape all space characters in a string, hence its omission in this map.
+
+*/
+var escapeChars = {
+  '¢' : 'cent',
+  '£' : 'pound',
+  '¥' : 'yen',
+  '€': 'euro',
+  '©' :'copy',
+  '®' : 'reg',
+  '<' : 'lt',
+  '>' : 'gt',
+  '"' : 'quot',
+  '&' : 'amp',
+  '\'' : '#39'
+};
+
+module.exports = escapeChars;
diff --git a/helper/escapeRegExp.js b/helper/escapeRegExp.js
new file mode 100644
index 0000000..01097fb
--- /dev/null
+++ b/helper/escapeRegExp.js
@@ -0,0 +1,5 @@
+var makeString = require('./makeString');
+
+module.exports = function escapeRegExp(str) {
+  return makeString(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
+};
diff --git a/helper/htmlEntities.js b/helper/htmlEntities.js
new file mode 100644
index 0000000..4b78d74
--- /dev/null
+++ b/helper/htmlEntities.js
@@ -0,0 +1,19 @@
+/*
+We're explicitly defining the list of entities that might see in escape HTML strings
+*/
+var htmlEntities = {
+  nbsp: ' ',
+  cent: '¢',
+  pound: '£',
+  yen: '¥',
+  euro: '€',
+  copy: '©',
+  reg: '®',
+  lt: '<',
+  gt: '>',
+  quot: '"',
+  amp: '&',
+  apos: '\''
+};
+
+module.exports = htmlEntities;
diff --git a/helper/makeString.js b/helper/makeString.js
new file mode 100644
index 0000000..3b279ab
--- /dev/null
+++ b/helper/makeString.js
@@ -0,0 +1,7 @@
+/**
+ * Ensure some object is a coerced to a string
+ **/
+module.exports = function makeString(object) {
+  if (object == null) return '';
+  return '' + object;
+};
diff --git a/helper/strRepeat.js b/helper/strRepeat.js
new file mode 100644
index 0000000..b60d876
--- /dev/null
+++ b/helper/strRepeat.js
@@ -0,0 +1,9 @@
+module.exports = function strRepeat(str, qty){
+  if (qty < 1) return '';
+  var result = '';
+  while (qty > 0) {
+    if (qty & 1) result += str;
+    qty >>= 1, str += str;
+  }
+  return result;
+};
diff --git a/helper/toPositive.js b/helper/toPositive.js
new file mode 100644
index 0000000..6dda0a3
--- /dev/null
+++ b/helper/toPositive.js
@@ -0,0 +1,3 @@
+module.exports = function toPositive(number) {
+  return number < 0 ? 0 : (+number || 0);
+};
diff --git a/humanize.js b/humanize.js
new file mode 100644
index 0000000..8f82d07
--- /dev/null
+++ b/humanize.js
@@ -0,0 +1,7 @@
+var capitalize = require('./capitalize');
+var underscored = require('./underscored');
+var trim = require('./trim');
+
+module.exports = function humanize(str) {
+  return capitalize(trim(underscored(str).replace(/_id$/, '').replace(/_/g, ' ')));
+};
diff --git a/include.js b/include.js
new file mode 100644
index 0000000..a2e910f
--- /dev/null
+++ b/include.js
@@ -0,0 +1,6 @@
+var makeString = require('./helper/makeString');
+
+module.exports = function include(str, needle) {
+  if (needle === '') return true;
+  return makeString(str).indexOf(needle) !== -1;
+};
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..3b91962
--- /dev/null
+++ b/index.js
@@ -0,0 +1,143 @@
+/*
+* Underscore.string
+* (c) 2010 Esa-Matti Suuronen <esa-matti aet suuronen dot org>
+* Underscore.string is freely distributable under the terms of the MIT license.
+* Documentation: https://github.com/epeli/underscore.string
+* Some code is borrowed from MooTools and Alexandru Marasteanu.
+* Version '3.3.4'
+* @preserve
+*/
+
+'use strict';
+
+function s(value) {
+  /* jshint validthis: true */
+  if (!(this instanceof s)) return new s(value);
+  this._wrapped = value;
+}
+
+s.VERSION = '3.3.4';
+
+s.isBlank          = require('./isBlank');
+s.stripTags        = require('./stripTags');
+s.capitalize       = require('./capitalize');
+s.decapitalize     = require('./decapitalize');
+s.chop             = require('./chop');
+s.trim             = require('./trim');
+s.clean            = require('./clean');
+s.cleanDiacritics  = require('./cleanDiacritics');
+s.count            = require('./count');
+s.chars            = require('./chars');
+s.swapCase         = require('./swapCase');
+s.escapeHTML       = require('./escapeHTML');
+s.unescapeHTML     = require('./unescapeHTML');
+s.splice           = require('./splice');
+s.insert           = require('./insert');
+s.replaceAll       = require('./replaceAll');
+s.include          = require('./include');
+s.join             = require('./join');
+s.lines            = require('./lines');
+s.dedent           = require('./dedent');
+s.reverse          = require('./reverse');
+s.startsWith       = require('./startsWith');
+s.endsWith         = require('./endsWith');
+s.pred             = require('./pred');
+s.succ             = require('./succ');
+s.titleize         = require('./titleize');
+s.camelize         = require('./camelize');
+s.underscored      = require('./underscored');
+s.dasherize        = require('./dasherize');
+s.classify         = require('./classify');
+s.humanize         = require('./humanize');
+s.ltrim            = require('./ltrim');
+s.rtrim            = require('./rtrim');
+s.truncate         = require('./truncate');
+s.prune            = require('./prune');
+s.words            = require('./words');
+s.pad              = require('./pad');
+s.lpad             = require('./lpad');
+s.rpad             = require('./rpad');
+s.lrpad            = require('./lrpad');
+s.sprintf          = require('./sprintf');
+s.vsprintf         = require('./vsprintf');
+s.toNumber         = require('./toNumber');
+s.numberFormat     = require('./numberFormat');
+s.strRight         = require('./strRight');
+s.strRightBack     = require('./strRightBack');
+s.strLeft          = require('./strLeft');
+s.strLeftBack      = require('./strLeftBack');
+s.toSentence       = require('./toSentence');
+s.toSentenceSerial = require('./toSentenceSerial');
+s.slugify          = require('./slugify');
+s.surround         = require('./surround');
+s.quote            = require('./quote');
+s.unquote          = require('./unquote');
+s.repeat           = require('./repeat');
+s.naturalCmp       = require('./naturalCmp');
+s.levenshtein      = require('./levenshtein');
+s.toBoolean        = require('./toBoolean');
+s.exports          = require('./exports');
+s.escapeRegExp     = require('./helper/escapeRegExp');
+s.wrap             = require('./wrap');
+s.map              = require('./map');
+
+// Aliases
+s.strip     = s.trim;
+s.lstrip    = s.ltrim;
+s.rstrip    = s.rtrim;
+s.center    = s.lrpad;
+s.rjust     = s.lpad;
+s.ljust     = s.rpad;
+s.contains  = s.include;
+s.q         = s.quote;
+s.toBool    = s.toBoolean;
+s.camelcase = s.camelize;
+s.mapChars  = s.map;
+
+
+// Implement chaining
+s.prototype = {
+  value: function value() {
+    return this._wrapped;
+  }
+};
+
+function fn2method(key, fn) {
+  if (typeof fn !== 'function') return;
+  s.prototype[key] = function() {
+    var args = [this._wrapped].concat(Array.prototype.slice.call(arguments));
+    var res = fn.apply(null, args);
+    // if the result is non-string stop the chain and return the value
+    return typeof res === 'string' ? new s(res) : res;
+  };
+}
+
+// Copy functions to instance methods for chaining
+for (var key in s) fn2method(key, s[key]);
+
+fn2method('tap', function tap(string, fn) {
+  return fn(string);
+});
+
+function prototype2method(methodName) {
+  fn2method(methodName, function(context) {
+    var args = Array.prototype.slice.call(arguments, 1);
+    return String.prototype[methodName].apply(context, args);
+  });
+}
+
+var prototypeMethods = [
+  'toUpperCase',
+  'toLowerCase',
+  'split',
+  'replace',
+  'slice',
+  'substring',
+  'substr',
+  'concat'
+];
+
+for (var method in prototypeMethods) prototype2method(prototypeMethods[method]);
+
+
+module.exports = s;
diff --git a/insert.js b/insert.js
new file mode 100644
index 0000000..1c99c3b
--- /dev/null
+++ b/insert.js
@@ -0,0 +1,5 @@
+var splice = require('./splice');
+
+module.exports = function insert(str, i, substr) {
+  return splice(str, i, 0, substr);
+};
diff --git a/isBlank.js b/isBlank.js
new file mode 100644
index 0000000..386e819
--- /dev/null
+++ b/isBlank.js
@@ -0,0 +1,5 @@
+var makeString = require('./helper/makeString');
+
+module.exports = function isBlank(str) {
+  return (/^\s*$/).test(makeString(str));
+};
diff --git a/join.js b/join.js
new file mode 100644
index 0000000..b1a18ca
--- /dev/null
+++ b/join.js
@@ -0,0 +1,9 @@
+var makeString = require('./helper/makeString');
+var slice = [].slice;
+
+module.exports = function join() {
+  var args = slice.call(arguments),
+    separator = args.shift();
+
+  return args.join(makeString(separator));
+};
diff --git a/levenshtein.js b/levenshtein.js
new file mode 100644
index 0000000..85f220c
--- /dev/null
+++ b/levenshtein.js
@@ -0,0 +1,52 @@
+var makeString = require('./helper/makeString');
+
+/**
+ * Based on the implementation here: https://github.com/hiddentao/fast-levenshtein
+ */
+module.exports = function levenshtein(str1, str2) {
+  'use strict';
+  str1 = makeString(str1);
+  str2 = makeString(str2);
+
+  // Short cut cases  
+  if (str1 === str2) return 0;
+  if (!str1 || !str2) return Math.max(str1.length, str2.length);
+
+  // two rows
+  var prevRow = new Array(str2.length + 1);
+
+  // initialise previous row
+  for (var i = 0; i < prevRow.length; ++i) {
+    prevRow[i] = i;
+  }
+
+  // calculate current row distance from previous row
+  for (i = 0; i < str1.length; ++i) {
+    var nextCol = i + 1;
+
+    for (var j = 0; j < str2.length; ++j) {
+      var curCol = nextCol;
+
+      // substution
+      nextCol = prevRow[j] + ( (str1.charAt(i) === str2.charAt(j)) ? 0 : 1 );
+      // insertion
+      var tmp = curCol + 1;
+      if (nextCol > tmp) {
+        nextCol = tmp;
+      }
+      // deletion
+      tmp = prevRow[j + 1] + 1;
+      if (nextCol > tmp) {
+        nextCol = tmp;
+      }
+
+      // copy current col value into previous (in preparation for next iteration)
+      prevRow[j] = curCol;
+    }
+
+    // copy last col value into previous (in preparation for next iteration)
+    prevRow[j] = nextCol;
+  }
+
+  return nextCol;
+};
diff --git a/lib/underscore.string.js b/lib/underscore.string.js
deleted file mode 100644
index 8761117..0000000
--- a/lib/underscore.string.js
+++ /dev/null
@@ -1,673 +0,0 @@
-//  Underscore.string
-//  (c) 2010 Esa-Matti Suuronen <esa-matti aet suuronen dot org>
-//  Underscore.string is freely distributable under the terms of the MIT license.
-//  Documentation: https://github.com/epeli/underscore.string
-//  Some code is borrowed from MooTools and Alexandru Marasteanu.
-//  Version '2.3.2'
-
-!function(root, String){
-  'use strict';
-
-  // Defining helper functions.
-
-  var nativeTrim = String.prototype.trim;
-  var nativeTrimRight = String.prototype.trimRight;
-  var nativeTrimLeft = String.prototype.trimLeft;
-
-  var parseNumber = function(source) { return source * 1 || 0; };
-
-  var strRepeat = function(str, qty){
-    if (qty < 1) return '';
-    var result = '';
-    while (qty > 0) {
-      if (qty & 1) result += str;
-      qty >>= 1, str += str;
-    }
-    return result;
-  };
-
-  var slice = [].slice;
-
-  var defaultToWhiteSpace = function(characters) {
-    if (characters == null)
-      return '\\s';
-    else if (characters.source)
-      return characters.source;
-    else
-      return '[' + _s.escapeRegExp(characters) + ']';
-  };
-
-  // Helper for toBoolean
-  function boolMatch(s, matchers) {
-    var i, matcher, down = s.toLowerCase();
-    matchers = [].concat(matchers);
-    for (i = 0; i < matchers.length; i += 1) {
-      matcher = matchers[i];
-      if (!matcher) continue;
-      if (matcher.test && matcher.test(s)) return true;
-      if (matcher.toLowerCase() === down) return true;
-    }
-  }
-
-  var escapeChars = {
-    lt: '<',
-    gt: '>',
-    quot: '"',
-    amp: '&',
-    apos: "'"
-  };
-
-  var reversedEscapeChars = {};
-  for(var key in escapeChars) reversedEscapeChars[escapeChars[key]] = key;
-  reversedEscapeChars["'"] = '#39';
-
-  // sprintf() for JavaScript 0.7-beta1
-  // http://www.diveintojavascript.com/projects/javascript-sprintf
-  //
-  // Copyright (c) Alexandru Marasteanu <alexaholic [at) gmail (dot] com>
-  // All rights reserved.
-
-  var sprintf = (function() {
-    function get_type(variable) {
-      return Object.prototype.toString.call(variable).slice(8, -1).toLowerCase();
-    }
-
-    var str_repeat = strRepeat;
-
-    var str_format = function() {
-      if (!str_format.cache.hasOwnProperty(arguments[0])) {
-        str_format.cache[arguments[0]] = str_format.parse(arguments[0]);
-      }
-      return str_format.format.call(null, str_format.cache[arguments[0]], arguments);
-    };
-
-    str_format.format = function(parse_tree, argv) {
-      var cursor = 1, tree_length = parse_tree.length, node_type = '', arg, output = [], i, k, match, pad, pad_character, pad_length;
-      for (i = 0; i < tree_length; i++) {
-        node_type = get_type(parse_tree[i]);
-        if (node_type === 'string') {
-          output.push(parse_tree[i]);
-        }
-        else if (node_type === 'array') {
-          match = parse_tree[i]; // convenience purposes only
-          if (match[2]) { // keyword argument
-            arg = argv[cursor];
-            for (k = 0; k < match[2].length; k++) {
-              if (!arg.hasOwnProperty(match[2][k])) {
-                throw new Error(sprintf('[_.sprintf] property "%s" does not exist', match[2][k]));
-              }
-              arg = arg[match[2][k]];
-            }
-          } else if (match[1]) { // positional argument (explicit)
-            arg = argv[match[1]];
-          }
-          else { // positional argument (implicit)
-            arg = argv[cursor++];
-          }
-
-          if (/[^s]/.test(match[8]) && (get_type(arg) != 'number')) {
-            throw new Error(sprintf('[_.sprintf] expecting number but found %s', get_type(arg)));
-          }
-          switch (match[8]) {
-            case 'b': arg = arg.toString(2); break;
-            case 'c': arg = String.fromCharCode(arg); break;
-            case 'd': arg = parseInt(arg, 10); break;
-            case 'e': arg = match[7] ? arg.toExponential(match[7]) : arg.toExponential(); break;
-            case 'f': arg = match[7] ? parseFloat(arg).toFixed(match[7]) : parseFloat(arg); break;
-            case 'o': arg = arg.toString(8); break;
-            case 's': arg = ((arg = String(arg)) && match[7] ? arg.substring(0, match[7]) : arg); break;
-            case 'u': arg = Math.abs(arg); break;
-            case 'x': arg = arg.toString(16); break;
-            case 'X': arg = arg.toString(16).toUpperCase(); break;
-          }
-          arg = (/[def]/.test(match[8]) && match[3] && arg >= 0 ? '+'+ arg : arg);
-          pad_character = match[4] ? match[4] == '0' ? '0' : match[4].charAt(1) : ' ';
-          pad_length = match[6] - String(arg).length;
-          pad = match[6] ? str_repeat(pad_character, pad_length) : '';
-          output.push(match[5] ? arg + pad : pad + arg);
-        }
-      }
-      return output.join('');
-    };
-
-    str_format.cache = {};
-
-    str_format.parse = function(fmt) {
-      var _fmt = fmt, match = [], parse_tree = [], arg_names = 0;
-      while (_fmt) {
-        if ((match = /^[^\x25]+/.exec(_fmt)) !== null) {
-          parse_tree.push(match[0]);
-        }
-        else if ((match = /^\x25{2}/.exec(_fmt)) !== null) {
-          parse_tree.push('%');
-        }
-        else if ((match = /^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(_fmt)) !== null) {
-          if (match[2]) {
-            arg_names |= 1;
-            var field_list = [], replacement_field = match[2], field_match = [];
-            if ((field_match = /^([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) {
-              field_list.push(field_match[1]);
-              while ((replacement_field = replacement_field.substring(field_match[0].length)) !== '') {
-                if ((field_match = /^\.([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) {
-                  field_list.push(field_match[1]);
-                }
-                else if ((field_match = /^\[(\d+)\]/.exec(replacement_field)) !== null) {
-                  field_list.push(field_match[1]);
-                }
-                else {
-                  throw new Error('[_.sprintf] huh?');
-                }
-              }
-            }
-            else {
-              throw new Error('[_.sprintf] huh?');
-            }
-            match[2] = field_list;
-          }
-          else {
-            arg_names |= 2;
-          }
-          if (arg_names === 3) {
-            throw new Error('[_.sprintf] mixing positional and named placeholders is not (yet) supported');
-          }
-          parse_tree.push(match);
-        }
-        else {
-          throw new Error('[_.sprintf] huh?');
-        }
-        _fmt = _fmt.substring(match[0].length);
-      }
-      return parse_tree;
-    };
-
-    return str_format;
-  })();
-
-
-
-  // Defining underscore.string
-
-  var _s = {
-
-    VERSION: '2.3.0',
-
-    isBlank: function(str){
-      if (str == null) str = '';
-      return (/^\s*$/).test(str);
-    },
-
-    stripTags: function(str){
-      if (str == null) return '';
-      return String(str).replace(/<\/?[^>]+>/g, '');
-    },
-
-    capitalize : function(str){
-      str = str == null ? '' : String(str);
-      return str.charAt(0).toUpperCase() + str.slice(1);
-    },
-
-    chop: function(str, step){
-      if (str == null) return [];
-      str = String(str);
-      step = ~~step;
-      return step > 0 ? str.match(new RegExp('.{1,' + step + '}', 'g')) : [str];
-    },
-
-    clean: function(str){
-      return _s.strip(str).replace(/\s+/g, ' ');
-    },
-
-    count: function(str, substr){
-      if (str == null || substr == null) return 0;
-
-      str = String(str);
-      substr = String(substr);
-
-      var count = 0,
-        pos = 0,
-        length = substr.length;
-
-      while (true) {
-        pos = str.indexOf(substr, pos);
-        if (pos === -1) break;
-        count++;
-        pos += length;
-      }
-
-      return count;
-    },
-
-    chars: function(str) {
-      if (str == null) return [];
-      return String(str).split('');
-    },
-
-    swapCase: function(str) {
-      if (str == null) return '';
-      return String(str).replace(/\S/g, function(c){
-        return c === c.toUpperCase() ? c.toLowerCase() : c.toUpperCase();
-      });
-    },
-
-    escapeHTML: function(str) {
-      if (str == null) return '';
-      return String(str).replace(/[&<>"']/g, function(m){ return '&' + reversedEscapeChars[m] + ';'; });
-    },
-
-    unescapeHTML: function(str) {
-      if (str == null) return '';
-      return String(str).replace(/\&([^;]+);/g, function(entity, entityCode){
-        var match;
-
-        if (entityCode in escapeChars) {
-          return escapeChars[entityCode];
-        } else if (match = entityCode.match(/^#x([\da-fA-F]+)$/)) {
-          return String.fromCharCode(parseInt(match[1], 16));
-        } else if (match = entityCode.match(/^#(\d+)$/)) {
-          return String.fromCharCode(~~match[1]);
-        } else {
-          return entity;
-        }
-      });
-    },
-
-    escapeRegExp: function(str){
-      if (str == null) return '';
-      return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
-    },
-
-    splice: function(str, i, howmany, substr){
-      var arr = _s.chars(str);
-      arr.splice(~~i, ~~howmany, substr);
-      return arr.join('');
-    },
-
-    insert: function(str, i, substr){
-      return _s.splice(str, i, 0, substr);
-    },
-
-    include: function(str, needle){
-      if (needle === '') return true;
-      if (str == null) return false;
-      return String(str).indexOf(needle) !== -1;
-    },
-
-    join: function() {
-      var args = slice.call(arguments),
-        separator = args.shift();
-
-      if (separator == null) separator = '';
-
-      return args.join(separator);
-    },
-
-    lines: function(str) {
-      if (str == null) return [];
-      return String(str).split("\n");
-    },
-
-    reverse: function(str){
-      return _s.chars(str).reverse().join('');
-    },
-
-    startsWith: function(str, starts){
-      if (starts === '') return true;
-      if (str == null || starts == null) return false;
-      str = String(str); starts = String(starts);
-      return str.length >= starts.length && str.slice(0, starts.length) === starts;
-    },
-
-    endsWith: function(str, ends){
-      if (ends === '') return true;
-      if (str == null || ends == null) return false;
-      str = String(str); ends = String(ends);
-      return str.length >= ends.length && str.slice(str.length - ends.length) === ends;
-    },
-
-    succ: function(str){
-      if (str == null) return '';
-      str = String(str);
-      return str.slice(0, -1) + String.fromCharCode(str.charCodeAt(str.length-1) + 1);
-    },
-
-    titleize: function(str){
-      if (str == null) return '';
-      str  = String(str).toLowerCase();
-      return str.replace(/(?:^|\s|-)\S/g, function(c){ return c.toUpperCase(); });
-    },
-
-    camelize: function(str){
-      return _s.trim(str).replace(/[-_\s]+(.)?/g, function(match, c){ return c ? c.toUpperCase() : ""; });
-    },
-
-    underscored: function(str){
-      return _s.trim(str).replace(/([a-z\d])([A-Z]+)/g, '$1_$2').replace(/[-\s]+/g, '_').toLowerCase();
-    },
-
-    dasherize: function(str){
-      return _s.trim(str).replace(/([A-Z])/g, '-$1').replace(/[-_\s]+/g, '-').toLowerCase();
-    },
-
-    classify: function(str){
-      return _s.titleize(String(str).replace(/[\W_]/g, ' ')).replace(/\s/g, '');
-    },
-
-    humanize: function(str){
-      return _s.capitalize(_s.underscored(str).replace(/_id$/,'').replace(/_/g, ' '));
-    },
-
-    trim: function(str, characters){
-      if (str == null) return '';
-      if (!characters && nativeTrim) return nativeTrim.call(str);
-      characters = defaultToWhiteSpace(characters);
-      return String(str).replace(new RegExp('\^' + characters + '+|' + characters + '+$', 'g'), '');
-    },
-
-    ltrim: function(str, characters){
-      if (str == null) return '';
-      if (!characters && nativeTrimLeft) return nativeTrimLeft.call(str);
-      characters = defaultToWhiteSpace(characters);
-      return String(str).replace(new RegExp('^' + characters + '+'), '');
-    },
-
-    rtrim: function(str, characters){
-      if (str == null) return '';
-      if (!characters && nativeTrimRight) return nativeTrimRight.call(str);
-      characters = defaultToWhiteSpace(characters);
-      return String(str).replace(new RegExp(characters + '+$'), '');
-    },
-
-    truncate: function(str, length, truncateStr){
-      if (str == null) return '';
-      str = String(str); truncateStr = truncateStr || '...';
-      length = ~~length;
-      return str.length > length ? str.slice(0, length) + truncateStr : str;
-    },
-
-    /**
-     * _s.prune: a more elegant version of truncate
-     * prune extra chars, never leaving a half-chopped word.
-     * @author github.com/rwz
-     */
-    prune: function(str, length, pruneStr){
-      if (str == null) return '';
-
-      str = String(str); length = ~~length;
-      pruneStr = pruneStr != null ? String(pruneStr) : '...';
-
-      if (str.length <= length) return str;
-
-      var tmpl = function(c){ return c.toUpperCase() !== c.toLowerCase() ? 'A' : ' '; },
-        template = str.slice(0, length+1).replace(/.(?=\W*\w*$)/g, tmpl); // 'Hello, world' -> 'HellAA AAAAA'
-
-      if (template.slice(template.length-2).match(/\w\w/))
-        template = template.replace(/\s*\S+$/, '');
-      else
-        template = _s.rtrim(template.slice(0, template.length-1));
-
-      return (template+pruneStr).length > str.length ? str : str.slice(0, template.length)+pruneStr;
-    },
-
-    words: function(str, delimiter) {
-      if (_s.isBlank(str)) return [];
-      return _s.trim(str, delimiter).split(delimiter || /\s+/);
-    },
-
-    pad: function(str, length, padStr, type) {
-      str = str == null ? '' : String(str);
-      length = ~~length;
-
-      var padlen  = 0;
-
-      if (!padStr)
-        padStr = ' ';
-      else if (padStr.length > 1)
-        padStr = padStr.charAt(0);
-
-      switch(type) {
-        case 'right':
-          padlen = length - str.length;
-          return str + strRepeat(padStr, padlen);
-        case 'both':
-          padlen = length - str.length;
-          return strRepeat(padStr, Math.ceil(padlen/2)) + str
-                  + strRepeat(padStr, Math.floor(padlen/2));
-        default: // 'left'
-          padlen = length - str.length;
-          return strRepeat(padStr, padlen) + str;
-        }
-    },
-
-    lpad: function(str, length, padStr) {
-      return _s.pad(str, length, padStr);
-    },
-
-    rpad: function(str, length, padStr) {
-      return _s.pad(str, length, padStr, 'right');
-    },
-
-    lrpad: function(str, length, padStr) {
-      return _s.pad(str, length, padStr, 'both');
-    },
-
-    sprintf: sprintf,
-
-    vsprintf: function(fmt, argv){
-      argv.unshift(fmt);
-      return sprintf.apply(null, argv);
-    },
-
-    toNumber: function(str, decimals) {
-      if (!str) return 0;
-      str = _s.trim(str);
-      if (!str.match(/^-?\d+(?:\.\d+)?$/)) return NaN;
-      return parseNumber(parseNumber(str).toFixed(~~decimals));
-    },
-
-    numberFormat : function(number, dec, dsep, tsep) {
-      if (isNaN(number) || number == null) return '';
-
-      number = number.toFixed(~~dec);
-      tsep = typeof tsep == 'string' ? tsep : ',';
-
-      var parts = number.split('.'), fnums = parts[0],
-        decimals = parts[1] ? (dsep || '.') + parts[1] : '';
-
-      return fnums.replace(/(\d)(?=(?:\d{3})+$)/g, '$1' + tsep) + decimals;
-    },
-
-    strRight: function(str, sep){
-      if (str == null) return '';
-      str = String(str); sep = sep != null ? String(sep) : sep;
-      var pos = !sep ? -1 : str.indexOf(sep);
-      return ~pos ? str.slice(pos+sep.length, str.length) : str;
-    },
-
-    strRightBack: function(str, sep){
-      if (str == null) return '';
-      str = String(str); sep = sep != null ? String(sep) : sep;
-      var pos = !sep ? -1 : str.lastIndexOf(sep);
-      return ~pos ? str.slice(pos+sep.length, str.length) : str;
-    },
-
-    strLeft: function(str, sep){
-      if (str == null) return '';
-      str = String(str); sep = sep != null ? String(sep) : sep;
-      var pos = !sep ? -1 : str.indexOf(sep);
-      return ~pos ? str.slice(0, pos) : str;
-    },
-
-    strLeftBack: function(str, sep){
-      if (str == null) return '';
-      str += ''; sep = sep != null ? ''+sep : sep;
-      var pos = str.lastIndexOf(sep);
-      return ~pos ? str.slice(0, pos) : str;
-    },
-
-    toSentence: function(array, separator, lastSeparator, serial) {
-      separator = separator || ', ';
-      lastSeparator = lastSeparator || ' and ';
-      var a = array.slice(), lastMember = a.pop();
-
-      if (array.length > 2 && serial) lastSeparator = _s.rtrim(separator) + lastSeparator;
-
-      return a.length ? a.join(separator) + lastSeparator + lastMember : lastMember;
-    },
-
-    toSentenceSerial: function() {
-      var args = slice.call(arguments);
-      args[3] = true;
-      return _s.toSentence.apply(_s, args);
-    },
-
-    slugify: function(str) {
-      if (str == null) return '';
-
-      var from  = "ąàáäâãåæăćęèéëêìíïîłńòóöôõøśșțùúüûñçżź",
-          to    = "aaaaaaaaaceeeeeiiiilnoooooosstuuuunczz",
-          regex = new RegExp(defaultToWhiteSpace(from), 'g');
-
-      str = String(str).toLowerCase().replace(regex, function(c){
-        var index = from.indexOf(c);
-        return to.charAt(index) || '-';
-      });
-
-      return _s.dasherize(str.replace(/[^\w\s-]/g, ''));
-    },
-
-    surround: function(str, wrapper) {
-      return [wrapper, str, wrapper].join('');
-    },
-
-    quote: function(str, quoteChar) {
-      return _s.surround(str, quoteChar || '"');
-    },
-
-    unquote: function(str, quoteChar) {
-      quoteChar = quoteChar || '"';
-      if (str[0] === quoteChar && str[str.length-1] === quoteChar)
-        return str.slice(1,str.length-1);
-      else return str;
-    },
-
-    exports: function() {
-      var result = {};
-
-      for (var prop in this) {
-        if (!this.hasOwnProperty(prop) || prop.match(/^(?:include|contains|reverse)$/)) continue;
-        result[prop] = this[prop];
-      }
-
-      return result;
-    },
-
-    repeat: function(str, qty, separator){
-      if (str == null) return '';
-
-      qty = ~~qty;
-
-      // using faster implementation if separator is not needed;
-      if (separator == null) return strRepeat(String(str), qty);
-
-      // this one is about 300x slower in Google Chrome
-      for (var repeat = []; qty > 0; repeat[--qty] = str) {}
-      return repeat.join(separator);
-    },
-
-    naturalCmp: function(str1, str2){
-      if (str1 == str2) return 0;
-      if (!str1) return -1;
-      if (!str2) return 1;
-
-      var cmpRegex = /(\.\d+)|(\d+)|(\D+)/g,
-        tokens1 = String(str1).toLowerCase().match(cmpRegex),
-        tokens2 = String(str2).toLowerCase().match(cmpRegex),
-        count = Math.min(tokens1.length, tokens2.length);
-
-      for(var i = 0; i < count; i++) {
-        var a = tokens1[i], b = tokens2[i];
-
-        if (a !== b){
-          var num1 = parseInt(a, 10);
-          if (!isNaN(num1)){
-            var num2 = parseInt(b, 10);
-            if (!isNaN(num2) && num1 - num2)
-              return num1 - num2;
-          }
-          return a < b ? -1 : 1;
-        }
-      }
-
-      if (tokens1.length === tokens2.length)
-        return tokens1.length - tokens2.length;
-
-      return str1 < str2 ? -1 : 1;
-    },
-
-    levenshtein: function(str1, str2) {
-      if (str1 == null && str2 == null) return 0;
-      if (str1 == null) return String(str2).length;
-      if (str2 == null) return String(str1).length;
-
-      str1 = String(str1); str2 = String(str2);
-
-      var current = [], prev, value;
-
-      for (var i = 0; i <= str2.length; i++)
-        for (var j = 0; j <= str1.length; j++) {
-          if (i && j)
-            if (str1.charAt(j - 1) === str2.charAt(i - 1))
-              value = prev;
-            else
-              value = Math.min(current[j], current[j - 1], prev) + 1;
-          else
-            value = i + j;
-
-          prev = current[j];
-          current[j] = value;
-        }
-
-      return current.pop();
-    },
-
-    toBoolean: function(str, trueValues, falseValues) {
-      if (typeof str === "number") str = "" + str;
-      if (typeof str !== "string") return !!str;
-      str = _s.trim(str);
-      if (boolMatch(str, trueValues || ["true", "1"])) return true;
-      if (boolMatch(str, falseValues || ["false", "0"])) return false;
-    }
-  };
-
-  // Aliases
-
-  _s.strip    = _s.trim;
-  _s.lstrip   = _s.ltrim;
-  _s.rstrip   = _s.rtrim;
-  _s.center   = _s.lrpad;
-  _s.rjust    = _s.lpad;
-  _s.ljust    = _s.rpad;
-  _s.contains = _s.include;
-  _s.q        = _s.quote;
-  _s.toBool   = _s.toBoolean;
-
-  // Exporting
-
-  // CommonJS module is defined
-  if (typeof exports !== 'undefined') {
-    if (typeof module !== 'undefined' && module.exports)
-      module.exports = _s;
-
-    exports._s = _s;
-  }
-
-  // Register as a named module with AMD.
-  if (typeof define === 'function' && define.amd)
-    define('underscore.string', [], function(){ return _s; });
-
-
-  // Integrate with Underscore.js if defined
-  // or create our own underscore object.
-  root._ = root._ || {};
-  root._.string = root._.str = _s;
-}(this, String);
diff --git a/lines.js b/lines.js
new file mode 100644
index 0000000..40b11cc
--- /dev/null
+++ b/lines.js
@@ -0,0 +1,4 @@
+module.exports = function lines(str) {
+  if (str == null) return [];
+  return String(str).split(/\r\n?|\n/);
+};
diff --git a/lpad.js b/lpad.js
new file mode 100644
index 0000000..ada8c7e
--- /dev/null
+++ b/lpad.js
@@ -0,0 +1,5 @@
+var pad = require('./pad');
+
+module.exports = function lpad(str, length, padStr) {
+  return pad(str, length, padStr);
+};
diff --git a/lrpad.js b/lrpad.js
new file mode 100644
index 0000000..e3162b0
--- /dev/null
+++ b/lrpad.js
@@ -0,0 +1,5 @@
+var pad = require('./pad');
+
+module.exports = function lrpad(str, length, padStr) {
+  return pad(str, length, padStr, 'both');
+};
diff --git a/ltrim.js b/ltrim.js
new file mode 100644
index 0000000..858936e
--- /dev/null
+++ b/ltrim.js
@@ -0,0 +1,10 @@
+var makeString = require('./helper/makeString');
+var defaultToWhiteSpace = require('./helper/defaultToWhiteSpace');
+var nativeTrimLeft = String.prototype.trimLeft;
+
+module.exports = function ltrim(str, characters) {
+  str = makeString(str);
+  if (!characters && nativeTrimLeft) return nativeTrimLeft.call(str);
+  characters = defaultToWhiteSpace(characters);
+  return str.replace(new RegExp('^' + characters + '+'), '');
+};
diff --git a/map.js b/map.js
new file mode 100644
index 0000000..c2910ae
--- /dev/null
+++ b/map.js
@@ -0,0 +1,9 @@
+var makeString = require('./helper/makeString');
+
+module.exports = function(str, callback) {
+  str = makeString(str);
+
+  if (str.length === 0 || typeof callback !== 'function') return str;
+
+  return str.replace(/./g, callback);
+};
diff --git a/meteor-post.js b/meteor-post.js
new file mode 100644
index 0000000..3f38d8d
--- /dev/null
+++ b/meteor-post.js
@@ -0,0 +1,2 @@
+// s will be picked up by Meteor and exported
+s = module.exports;
diff --git a/meteor-pre.js b/meteor-pre.js
new file mode 100644
index 0000000..e692bc3
--- /dev/null
+++ b/meteor-pre.js
@@ -0,0 +1,6 @@
+// Defining this will trick dist/underscore.string.js into putting its exports into module.exports
+// Credit to Tim Heckel for this trick - see https://github.com/TimHeckel/meteor-underscore-string
+module = {};
+
+// This also needed, otherwise above doesn't work???
+exports = {};
diff --git a/naturalCmp.js b/naturalCmp.js
new file mode 100644
index 0000000..7cb94e6
--- /dev/null
+++ b/naturalCmp.js
@@ -0,0 +1,29 @@
+module.exports = function naturalCmp(str1, str2) {
+  if (str1 == str2) return 0;
+  if (!str1) return -1;
+  if (!str2) return 1;
+
+  var cmpRegex = /(\.\d+|\d+|\D+)/g,
+    tokens1 = String(str1).match(cmpRegex),
+    tokens2 = String(str2).match(cmpRegex),
+    count = Math.min(tokens1.length, tokens2.length);
+
+  for (var i = 0; i < count; i++) {
+    var a = tokens1[i],
+      b = tokens2[i];
+
+    if (a !== b) {
+      var num1 = +a;
+      var num2 = +b;
+      if (num1 === num1 && num2 === num2) {
+        return num1 > num2 ? 1 : -1;
+      }
+      return a < b ? -1 : 1;
+    }
+  }
+
+  if (tokens1.length != tokens2.length)
+    return tokens1.length - tokens2.length;
+
+  return str1 < str2 ? -1 : 1;
+};
diff --git a/numberFormat.js b/numberFormat.js
new file mode 100644
index 0000000..6a681fe
--- /dev/null
+++ b/numberFormat.js
@@ -0,0 +1,12 @@
+module.exports = function numberFormat(number, dec, dsep, tsep) {
+  if (isNaN(number) || number == null) return '';
+
+  number = number.toFixed(~~dec);
+  tsep = typeof tsep == 'string' ? tsep : ',';
+
+  var parts = number.split('.'),
+    fnums = parts[0],
+    decimals = parts[1] ? (dsep || '.') + parts[1] : '';
+
+  return fnums.replace(/(\d)(?=(?:\d{3})+$)/g, '$1' + tsep) + decimals;
+};
diff --git a/package.js b/package.js
new file mode 100644
index 0000000..96f0d42
--- /dev/null
+++ b/package.js
@@ -0,0 +1,16 @@
+// package metadata file for Meteor.js
+
+Package.describe({
+  name: 'underscorestring:underscore.string',
+  summary: 'underscore.string (official): String manipulation extensions for Underscore.js javascript library.',
+  version: '3.3.4',
+  git: 'https://github.com/epeli/underscore.string.git',
+  documentation: 'README.markdown'
+});
+
+
+Package.onUse(function (api) {
+  api.versionsFrom('METEOR at 1.0');
+  api.addFiles(['meteor-pre.js','dist/underscore.string.js','meteor-post.js']);
+  api.export("s");
+});
diff --git a/package.json b/package.json
index 34538ca..25232a8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "underscore.string",
-  "version": "2.3.3",
+  "version": "3.3.4",
   "description": "String manipulation extensions for Underscore.js javascript library.",
   "homepage": "http://epeli.github.com/underscore.string/",
   "contributors": [
@@ -11,15 +11,16 @@
     "Vladimir Dronnikov <dronnikov at gmail.com>",
     "Pete Kruckenberg (<https://github.com/kruckenb>)",
     "Paul Chavard <paul at chavard.net> (<http://tchak.net>)",
-    "Ed Finkler <coj at funkatron.com> (<http://funkatron.com>)"
+    "Ed Finkler <coj at funkatron.com> (<http://funkatron.com>)",
+    "Christoph Hermann <schtoeffel at gmail.com> (<https://github.com/stoeffel>)"
   ],
   "keywords": [
     "underscore",
     "string"
   ],
-  "main": "./lib/underscore.string",
+  "main": "./index.js",
   "directories": {
-    "lib": "./lib"
+    "lib": "./"
   },
   "engines": {
     "node": "*"
@@ -31,9 +32,41 @@
   "bugs": {
     "url": "https://github.com/epeli/underscore.string/issues"
   },
-  "licenses": [
-    {
-      "type": "MIT"
+  "license": "MIT",
+  "scripts": {
+    "test": "npm run test:lint && npm run test:unit && npm run coverage",
+    "test:unit": "mocha --ui=qunit tests",
+    "test:lint": "eslint -c .eslintrc .",
+    "coverage": "istanbul cover ./node_modules/mocha/bin/_mocha  -- --report=lcov --ui=qunit tests",
+    "build": "npm run build:clean && npm run build:bundle && npm run build:min",
+    "build:clean": "rm -rf dist",
+    "build:bundle": "mkdir dist && browserify index.js -o dist/underscore.string.js -p browserify-header -s s",
+    "build:min": "uglifyjs dist/underscore.string.js -o dist/underscore.string.min.js --comments",
+    "release": "npm test && npm run release:version && npm run build && npm run release:push",
+    "release:version": "node scripts/bump-version.js",
+    "release:push": "node scripts/push-tags.js"
+  },
+  "devDependencies": {
+    "browserify": "^13.0.0",
+    "browserify-header": "^0.9.2",
+    "eslint": "^1.10.3",
+    "istanbul": "^0.4.2",
+    "mocha": "^2.1.0",
+    "mocha-lcov-reporter": "^1.0.0",
+    "replace": "^0.3.0",
+    "uglifyjs": "^2.4.10",
+    "underscore": "^1.7.0"
+  },
+  "jshintConfig": {
+    "node": true,
+    "browser": true,
+    "qunit": true,
+    "globals": {
+      "s": true
     }
-  ]
-}
\ No newline at end of file
+  },
+  "dependencies": {
+    "sprintf-js": "^1.0.3",
+    "util-deprecate": "^1.0.2"
+  }
+}
diff --git a/pad.js b/pad.js
new file mode 100644
index 0000000..9a2a87d
--- /dev/null
+++ b/pad.js
@@ -0,0 +1,26 @@
+var makeString = require('./helper/makeString');
+var strRepeat = require('./helper/strRepeat');
+
+module.exports = function pad(str, length, padStr, type) {
+  str = makeString(str);
+  length = ~~length;
+
+  var padlen = 0;
+
+  if (!padStr)
+    padStr = ' ';
+  else if (padStr.length > 1)
+    padStr = padStr.charAt(0);
+
+  switch (type) {
+  case 'right':
+    padlen = length - str.length;
+    return str + strRepeat(padStr, padlen);
+  case 'both':
+    padlen = length - str.length;
+    return strRepeat(padStr, Math.ceil(padlen / 2)) + str + strRepeat(padStr, Math.floor(padlen / 2));
+  default: // 'left'
+    padlen = length - str.length;
+    return strRepeat(padStr, padlen) + str;
+  }
+};
diff --git a/pred.js b/pred.js
new file mode 100644
index 0000000..a123701
--- /dev/null
+++ b/pred.js
@@ -0,0 +1,5 @@
+var adjacent = require('./helper/adjacent');
+
+module.exports = function succ(str) {
+  return adjacent(str, -1);
+};
diff --git a/prune.js b/prune.js
new file mode 100644
index 0000000..85b8398
--- /dev/null
+++ b/prune.js
@@ -0,0 +1,27 @@
+/**
+ * _s.prune: a more elegant version of truncate
+ * prune extra chars, never leaving a half-chopped word.
+ * @author github.com/rwz
+ */
+var makeString = require('./helper/makeString');
+var rtrim = require('./rtrim');
+
+module.exports = function prune(str, length, pruneStr) {
+  str = makeString(str);
+  length = ~~length;
+  pruneStr = pruneStr != null ? String(pruneStr) : '...';
+
+  if (str.length <= length) return str;
+
+  var tmpl = function(c) {
+      return c.toUpperCase() !== c.toLowerCase() ? 'A' : ' ';
+    },
+    template = str.slice(0, length + 1).replace(/.(?=\W*\w*$)/g, tmpl); // 'Hello, world' -> 'HellAA AAAAA'
+
+  if (template.slice(template.length - 2).match(/\w\w/))
+    template = template.replace(/\s*\S+$/, '');
+  else
+    template = rtrim(template.slice(0, template.length - 1));
+
+  return (template + pruneStr).length > str.length ? str : str.slice(0, template.length) + pruneStr;
+};
diff --git a/quote.js b/quote.js
new file mode 100644
index 0000000..1e90f63
--- /dev/null
+++ b/quote.js
@@ -0,0 +1,5 @@
+var surround = require('./surround');
+
+module.exports = function quote(str, quoteChar) {
+  return surround(str, quoteChar || '"');
+};
diff --git a/repeat.js b/repeat.js
new file mode 100644
index 0000000..71228ed
--- /dev/null
+++ b/repeat.js
@@ -0,0 +1,16 @@
+var makeString = require('./helper/makeString');
+var strRepeat = require('./helper/strRepeat');
+
+module.exports = function repeat(str, qty, separator) {
+  str = makeString(str);
+
+  qty = ~~qty;
+
+  // using faster implementation if separator is not needed;
+  if (separator == null) return strRepeat(str, qty);
+
+  // this one is about 300x slower in Google Chrome
+  /*eslint no-empty: 0*/
+  for (var repeat = []; qty > 0; repeat[--qty] = str) {}
+  return repeat.join(separator);
+};
diff --git a/replaceAll.js b/replaceAll.js
new file mode 100644
index 0000000..93f6c0d
--- /dev/null
+++ b/replaceAll.js
@@ -0,0 +1,8 @@
+var makeString = require('./helper/makeString');
+
+module.exports = function replaceAll(str, find, replace, ignorecase) {
+  var flags = (ignorecase === true)?'gi':'g';
+  var reg = new RegExp(find, flags);
+
+  return makeString(str).replace(reg, replace);
+};
diff --git a/reverse.js b/reverse.js
new file mode 100644
index 0000000..b9ef2e6
--- /dev/null
+++ b/reverse.js
@@ -0,0 +1,5 @@
+var chars = require('./chars');
+
+module.exports = function reverse(str) {
+  return chars(str).reverse().join('');
+};
diff --git a/rpad.js b/rpad.js
new file mode 100644
index 0000000..b37d386
--- /dev/null
+++ b/rpad.js
@@ -0,0 +1,5 @@
+var pad = require('./pad');
+
+module.exports = function rpad(str, length, padStr) {
+  return pad(str, length, padStr, 'right');
+};
diff --git a/rtrim.js b/rtrim.js
new file mode 100644
index 0000000..e6be2ed
--- /dev/null
+++ b/rtrim.js
@@ -0,0 +1,10 @@
+var makeString = require('./helper/makeString');
+var defaultToWhiteSpace = require('./helper/defaultToWhiteSpace');
+var nativeTrimRight = String.prototype.trimRight;
+
+module.exports = function rtrim(str, characters) {
+  str = makeString(str);
+  if (!characters && nativeTrimRight) return nativeTrimRight.call(str);
+  characters = defaultToWhiteSpace(characters);
+  return str.replace(new RegExp(characters + '+$'), '');
+};
diff --git a/scripts/bump-version.js b/scripts/bump-version.js
new file mode 100644
index 0000000..204f297
--- /dev/null
+++ b/scripts/bump-version.js
@@ -0,0 +1,19 @@
+var replace = require('replace');
+var package = require('../package.json');
+var VERSION_FILES = ['./component.json', './bower.json', './index.js', './package.js'];
+ 
+replace({
+  regex: /(version?\s?=?\:?\s\')([\d\.]*)\'/gi,
+  replacement: '$1' + package.version + "'",
+  paths: VERSION_FILES,
+  recursive: false,
+  silent: false
+});
+
+replace({
+  regex: /(version?"\s?:?\:?\s")([\d\.]*)"/gi,
+  replacement: '$1' + package.version + "\"",
+  paths: VERSION_FILES,
+  recursive: false,
+  silent: false
+});
diff --git a/scripts/push-tags.js b/scripts/push-tags.js
new file mode 100644
index 0000000..5bddd62
--- /dev/null
+++ b/scripts/push-tags.js
@@ -0,0 +1,4 @@
+var exec = require('child_process').exec;
+var version = require('../package.json').version;
+
+exec('git add -A && git commit -m "Version ' + version + '" && git push origin master && git tag -a ' + version + ' -m "' + version + '" && git push origin --tags && npm publish');
diff --git a/slugify.js b/slugify.js
new file mode 100644
index 0000000..3701ccd
--- /dev/null
+++ b/slugify.js
@@ -0,0 +1,7 @@
+var trim = require('./trim');
+var dasherize = require('./dasherize');
+var cleanDiacritics = require('./cleanDiacritics');
+
+module.exports = function slugify(str) {
+  return trim(dasherize(cleanDiacritics(str).replace(/[^\w\s-]/g, '-').toLowerCase()), '-');
+};
diff --git a/splice.js b/splice.js
new file mode 100644
index 0000000..34c0410
--- /dev/null
+++ b/splice.js
@@ -0,0 +1,7 @@
+var chars = require('./chars');
+
+module.exports = function splice(str, i, howmany, substr) {
+  var arr = chars(str);
+  arr.splice(~~i, ~~howmany, substr);
+  return arr.join('');
+};
diff --git a/sprintf.js b/sprintf.js
new file mode 100644
index 0000000..427e620
--- /dev/null
+++ b/sprintf.js
@@ -0,0 +1,4 @@
+var deprecate = require('util-deprecate');
+
+module.exports = deprecate(require('sprintf-js').sprintf,
+  'sprintf() will be removed in the next major release, use the sprintf-js package instead.');
diff --git a/startsWith.js b/startsWith.js
new file mode 100644
index 0000000..a9f4790
--- /dev/null
+++ b/startsWith.js
@@ -0,0 +1,9 @@
+var makeString = require('./helper/makeString');
+var toPositive = require('./helper/toPositive');
+
+module.exports = function startsWith(str, starts, position) {
+  str = makeString(str);
+  starts = '' + starts;
+  position = position == null ? 0 : Math.min(toPositive(position), str.length);
+  return str.lastIndexOf(starts, position) === position;
+};
diff --git a/strLeft.js b/strLeft.js
new file mode 100644
index 0000000..0602984
--- /dev/null
+++ b/strLeft.js
@@ -0,0 +1,8 @@
+var makeString = require('./helper/makeString');
+
+module.exports = function strLeft(str, sep) {
+  str = makeString(str);
+  sep = makeString(sep);
+  var pos = !sep ? -1 : str.indexOf(sep);
+  return~ pos ? str.slice(0, pos) : str;
+};
diff --git a/strLeftBack.js b/strLeftBack.js
new file mode 100644
index 0000000..0136e20
--- /dev/null
+++ b/strLeftBack.js
@@ -0,0 +1,8 @@
+var makeString = require('./helper/makeString');
+
+module.exports = function strLeftBack(str, sep) {
+  str = makeString(str);
+  sep = makeString(sep);
+  var pos = str.lastIndexOf(sep);
+  return~ pos ? str.slice(0, pos) : str;
+};
diff --git a/strRight.js b/strRight.js
new file mode 100644
index 0000000..67b45b5
--- /dev/null
+++ b/strRight.js
@@ -0,0 +1,8 @@
+var makeString = require('./helper/makeString');
+
+module.exports = function strRight(str, sep) {
+  str = makeString(str);
+  sep = makeString(sep);
+  var pos = !sep ? -1 : str.indexOf(sep);
+  return~ pos ? str.slice(pos + sep.length, str.length) : str;
+};
diff --git a/strRightBack.js b/strRightBack.js
new file mode 100644
index 0000000..43de0e9
--- /dev/null
+++ b/strRightBack.js
@@ -0,0 +1,8 @@
+var makeString = require('./helper/makeString');
+
+module.exports = function strRightBack(str, sep) {
+  str = makeString(str);
+  sep = makeString(sep);
+  var pos = !sep ? -1 : str.lastIndexOf(sep);
+  return~ pos ? str.slice(pos + sep.length, str.length) : str;
+};
diff --git a/stripTags.js b/stripTags.js
new file mode 100644
index 0000000..8948d36
--- /dev/null
+++ b/stripTags.js
@@ -0,0 +1,5 @@
+var makeString = require('./helper/makeString');
+
+module.exports = function stripTags(str) {
+  return makeString(str).replace(/<\/?[^>]+>/g, '');
+};
diff --git a/succ.js b/succ.js
new file mode 100644
index 0000000..313c8e8
--- /dev/null
+++ b/succ.js
@@ -0,0 +1,5 @@
+var adjacent = require('./helper/adjacent');
+
+module.exports = function succ(str) {
+  return adjacent(str, 1);
+};
diff --git a/surround.js b/surround.js
new file mode 100644
index 0000000..9cb7f7e
--- /dev/null
+++ b/surround.js
@@ -0,0 +1,3 @@
+module.exports = function surround(str, wrapper) {
+  return [wrapper, str, wrapper].join('');
+};
diff --git a/swapCase.js b/swapCase.js
new file mode 100644
index 0000000..0857262
--- /dev/null
+++ b/swapCase.js
@@ -0,0 +1,7 @@
+var makeString = require('./helper/makeString');
+
+module.exports = function swapCase(str) {
+  return makeString(str).replace(/\S/g, function(c) {
+    return c === c.toUpperCase() ? c.toLowerCase() : c.toUpperCase();
+  });
+};
diff --git a/tests/camelize.js b/tests/camelize.js
new file mode 100644
index 0000000..98e4d36
--- /dev/null
+++ b/tests/camelize.js
@@ -0,0 +1,35 @@
+var equal = require('assert').equal;
+var camelize = require('../camelize');
+
+
+test('#camelize', function(){
+  equal(camelize('the_camelize_string_method'), 'theCamelizeStringMethod');
+  equal(camelize('webkit-transform'), 'webkitTransform');
+  equal(camelize('-the-camelize-string-method'), 'TheCamelizeStringMethod');
+  equal(camelize('_the_camelize_string_method'), 'TheCamelizeStringMethod');
+  equal(camelize('The-camelize-string-method'), 'TheCamelizeStringMethod');
+  equal(camelize('the camelize string method'), 'theCamelizeStringMethod');
+  equal(camelize(' the camelize  string method'), 'theCamelizeStringMethod');
+  equal(camelize('the camelize   string method'), 'theCamelizeStringMethod');
+  equal(camelize(' with   spaces'), 'withSpaces');
+  equal(camelize('_som eWeird---name-'), 'SomEWeirdName');
+  equal(camelize(''), '', 'Camelize empty string returns empty string');
+  equal(camelize(null), '', 'Camelize null returns empty string');
+  equal(camelize(undefined), '', 'Camelize undefined returns empty string');
+  equal(camelize(123), '123');
+  equal(camelize('the_camelize_string_method', true), 'theCamelizeStringMethod');
+  equal(camelize('webkit-transform', true), 'webkitTransform');
+  equal(camelize('-the-camelize-string-method', true), 'theCamelizeStringMethod');
+  equal(camelize('_the_camelize_string_method', true), 'theCamelizeStringMethod');
+  equal(camelize('The-camelize-string-method', true), 'theCamelizeStringMethod');
+  equal(camelize('the camelize string method', true), 'theCamelizeStringMethod');
+  equal(camelize(' the camelize  string method', true), 'theCamelizeStringMethod');
+  equal(camelize('the camelize   string method', true), 'theCamelizeStringMethod');
+  equal(camelize(' with   spaces', true), 'withSpaces');
+  equal(camelize('_som eWeird---name-', true), 'somEWeirdName');
+  equal(camelize('', true), '', 'Camelize empty string returns empty string');
+  equal(camelize(null, true), '', 'Camelize null returns empty string');
+  equal(camelize(undefined, true), '', 'Camelize undefined returns empty string');
+  equal(camelize(123, true), '123');
+});
+
diff --git a/tests/capitalize.js b/tests/capitalize.js
new file mode 100644
index 0000000..3836218
--- /dev/null
+++ b/tests/capitalize.js
@@ -0,0 +1,29 @@
+var equal = require('assert').equal;
+var capitalize = require('../capitalize');
+
+
+test('#capitalize', function() {
+  equal(capitalize('fabio'), 'Fabio', 'First letter is upper case');
+  equal(capitalize('fabio'), 'Fabio', 'First letter is upper case');
+  equal(capitalize('FOO'), 'FOO', 'Other letters unchanged');
+  equal(capitalize('FOO', false), 'FOO', 'Other letters unchanged');
+  equal(capitalize('foO', false), 'FoO', 'Other letters unchanged');
+  equal(capitalize('FOO', true), 'Foo', 'Other letters are lowercased');
+  equal(capitalize('foO', true), 'Foo', 'Other letters are lowercased');
+  equal(capitalize('f', false), 'F', 'Should uppercase 1 letter');
+  equal(capitalize('f', true), 'F', 'Should uppercase 1 letter');
+  equal(capitalize('f'), 'F', 'Should uppercase 1 letter');
+  equal(capitalize(123), '123', 'Non string');
+  equal(capitalize(123, true), '123', 'Non string');
+  equal(capitalize(123, false), '123', 'Non string');
+  equal(capitalize(''), '', 'Capitalizing empty string returns empty string');
+  equal(capitalize(null), '', 'Capitalizing null returns empty string');
+  equal(capitalize(undefined), '', 'Capitalizing undefined returns empty string');
+  equal(capitalize('', true), '', 'Capitalizing empty string returns empty string');
+  equal(capitalize(null, true), '', 'Capitalizing null returns empty string');
+  equal(capitalize(undefined, true), '', 'Capitalizing undefined returns empty string');
+  equal(capitalize('', false), '', 'Capitalizing empty string returns empty string');
+  equal(capitalize(null, false), '', 'Capitalizing null returns empty string');
+  equal(capitalize(undefined, false), '', 'Capitalizing undefined returns empty string');
+});
+
diff --git a/tests/chars.js b/tests/chars.js
new file mode 100644
index 0000000..d2e5ec4
--- /dev/null
+++ b/tests/chars.js
@@ -0,0 +1,12 @@
+var equal = require('assert').equal;
+var chars = require('../chars');
+
+
+test('#chars', function() {
+  equal(chars('Hello').length, 5);
+  equal(chars(123).length, 3);
+  equal(chars('').length, 0);
+  equal(chars(null).length, 0);
+  equal(chars(undefined).length, 0);
+});
+
diff --git a/tests/chop.js b/tests/chop.js
new file mode 100644
index 0000000..8106337
--- /dev/null
+++ b/tests/chop.js
@@ -0,0 +1,12 @@
+var ok = require('assert').ok;
+var chop = require('../chop');
+
+
+test('#chop', function(){
+  ok(chop(null, 2).length === 0, 'output []');
+  ok(chop('whitespace', 2).length === 5, 'output [wh, it, es, pa, ce]');
+  ok(chop('whitespace', 3).length === 4, 'output [whi, tes, pac, e]');
+  ok(chop('whitespace')[0].length === 10, 'output [whitespace]');
+  ok(chop(12345, 1).length === 5, 'output [1, 2, 3,  4, 5]');
+});
+
diff --git a/tests/classify.js b/tests/classify.js
new file mode 100644
index 0000000..318258c
--- /dev/null
+++ b/tests/classify.js
@@ -0,0 +1,17 @@
+var equal = require('assert').equal;
+var classify = require('../classify');
+
+
+test('#classify', function(){
+  equal(classify(1), '1');
+  equal(classify('some_class_name'), 'SomeClassName');
+  equal(classify('my wonderfull class_name'), 'MyWonderfullClassName');
+  equal(classify('my wonderfull.class.name'), 'MyWonderfullClassName');
+  equal(classify('myLittleCamel'), 'MyLittleCamel');
+  equal(classify('myLittleCamel.class.name'), 'MyLittleCamelClassName');
+  equal(classify(123), '123');
+  equal(classify(''), '');
+  equal(classify(null), '');
+  equal(classify(undefined), '');
+});
+
diff --git a/tests/clean.js b/tests/clean.js
new file mode 100644
index 0000000..a613cab
--- /dev/null
+++ b/tests/clean.js
@@ -0,0 +1,11 @@
+var equal = require('assert').equal;
+var clean = require('../clean');
+
+
+test('#clean', function() {
+  equal(clean(' foo    bar   '), 'foo bar');
+  equal(clean(123), '123');
+  equal(clean(''), '', 'claning empty string returns empty string');
+  equal(clean(null), '', 'claning null returns empty string');
+  equal(clean(undefined), '', 'claning undefined returns empty string');
+});
diff --git a/tests/cleanDiacritics.js b/tests/cleanDiacritics.js
new file mode 100644
index 0000000..01b378f
--- /dev/null
+++ b/tests/cleanDiacritics.js
@@ -0,0 +1,24 @@
+
+var equal = require('assert').equal;
+var cleanDiacritics = require('../cleanDiacritics');
+
+var from  = 'ąàáäâãåæăćčĉęèéëêĝĥìíïîĵłľńňòóöőôõðøśșşšŝťțţŭùúüűûñÿýçżźž',
+  to    = 'aaaaaaaaaccceeeeeghiiiijllnnoooooooossssstttuuuuuunyyczzz';
+
+test('#cleanDiacritics', function() {
+
+  equal(cleanDiacritics(from), to);
+  equal(cleanDiacritics(from.toUpperCase()), to.toUpperCase());
+
+
+  equal(cleanDiacritics('ä'), 'a');
+  equal(cleanDiacritics('Ä Ø'), 'A O');
+  equal(cleanDiacritics('1 foo ääkkönen'), '1 foo aakkonen');
+  equal(cleanDiacritics('Äöö ÖÖ'), 'Aoo OO');
+  equal(cleanDiacritics(' ä '), ' a ');
+  equal(cleanDiacritics('- " , £ $ ä'), '- " , £ $ a');
+
+  equal(cleanDiacritics('ß'), 'ss');
+  equal(cleanDiacritics('Schuß'), 'Schuss');
+});
+
diff --git a/tests/count.js b/tests/count.js
new file mode 100644
index 0000000..3e765ed
--- /dev/null
+++ b/tests/count.js
@@ -0,0 +1,22 @@
+var equal = require('assert').equal;
+var count = require('../count');
+
+
+test('#count', function(){
+  equal(count('Hello world', 'l'), 3);
+  equal(count('Hello world', 'Hello'), 1);
+  equal(count('Hello world', 'foo'), 0);
+  equal(count('x.xx....x.x', 'x'), 5);
+  equal(count('', 'x'), 0);
+  equal(count(null, 'x'), 0);
+  equal(count(undefined, 'x'), 0);
+  equal(count(12345, 1), 1);
+  equal(count(11345, 1), 2);
+  equal(count('Hello World', ''), 0);
+  equal(count('Hello World', null), 0);
+  equal(count('Hello World', undefined), 0);
+  equal(count('', ''), 0);
+  equal(count(null, null), 0);
+  equal(count(undefined, undefined), 0);
+});
+
diff --git a/tests/dasherize.js b/tests/dasherize.js
new file mode 100644
index 0000000..6c18ecd
--- /dev/null
+++ b/tests/dasherize.js
@@ -0,0 +1,22 @@
+var equal = require('assert').equal;
+var dasherize = require('../dasherize');
+
+
+test('#dasherize', function(){
+  equal(dasherize('the_dasherize_string_method'), 'the-dasherize-string-method');
+  equal(dasherize('TheDasherizeStringMethod'), '-the-dasherize-string-method');
+  equal(dasherize('thisIsATest'), 'this-is-a-test');
+  equal(dasherize('this Is A Test'), 'this-is-a-test');
+  equal(dasherize('thisIsATest123'), 'this-is-a-test123');
+  equal(dasherize('123thisIsATest'), '123this-is-a-test');
+  equal(dasherize('the dasherize string method'), 'the-dasherize-string-method');
+  equal(dasherize('the  dasherize string method  '), 'the-dasherize-string-method');
+  equal(dasherize('téléphone'), 'téléphone');
+  equal(dasherize('foo$bar'), 'foo$bar');
+  equal(dasherize('input with a-dash'), 'input-with-a-dash');
+  equal(dasherize(''), '');
+  equal(dasherize(null), '');
+  equal(dasherize(undefined), '');
+  equal(dasherize(123), '123');
+});
+
diff --git a/tests/decapitalize.js b/tests/decapitalize.js
new file mode 100644
index 0000000..a8e6adb
--- /dev/null
+++ b/tests/decapitalize.js
@@ -0,0 +1,13 @@
+var equal = require('assert').equal;
+var decapitalize = require('../decapitalize');
+
+
+test('#decapitalize', function() {
+  equal(decapitalize('Fabio'), 'fabio', 'First letter is lower case');
+  equal(decapitalize('FOO'), 'fOO', 'Other letters unchanged');
+  equal(decapitalize(123), '123', 'Non string');
+  equal(decapitalize(''), '', 'Decapitalizing empty string returns empty string');
+  equal(decapitalize(null), '', 'Decapitalizing null returns empty string');
+  equal(decapitalize(undefined), '', 'Decapitalizing undefined returns empty string');
+});
+
diff --git a/tests/dedent.js b/tests/dedent.js
new file mode 100644
index 0000000..625dfae
--- /dev/null
+++ b/tests/dedent.js
@@ -0,0 +1,35 @@
+var equal = require('assert').equal;
+var deepEqual = require('assert').deepEqual;
+var dedent = require('../dedent');
+
+
+test('#dedent', function() {
+  equal(dedent('Hello\nWorld'), 'Hello\nWorld');
+  equal(dedent('Hello\t\nWorld'), 'Hello\t\nWorld');
+  equal(dedent('Hello \nWorld'), 'Hello \nWorld');
+  equal(dedent('Hello\n  World'), 'Hello\n  World');
+  equal(dedent('    Hello\n  World'), '  Hello\nWorld');
+  equal(dedent('  Hello\nWorld'), '  Hello\nWorld');
+  equal(dedent('  Hello World'), 'Hello World');
+  equal(dedent('  Hello\n  World'), 'Hello\nWorld');
+  equal(dedent('  Hello\n    World'), 'Hello\n  World');
+  equal(dedent('\t\tHello\tWorld'), 'Hello\tWorld');
+  equal(dedent('\t\tHello\n\t\tWorld'), 'Hello\nWorld');
+  equal(dedent('Hello\n\t\tWorld'), 'Hello\n\t\tWorld');
+  equal(dedent('\t\tHello\n\t\t\t\tWorld'), 'Hello\n\t\tWorld');
+  equal(dedent('\t\tHello\r\n\t\t\t\tWorld'), 'Hello\r\n\t\tWorld');
+  equal(dedent('\t\tHello\r\n\r\n\t\t\t\tWorld'), 'Hello\r\n\r\n\t\tWorld');
+  equal(dedent('\t\tHello\n\n\n\n\t\t\t\tWorld'), 'Hello\n\n\n\n\t\tWorld');
+  equal(dedent('\t\t\tHello\n\t\tWorld', '\\t'), '\t\tHello\n\tWorld');
+  equal(dedent('    Hello\n    World', '  '), '  Hello\n  World');
+  equal(dedent('    Hello\n    World', ''), '    Hello\n    World');
+  equal(dedent('\t\tHello\n\n\n\n\t\t\t\tWorld', '\\t'), '\tHello\n\n\n\n\t\t\tWorld');
+  equal(dedent('Hello\n\t\tWorld', '\t'), 'Hello\n\t\tWorld');
+  equal(dedent('Hello\n  World', ' '), 'Hello\n  World');
+  equal(dedent('  Hello\nWorld', ' '), '  Hello\nWorld');
+  deepEqual(dedent(123), '123');
+  deepEqual(dedent(''), '');
+  deepEqual(dedent(null), '');
+  deepEqual(dedent(undefined), '');
+});
+
diff --git a/tests/endsWith.js b/tests/endsWith.js
new file mode 100644
index 0000000..5836a53
--- /dev/null
+++ b/tests/endsWith.js
@@ -0,0 +1,42 @@
+var ok = require('assert').ok;
+var strictEqual = require('assert').strictEqual;
+var endsWith = require('../endsWith');
+
+
+test('#endsWith', function() {
+  ok(endsWith('foobar', 'bar'), 'foobar ends with bar');
+  ok(endsWith('foobarfoobar', 'bar'), 'foobar ends with bar');
+  ok(endsWith('foo', 'o'), 'foobar ends with o');
+  ok(endsWith('foobar', 'bar'), 'foobar ends with bar');
+  ok(endsWith('00018-0000062.Plone.sdh264.1a7264e6912a91aa4a81b64dc5517df7b8875994.mp4', 'mp4'), 'endsWith .mp4');
+  ok(!endsWith('fooba', 'bar'), 'fooba does not end with bar');
+  ok(endsWith(12345, 45), '12345 ends with 45');
+  ok(!endsWith(12345, 6), '12345 does not end with 6');
+  ok(endsWith('', ''), 'empty string ends with empty string');
+  ok(endsWith(null, ''), 'null ends with empty string');
+  ok(!endsWith(null, 'foo'), 'null ends with foo');
+  ok(endsWith('foobar?', 'bar', 6), 'foobar ends with bar at position 6');
+  ok(endsWith(12345, 34, 4), 'number ends with 34 at position 4');
+  ok(!endsWith(12345, 45, 4), 'number ends not with 45 at position 4');
+  ok(endsWith('foobä', 'ä'), 'string ends with a unicode');
+
+  strictEqual(endsWith('vader', 'der'), true);
+  strictEqual(endsWith('VADER', 'DER'), true);
+  strictEqual(endsWith('VADER', 'der'), false);
+  strictEqual(endsWith('VADER', 'DeR'), false);
+  strictEqual(endsWith('VADER'), false);
+  strictEqual(endsWith('undefined'), true);
+  strictEqual(endsWith('null', null), true);
+  strictEqual(endsWith('vader', 'der', 5), true);
+  strictEqual(endsWith('VADER', 'DER', 5), true);
+  strictEqual(endsWith('VADER', 'der', 5), false);
+  strictEqual(endsWith('VADER', 'DER', 5), true);
+  strictEqual(endsWith('VADER', 'der', 5), false);
+  strictEqual(endsWith('vader', 'der', -20), false);
+  strictEqual(endsWith('vader', 'der', 0), false);
+  strictEqual(endsWith('vader', 'der', 1), false);
+  strictEqual(endsWith('vader', 'der', 2), false);
+  strictEqual(endsWith('vader', 'der', 3), false);
+  strictEqual(endsWith('vader', 'der', 4), false);
+});
+
diff --git a/tests/escapeHTML.js b/tests/escapeHTML.js
new file mode 100644
index 0000000..acf75f2
--- /dev/null
+++ b/tests/escapeHTML.js
@@ -0,0 +1,15 @@
+var equal = require('assert').equal;
+var escapeHTML = require('../escapeHTML');
+
+
+test('#escapeHTML', function(){
+  equal(escapeHTML('<div>Blah & "blah" & \'blah\'</div>'), '<div>Blah & "blah" & 'blah'</div>');
+  equal(escapeHTML('<'), '&lt;');
+  equal(escapeHTML(' '), ' ');
+  equal(escapeHTML('¢'), '¢');
+  equal(escapeHTML('¢ £ ¥ € © ®'), '¢ £ ¥ € © ®');
+  equal(escapeHTML(5), '5');
+  equal(escapeHTML(''), '');
+  equal(escapeHTML(null), '');
+  equal(escapeHTML(undefined), '');
+});
diff --git a/tests/escapeRegExp.js b/tests/escapeRegExp.js
new file mode 100644
index 0000000..1b98f35
--- /dev/null
+++ b/tests/escapeRegExp.js
@@ -0,0 +1,9 @@
+var equal = require('assert').equal;
+var escapeRegExp = require('../helper/escapeRegExp');
+
+
+test('#escapeRegExp', function(){
+  equal(escapeRegExp(/hello(?=\sworld)/.source), 'hello\\(\\?\\=\\\\sworld\\)', 'with lookahead');
+  equal(escapeRegExp(/hello(?!\shell)/.source), 'hello\\(\\?\\!\\\\shell\\)', 'with negative lookahead');
+});
+
diff --git a/tests/exports.js b/tests/exports.js
new file mode 100644
index 0000000..48d16b8
--- /dev/null
+++ b/tests/exports.js
@@ -0,0 +1,9 @@
+var _ = require('underscore');
+var deepEqual = require('assert').deepEqual;
+var s = require('../');
+
+test('#exports', function() {
+  deepEqual(_.intersection(Object.keys(s.exports()), _.functions(_)), [],
+    'Conflicts exist between exports and underscore functions'
+  );
+});
diff --git a/tests/humanize.js b/tests/humanize.js
new file mode 100644
index 0000000..126bb98
--- /dev/null
+++ b/tests/humanize.js
@@ -0,0 +1,18 @@
+var equal = require('assert').equal;
+var humanize = require('../humanize');
+
+
+test('#humanize', function(){
+  equal(humanize('the_humanize_string_method'), 'The humanize string method');
+  equal(humanize('ThehumanizeStringMethod'), 'Thehumanize string method');
+  equal(humanize('-ThehumanizeStringMethod'), 'Thehumanize string method');
+  equal(humanize('the humanize string method'), 'The humanize string method');
+  equal(humanize('the humanize_id string method_id'), 'The humanize id string method');
+  equal(humanize('the  humanize string method  '), 'The humanize string method');
+  equal(humanize('   capitalize dash-CamelCase_underscore trim  '), 'Capitalize dash camel case underscore trim');
+  equal(humanize(123), '123');
+  equal(humanize(''), '');
+  equal(humanize(null), '');
+  equal(humanize(undefined), '');
+});
+
diff --git a/tests/include.js b/tests/include.js
new file mode 100644
index 0000000..ef2d3a8
--- /dev/null
+++ b/tests/include.js
@@ -0,0 +1,15 @@
+var ok = require('assert').ok;
+var include = require('../include');
+var s = require('../');
+
+
+test('#include', function() {
+  ok(include('foobar', 'bar'), 'foobar includes bar');
+  ok(!include('foobar', 'buzz'), 'foobar does not includes buzz');
+  ok(include(12345, 34), '12345 includes 34');
+  ok(!s.contains(12345, 6), '12345 does not include 6');
+  ok(!include('', 34), 'empty string includes 34');
+  ok(!include(null, 34), 'null includes 34');
+  ok(include(null, ''), 'null includes empty string');
+});
+
diff --git a/tests/insert.js b/tests/insert.js
new file mode 100644
index 0000000..612003c
--- /dev/null
+++ b/tests/insert.js
@@ -0,0 +1,15 @@
+var equal = require('assert').equal;
+var insert = require('../insert');
+
+
+test('#insert', function(){
+  equal(insert('Hello ', 6, 'Jessy'), 'Hello Jessy');
+  equal(insert('Hello', 0, 'Jessy '), 'Jessy Hello');
+  equal(insert('Hello ', 100, 'Jessy'), 'Hello Jessy');
+  equal(insert('', 100, 'Jessy'), 'Jessy');
+  equal(insert(null, 100, 'Jessy'), 'Jessy');
+  equal(insert(undefined, 100, 'Jessy'), 'Jessy');
+  equal(insert(12345, 5, 'Jessy'), '12345Jessy');
+  equal(insert(12345, 3, 'Jessy'), '123Jessy45');
+});
+
diff --git a/tests/isBlank.js b/tests/isBlank.js
new file mode 100644
index 0000000..750da8c
--- /dev/null
+++ b/tests/isBlank.js
@@ -0,0 +1,17 @@
+var ok = require('assert').ok;
+var isBlank = require('../isBlank');
+
+
+test('#isBlank', function(){
+  ok(isBlank(''));
+  ok(isBlank(' '));
+  ok(isBlank('\n'));
+  ok(!isBlank('a'));
+  ok(!isBlank('0'));
+  ok(!isBlank(0));
+  ok(isBlank(''));
+  ok(isBlank(null));
+  ok(isBlank(undefined));
+  ok(!isBlank(false));
+});
+
diff --git a/tests/join.js b/tests/join.js
new file mode 100644
index 0000000..cf8db8e
--- /dev/null
+++ b/tests/join.js
@@ -0,0 +1,18 @@
+var equal = require('assert').equal;
+var join = require('../join');
+
+
+test('#join', function() {
+  equal(join('', 'foo', 'bar'), 'foobar', 'basic join');
+  equal(join('', 1, 'foo', 2), '1foo2', 'join numbers and strings');
+  equal(join(' ','foo', 'bar'), 'foo bar', 'join with spaces');
+  equal(join('1', '2', '2'), '212', 'join number strings');
+  equal(join(1, 2, 2), '212', 'join numbers');
+  equal(join('','foo', null), 'foo', 'join null with string returns string');
+  equal(join(null,'foo', 'bar'), 'foobar', 'join strings with null returns string');
+  equal(join(1, 2, 3, 4), '21314');
+  equal(join('|', 'foo', 'bar', 'baz'), 'foo|bar|baz');
+  equal(join('',2,3,null), '23');
+  equal(join(null,2,3), '23');
+});
+
diff --git a/tests/levenshtein.js b/tests/levenshtein.js
new file mode 100644
index 0000000..21fe66e
--- /dev/null
+++ b/tests/levenshtein.js
@@ -0,0 +1,22 @@
+var equal = require('assert').equal;
+var levenshtein = require('../levenshtein');
+
+test('#levenshtein', function() {
+  equal(levenshtein('Godfather', 'Godfather'), 0);
+  equal(levenshtein('Godfather', 'Godfathe'), 1);
+  equal(levenshtein('Godfather', 'odfather'), 1);
+  equal(levenshtein('Godfather', 'godfather'), 1);
+  equal(levenshtein('Godfather', 'Gdfthr'), 3);
+  equal(levenshtein('seven', 'eight'), 5);
+  equal(levenshtein('123', 123), 0);
+  equal(levenshtein(321, '321'), 0);
+  equal(levenshtein('lol', null), 3);
+  equal(levenshtein('lol'), 3);
+  equal(levenshtein(null, 'lol'), 3);
+  equal(levenshtein(undefined, 'lol'), 3);
+  equal(levenshtein(), 0);
+});
+
+test('#levenshtein non-latin', function() {
+  equal(levenshtein('因為我是中國人所以我會說中文', '因為我是英國人所以我會說英文'), 2);
+});
diff --git a/tests/lines.js b/tests/lines.js
new file mode 100644
index 0000000..213b381
--- /dev/null
+++ b/tests/lines.js
@@ -0,0 +1,20 @@
+var equal = require('assert').equal;
+var deepEqual = require('assert').deepEqual;
+var lines = require('../lines');
+
+
+test('#lines', function() {
+  equal(lines('Hello\nWorld').length, 2);
+  equal(lines('Hello\rWorld').length, 2);
+  equal(lines('Hello World').length, 1);
+  equal(lines('\r\n\n\r').length, 4);
+  equal(lines('Hello\r\r\nWorld').length, 3);
+  equal(lines('Hello\r\rWorld').length, 3);
+  equal(lines(123).length, 1);
+  deepEqual(lines(''), ['']);
+  deepEqual(lines(null), []);
+  deepEqual(lines(undefined), []);
+  deepEqual(lines('Hello\rWorld'), ['Hello', 'World']);
+  deepEqual(lines('Hello\r\nWorld'), ['Hello', 'World']);
+});
+
diff --git a/tests/lpad.js b/tests/lpad.js
new file mode 100644
index 0000000..d2be782
--- /dev/null
+++ b/tests/lpad.js
@@ -0,0 +1,14 @@
+var equal = require('assert').equal;
+var lpad = require('../lpad');
+
+
+test('#lpad', function() {
+  equal(lpad('1', 8), '       1');
+  equal(lpad(1, 8), '       1');
+  equal(lpad('1', 8, '0'), '00000001');
+  equal(lpad('1', 8, '0', 'left'), '00000001');
+  equal(lpad('', 2), '  ');
+  equal(lpad(null, 2), '  ');
+  equal(lpad(undefined, 2), '  ');
+});
+
diff --git a/tests/lrpad.js b/tests/lrpad.js
new file mode 100644
index 0000000..0b95088
--- /dev/null
+++ b/tests/lrpad.js
@@ -0,0 +1,16 @@
+var equal = require('assert').equal;
+var lrpad = require('../lrpad');
+
+
+test('#lrpad', function() {
+  equal(lrpad('1', 8), '    1   ');
+  equal(lrpad(1, 8), '    1   ');
+  equal(lrpad('1', 8, '0'), '00001000');
+  equal(lrpad('foo', 8, '0'), '000foo00');
+  equal(lrpad('foo', 7, '0'), '00foo00');
+  equal(lrpad('foo', 7, '!@$%dofjrofj'), '!!foo!!');
+  equal(lrpad('', 2), '  ');
+  equal(lrpad(null, 2), '  ');
+  equal(lrpad(undefined, 2), '  ');
+});
+
diff --git a/tests/ltrim.js b/tests/ltrim.js
new file mode 100644
index 0000000..06aff85
--- /dev/null
+++ b/tests/ltrim.js
@@ -0,0 +1,21 @@
+var equal = require('assert').equal;
+var ltrim = require('../ltrim');
+
+test('#ltrim', function() {
+  equal(ltrim(' foo'), 'foo');
+  equal(ltrim('    foo'), 'foo');
+  equal(ltrim('foo '), 'foo ');
+  equal(ltrim(' foo '), 'foo ');
+  equal(ltrim(''), '', 'ltrim empty string should return empty string');
+  equal(ltrim(null), '', 'ltrim null should return empty string');
+  equal(ltrim(undefined), '', 'ltrim undefined should return empty string');
+
+  equal(ltrim('ffoo', 'f'), 'oo');
+  equal(ltrim('ooff', 'f'), 'ooff');
+  equal(ltrim('ffooff', 'f'), 'ooff');
+
+  equal(ltrim('_-foobar-_', '_-'), 'foobar-_');
+
+  equal(ltrim(123, 1), '23');
+});
+
diff --git a/tests/map.js b/tests/map.js
new file mode 100644
index 0000000..85a2fab
--- /dev/null
+++ b/tests/map.js
@@ -0,0 +1,31 @@
+var equal = require('assert').equal;
+var map = require('../map');
+
+
+test('#map', function() {
+  equal(map('Hello world', function(x) {
+    return x;
+  }), 'Hello world');
+  equal(map(12345, function(x) {
+    return x;
+  }), '12345');
+  equal(map('Hello world', function(x) {
+    if (x === 'o') x = 'O';
+    return x;
+  }), 'HellO wOrld');
+  equal(map('', function(x) {
+    return x;
+  }), '');
+  equal(map(null, function(x) {
+    return x;
+  }), '');
+  equal(map(undefined, function(x) {
+    return x;
+  }), '');
+  equal(map('Hello world', ''), 'Hello world');
+  equal(map('Hello world', null), 'Hello world');
+  equal(map('Hello world', undefined), 'Hello world');
+  equal(map('', ''), '');
+  equal(map(null, null), '');
+  equal(map(undefined, undefined), '');
+});
diff --git a/tests/naturalCmp.js b/tests/naturalCmp.js
new file mode 100644
index 0000000..6ead619
--- /dev/null
+++ b/tests/naturalCmp.js
@@ -0,0 +1,40 @@
+var naturalCmp = require('../naturalCmp');
+var _ = require('underscore');
+var equal = require('assert').equal;
+
+test('#naturalCmp', function() {
+  // Should be associative
+  _.each([
+    ['abc', null],
+    ['abc', '123'],
+    ['def', 'abc'],
+    ['ab', 'a'],
+    ['r69', 'r9'],
+    ['123', '122'],
+    ['ac2', 'ab3'],
+    ['a-12', 'a-11'],
+    ['11', '-12'],
+    ['15.05', '15'],
+    ['15ac', '15ab32'],
+    ['16', '15ab'],
+    ['15a123', '15a122'],
+    ['15ab16', '15ab'],
+    ['abc', 'Abc'],
+    ['abc', 'aBc'],
+    ['aBc', 'Abc']
+  ], function(vals) {
+    var a = vals[0], b = vals[1];
+    equal(naturalCmp(a, b), 1, '\'' + a + '\' >= \'' + b + '\'');
+    equal(naturalCmp(b, a), -1, '\'' + b + '\' <= \'' + a + '\'');
+  });
+  _.each([
+    ['123', '123'],
+    ['abc', 'abc'],
+    ['r12', 'r12'],
+    ['12a', '12a']
+  ], function(vals) {
+    var a = vals[0], b = vals[1];
+    equal(naturalCmp(a, b), 0, '\'' + a + '\' == \'' + b + '\'');
+    equal(naturalCmp(b, a), 0, '\'' + b + '\' == \'' + a + '\'');
+  });
+});
diff --git a/tests/naturalSort.js b/tests/naturalSort.js
new file mode 100644
index 0000000..e1c2dff
--- /dev/null
+++ b/tests/naturalSort.js
@@ -0,0 +1,8 @@
+var assert = require('assert');
+var naturalCmp = require('../naturalCmp');
+
+test('#naturalSort', function() {
+  var arr =  ['foo2', 'foo1', 'foo10', 'foo30', 'foo100', 'foo10bar'],
+    sorted = ['foo1', 'foo2', 'foo10', 'foo10bar', 'foo30', 'foo100'];
+  assert.deepEqual(arr.sort(naturalCmp), sorted);
+});
diff --git a/tests/numberFormat.js b/tests/numberFormat.js
new file mode 100644
index 0000000..4274edd
--- /dev/null
+++ b/tests/numberFormat.js
@@ -0,0 +1,25 @@
+var equal = require('assert').equal;
+var numberFormat = require('../numberFormat');
+
+
+test('#numberFormat', function() {
+  equal(numberFormat(9000), '9,000');
+  equal(numberFormat(9000, 0), '9,000');
+  equal(numberFormat(9000, 0, '', ''), '9000');
+  equal(numberFormat(90000, 2), '90,000.00');
+  equal(numberFormat(1000.754), '1,001');
+  equal(numberFormat(1000.754, 2), '1,000.75');
+  equal(numberFormat(1000.755, 2), '1,000.75');
+  equal(numberFormat(1000.756, 2), '1,000.76');
+  equal(numberFormat(1000.754, 0, ',', '.'), '1.001');
+  equal(numberFormat(1000.754, 2, ',', '.'), '1.000,75');
+  equal(numberFormat(1000000.754, 2, ',', '.'), '1.000.000,75');
+  equal(numberFormat(1000000000), '1,000,000,000');
+  equal(numberFormat(100000000), '100,000,000');
+  equal(numberFormat('not number'), '');
+  equal(numberFormat(), '');
+  equal(numberFormat(null, '.', ','), '');
+  equal(numberFormat(undefined, '.', ','), '');
+  equal(numberFormat(new Number(5000)), '5,000');
+});
+
diff --git a/tests/pad.js b/tests/pad.js
new file mode 100644
index 0000000..7548502
--- /dev/null
+++ b/tests/pad.js
@@ -0,0 +1,19 @@
+var equal = require('assert').equal;
+var pad = require('../pad');
+
+
+test('#pad', function() {
+  equal(pad('1', 8), '       1');
+  equal(pad(1, 8), '       1');
+  equal(pad('1', 8, '0'), '00000001');
+  equal(pad('1', 8, '0', 'left'), '00000001');
+  equal(pad('1', 8, '0', 'right'), '10000000');
+  equal(pad('1', 8, '0', 'both'), '00001000');
+  equal(pad('foo', 8, '0', 'both'), '000foo00');
+  equal(pad('foo', 7, '0', 'both'), '00foo00');
+  equal(pad('foo', 7, '!@$%dofjrofj', 'both'), '!!foo!!');
+  equal(pad('', 2), '  ');
+  equal(pad(null, 2), '  ');
+  equal(pad(undefined, 2), '  ');
+});
+
diff --git a/tests/pred.js b/tests/pred.js
new file mode 100644
index 0000000..40a7dc5
--- /dev/null
+++ b/tests/pred.js
@@ -0,0 +1,20 @@
+var equal = require('assert').equal;
+var deepEqual = require('assert').deepEqual;
+var pred = require('../pred');
+
+
+test('#pred', function(){
+  equal(pred('b'), 'a');
+  equal(pred('B'), 'A');
+  equal(pred(','), '+');
+  equal(pred(2), '1');
+  deepEqual(pred().length, 0);
+  deepEqual(pred('').length, 0);
+  deepEqual(pred(null).length, 0);
+  deepEqual(pred(undefined).length, 0);
+  deepEqual(pred(), '');
+  deepEqual(pred(''), '');
+  deepEqual(pred(null), '');
+  deepEqual(pred(undefined), '');
+});
+
diff --git a/tests/prune.js b/tests/prune.js
new file mode 100644
index 0000000..db65dce
--- /dev/null
+++ b/tests/prune.js
@@ -0,0 +1,25 @@
+var equal = require('assert').equal;
+var prune = require('../prune');
+
+
+test('#prune', function(){
+  equal(prune('Hello, cruel world', 6, ' read more'), 'Hello read more');
+  equal(prune('Hello, world', 5, 'read a lot more'), 'Hello, world');
+  equal(prune('Hello, world', 5), 'Hello...');
+  equal(prune('Hello, world', 8), 'Hello...');
+  equal(prune('Hello, cruel world', 15), 'Hello, cruel...');
+  equal(prune('Hello world', 22), 'Hello world');
+  equal(prune('Привет, жестокий мир', 6, ' read more'), 'Привет read more');
+  equal(prune('Привет, мир', 6, 'read a lot more'), 'Привет, мир');
+  equal(prune('Привет, мир', 6), 'Привет...');
+  equal(prune('Привет, мир', 8), 'Привет...');
+  equal(prune('Привет, жестокий мир', 16), 'Привет, жестокий...');
+  equal(prune('Привет, мир', 22), 'Привет, мир');
+  equal(prune('alksjd!!!!!!....', 100, ''), 'alksjd!!!!!!....');
+  equal(prune(123, 10), '123');
+  equal(prune(123, 1, 321), '321');
+  equal(prune('', 5), '');
+  equal(prune(null, 5), '');
+  equal(prune(undefined, 5), '');
+});
+
diff --git a/tests/quote.js b/tests/quote.js
new file mode 100644
index 0000000..42b15e7
--- /dev/null
+++ b/tests/quote.js
@@ -0,0 +1,18 @@
+var equal = require('assert').equal;
+var quote = require('../quote');
+var q = require('../').q;
+
+
+test('#quote', function(){
+  equal(quote('foo'), '"foo"');
+  equal(quote('"foo"'), '""foo""');
+  equal(quote(1), '"1"');
+  equal(quote('foo', '\''), '\'foo\'');
+
+  // alias
+  equal(q('foo'), '"foo"');
+  equal(q(''), '""');
+  equal(q(null), '""');
+  equal(q(undefined), '""');
+});
+
diff --git a/tests/repeat.js b/tests/repeat.js
new file mode 100644
index 0000000..5784d1f
--- /dev/null
+++ b/tests/repeat.js
@@ -0,0 +1,16 @@
+var equal = require('assert').equal;
+var repeat = require('../repeat');
+
+
+test('#repeat', function() {
+  equal(repeat('foo'), '');
+  equal(repeat('foo', 3), 'foofoofoo');
+  equal(repeat('foo', '3'), 'foofoofoo');
+  equal(repeat(123, 2), '123123');
+  equal(repeat(1234, 2, '*'), '1234*1234');
+  equal(repeat(1234, 2, 5), '123451234');
+  equal(repeat('', 2), '');
+  equal(repeat(null, 2), '');
+  equal(repeat(undefined, 2), '');
+});
+
diff --git a/tests/replaceAll.js b/tests/replaceAll.js
new file mode 100644
index 0000000..cf16bd2
--- /dev/null
+++ b/tests/replaceAll.js
@@ -0,0 +1,20 @@
+var equal = require('assert').equal;
+var replaceAll = require('../replaceAll');
+
+
+test('#replaceAll', function(){
+  equal(replaceAll('a', 'a', 'b'), 'b');
+  equal(replaceAll('aa', 'a', 'b'), 'bb');
+  equal(replaceAll('aca', 'a', 'b'), 'bcb');
+  equal(replaceAll('ccc', 'a', 'b'), 'ccc');
+  equal(replaceAll('AAa', 'a', 'b'), 'AAb');
+  equal(replaceAll('Aa', 'a', 'b', true), 'bb');
+  equal(replaceAll('foo bar foo', 'foo', 'moo'), 'moo bar moo');
+  equal(replaceAll('foo bar\n foo', 'foo', 'moo'), 'moo bar\n moo');
+  equal(replaceAll('foo bar FoO', 'foo', 'moo', true), 'moo bar moo');
+  equal(replaceAll('', 'a', 'b'), '');
+  equal(replaceAll(null, 'a', 'b'), '');
+  equal(replaceAll(undefined, 'a', 'b'), '');
+  equal(replaceAll(12345, 'a', 'b'), 12345);
+});
+
diff --git a/tests/reverse.js b/tests/reverse.js
new file mode 100644
index 0000000..edfcdbf
--- /dev/null
+++ b/tests/reverse.js
@@ -0,0 +1,16 @@
+var equal = require('assert').equal;
+var reverse = require('../reverse');
+
+
+test('#reverse', function() {
+  equal(reverse('foo'), 'oof' );
+  equal(reverse('foobar'), 'raboof' );
+  equal(reverse('foo bar'), 'rab oof' );
+  equal(reverse('saippuakauppias'), 'saippuakauppias' );
+  equal(reverse(123), '321', 'Non string');
+  equal(reverse(123.45), '54.321', 'Non string');
+  equal(reverse(''), '', 'reversing empty string returns empty string' );
+  equal(reverse(null), '', 'reversing null returns empty string' );
+  equal(reverse(undefined), '', 'reversing undefined returns empty string' );
+});
+
diff --git a/tests/rpad.js b/tests/rpad.js
new file mode 100644
index 0000000..7cc073c
--- /dev/null
+++ b/tests/rpad.js
@@ -0,0 +1,15 @@
+var equal = require('assert').equal;
+var rpad = require('../rpad');
+
+
+test('#rpad', function() {
+  equal(rpad('1', 8), '1       ');
+  equal(rpad(1, 8), '1       ');
+  equal(rpad('1', 8, '0'), '10000000');
+  equal(rpad('foo', 8, '0'), 'foo00000');
+  equal(rpad('foo', 7, '0'), 'foo0000');
+  equal(rpad('', 2), '  ');
+  equal(rpad(null, 2), '  ');
+  equal(rpad(undefined, 2), '  ');
+});
+
diff --git a/tests/rtrim.js b/tests/rtrim.js
new file mode 100644
index 0000000..6c157a3
--- /dev/null
+++ b/tests/rtrim.js
@@ -0,0 +1,22 @@
+var equal = require('assert').equal;
+var rtrim = require('../rtrim');
+
+test('#rtrim', function() {
+  equal(rtrim('http://foo/', '/'), 'http://foo', 'clean trailing slash');
+  equal(rtrim(' foo'), ' foo');
+  equal(rtrim('foo '), 'foo');
+  equal(rtrim('foo     '), 'foo');
+  equal(rtrim('foo  bar     '), 'foo  bar');
+  equal(rtrim(' foo '), ' foo');
+
+  equal(rtrim('ffoo', 'f'), 'ffoo');
+  equal(rtrim('ooff', 'f'), 'oo');
+  equal(rtrim('ffooff', 'f'), 'ffoo');
+
+  equal(rtrim('_-foobar-_', '_-'), '_-foobar');
+
+  equal(rtrim(123, 3), '12');
+  equal(rtrim(''), '', 'rtrim empty string should return empty string');
+  equal(rtrim(null), '', 'rtrim null should return empty string');
+});
+
diff --git a/tests/slugify.js b/tests/slugify.js
new file mode 100644
index 0000000..2b77998
--- /dev/null
+++ b/tests/slugify.js
@@ -0,0 +1,16 @@
+var equal = require('assert').equal;
+var slugify = require('../slugify');
+
+
+test('#slugify', function() {
+  equal(slugify('Jack & Jill like numbers 1,2,3 and 4 and silly characters ?%.$!/'), 'jack-jill-like-numbers-1-2-3-and-4-and-silly-characters');
+  equal(slugify('Un éléphant à l\'orée du bois'), 'un-elephant-a-l-oree-du-bois');
+  equal(slugify('I know latin characters: á í ó ú ç ã õ ñ ü ă ș ț'), 'i-know-latin-characters-a-i-o-u-c-a-o-n-u-a-s-t');
+  equal(slugify('I am a word too, even though I am but a single letter: i!'), 'i-am-a-word-too-even-though-i-am-but-a-single-letter-i');
+  equal(slugify('Some asian 天地人 characters'), 'some-asian-characters');
+  equal(slugify('SOME Capital Letters'), 'some-capital-letters');
+  equal(slugify(''), '');
+  equal(slugify(null), '');
+  equal(slugify(undefined), '');
+});
+
diff --git a/tests/splice.js b/tests/splice.js
new file mode 100644
index 0000000..208b7c4
--- /dev/null
+++ b/tests/splice.js
@@ -0,0 +1,10 @@
+var equal = require('assert').equal;
+var splice = require('../splice');
+
+
+test('#splice', function(){
+  equal(splice('https://edtsech@bitbucket.org/edtsech/underscore.strings', 30, 7, 'epeli'),
+         'https://edtsech@bitbucket.org/epeli/underscore.strings');
+  equal(splice(12345, 1, 2, 321), '132145', 'Non strings');
+});
+
diff --git a/tests/sprintf.js b/tests/sprintf.js
new file mode 100644
index 0000000..c294528
--- /dev/null
+++ b/tests/sprintf.js
@@ -0,0 +1,15 @@
+var equal = require('assert').equal;
+var sprintf = require('../sprintf');
+
+
+test('#sprintf', function() {
+  // Should be very tested function already.  Thanks to
+  // http://www.diveintojavascript.com/projects/sprintf-for-javascript
+  equal(sprintf('Hello %s', 'me'), 'Hello me', 'basic');
+  equal(sprintf('Hello %s', 'me'), 'Hello me', 'object');
+  equal(sprintf('%.1f', 1.22222), '1.2', 'round');
+  equal(sprintf('%.1f', 1.17), '1.2', 'round 2');
+  equal(sprintf('%(id)d - %(name)s', {id: 824, name: 'Hello World'}), '824 - Hello World', 'Named replacements work');
+  equal(sprintf('%(args[0].id)d - %(args[1].name)s', {args: [{id: 824}, {name: 'Hello World'}]}), '824 - Hello World', 'Named replacements with arrays work');
+});
+
diff --git a/tests/standalone.js b/tests/standalone.js
new file mode 100644
index 0000000..9c7ff50
--- /dev/null
+++ b/tests/standalone.js
@@ -0,0 +1,97 @@
+var s = require('../');
+var equal = require('assert').equal;
+var deepEqual = require('assert').deepEqual;
+var strictEqual = require('assert').strictEqual;
+
+
+test('provides standalone functions via the s global', function() {
+  equal(typeof s.trim, 'function');
+});
+
+test('has standalone chaining', function() {
+  var res = s('  foo  ').trim().capitalize().value();
+  equal(res, 'Foo');
+});
+
+test('chaining supports tapping', function() {
+  var res = s('foo').tap(function(value) {
+    return 'BAR' + value + 'BAR';
+  }).value();
+  equal(res, 'BARfooBAR');
+});
+
+test('tap breaks the chain if the return value is not a string', function() {
+  var res = s('foo').tap(function(value) {
+    return value === 'foo';
+  });
+
+  strictEqual(res, true);
+});
+
+test('chain objects are immutable', function() {
+  var chain = s('foo');
+  chain.capitalize();
+  equal(chain.value(), 'foo');
+});
+
+test('methods returning non-string values stops the chain', function() {
+  strictEqual(s('foobar').startsWith('foo'), true);
+  strictEqual(s('foobar').endsWith('foo'), false);
+  deepEqual(s('hello\nworld').lines(), ['hello', 'world']);
+});
+
+test('prototype methods are available in the chain', function() {
+  var chain = s('foo');
+  [
+    'toUpperCase',
+    'toLowerCase',
+    'split',
+    'replace',
+    'slice',
+    'substring',
+    'substr',
+    'concat'
+  ].forEach(function(method) {
+    equal(typeof chain[method], 'function', 'has method: ' + method);
+  });
+
+});
+
+test('PROTOTYPE: toUpperCase', function() {
+  equal(s('foo').toUpperCase().value(), 'FOO');
+});
+
+test('PROTOTYPE: toLowerCase', function() {
+  equal(s('BAR').toLowerCase().value(), 'bar');
+});
+
+test('PROTOTYPE: split', function() {
+  deepEqual(s('foo bar').split(' '), ['foo', 'bar']);
+});
+
+test('PROTOTYPE: replace', function() {
+  equal(s('faa').replace('a', 'o').value(), 'foa');
+});
+
+test('PROTOTYPE: slice', function() {
+  equal(s('#anchor').slice(1).value(), 'anchor');
+});
+
+test('PROTOTYPE: substring', function() {
+  equal(s('foobar').substring(0, 3).value(), 'foo');
+});
+
+test('PROTOTYPE: substring', function() {
+  equal(s('foobar!').substr(3, 3).value(), 'bar');
+});
+
+test('PROTOTYPE: concat', function() {
+  equal(s('foo').concat('bar').value(), 'foobar');
+});
+
+test('PROTOTYPE: can combine methods', function() {
+  equal(
+    s('  foo  bar').toUpperCase().concat('   BAZ').clean().value(),
+    'FOO BAR BAZ'
+  );
+});
diff --git a/tests/startsWith.js b/tests/startsWith.js
new file mode 100644
index 0000000..144fb6f
--- /dev/null
+++ b/tests/startsWith.js
@@ -0,0 +1,39 @@
+var ok = require('assert').ok;
+var strictEqual = require('assert').strictEqual;
+var startsWith = require('../startsWith');
+
+
+test('#startsWith', function() {
+  ok(startsWith('foobar', 'foo'), 'foobar starts with foo');
+  ok(!startsWith('oobar', 'foo'), 'oobar does not start with foo');
+  ok(startsWith('oobar', 'o'), 'oobar starts with o');
+  ok(startsWith(12345, 123), '12345 starts with 123');
+  ok(!startsWith(2345, 123), '2345 does not start with 123');
+  ok(startsWith('', ''), 'empty string starts with empty string');
+  ok(startsWith(null, ''), 'null starts with empty string');
+  ok(!startsWith(null, 'foo'), 'null starts with foo');
+  ok(startsWith('-foobar', 'foo', 1), 'foobar starts with foo at position 1');
+  ok(startsWith('foobar', 'foo', 0), 'foobar starts with foo at position 0');
+  ok(!startsWith('foobar', 'foo', 1), 'foobar starts not with foo at position 1');
+  ok(startsWith('Äpfel', 'Ä'), 'string starts with a unicode');
+
+  strictEqual(startsWith('hello', 'hell'), true);
+  strictEqual(startsWith('HELLO', 'HELL'), true);
+  strictEqual(startsWith('HELLO', 'hell'), false);
+  strictEqual(startsWith('HELLO', 'hell'), false);
+  strictEqual(startsWith('hello', 'hell', 0), true);
+  strictEqual(startsWith('HELLO', 'HELL', 0), true);
+  strictEqual(startsWith('HELLO', 'hell', 0), false);
+  strictEqual(startsWith('HELLO', 'hell', 0), false);
+  strictEqual(startsWith('HELLO'), false);
+  strictEqual(startsWith('undefined'), true);
+  strictEqual(startsWith('null', null), true);
+  strictEqual(startsWith('hello', 'hell', -20), true);
+  strictEqual(startsWith('hello', 'hell', 1), false);
+  strictEqual(startsWith('hello', 'hell', 2), false);
+  strictEqual(startsWith('hello', 'hell', 3), false);
+  strictEqual(startsWith('hello', 'hell', 4), false);
+  strictEqual(startsWith('hello', 'hell', 5), false);
+  strictEqual(startsWith('hello', 'hell', 20), false);
+});
+
diff --git a/tests/strLeft.js b/tests/strLeft.js
new file mode 100644
index 0000000..cdfec02
--- /dev/null
+++ b/tests/strLeft.js
@@ -0,0 +1,16 @@
+var equal = require('assert').equal;
+var strLeft = require('../strLeft');
+
+
+test('#strLeft', function() {
+  equal(strLeft('This_is_a_test_string', '_'), 'This');
+  equal(strLeft('This_is_a_test_string', 'This'), '');
+  equal(strLeft('This_is_a_test_string'), 'This_is_a_test_string');
+  equal(strLeft('This_is_a_test_string', ''), 'This_is_a_test_string');
+  equal(strLeft('This_is_a_test_string', '-'), 'This_is_a_test_string');
+  equal(strLeft('', 'foo'), '');
+  equal(strLeft(null, 'foo'), '');
+  equal(strLeft(undefined, 'foo'), '');
+  equal(strLeft(123454321, 3), '12');
+});
+
diff --git a/tests/strLeftBack.js b/tests/strLeftBack.js
new file mode 100644
index 0000000..ab66528
--- /dev/null
+++ b/tests/strLeftBack.js
@@ -0,0 +1,16 @@
+var equal = require('assert').equal;
+var strLeftBack = require('../strLeftBack');
+
+
+test('#strLeftBack', function() {
+  equal(strLeftBack('This_is_a_test_string', '_'), 'This_is_a_test');
+  equal(strLeftBack('This_is_a_test_string', 'This'), '');
+  equal(strLeftBack('This_is_a_test_string'), 'This_is_a_test_string');
+  equal(strLeftBack('This_is_a_test_string', ''), 'This_is_a_test_string');
+  equal(strLeftBack('This_is_a_test_string', '-'), 'This_is_a_test_string');
+  equal(strLeftBack('', 'foo'), '');
+  equal(strLeftBack(null, 'foo'), '');
+  equal(strLeftBack(undefined, 'foo'), '');
+  equal(strLeftBack(123454321, 3), '123454');
+});
+
diff --git a/tests/strRight.js b/tests/strRight.js
new file mode 100644
index 0000000..4ac1130
--- /dev/null
+++ b/tests/strRight.js
@@ -0,0 +1,17 @@
+var equal = require('assert').equal;
+var strRight = require('../strRight');
+
+
+test('#strRight', function() {
+  equal(strRight('This_is_a_test_string', '_'), 'is_a_test_string');
+  equal(strRight('This_is_a_test_string', 'string'), '');
+  equal(strRight('This_is_a_test_string'), 'This_is_a_test_string');
+  equal(strRight('This_is_a_test_string', ''), 'This_is_a_test_string');
+  equal(strRight('This_is_a_test_string', '-'), 'This_is_a_test_string');
+  equal(strRight('This_is_a_test_string', ''), 'This_is_a_test_string');
+  equal(strRight('', 'foo'), '');
+  equal(strRight(null, 'foo'), '');
+  equal(strRight(undefined, 'foo'), '');
+  equal(strRight(12345, 2), '345');
+});
+
diff --git a/tests/strRightBack.js b/tests/strRightBack.js
new file mode 100644
index 0000000..8c7e5ad
--- /dev/null
+++ b/tests/strRightBack.js
@@ -0,0 +1,16 @@
+var equal = require('assert').equal;
+var strRightBack = require('../strRightBack');
+
+
+test('#strRightBack', function() {
+  equal(strRightBack('This_is_a_test_string', '_'), 'string');
+  equal(strRightBack('This_is_a_test_string', 'string'), '');
+  equal(strRightBack('This_is_a_test_string'), 'This_is_a_test_string');
+  equal(strRightBack('This_is_a_test_string', ''), 'This_is_a_test_string');
+  equal(strRightBack('This_is_a_test_string', '-'), 'This_is_a_test_string');
+  equal(strRightBack('', 'foo'), '');
+  equal(strRightBack(null, 'foo'), '');
+  equal(strRightBack(undefined, 'foo'), '');
+  equal(strRightBack(12345, 2), '345');
+});
+
diff --git a/tests/stripTags.js b/tests/stripTags.js
new file mode 100644
index 0000000..f0d6377
--- /dev/null
+++ b/tests/stripTags.js
@@ -0,0 +1,14 @@
+var equal = require('assert').equal;
+var stripTags = require('../stripTags');
+
+
+test('#stripTags', function() {
+  equal(stripTags('a <a href="#">link</a>'), 'a link');
+  equal(stripTags('a <a href="#">link</a><script>alert("hello world!")</scr'+'ipt>'), 'a linkalert("hello world!")');
+  equal(stripTags('<html><body>hello world</body></html>'), 'hello world');
+  equal(stripTags(123), '123');
+  equal(stripTags(''), '');
+  equal(stripTags(null), '');
+  equal(stripTags(undefined), '');
+});
+
diff --git a/tests/succ.js b/tests/succ.js
new file mode 100644
index 0000000..41024b5
--- /dev/null
+++ b/tests/succ.js
@@ -0,0 +1,20 @@
+var equal = require('assert').equal;
+var deepEqual = require('assert').deepEqual;
+var succ = require('../succ');
+
+
+test('#succ', function(){
+  equal(succ('a'), 'b');
+  equal(succ('A'), 'B');
+  equal(succ('+'), ',');
+  equal(succ(1), '2');
+  deepEqual(succ().length, 0);
+  deepEqual(succ('').length, 0);
+  deepEqual(succ(null).length, 0);
+  deepEqual(succ(undefined).length, 0);
+  deepEqual(succ(), '');
+  deepEqual(succ(''), '');
+  deepEqual(succ(null), '');
+  deepEqual(succ(undefined), '');
+});
+
diff --git a/tests/surround.js b/tests/surround.js
new file mode 100644
index 0000000..15363aa
--- /dev/null
+++ b/tests/surround.js
@@ -0,0 +1,15 @@
+var equal = require('assert').equal;
+var surround = require('../surround');
+
+
+test('#surround', function(){
+  equal(surround('foo', 'ab'), 'abfooab');
+  equal(surround(1, 'ab'), 'ab1ab');
+  equal(surround(1, 2), '212');
+  equal(surround('foo', 1), '1foo1');
+  equal(surround('', 1), '11');
+  equal(surround(null, 1), '11');
+  equal(surround('foo', ''), 'foo');
+  equal(surround('foo', null), 'foo');
+});
+
diff --git a/tests/swapCase.js b/tests/swapCase.js
new file mode 100644
index 0000000..1683d3a
--- /dev/null
+++ b/tests/swapCase.js
@@ -0,0 +1,12 @@
+var equal = require('assert').equal;
+var swapCase = require('../swapCase');
+
+
+test('#swapCase', function(){
+  equal(swapCase('AaBbCcDdEe'), 'aAbBcCdDeE');
+  equal(swapCase('Hello World'), 'hELLO wORLD');
+  equal(swapCase(''), '');
+  equal(swapCase(null), '');
+  equal(swapCase(undefined), '');
+});
+
diff --git a/tests/titleize.js b/tests/titleize.js
new file mode 100644
index 0000000..fa1b9e7
--- /dev/null
+++ b/tests/titleize.js
@@ -0,0 +1,16 @@
+var equal = require('assert').equal;
+var titleize = require('../titleize');
+
+
+test('#titleize', function(){
+  equal(titleize('the titleize string method'), 'The Titleize String Method');
+  equal(titleize('the titleize string  method'), 'The Titleize String  Method');
+  equal(titleize(''), '', 'Titleize empty string returns empty string');
+  equal(titleize(null), '', 'Titleize null returns empty string');
+  equal(titleize(undefined), '', 'Titleize undefined returns empty string');
+  equal(titleize('let\'s have some fun'), 'Let\'s Have Some Fun');
+  equal(titleize('a-dash-separated-string'), 'A-Dash-Separated-String');
+  equal(titleize('A-DASH-SEPARATED-STRING'), 'A-Dash-Separated-String');
+  equal(titleize(123), '123');
+});
+
diff --git a/tests/toBoolean.js b/tests/toBoolean.js
new file mode 100644
index 0000000..de79fe4
--- /dev/null
+++ b/tests/toBoolean.js
@@ -0,0 +1,29 @@
+var strictEqual = require('assert').strictEqual;
+var toBoolean = require('../toBoolean');
+
+test('#toBoolean', function() {
+  strictEqual(toBoolean('false'), false);
+  strictEqual(toBoolean('false'), false);
+  strictEqual(toBoolean('False'), false);
+  strictEqual(toBoolean('Falsy',null,['false', 'falsy']), false);
+  strictEqual(toBoolean('true'), true);
+  strictEqual(toBoolean('the truth', 'the truth', 'this is falsy'), true);
+  strictEqual(toBoolean('this is falsy', 'the truth', 'this is falsy'), false);
+  strictEqual(toBoolean('true'), true);
+  strictEqual(toBoolean('trUe'), true);
+  strictEqual(toBoolean('trUe', /tru?/i), true);
+  strictEqual(toBoolean('something else'), undefined);
+  strictEqual(toBoolean(function(){}), true);
+  strictEqual(toBoolean(/regexp/), true);
+  strictEqual(toBoolean(''), undefined);
+  strictEqual(toBoolean(0), false);
+  strictEqual(toBoolean(1), true);
+  strictEqual(toBoolean('1'), true);
+  strictEqual(toBoolean('0'), false);
+  strictEqual(toBoolean(2), undefined);
+  strictEqual(toBoolean('foo true bar'), undefined);
+  strictEqual(toBoolean('foo true bar', /true/), true);
+  strictEqual(toBoolean('foo FALSE bar', null, /FALSE/), false);
+  strictEqual(toBoolean(' true  '), true);
+});
+
diff --git a/tests/toNumber.js b/tests/toNumber.js
new file mode 100644
index 0000000..c3c0f3c
--- /dev/null
+++ b/tests/toNumber.js
@@ -0,0 +1,45 @@
+var equal = require('assert').equal;
+var ok = require('assert').ok;
+var _ = require('underscore');
+var toNumber = require('../toNumber');
+
+
+test('#toNumber', function() {
+  _.each(['not a number', NaN, {}, [/a/], 'alpha6'], function(val) {
+    ok(isNaN(toNumber('not a number')));
+    equal(toNumber(Math.PI, val), 3);
+  });
+  equal(toNumber(0), 0);
+  equal(toNumber('0'), 0);
+  equal(toNumber('0.0'), 0);
+  equal(toNumber('        0.0    '), 0);
+  equal(toNumber('0.1'), 0);
+  equal(toNumber('0.1', 1), 0.1);
+  equal(toNumber('  0.1 ', 1), 0.1);
+  equal(toNumber('0000'), 0);
+  equal(toNumber('2.345'), 2);
+  equal(toNumber('2.345', NaN), 2);
+  equal(toNumber('2.345', 2), 2.35);
+  equal(toNumber('2.344', 2), 2.34);
+  equal(toNumber('2', 2), 2.00);
+  equal(toNumber(2, 2), 2.00);
+  equal(toNumber(-2), -2);
+  equal(toNumber('-2'), -2);
+  equal(toNumber(-2.5123, 3), -2.512);
+
+  // Negative precisions
+  equal(toNumber(-234, -1), -230);
+  equal(toNumber(234, -2), 200);
+  equal(toNumber('234', -2), 200);
+
+  _.each(['', null, undefined], function(val) {
+    equal(toNumber(val), 0);
+  });
+
+  _.each([Infinity, -Infinity], function(val) {
+    equal(toNumber(val), val);
+    equal(toNumber(val, val), val);
+    equal(toNumber(1, val), 1);
+  });
+});
+
diff --git a/tests/toSentence.js b/tests/toSentence.js
new file mode 100644
index 0000000..b299e47
--- /dev/null
+++ b/tests/toSentence.js
@@ -0,0 +1,12 @@
+var equal = require('assert').equal;
+var toSentence = require('../toSentence');
+
+
+test('#toSentence', function() {
+  equal(toSentence(['jQuery']), 'jQuery', 'array with a single element');
+  equal(toSentence(['jQuery', 'MooTools']), 'jQuery and MooTools', 'array with two elements');
+  equal(toSentence(['jQuery', 'MooTools', 'Prototype']), 'jQuery, MooTools and Prototype', 'array with three elements');
+  equal(toSentence(['jQuery', 'MooTools', 'Prototype', 'YUI']), 'jQuery, MooTools, Prototype and YUI', 'array with multiple elements');
+  equal(toSentence(['jQuery', 'MooTools', 'Prototype'], ',', ' or '), 'jQuery,MooTools or Prototype', 'handles custom separators');
+});
+
diff --git a/tests/toSentenceSerial.js b/tests/toSentenceSerial.js
new file mode 100644
index 0000000..9b4555b
--- /dev/null
+++ b/tests/toSentenceSerial.js
@@ -0,0 +1,10 @@
+var equal = require('assert').equal;
+var toSentenceSerial = require('../toSentenceSerial');
+
+
+test('#toSentenceSerial', function (){
+  equal(toSentenceSerial(['jQuery']), 'jQuery');
+  equal(toSentenceSerial(['jQuery', 'MooTools']), 'jQuery and MooTools');
+  equal(toSentenceSerial(['jQuery', 'MooTools', 'Prototype']), 'jQuery, MooTools, and Prototype');
+});
+
diff --git a/tests/trim.js b/tests/trim.js
new file mode 100644
index 0000000..c84b5fb
--- /dev/null
+++ b/tests/trim.js
@@ -0,0 +1,29 @@
+var trim = require('../trim');
+var equal = require('assert').equal;
+
+test('#trim', function() {
+  equal(trim(123), '123', 'Non string');
+  equal(trim(' foo'), 'foo');
+  equal(trim('foo '), 'foo');
+  equal(trim(' foo '), 'foo');
+  equal(trim('    foo     '), 'foo');
+  equal(trim('    foo     '), 'foo', 'Manually set whitespace');
+  equal(trim('\t    foo \t  '), 'foo', 'Manually set RegExp /\\s+/');
+
+  equal(trim('ffoo', 'ff'), 'oo');
+  equal(trim('ooff', 'ff'), 'oo');
+  equal(trim('ffooff', 'ff'), 'oo');
+
+
+  equal(trim('_-foobar-_', '_-'), 'foobar');
+
+  equal(trim('http://foo/', '/'), 'http://foo');
+  equal(trim('c:\\', '\\'), 'c:');
+
+  equal(trim(123), '123');
+  equal(trim(123, 3), '12');
+  equal(trim(''), '', 'Trim empty string should return empty string');
+  equal(trim(null), '', 'Trim null should return empty string');
+  equal(trim(undefined), '', 'Trim undefined should return empty string');
+});
+
diff --git a/tests/truncate.js b/tests/truncate.js
new file mode 100644
index 0000000..4455ce2
--- /dev/null
+++ b/tests/truncate.js
@@ -0,0 +1,14 @@
+var equal = require('assert').equal;
+var truncate = require('../truncate');
+
+
+test('#truncate', function(){
+  equal(truncate('Hello world', 6, 'read more'), 'Hello read more');
+  equal(truncate('Hello world', 5), 'Hello...');
+  equal(truncate('Hello', 10), 'Hello');
+  equal(truncate('', 10), '');
+  equal(truncate(null, 10), '');
+  equal(truncate(undefined, 10), '');
+  equal(truncate(1234567890, 5), '12345...');
+});
+
diff --git a/tests/underscored.js b/tests/underscored.js
new file mode 100644
index 0000000..76be00f
--- /dev/null
+++ b/tests/underscored.js
@@ -0,0 +1,15 @@
+var equal = require('assert').equal;
+var underscored = require('../underscored');
+
+
+test('#underscored', function(){
+  equal(underscored('the-underscored-string-method'), 'the_underscored_string_method');
+  equal(underscored('theUnderscoredStringMethod'), 'the_underscored_string_method');
+  equal(underscored('TheUnderscoredStringMethod'), 'the_underscored_string_method');
+  equal(underscored(' the underscored  string method'), 'the_underscored_string_method');
+  equal(underscored(''), '');
+  equal(underscored(null), '');
+  equal(underscored(undefined), '');
+  equal(underscored(123), '123');
+});
+
diff --git a/tests/unescapeHTML.js b/tests/unescapeHTML.js
new file mode 100644
index 0000000..583ae05
--- /dev/null
+++ b/tests/unescapeHTML.js
@@ -0,0 +1,31 @@
+var equal = require('assert').equal;
+var unescapeHTML = require('../unescapeHTML');
+
+
+test('#unescapeHTML', function(){
+  equal(unescapeHTML('<div>Blah & "blah" & 'blah'</div>'),
+           '<div>Blah & "blah" & \'blah\'</div>');
+  equal(unescapeHTML('&lt;'), '<');
+  equal(unescapeHTML('''), '\'');
+  equal(unescapeHTML('''), '\'');
+  equal(unescapeHTML('''), '\'');
+  equal(unescapeHTML('&#x4a;'), 'J');
+  equal(unescapeHTML('&#x04A;'), 'J');
+  equal(unescapeHTML('&#X4A;'), '&#X4A;');
+  equal(unescapeHTML('&_#39;'), '&_#39;');
+  equal(unescapeHTML('&#39_;'), '&#39_;');
+  equal(unescapeHTML('&#38;'), '&');
+  equal(unescapeHTML('&amp;'), '&');
+  equal(unescapeHTML('''), '\'');
+  equal(unescapeHTML(''), '');
+  equal(unescapeHTML(' '), ' ');
+  equal(unescapeHTML('what is the ¥ to £ to € conversion process?'), 'what is the ¥ to £ to € conversion process?');
+  equal(unescapeHTML('® trademark'), '® trademark');
+  equal(unescapeHTML('© 1992. License available for 50 ¢'), '© 1992. License available for 50 ¢');
+  equal(unescapeHTML(' '), ' ');
+  equal(unescapeHTML(' '), ' ');
+
+  equal(unescapeHTML(null), '');
+  equal(unescapeHTML(undefined), '');
+  equal(unescapeHTML(5), '5');
+});
diff --git a/tests/unquote.js b/tests/unquote.js
new file mode 100644
index 0000000..6688ffb
--- /dev/null
+++ b/tests/unquote.js
@@ -0,0 +1,11 @@
+var equal = require('assert').equal;
+var unquote = require('../unquote');
+
+
+test('#unquote', function(){
+  equal(unquote('"foo"'), 'foo');
+  equal(unquote('""foo""'), '"foo"');
+  equal(unquote('"1"'), '1');
+  equal(unquote('\'foo\'', '\''), 'foo');
+});
+
diff --git a/tests/vsprintf.js b/tests/vsprintf.js
new file mode 100644
index 0000000..7fb3d70
--- /dev/null
+++ b/tests/vsprintf.js
@@ -0,0 +1,13 @@
+var equal = require('assert').equal;
+var vsprintf = require('../vsprintf');
+
+
+test('#vsprintf', function() {
+  equal(vsprintf('Hello %s', ['me']), 'Hello me', 'basic');
+  equal(vsprintf('Hello %s', ['me']), 'Hello me', 'object');
+  equal(vsprintf('%.1f', [1.22222]), '1.2', 'round');
+  equal(vsprintf('%.1f', [1.17]), '1.2', 'round 2');
+  equal(vsprintf('%(id)d - %(name)s', [{id: 824, name: 'Hello World'}]), '824 - Hello World', 'Named replacement works');
+  equal(vsprintf('%(args[0].id)d - %(args[1].name)s', [{args: [{id: 824}, {name: 'Hello World'}]}]), '824 - Hello World', 'Named replacement with arrays works');
+});
+
diff --git a/tests/words.js b/tests/words.js
new file mode 100644
index 0000000..95fa5af
--- /dev/null
+++ b/tests/words.js
@@ -0,0 +1,17 @@
+var deepEqual = require('assert').deepEqual;
+var words = require('../words');
+
+
+test('#words', function() {
+  deepEqual(words('I love you!'), ['I', 'love', 'you!']);
+  deepEqual(words(' I    love   you!  '), ['I', 'love', 'you!']);
+  deepEqual(words('I_love_you!', '_'), ['I', 'love', 'you!']);
+  deepEqual(words('I-love-you!', /-/), ['I', 'love', 'you!']);
+  deepEqual(words(123), ['123'], '123 number has one word "123".');
+  deepEqual(words(0), ['0'], 'Zero number has one word "0".');
+  deepEqual(words(''), [], 'Empty strings has no words.');
+  deepEqual(words('   '), [], 'Blank strings has no words.');
+  deepEqual(words(null), [], 'null has no words.');
+  deepEqual(words(undefined), [], 'undefined has no words.');
+});
+
diff --git a/tests/wrap.js b/tests/wrap.js
new file mode 100644
index 0000000..bc398b5
--- /dev/null
+++ b/tests/wrap.js
@@ -0,0 +1,35 @@
+var equal = require('assert').equal;
+var wrap = require('../wrap');
+
+test('#wrap', function(){
+
+  // without trailing spaces
+  equal(wrap('My name is', { width: 2, seperator:'.', cut:false, trailingSpaces:false } ), 'My.name.is', 'works with width 2 and cut = false');
+  equal(wrap('My name is', { width: 2, seperator:'.', cut:true, trailingSpaces:false } ), 'My. n.am.e .is', 'works with width 2 and cut = true');
+  equal(wrap('My name is', { width: 3, seperator:'.', cut:false, trailingSpaces:false } ), 'My.name.is', 'works with width 3 and cut = true');
+  equal(wrap('My name is', { width: 3, seperator:'.', cut:true, trailingSpaces:false } ), 'My .nam.e i.s', 'works with width 3 and cut = true');
+
+  // with trailing spaces
+  equal(wrap('My name is', { width: 2, seperator:'.', cut:false, trailingSpaces:true } ), 'My.name.is', 'works with width 2 and cut = false and trailingSpaces = true');
+  equal(wrap('My name is', { width: 2, seperator:'.', cut:true, trailingSpaces:true } ), 'My. n.am.e .is', 'works with width 2 and cut = true and trailingSpaces = true');
+  equal(wrap('My name is', { width: 3, seperator:'.', cut:false, trailingSpaces:true } ), 'My .name.is ', 'works with width 3 and cut = true and trailingSpaces = true');
+  equal(wrap('My name is', { width: 3, seperator:'.', cut:true, trailingSpaces:true } ), 'My .nam.e i.s  ', 'works with width 3 and cut = true and trailingSpaces = true');
+
+  // with preserveSpaces
+  equal(wrap('My name is', {width: 2, seperator:'.', cut:false, preserveSpaces:true }), 'My .name .is', 'preserve spaces keeps the space at the end of a line');
+  equal(wrap('My name is', {width: 3, seperator:'.', cut:false, preserveSpaces:true }), 'My .name .is', 'preserve spaces keeps the space at the end of a line');
+
+  // with preserveSpaces and trailingSpaces
+  equal(wrap('My name is', {width: 2, seperator:'.', cut:false, preserveSpaces:true, trailingSpaces:true }), 'My .name .is', 'preserve spaces takes precedence over trailing spaces');
+
+
+  // defaults
+  equal(wrap('My name is', { width: 3 } ), 'My\nname\nis', 'Default parameters work');
+  equal(wrap('My name is'), 'My name is', 'Default parameters work');
+  equal(wrap('', { width: 5 } ), '', 'Empty string');
+  equal(wrap('My name is', { width: 0 } ), 'My name is', 'Just return original line if width <= 0');
+  equal(wrap('My name is', { width: -1 } ), 'My name is', 'Just return original line if width <= 0');
+  equal(wrap(null, { width: 5 } ), '', 'null');
+  equal(wrap(undefined, { width: 5 } ), '', 'undefined');
+
+});
diff --git a/titleize.js b/titleize.js
new file mode 100644
index 0000000..c4a8a47
--- /dev/null
+++ b/titleize.js
@@ -0,0 +1,7 @@
+var makeString = require('./helper/makeString');
+
+module.exports = function titleize(str) {
+  return makeString(str).toLowerCase().replace(/(?:^|\s|-)\S/g, function(c) {
+    return c.toUpperCase();
+  });
+};
diff --git a/toBoolean.js b/toBoolean.js
new file mode 100644
index 0000000..f2c184e
--- /dev/null
+++ b/toBoolean.js
@@ -0,0 +1,20 @@
+var trim = require('./trim');
+
+function boolMatch(s, matchers) {
+  var i, matcher, down = s.toLowerCase();
+  matchers = [].concat(matchers);
+  for (i = 0; i < matchers.length; i += 1) {
+    matcher = matchers[i];
+    if (!matcher) continue;
+    if (matcher.test && matcher.test(s)) return true;
+    if (matcher.toLowerCase() === down) return true;
+  }
+}
+
+module.exports = function toBoolean(str, trueValues, falseValues) {
+  if (typeof str === 'number') str = '' + str;
+  if (typeof str !== 'string') return !!str;
+  str = trim(str);
+  if (boolMatch(str, trueValues || ['true', '1'])) return true;
+  if (boolMatch(str, falseValues || ['false', '0'])) return false;
+};
diff --git a/toNumber.js b/toNumber.js
new file mode 100644
index 0000000..92d47dd
--- /dev/null
+++ b/toNumber.js
@@ -0,0 +1,5 @@
+module.exports = function toNumber(num, precision) {
+  if (num == null) return 0;
+  var factor = Math.pow(10, isFinite(precision) ? precision : 0);
+  return Math.round(num * factor) / factor;
+};
diff --git a/toSentence.js b/toSentence.js
new file mode 100644
index 0000000..2284bd9
--- /dev/null
+++ b/toSentence.js
@@ -0,0 +1,12 @@
+var rtrim = require('./rtrim');
+
+module.exports = function toSentence(array, separator, lastSeparator, serial) {
+  separator = separator || ', ';
+  lastSeparator = lastSeparator || ' and ';
+  var a = array.slice(),
+    lastMember = a.pop();
+
+  if (array.length > 2 && serial) lastSeparator = rtrim(separator) + lastSeparator;
+
+  return a.length ? a.join(separator) + lastSeparator + lastMember : lastMember;
+};
diff --git a/toSentenceSerial.js b/toSentenceSerial.js
new file mode 100644
index 0000000..2b8d350
--- /dev/null
+++ b/toSentenceSerial.js
@@ -0,0 +1,5 @@
+var toSentence = require('./toSentence');
+
+module.exports = function toSentenceSerial(array, sep, lastSep) {
+  return toSentence(array, sep, lastSep, true);
+};
diff --git a/trim.js b/trim.js
new file mode 100644
index 0000000..0f2a33d
--- /dev/null
+++ b/trim.js
@@ -0,0 +1,10 @@
+var makeString = require('./helper/makeString');
+var defaultToWhiteSpace = require('./helper/defaultToWhiteSpace');
+var nativeTrim = String.prototype.trim;
+
+module.exports = function trim(str, characters) {
+  str = makeString(str);
+  if (!characters && nativeTrim) return nativeTrim.call(str);
+  characters = defaultToWhiteSpace(characters);
+  return str.replace(new RegExp('^' + characters + '+|' + characters + '+$', 'g'), '');
+};
diff --git a/truncate.js b/truncate.js
new file mode 100644
index 0000000..dbb8fd7
--- /dev/null
+++ b/truncate.js
@@ -0,0 +1,8 @@
+var makeString = require('./helper/makeString');
+
+module.exports = function truncate(str, length, truncateStr) {
+  str = makeString(str);
+  truncateStr = truncateStr || '...';
+  length = ~~length;
+  return str.length > length ? str.slice(0, length) + truncateStr : str;
+};
diff --git a/underscored.js b/underscored.js
new file mode 100644
index 0000000..b9d1628
--- /dev/null
+++ b/underscored.js
@@ -0,0 +1,5 @@
+var trim = require('./trim');
+
+module.exports = function underscored(str) {
+  return trim(str).replace(/([a-z\d])([A-Z]+)/g, '$1_$2').replace(/[-\s]+/g, '_').toLowerCase();
+};
diff --git a/unescapeHTML.js b/unescapeHTML.js
new file mode 100644
index 0000000..78b59c2
--- /dev/null
+++ b/unescapeHTML.js
@@ -0,0 +1,20 @@
+var makeString = require('./helper/makeString');
+var htmlEntities = require('./helper/htmlEntities');
+
+module.exports = function unescapeHTML(str) {
+  return makeString(str).replace(/\&([^;]+);/g, function(entity, entityCode) {
+    var match;
+
+    if (entityCode in htmlEntities) {
+      return htmlEntities[entityCode];
+    /*eslint no-cond-assign: 0*/
+    } else if (match = entityCode.match(/^#x([\da-fA-F]+)$/)) {
+      return String.fromCharCode(parseInt(match[1], 16));
+    /*eslint no-cond-assign: 0*/
+    } else if (match = entityCode.match(/^#(\d+)$/)) {
+      return String.fromCharCode(~~match[1]);
+    } else {
+      return entity;
+    }
+  });
+};
diff --git a/unquote.js b/unquote.js
new file mode 100644
index 0000000..fefba49
--- /dev/null
+++ b/unquote.js
@@ -0,0 +1,6 @@
+module.exports = function unquote(str, quoteChar) {
+  quoteChar = quoteChar || '"';
+  if (str[0] === quoteChar && str[str.length - 1] === quoteChar)
+    return str.slice(1, str.length - 1);
+  else return str;
+};
diff --git a/vsprintf.js b/vsprintf.js
new file mode 100644
index 0000000..6ce2c98
--- /dev/null
+++ b/vsprintf.js
@@ -0,0 +1,4 @@
+var deprecate = require('util-deprecate');
+
+module.exports = deprecate(require('sprintf-js').vsprintf,
+  'vsprintf() will be removed in the next major release, use the sprintf-js package instead.');
diff --git a/words.js b/words.js
new file mode 100644
index 0000000..be55c9c
--- /dev/null
+++ b/words.js
@@ -0,0 +1,7 @@
+var isBlank = require('./isBlank');
+var trim = require('./trim');
+
+module.exports = function words(str, delimiter) {
+  if (isBlank(str)) return [];
+  return trim(str, delimiter).split(delimiter || /\s+/);
+};
diff --git a/wrap.js b/wrap.js
new file mode 100644
index 0000000..e4e1756
--- /dev/null
+++ b/wrap.js
@@ -0,0 +1,102 @@
+// Wrap
+// wraps a string by a certain width
+
+var makeString = require('./helper/makeString');
+
+module.exports = function wrap(str, options){
+  str = makeString(str);
+  
+  options = options || {};
+  
+  var width = options.width || 75;
+  var seperator = options.seperator || '\n';
+  var cut = options.cut || false;
+  var preserveSpaces = options.preserveSpaces || false;
+  var trailingSpaces = options.trailingSpaces || false;
+  
+  var result;
+  
+  if(width <= 0){
+    return str;
+  }
+  
+  else if(!cut){
+  
+    var words = str.split(' ');
+    var current_column = 0;
+    result = '';
+  
+    while(words.length > 0){
+      
+      // if adding a space and the next word would cause this line to be longer than width...
+      if(1 + words[0].length + current_column > width){
+        //start a new line if this line is not already empty
+        if(current_column > 0){
+          // add a space at the end of the line is preserveSpaces is true
+          if (preserveSpaces){
+            result += ' ';
+            current_column++;
+          }
+          // fill the rest of the line with spaces if trailingSpaces option is true
+          else if(trailingSpaces){
+            while(current_column < width){
+              result += ' ';
+              current_column++;
+            }            
+          }
+          //start new line
+          result += seperator;
+          current_column = 0;
+        }
+      }
+  
+      // if not at the begining of the line, add a space in front of the word
+      if(current_column > 0){
+        result += ' ';
+        current_column++;
+      }
+  
+      // tack on the next word, update current column, a pop words array
+      result += words[0];
+      current_column += words[0].length;
+      words.shift();
+  
+    }
+  
+    // fill the rest of the line with spaces if trailingSpaces option is true
+    if(trailingSpaces){
+      while(current_column < width){
+        result += ' ';
+        current_column++;
+      }            
+    }
+  
+    return result;
+  
+  }
+  
+  else {
+  
+    var index = 0;
+    result = '';
+  
+    // walk through each character and add seperators where appropriate
+    while(index < str.length){
+      if(index % width == 0 && index > 0){
+        result += seperator;
+      }
+      result += str.charAt(index);
+      index++;
+    }
+  
+    // fill the rest of the line with spaces if trailingSpaces option is true
+    if(trailingSpaces){
+      while(index % width > 0){
+        result += ' ';
+        index++;
+      }            
+    }
+    
+    return result;
+  }
+};

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-javascript/underscore.string.git



More information about the Pkg-javascript-commits mailing list