[Pkg-javascript-commits] [node-magic-string] 01/02: New upstream version 0.16.0

Julien Puydt julien.puydt at laposte.net
Tue Nov 15 07:18:47 UTC 2016


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

jpuydt-guest pushed a commit to branch master
in repository node-magic-string.

commit c0a21340e5c9d698966b20ca32f1c0b1357fc87f
Author: Julien Puydt <julien.puydt at laposte.net>
Date:   Tue Nov 1 15:42:31 2016 +0100

    New upstream version 0.16.0
---
 .eslintrc                    |   34 +
 .gitignore                   |    8 +
 .travis.yml                  |   11 +
 CHANGELOG.md                 |  273 +++++++
 README.md                    |  233 ++++++
 appveyor.yml                 |   23 +
 example/app.inlinemap.js     |    4 +
 example/app.js               |    3 +
 example/app.js.map           |    1 +
 example/app.source.js        |    2 +
 example/build.js             |   35 +
 example/index.html           |    1 +
 package.json                 |   59 ++
 rollup.config.js             |   21 +
 src/Bundle.js                |  240 ++++++
 src/Chunk.js                 |  146 ++++
 src/MagicString.js           |  519 +++++++++++++
 src/index-legacy.js          |    6 +
 src/index.js                 |    4 +
 src/utils/SourceMap.js       |   21 +
 src/utils/Stats.js           |   18 +
 src/utils/btoa.js            |   13 +
 src/utils/encodeMappings.js  |  137 ++++
 src/utils/getLocator.js      |   35 +
 src/utils/getRelativePath.js |   18 +
 src/utils/getSemis.js        |    3 +
 src/utils/guessIndent.js     |   25 +
 src/utils/hasOwnProp.js      |    1 +
 src/utils/isObject.js        |    5 +
 test/index.js                | 1684 ++++++++++++++++++++++++++++++++++++++++++
 30 files changed, 3583 insertions(+)

diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..dfefff3
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,34 @@
+{
+    "rules": {
+        "indent": [ 2, "tab", { "SwitchCase": 1 }],
+        "quotes": [ 2, "single" ],
+        "linebreak-style": [ 2, "unix" ],
+        "semi": [ 2, "always" ],
+        "no-mixed-spaces-and-tabs": [ 2, "smart-tabs" ],
+        "object-shorthand":  [2, "always" ],
+        "no-const-assign": 2,
+        "no-class-assign": 2,
+        "no-this-before-super": 2,
+        "no-var": 2,
+        "quote-props": [ 2, "as-needed" ],
+        "one-var": [ 2, "never" ],
+        "prefer-arrow-callback": 2,
+        "arrow-spacing": 2,
+
+        "no-cond-assign": 0
+    },
+    "env": {
+        "es6": true,
+        "browser": true
+    },
+    "globals": {
+        "DEBUG": true,
+        "process": true,
+        "Buffer": true
+    },
+    "extends": "eslint:recommended",
+    "parserOptions": {
+        "ecmaVersion": 6,
+        "sourceType": "module"
+    }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7a9e43e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+.DS_Store
+node_modules
+sandbox
+.gobbl*
+.tmp
+!.babelrc
+coverage
+dist
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..f5bf60c
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,11 @@
+sudo: false
+language: node_js
+node_js:
+  - "0.12"
+  - "4"
+  - "5"
+env:
+  global:
+    - BUILD_TIMEOUT=10000
+install: npm install
+script: npm test
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..20fa5b0
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,273 @@
+# magic-string changelog
+
+## 0.16.0
+
+* Include inserts in range for `overwrite` and `remove` operations ([#89](https://github.com/Rich-Harris/magic-string/pull/89))
+* Make options optional for `bundle.generateMap(...)` ([#73](https://github.com/Rich-Harris/magic-string/pull/73))
+
+## 0.15.2
+
+* Generate correct bundle sourcemap with prepended/appended content
+
+## 0.15.1
+
+* Minor sourcemap fixes
+
+## 0.15.0
+
+* Use named export of `Bundle` in ES build, so ES consumers of magic-string can tree-shake it out
+
+## 0.14.0
+
+* Throw if overwrite of zero-length range is attempted
+* Correctly handle redundant move operations
+
+## 0.13.1
+
+* Fix a bevy of `s.slice()` issues ([#62](https://github.com/Rich-Harris/magic-string/pull/62))
+
+## 0.13.0
+
+* Breaking: `insertAfter` is now `insertLeft`, `insertBefore` is now `insertRight`
+* Breaking: `insert` is no longer available. Use `insertLeft` and `insertRight`
+* Significant performance improvements
+
+## 0.12.1
+
+* Fix sourcemap generation with `insertAfter` and `insertBefore`
+
+## 0.12.0
+
+* Add `insertAfter` and `insertBefore` methods
+
+## 0.11.4
+
+* Fix two regression bugs with `trim()`
+* More informative error message on illegal removals
+
+## 0.11.3
+
+* Fix trim methods to ensure correct sourcemaps with trimmed content ([#53](https://github.com/Rich-Harris/magic-string/pull/53))
+
+## 0.11.2
+
+* Support sourcemaps with moved content
+
+## 0.11.1
+
+* Use `findIndex` helper for 0.12 support
+
+## 0.11.0
+
+* Add experimental `move()` method
+* Refactor internals to support `move()`
+
+## 0.10.2
+
+* Do not overwrite inserts at the end of patched ranges ([#35](https://github.com/Rich-Harris/magic-string/pull/35))
+
+## 0.10.1
+
+* Zero-length inserts are not removed on adjacent overwrites
+
+## 0.10.0
+
+* Complete rewrite, resulting in ~40x speed increase ([#30](https://github.com/Rich-Harris/magic-string/pull/30))
+* Breaking – `magicString.locate` and `locateOrigin` are deprecated
+* More forgiving rules about contiguous patches, and which ranges are valid with `magicString.slice(...)`
+
+## 0.9.1
+
+* Update deps
+
+## 0.9.0
+
+* Update build process
+
+## 0.8.0
+
+* Add an ES6 build, change default UMD build to CommonJS (but keeping existing UMD build with bundled dependencies)
+* Make properties non-enumerable, for cleaner logging
+* Update dependencies
+
+## 0.7.0
+
+* The `names` array is populated when generating sourcemaps, and mappings include name indices where appropriate ([#16](https://github.com/Rich-Harris/magic-string/issues/16))
+* Replaced content is mapped correctly in sourcemaps ([#15](https://github.com/Rich-Harris/magic-string/issues/15))
+
+## 0.6.6
+
+* Adjust mappings correctly when removing replaced content
+* Error correctly when removed characters are used as slice anchors
+
+## 0.6.5
+
+* Fix `jsnext:main` in package.json
+
+## 0.6.4
+
+* Fix bug with positive integer coercion
+
+## 0.6.3
+
+* Intro content is correctly indented
+* Content following an intro with trailing newline is correctly indented
+
+## 0.6.2
+
+* Noop indents are still chainable (fixes bug introduced in 0.6.1)
+
+## 0.6.1
+
+* Indenting with an empty string is a noop
+
+## 0.6.0
+
+* Use rollup for bundling, instead of esperanto
+
+## 0.5.3
+
+* Correct sourcemap generation with bundles containing varied separators
+* `s.clone()` clones indent exclusion ranges and sourcemap locations
+
+## 0.5.2
+
+* `s.slice()` accepts negative numbers, and the second argument can be omitted (means 'original string length'), just like `String.prototype.slice`
+* More informative error message when trying to overwrite content illegally
+
+## 0.5.1
+
+* Allow bundle separator to be the empty string
+* Indenting is handled correctly with empty string separator
+
+## 0.5.0
+
+* `s.replace()` is deprecated in favour of `s.overwrite()` (identical signature)
+* `bundle.addSource()` can take a `MagicString` instance as its sole argument, for convenience
+* The `options` in `new MagicString(str, options)` can include `filename` and `indentExclusionRanges` options, which will be used when bundling
+* New method: `s.snip( start, end )`
+
+## 0.4.9
+
+* `file` option is optional when generating a bundle sourcemap
+
+## 0.4.7
+
+* Repeated insertions at position 0 behave the same as other positions ([#10](https://github.com/Rich-Harris/magic-string/pull/10))
+
+## 0.4.6
+
+* Overlapping ranges can be removed
+* Non-string content is rejected ([#9](https://github.com/Rich-Harris/magic-string/pull/9))
+
+## 0.4.5
+
+* Implement `source.addSourcemapLocation()`
+
+## 0.4.4
+
+* Another Windows fix, this time for file paths when bundling
+
+## 0.4.3
+
+* Handle Windows-style CRLF newlines when determining whether a line is empty
+
+## 0.4.2
+
+* Fix typo in package.json (d'oh again)
+* Use only relative paths for internal modules - makes bundling with dependents (i.e. esperanto) possible
+
+## 0.4.1
+
+* Includes correct files in npm package (d'oh)
+
+## 0.4.0
+
+* Using experimental Esperanto feature ([esperantojs/esperanto#68](https://github.com/esperantojs/esperanto/issues/68)) to generate version with `vlq` dependency included
+
+## 0.3.1
+
+* Fixes a bug whereby multiple insertions at the same location would cause text to repeat ([#5](https://github.com/Rich-Harris/magic-string/issues/5))
+
+## 0.3.0
+
+* Breaking change - `source.indentStr` is `null` if no lines are indented. Use `source.getIndentString()` for the old behaviour (guess, and if no lines are indented, return `\t`)
+* `bundle.getIndentString()` ignores sources with no indented lines when guessing indentation ([#3](https://github.com/Rich-Harris/magic-string/issues/3))
+
+## 0.2.7
+
+* `source.trimLines()` removes empty lines from start/end of source, leaving other whitespace untouched
+* Indentation is not added to an empty source
+
+## 0.2.6
+
+* Performance improvement - adjustments are only made when necessary
+
+## 0.2.5
+
+* Single spaces are ignored when guessing indentation - experience shows these are more likely to be e.g. JSDoc comments than actual indentation
+* `bundle.addSource()` can take an `indentExclusionRanges` option
+
+## 0.2.4
+
+* Empty lines are not indented
+
+## 0.2.3
+
+* Fixes edge case with bundle sourcemaps
+
+## 0.2.2
+
+* Make `sources` paths in sourcemaps relative to `options.file`
+
+## 0.2.1
+
+* Minor fix for `bundle.indent()`
+
+## 0.2.0
+
+* Implement `MagicString.Bundle` for concatenating magic strings
+
+## 0.1.10
+
+* Fix sourcemap encoding
+
+## 0.1.9
+
+* Better performance when indenting large chunks of code
+
+## 0.1.8
+
+* Sourcemaps generated with `s.generateMap()` have a `toUrl()` method that generates a DataURI
+
+## 0.1.7
+
+* Implement `s.insert( index, content )` - roughly equivalent to `s.replace( index, index, content )`
+
+## 0.1.6
+
+* Version bump for npm's benefit
+
+## 0.1.5
+
+* `s.indent({ exclude: [ x, y ] })` prevents lines between (original) characters `x` and `y` from being indented. Multiple exclusion ranges are also supported (e.g. `exclude: [[a, b], [c, d]]`)
+
+## 0.1.4
+
+* `s.locate()` doesn't throw out-of-bound error if index is equal to original string's length
+
+## 0.1.3
+
+* `s.trim()` returns `this` (i.e. is chainable)
+
+## 0.1.2
+
+* Implement `s.slice()`
+
+## 0.1.1
+
+* Implement `s.trim()`
+
+## 0.1.0
+
+* First release
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b971bd4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,233 @@
+# magic-string
+
+<p align="center">
+  <a href="https://travis-ci.org/Rich-Harris/magic-string">
+    <img src="http://img.shields.io/travis/Rich-Harris/magic-string.svg"
+         alt="build status">
+  </a>
+  <a href="https://npmjs.org/package/magic-string">
+    <img src="https://img.shields.io/npm/v/magic-string.svg"
+         alt="npm version">
+  </a>
+  <a href="https://github.com/Rich-Harris/magic-string/blob/master/LICENSE.md">
+    <img src="https://img.shields.io/npm/l/magic-string.svg"
+         alt="license">
+  </a>
+  <a href="https://david-dm.org/Rich-Harris/magic-string">
+    <img src="https://david-dm.org/Rich-Harris/magic-string.svg"
+         alt="dependency status">
+  </a>
+  <a href="http://codecov.io/github/Rich-Harris/magic-string?branch=master">
+    <img src="http://codecov.io/github/Rich-Harris/magic-string/coverage.svg?branch=master" alt="Coverage via Codecov" />
+  </a>
+</p>
+
+Suppose you have some source code. You want to make some light modifications to it - replacing a few characters here and there, wrapping it with a header and footer, etc - and ideally you'd like to generate a source map at the end of it. You've thought about using something like [recast](https://github.com/benjamn/recast) (which allows you to generate an AST from some JavaScript, manipulate it, and reprint it with a sourcemap without losing your comments and formatting), but it seems lik [...]
+
+Your requirements are, frankly, rather niche. But they're requirements that I also have, and for which I made magic-string. It's a small, fast utility for manipulating strings and generating sourcemaps.
+
+## Installation
+
+magic-string works in both node.js and browser environments. For node, install with npm:
+
+```bash
+npm i magic-string
+```
+
+To use in browser, grab the [magic-string.deps.js](https://raw.githubusercontent.com/Rich-Harris/magic-string/master/dist/magic-string.deps.js) file and add it to your page:
+
+```html
+<script src='magic-string.deps.js'></script>
+```
+
+(It also works with various module systems, if you prefer that sort of thing - it has a dependency on [vlq](https://github.com/Rich-Harris/vlq).)
+
+## Usage
+
+These examples assume you're in node.js, or something similar:
+
+```js
+var MagicString = require( 'magic-string' );
+var s = new MagicString( 'problems = 99' );
+
+s.overwrite( 0, 8, 'answer' );
+s.toString(); // 'answer = 99'
+
+s.overwrite( 11, 13, '42' ); // character indices always refer to the original string
+s.toString(); // 'answer = 42'
+
+s.prepend( 'var ' ).append( ';' ); // most methods are chainable
+s.toString(); // 'var answer = 42;'
+
+var map = s.generateMap({
+  source: 'source.js',
+  file: 'converted.js.map',
+  includeContent: true
+}); // generates a v3 sourcemap
+
+require( 'fs' ).writeFile( 'converted.js', s.toString() );
+require( 'fs' ).writeFile( 'converted.js.map', map.toString() );
+```
+
+You can pass an options argument:
+
+```js
+var s = new MagicString( someCode, {
+  // both these options will be used if you later
+  // call `bundle.addSource( s )` - see below
+  filename: 'foo.js',
+  indentExclusionRanges: [/*...*/]
+});
+```
+
+## Methods
+
+### s.addSourcemapLocation( index )
+
+Adds the specified character index (with respect to the original string) to sourcemap mappings, if `hires` is `false` (see below).
+
+### s.append( content )
+
+Appends the specified content to the end of the string. Returns `this`.
+
+### s.clone()
+
+Does what you'd expect.
+
+### s.generateMap( options )
+
+Generates a [version 3 sourcemap](https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit). All options are, well, optional:
+
+* `file` - the filename where you plan to write the sourcemap
+* `source` - the filename of the file containing the original source
+* `includeContent` - whether to include the original content in the map's `sourcesContent` array
+* `hires` - whether the mapping should be high-resolution. Hi-res mappings map every single character, meaning (for example) your devtools will always be able to pinpoint the exact location of function calls and so on. With lo-res mappings, devtools may only be able to identify the correct line - but they're quicker to generate and less bulky. If sourcemap locations have been specified with `s.addSourceMapLocation()`, they will be used here.
+
+The returned sourcemap has two (non-enumerable) methods attached for convenience:
+
+* `toString` - returns the equivalent of `JSON.stringify(map)`
+* `toUrl` - returns a DataURI containing the sourcemap. Useful for doing this sort of thing:
+
+```js
+code += '\n//# sourceMappingURL=' + map.toUrl();
+```
+
+### s.indent( prefix[, options] )
+
+Prefixes each line of the string with `prefix`. If `prefix` is not supplied, the indentation will be guessed from the original content, falling back to a single tab character. Returns `this`.
+
+The `options` argument can have an `exclude` property, which is an array of `[start, end]` character ranges. These ranges will be excluded from the indentation - useful for (e.g.) multiline strings.
+
+### s.insertLeft( index, content )
+
+Inserts the specified `content` at the `index` in the original string. If a range *ending* with `index` is subsequently moved, the insert will be moved with it. Returns `this`.
+
+### s.insertRight( index, content )
+
+Inserts the specified `content` at the `index` in the original string. If a range *starting* with `index` is subsequently moved, the insert will be moved with it. Returns `this`.
+
+### s.locate( index )
+
+**DEPRECATED** since 0.10 – see [#30](https://github.com/Rich-Harris/magic-string/pull/30)
+
+### s.locateOrigin( index )
+
+**DEPRECATED** since 0.10 – see [#30](https://github.com/Rich-Harris/magic-string/pull/30)
+
+### s.move( start, end, newIndex )
+
+Moves the characters from `start` and `end` to `index`. Returns `this`.
+
+### s.overwrite( start, end, content[, storeName] )
+
+Replaces the characters from `start` to `end` with `content`. The same restrictions as `s.remove()` apply. Returns `this`. If `storeName` is `true`, the original name will be stored for later inclusion in a sourcemap's `names` array.
+
+### s.prepend( content )
+
+Prepends the string with the specified content. Returns `this`.
+
+### s.remove( start, end )
+
+Removes the characters from `start` to `end` (of the original string, **not** the generated string). Removing the same content twice, or making removals that partially overlap, will cause an error. Returns `this`.
+
+### s.slice( start, end )
+
+Returns the content of the generated string that corresponds to the slice between `start` and `end` of the original string. Throws error if the indices are for characters that were already removed.
+
+### s.snip( start, end )
+
+Returns a clone of `s`, with all content before the `start` and `end` characters of the original string removed.
+
+### s.toString()
+
+Returns the generated string.
+
+### s.trim([ charType ])
+
+Trims content matching `charType` (defaults to `\s`, i.e. whitespace) from the start and end. Returns `this`.
+
+### s.trimStart([ charType ])
+
+Trims content matching `charType` (defaults to `\s`, i.e. whitespace) from the start. Returns `this`.
+
+### s.trimEnd([ charType ])
+
+Trims content matching `charType` (defaults to `\s`, i.e. whitespace) from the end. Returns `this`.
+
+### s.trimLines()
+
+Removes empty lines from the start and end. Returns `this`.
+
+## Bundling
+
+To concatenate several sources, use `MagicString.Bundle`:
+
+```js
+var bundle = new MagicString.Bundle();
+
+bundle.addSource({
+  filename: 'foo.js',
+  content: new MagicString( 'var answer = 42;' )
+});
+
+bundle.addSource({
+  filename: 'bar.js',
+  content: new MagicString( 'console.log( answer )' )
+});
+
+// Advanced: a source can include an `indentExclusionRanges` property
+// alongside `filename` and `content`. This will be passed to `s.indent()`
+// - see documentation above
+
+bundle.indent() // optionally, pass an indent string, otherwise it will be guessed
+  .prepend( '(function () {\n' )
+  .append( '}());' );
+
+bundle.toString();
+// (function () {
+//   var answer = 42;
+//   console.log( answer );
+// }());
+
+// options are as per `s.generateMap()` above
+var map = bundle.generateMap({
+  file: 'bundle.js',
+  includeContent: true,
+  hires: true
+});
+```
+
+As an alternative syntax, if you a) don't have `filename` or `indentExclusionRanges` options, or b) passed those in when you used `new MagicString(...)`, you can simply pass the `MagicString` instance itself:
+
+```js
+var bundle = new MagicString.Bundle();
+var source = new MagicString( someCode, {
+  filename: 'foo.js'
+});
+
+bundle.addSource( source );
+```
+
+## License
+
+MIT
diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644
index 0000000..e326df6
--- /dev/null
+++ b/appveyor.yml
@@ -0,0 +1,23 @@
+# Test against this version of Node.js
+environment:
+  matrix:
+  # node.js
+  - nodejs_version: "5"
+
+# Install scripts. (runs after repo cloning)
+install:
+  # Get the latest stable version of Node.js or io.js
+  - ps: Install-Product node $env:nodejs_version
+  # install modules
+  - npm install
+
+# Post-install test scripts.
+test_script:
+  # Output useful info for debugging.
+  - node --version
+  - npm --version
+  # run tests
+  - npm test
+
+# Don't actually build.
+build: off
diff --git a/example/app.inlinemap.js b/example/app.inlinemap.js
new file mode 100644
index 0000000..4666ba7
--- /dev/null
+++ b/example/app.inlinemap.js
@@ -0,0 +1,4 @@
+var answer = 'yes';
+console.log( answer );
+//# sourceMappingURL=app.js.map
+//#sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXBwLmpzLm1hcCIsInNvdXJjZXMiOlsiYXBwLnNvdXJjZS5qcyJdLCJzb3VyY2VzQ29udGVudCI6WyJ2YXIgZm9vID0gJ3llcyc7XG5jb25zb2xlLmxvZyggZm9vICk7Il0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLENBQUMsQ0FBQyxDQUFDLENBQ1csQ0FBQSxDQUFBLENBQUEsQ0FBQSxDQUFBLENBRFAsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBQ2YsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBRSxDQUFBLENBQUEsQ0FBQ [...]
\ No newline at end of file
diff --git a/example/app.js b/example/app.js
new file mode 100644
index 0000000..4fe076c
--- /dev/null
+++ b/example/app.js
@@ -0,0 +1,3 @@
+var answer = 'yes';
+console.log( answer );
+//# sourceMappingURL=app.js.map
\ No newline at end of file
diff --git a/example/app.js.map b/example/app.js.map
new file mode 100644
index 0000000..3c24c79
--- /dev/null
+++ b/example/app.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"app.js.map","sources":["app.source.js"],"sourcesContent":["var foo = 'yes';\nconsole.log( foo );"],"names":[],"mappings":"AAAA,CAAC,CAAC,CAAC,CACW,CAAA,CAAA,CAAA,CAAA,CAAA,CADP,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACf,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAd,CAAC,CAAC"}
\ No newline at end of file
diff --git a/example/app.source.js b/example/app.source.js
new file mode 100644
index 0000000..197cc9e
--- /dev/null
+++ b/example/app.source.js
@@ -0,0 +1,2 @@
+var foo = 'yes';
+console.log( foo );
\ No newline at end of file
diff --git a/example/build.js b/example/build.js
new file mode 100644
index 0000000..644255e
--- /dev/null
+++ b/example/build.js
@@ -0,0 +1,35 @@
+var fs = require( 'fs' ),
+	MagicString = require( '../' );
+
+process.chdir( __dirname );
+
+fs.readFile( 'app.source.js', function ( err, result ) {
+	var source,
+		magicString,
+		pattern = /foo/g,
+		match,
+		transpiled,
+		map;
+
+	if ( err ) throw err;
+
+	source = result.toString();
+	magicString = new MagicString( result.toString() );
+
+	while ( match = pattern.exec( source ) ) {
+		magicString.replace( match.index, match.index + 3, 'answer' );
+	}
+
+	transpiled = magicString.toString() + '\n//# sourceMappingURL=app.js.map';
+	map = magicString.generateMap({
+		file: 'app.js.map',
+		source: 'app.source.js',
+		includeContent: true,
+		hires: true
+	});
+
+	fs.writeFile( 'app.js', transpiled );
+	fs.writeFile( 'app.js.map', map );
+
+	fs.writeFile( 'app.inlinemap.js', transpiled + '\n//#sourceMappingURL=' + map.toUrl() );
+});
\ No newline at end of file
diff --git a/example/index.html b/example/index.html
new file mode 100644
index 0000000..04f4ffb
--- /dev/null
+++ b/example/index.html
@@ -0,0 +1 @@
+<script src='app.inlinemap.js'></script>
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..3c6f944
--- /dev/null
+++ b/package.json
@@ -0,0 +1,59 @@
+{
+  "name": "magic-string",
+  "description": "Modify strings, generate sourcemaps",
+  "author": "Rich Harris",
+  "version": "0.16.0",
+  "repository": "https://github.com/rich-harris/magic-string",
+  "main": "dist/magic-string.cjs.js",
+  "module": "dist/magic-string.es6.js",
+  "jsnext:main": "dist/magic-string.es6.js",
+  "license": "MIT",
+  "dependencies": {
+    "vlq": "^0.2.1"
+  },
+  "devDependencies": {
+    "codecov.io": "^0.1.6",
+    "console-group": "^0.2.1",
+    "eslint": "^2.11.1",
+    "istanbul": "^0.4.3",
+    "mocha": "^3.0.1",
+    "remap-istanbul": "^0.6.4",
+    "resolve": "^1.1.7",
+    "rollup": "^0.34.5",
+    "rollup-plugin-buble": "^0.12.1",
+    "rollup-plugin-node-resolve": "^2.0.0",
+    "rollup-plugin-replace": "^1.1.0",
+    "source-map": "^0.5.6",
+    "source-map-support": "^0.4.0"
+  },
+  "keywords": [
+    "string",
+    "string manipulation",
+    "sourcemap",
+    "templating",
+    "transpilation"
+  ],
+  "scripts": {
+    "test": "mocha",
+    "pretest": "npm run build:cjs",
+    "pretest-coverage": "npm run build:cjs",
+    "test-coverage": "rm -rf coverage/* && istanbul cover --report json node_modules/.bin/_mocha -- -u exports -R spec test/index.js",
+    "posttest-coverage": "remap-istanbul -i coverage/coverage-final.json -o coverage/coverage-remapped.json -b dist && remap-istanbul -i coverage/coverage-final.json -o coverage/coverage-remapped.lcov -t lcovonly -b dist && remap-istanbul -i coverage/coverage-final.json -o coverage/coverage-remapped -t html -b dist",
+    "ci": "npm run test-coverage && codecov < coverage/coverage-remapped.lcov",
+    "build:cjs": "rollup -c",
+    "build:es6": "rollup -c --environment ES",
+    "build:umd": "rollup -c --environment DEPS",
+    "build": " npm run build:cjs && npm run build:es6 && npm run build:umd",
+    "prepublish": "rm -rf dist && npm test && npm run build:es6 && npm run build:umd",
+    "lint": "eslint src",
+    "watch:cjs": "rollup -w -c",
+    "watch:es6": "rollup -w -c --environment ES",
+    "watch:umd": "rollup -w -c --environment DEPS",
+    "watch": "npm run watch:es6"
+  },
+  "files": [
+    "src/*",
+    "dist/*",
+    "README.md"
+  ]
+}
diff --git a/rollup.config.js b/rollup.config.js
new file mode 100644
index 0000000..ef0aa20
--- /dev/null
+++ b/rollup.config.js
@@ -0,0 +1,21 @@
+import buble from 'rollup-plugin-buble';
+import nodeResolve from 'rollup-plugin-node-resolve';
+import replace from 'rollup-plugin-replace';
+
+var external = process.env.DEPS ? null : [ 'vlq' ];
+var format = process.env.DEPS ? 'umd' : process.env.ES ? 'es6' : 'cjs';
+
+export default {
+	entry: process.env.ES ? 'src/index.js' : 'src/index-legacy.js',
+	dest: 'dist/magic-string.' + format + '.js',
+	format: format,
+	exports: process.env.ES ? 'named' : 'default',
+	plugins: [
+		buble({ exclude: 'node_modules/**' }),
+		nodeResolve({ jsnext: true, skip: external }),
+		replace({ DEBUG: false })
+	],
+	moduleName: 'MagicString',
+	external: external,
+	sourceMap: true
+};
diff --git a/src/Bundle.js b/src/Bundle.js
new file mode 100644
index 0000000..7814ea8
--- /dev/null
+++ b/src/Bundle.js
@@ -0,0 +1,240 @@
+import MagicString from './MagicString.js';
+import SourceMap from './utils/SourceMap.js';
+import getSemis from './utils/getSemis.js';
+import getRelativePath from './utils/getRelativePath.js';
+import hasOwnProp from './utils/hasOwnProp.js';
+import isObject from './utils/isObject.js';
+
+export default function Bundle ( options = {} ) {
+	this.intro = options.intro || '';
+	this.separator = options.separator !== undefined ? options.separator : '\n';
+
+	this.sources = [];
+
+	this.uniqueSources = [];
+	this.uniqueSourceIndexByFilename = {};
+}
+
+Bundle.prototype = {
+	addSource ( source ) {
+		if ( source instanceof MagicString ) {
+			return this.addSource({
+				content: source,
+				filename: source.filename,
+				separator: this.separator
+			});
+		}
+
+		if ( !isObject( source ) || !source.content ) {
+			throw new Error( 'bundle.addSource() takes an object with a `content` property, which should be an instance of MagicString, and an optional `filename`' );
+		}
+
+		[ 'filename', 'indentExclusionRanges', 'separator' ].forEach( option => {
+			if ( !hasOwnProp.call( source, option ) ) source[ option ] = source.content[ option ];
+		});
+
+		if ( source.separator === undefined ) { // TODO there's a bunch of this sort of thing, needs cleaning up
+			source.separator = this.separator;
+		}
+
+		if ( source.filename ) {
+			if ( !hasOwnProp.call( this.uniqueSourceIndexByFilename, source.filename ) ) {
+				this.uniqueSourceIndexByFilename[ source.filename ] = this.uniqueSources.length;
+				this.uniqueSources.push({ filename: source.filename, content: source.content.original });
+			} else {
+				const uniqueSource = this.uniqueSources[ this.uniqueSourceIndexByFilename[ source.filename ] ];
+				if ( source.content.original !== uniqueSource.content ) {
+					throw new Error( `Illegal source: same filename (${source.filename}), different contents` );
+				}
+			}
+		}
+
+		this.sources.push( source );
+		return this;
+	},
+
+	append ( str, options ) {
+		this.addSource({
+			content: new MagicString( str ),
+			separator: ( options && options.separator ) || ''
+		});
+
+		return this;
+	},
+
+	clone () {
+		const bundle = new Bundle({
+			intro: this.intro,
+			separator: this.separator
+		});
+
+		this.sources.forEach( source => {
+			bundle.addSource({
+				filename: source.filename,
+				content: source.content.clone(),
+				separator: source.separator
+			});
+		});
+
+		return bundle;
+	},
+
+	generateMap ( options ) {
+		options = options || {};
+
+		let offsets = {};
+
+		let names = [];
+		this.sources.forEach( source => {
+			Object.keys( source.content.storedNames ).forEach( name => {
+				if ( !~names.indexOf( name ) ) names.push( name );
+			});
+		});
+
+		const encoded = (
+			getSemis( this.intro ) +
+			this.sources.map( ( source, i ) => {
+				const prefix = ( i > 0 ) ? ( getSemis( source.separator ) || ',' ) : '';
+				let mappings;
+
+				// we don't bother encoding sources without a filename
+				if ( !source.filename ) {
+					mappings = getSemis( source.content.toString() );
+				} else {
+					const sourceIndex = this.uniqueSourceIndexByFilename[ source.filename ];
+					mappings = source.content.getMappings( options.hires, sourceIndex, offsets, names );
+				}
+
+				return prefix + mappings;
+			}).join( '' )
+		);
+
+		return new SourceMap({
+			file: ( options.file ? options.file.split( /[\/\\]/ ).pop() : null ),
+			sources: this.uniqueSources.map( source => {
+				return options.file ? getRelativePath( options.file, source.filename ) : source.filename;
+			}),
+			sourcesContent: this.uniqueSources.map( source => {
+				return options.includeContent ? source.content : null;
+			}),
+			names,
+			mappings: encoded
+		});
+	},
+
+	getIndentString () {
+		let indentStringCounts = {};
+
+		this.sources.forEach( source => {
+			const indentStr = source.content.indentStr;
+
+			if ( indentStr === null ) return;
+
+			if ( !indentStringCounts[ indentStr ] ) indentStringCounts[ indentStr ] = 0;
+			indentStringCounts[ indentStr ] += 1;
+		});
+
+		return ( Object.keys( indentStringCounts ).sort( ( a, b ) => {
+			return indentStringCounts[a] - indentStringCounts[b];
+		})[0] ) || '\t';
+	},
+
+	indent ( indentStr ) {
+		if ( !arguments.length ) {
+			indentStr = this.getIndentString();
+		}
+
+		if ( indentStr === '' ) return this; // noop
+
+		let trailingNewline = !this.intro || this.intro.slice( -1 ) === '\n';
+
+		this.sources.forEach( ( source, i ) => {
+			const separator = source.separator !== undefined ? source.separator : this.separator;
+			const indentStart = trailingNewline || ( i > 0 && /\r?\n$/.test( separator ) );
+
+			source.content.indent( indentStr, {
+				exclude: source.indentExclusionRanges,
+				indentStart//: trailingNewline || /\r?\n$/.test( separator )  //true///\r?\n/.test( separator )
+			});
+
+			// TODO this is a very slow way to determine this
+			trailingNewline = source.content.toString().slice( 0, -1 ) === '\n';
+		});
+
+		if ( this.intro ) {
+			this.intro = indentStr + this.intro.replace( /^[^\n]/gm, ( match, index ) => {
+				return index > 0 ? indentStr + match : match;
+			});
+		}
+
+		return this;
+	},
+
+	prepend ( str ) {
+		this.intro = str + this.intro;
+		return this;
+	},
+
+	toString () {
+		const body = this.sources.map( ( source, i ) => {
+			const separator = source.separator !== undefined ? source.separator : this.separator;
+			let str = ( i > 0 ? separator : '' ) + source.content.toString();
+
+			return str;
+		}).join( '' );
+
+		return this.intro + body;
+	},
+
+	trimLines () {
+		return this.trim('[\\r\\n]');
+	},
+
+	trim ( charType ) {
+		return this.trimStart( charType ).trimEnd( charType );
+	},
+
+	trimStart ( charType ) {
+		const rx = new RegExp( '^' + ( charType || '\\s' ) + '+' );
+		this.intro = this.intro.replace( rx, '' );
+
+		if ( !this.intro ) {
+			let source;
+			let i = 0;
+
+			do {
+				source = this.sources[i];
+
+				if ( !source ) {
+					break;
+				}
+
+				source.content.trimStart( charType );
+				i += 1;
+			} while ( source.content.toString() === '' ); // TODO faster way to determine non-empty source?
+		}
+
+		return this;
+	},
+
+	trimEnd ( charType ) {
+		const rx = new RegExp( ( charType || '\\s' ) + '+$' );
+
+		let source;
+		let i = this.sources.length - 1;
+
+		do {
+			source = this.sources[i];
+
+			if ( !source ) {
+				this.intro = this.intro.replace( rx, '' );
+				break;
+			}
+
+			source.content.trimEnd( charType );
+			i -= 1;
+		} while ( source.content.toString() === '' ); // TODO faster way to determine non-empty source?
+
+		return this;
+	}
+};
diff --git a/src/Chunk.js b/src/Chunk.js
new file mode 100644
index 0000000..dfe0152
--- /dev/null
+++ b/src/Chunk.js
@@ -0,0 +1,146 @@
+export default function Chunk ( start, end, content ) {
+	this.start = start;
+	this.end = end;
+	this.original = content;
+
+	this.intro = '';
+	this.outro = '';
+
+	this.content = content;
+	this.storeName = false;
+	this.edited = false;
+
+	// we make these non-enumerable, for sanity while debugging
+	Object.defineProperties( this, {
+		previous: { writable: true, value: null },
+		next: { writable: true, value: null }
+	});
+}
+
+Chunk.prototype = {
+	append ( content ) {
+		this.outro += content;
+	},
+
+	clone () {
+		const chunk = new Chunk( this.start, this.end, this.original );
+
+		chunk.intro = this.intro;
+		chunk.outro = this.outro;
+		chunk.content = this.content;
+		chunk.storeName = this.storeName;
+		chunk.edited = this.edited;
+
+		return chunk;
+	},
+
+	contains ( index ) {
+		return this.start < index && index < this.end;
+	},
+
+	eachNext ( fn ) {
+		let chunk = this;
+		while ( chunk ) {
+			fn( chunk );
+			chunk = chunk.next;
+		}
+	},
+
+	eachPrevious ( fn ) {
+		let chunk = this;
+		while ( chunk ) {
+			fn( chunk );
+			chunk = chunk.previous;
+		}
+	},
+
+	edit ( content, storeName ) {
+		this.content = content;
+		this.intro = '';
+		this.outro = '';
+		this.storeName = storeName;
+
+		this.edited = true;
+
+		return this;
+	},
+
+	prepend ( content ) {
+		this.intro = content + this.intro;
+	},
+
+	split ( index ) {
+		const sliceIndex = index - this.start;
+
+		const originalBefore = this.original.slice( 0, sliceIndex );
+		const originalAfter = this.original.slice( sliceIndex );
+
+		this.original = originalBefore;
+
+		const newChunk = new Chunk( index, this.end, originalAfter );
+		newChunk.outro = this.outro;
+		this.outro = '';
+
+		this.end = index;
+
+		if ( this.edited ) {
+			// TODO is this block necessary?...
+			newChunk.edit( '', false );
+			this.content = '';
+		} else {
+			this.content = originalBefore;
+		}
+
+		newChunk.next = this.next;
+		if ( newChunk.next ) newChunk.next.previous = newChunk;
+		newChunk.previous = this;
+		this.next = newChunk;
+
+		return newChunk;
+	},
+
+	toString () {
+		return this.intro + this.content + this.outro;
+	},
+
+	trimEnd ( rx ) {
+		this.outro = this.outro.replace( rx, '' );
+		if ( this.outro.length ) return true;
+
+		const trimmed = this.content.replace( rx, '' );
+
+		if ( trimmed.length ) {
+			if ( trimmed !== this.content ) {
+				this.split( this.start + trimmed.length ).edit( '', false );
+			}
+
+			return true;
+		} else {
+			this.edit( '', false );
+
+			this.intro = this.intro.replace( rx, '' );
+			if ( this.intro.length ) return true;
+		}
+	},
+
+	trimStart ( rx ) {
+		this.intro = this.intro.replace( rx, '' );
+		if ( this.intro.length ) return true;
+
+		const trimmed = this.content.replace( rx, '' );
+
+		if ( trimmed.length ) {
+			if ( trimmed !== this.content ) {
+				this.split( this.end - trimmed.length );
+				this.edit( '', false );
+			}
+
+			return true;
+		} else {
+			this.edit( '', false );
+
+			this.outro = this.outro.replace( rx, '' );
+			if ( this.outro.length ) return true;
+		}
+	}
+};
diff --git a/src/MagicString.js b/src/MagicString.js
new file mode 100644
index 0000000..f1756a0
--- /dev/null
+++ b/src/MagicString.js
@@ -0,0 +1,519 @@
+import Chunk from './Chunk.js';
+import SourceMap from './utils/SourceMap.js';
+import guessIndent from './utils/guessIndent.js';
+import encodeMappings from './utils/encodeMappings.js';
+import getRelativePath from './utils/getRelativePath.js';
+import isObject from './utils/isObject.js';
+import getLocator from './utils/getLocator.js';
+import Stats from './utils/Stats.js';
+
+export default function MagicString ( string, options = {} ) {
+	const chunk = new Chunk( 0, string.length, string );
+
+	Object.defineProperties( this, {
+		original:              { writable: true, value: string },
+		outro:                 { writable: true, value: '' },
+		intro:                 { writable: true, value: '' },
+		firstChunk:            { writable: true, value: chunk },
+		lastChunk:             { writable: true, value: chunk },
+		lastSearchedChunk:     { writable: true, value: chunk },
+		byStart:               { writable: true, value: {} },
+		byEnd:                 { writable: true, value: {} },
+		filename:              { writable: true, value: options.filename },
+		indentExclusionRanges: { writable: true, value: options.indentExclusionRanges },
+		sourcemapLocations:    { writable: true, value: {} },
+		storedNames:           { writable: true, value: {} },
+		indentStr:             { writable: true, value: guessIndent( string ) }
+	});
+
+	if ( DEBUG ) {
+		Object.defineProperty( this, 'stats', { value: new Stats() });
+	}
+
+	this.byStart[ 0 ] = chunk;
+	this.byEnd[ string.length ] = chunk;
+}
+
+MagicString.prototype = {
+	addSourcemapLocation ( char ) {
+		this.sourcemapLocations[ char ] = true;
+	},
+
+	append ( content ) {
+		if ( typeof content !== 'string' ) throw new TypeError( 'outro content must be a string' );
+
+		this.outro += content;
+		return this;
+	},
+
+	clone () {
+		let cloned = new MagicString( this.original, { filename: this.filename });
+
+		let originalChunk = this.firstChunk;
+		let clonedChunk = cloned.firstChunk = cloned.lastSearchedChunk = originalChunk.clone();
+
+		while ( originalChunk ) {
+			cloned.byStart[ clonedChunk.start ] = clonedChunk;
+			cloned.byEnd[ clonedChunk.end ] = clonedChunk;
+
+			const nextOriginalChunk = originalChunk.next;
+			const nextClonedChunk = nextOriginalChunk && nextOriginalChunk.clone();
+
+			if ( nextClonedChunk ) {
+				clonedChunk.next = nextClonedChunk;
+				nextClonedChunk.previous = clonedChunk;
+
+				clonedChunk = nextClonedChunk;
+			}
+
+			originalChunk = nextOriginalChunk;
+		}
+
+		cloned.lastChunk = clonedChunk;
+
+		if ( this.indentExclusionRanges ) {
+			cloned.indentExclusionRanges = typeof this.indentExclusionRanges[0] === 'number' ?
+				[ this.indentExclusionRanges[0], this.indentExclusionRanges[1] ] :
+				this.indentExclusionRanges.map( range => [ range.start, range.end ] );
+		}
+
+		Object.keys( this.sourcemapLocations ).forEach( loc => {
+			cloned.sourcemapLocations[ loc ] = true;
+		});
+
+		return cloned;
+	},
+
+	generateMap ( options ) {
+		options = options || {};
+
+		const names = Object.keys( this.storedNames );
+
+		if ( DEBUG ) this.stats.time( 'generateMap' );
+		const map = new SourceMap({
+			file: ( options.file ? options.file.split( /[\/\\]/ ).pop() : null ),
+			sources: [ options.source ? getRelativePath( options.file || '', options.source ) : null ],
+			sourcesContent: options.includeContent ? [ this.original ] : [ null ],
+			names,
+			mappings: this.getMappings( options.hires, 0, {}, names )
+		});
+		if ( DEBUG ) this.stats.timeEnd( 'generateMap' );
+
+		return map;
+	},
+
+	getIndentString () {
+		return this.indentStr === null ? '\t' : this.indentStr;
+	},
+
+	getMappings ( hires, sourceIndex, offsets, names ) {
+		return encodeMappings( this.original, this.intro, this.outro, this.firstChunk, hires, this.sourcemapLocations, sourceIndex, offsets, names );
+	},
+
+	indent ( indentStr, options ) {
+		const pattern = /^[^\r\n]/gm;
+
+		if ( isObject( indentStr ) ) {
+			options = indentStr;
+			indentStr = undefined;
+		}
+
+		indentStr = indentStr !== undefined ? indentStr : ( this.indentStr || '\t' );
+
+		if ( indentStr === '' ) return this; // noop
+
+		options = options || {};
+
+		// Process exclusion ranges
+		let isExcluded = {};
+
+		if ( options.exclude ) {
+			let exclusions = typeof options.exclude[0] === 'number' ? [ options.exclude ] : options.exclude;
+			exclusions.forEach( exclusion => {
+				for ( let i = exclusion[0]; i < exclusion[1]; i += 1 ) {
+					isExcluded[i] = true;
+				}
+			});
+		}
+
+		let shouldIndentNextCharacter = options.indentStart !== false;
+		const replacer = match => {
+			if ( shouldIndentNextCharacter ) return `${indentStr}${match}`;
+			shouldIndentNextCharacter = true;
+			return match;
+		};
+
+		this.intro = this.intro.replace( pattern, replacer );
+
+		let charIndex = 0;
+
+		let chunk = this.firstChunk;
+
+		while ( chunk ) {
+			const end = chunk.end;
+
+			if ( chunk.edited ) {
+				if ( !isExcluded[ charIndex ] ) {
+					chunk.content = chunk.content.replace( pattern, replacer );
+
+					if ( chunk.content.length ) {
+						shouldIndentNextCharacter = chunk.content[ chunk.content.length - 1 ] === '\n';
+					}
+				}
+			} else {
+				charIndex = chunk.start;
+
+				while ( charIndex < end ) {
+					if ( !isExcluded[ charIndex ] ) {
+						const char = this.original[ charIndex ];
+
+						if ( char === '\n' ) {
+							shouldIndentNextCharacter = true;
+						} else if ( char !== '\r' && shouldIndentNextCharacter ) {
+							shouldIndentNextCharacter = false;
+
+							if ( charIndex === chunk.start ) {
+								chunk.prepend( indentStr );
+							} else {
+								const rhs = chunk.split( charIndex );
+								rhs.prepend( indentStr );
+
+								this.byStart[ charIndex ] = rhs;
+								this.byEnd[ charIndex ] = chunk;
+
+								chunk = rhs;
+							}
+						}
+					}
+
+					charIndex += 1;
+				}
+			}
+
+			charIndex = chunk.end;
+			chunk = chunk.next;
+		}
+
+		this.outro = this.outro.replace( pattern, replacer );
+
+		return this;
+	},
+
+	insert () {
+		throw new Error( 'magicString.insert(...) is deprecated. Use insertRight(...) or insertLeft(...)' );
+	},
+
+	insertLeft ( index, content ) {
+		if ( typeof content !== 'string' ) throw new TypeError( 'inserted content must be a string' );
+
+		if ( DEBUG ) this.stats.time( 'insertLeft' );
+
+		this._split( index );
+
+		const chunk = this.byEnd[ index ];
+
+		if ( chunk ) {
+			chunk.append( content );
+		} else {
+			this.intro += content;
+		}
+
+		if ( DEBUG ) this.stats.timeEnd( 'insertLeft' );
+		return this;
+	},
+
+	insertRight ( index, content ) {
+		if ( typeof content !== 'string' ) throw new TypeError( 'inserted content must be a string' );
+
+		if ( DEBUG ) this.stats.time( 'insertRight' );
+
+		this._split( index );
+
+		const chunk = this.byStart[ index ];
+
+		if ( chunk ) {
+			chunk.prepend( content );
+		} else {
+			this.outro += content;
+		}
+
+		if ( DEBUG ) this.stats.timeEnd( 'insertRight' );
+		return this;
+	},
+
+	move ( start, end, index ) {
+		if ( index >= start && index <= end ) throw new Error( 'Cannot move a selection inside itself' );
+
+		if ( DEBUG ) this.stats.time( 'move' );
+
+		this._split( start );
+		this._split( end );
+		this._split( index );
+
+		const first = this.byStart[ start ];
+		const last = this.byEnd[ end ];
+
+		const oldLeft = first.previous;
+		const oldRight = last.next;
+
+		const newRight = this.byStart[ index ];
+		if ( !newRight && last === this.lastChunk ) return this;
+		const newLeft = newRight ? newRight.previous : this.lastChunk;
+
+		if ( oldLeft ) oldLeft.next = oldRight;
+		if ( oldRight ) oldRight.previous = oldLeft;
+
+		if ( newLeft ) newLeft.next = first;
+		if ( newRight ) newRight.previous = last;
+
+		if ( !first.previous ) this.firstChunk = last.next;
+		if ( !last.next ) {
+			this.lastChunk = first.previous;
+			this.lastChunk.next = null;
+		}
+
+		first.previous = newLeft;
+		last.next = newRight;
+
+		if ( !newLeft ) this.firstChunk = first;
+		if ( !newRight ) this.lastChunk = last;
+
+		if ( DEBUG ) this.stats.timeEnd( 'move' );
+		return this;
+	},
+
+	overwrite ( start, end, content, storeName ) {
+		if ( typeof content !== 'string' ) throw new TypeError( 'replacement content must be a string' );
+
+		while ( start < 0 ) start += this.original.length;
+		while ( end < 0 ) end += this.original.length;
+
+		if ( end > this.original.length ) throw new Error( 'end is out of bounds' );
+		if ( start === end ) throw new Error( 'Cannot overwrite a zero-length range – use insertLeft or insertRight instead' );
+
+		if ( DEBUG ) this.stats.time( 'overwrite' );
+
+		this._split( start );
+		this._split( end );
+
+		if ( storeName ) {
+			const original = this.original.slice( start, end );
+			this.storedNames[ original ] = true;
+		}
+
+		const first = this.byStart[ start ];
+		const last = this.byEnd[ end ];
+
+		if ( first ) {
+			first.edit( content, storeName );
+
+			if ( first !== last ) {
+				let chunk = first.next;
+				while ( chunk !== last ) {
+					chunk.edit( '', false );
+					chunk = chunk.next;
+				}
+
+				chunk.edit( '', false );
+			}
+		}
+
+		else {
+			// must be inserting at the end
+			const newChunk = new Chunk( start, end, '' ).edit( content, storeName );
+
+			// TODO last chunk in the array may not be the last chunk, if it's moved...
+			last.next = newChunk;
+			newChunk.previous = last;
+		}
+
+		if ( DEBUG ) this.stats.timeEnd( 'overwrite' );
+		return this;
+	},
+
+	prepend ( content ) {
+		if ( typeof content !== 'string' ) throw new TypeError( 'outro content must be a string' );
+
+		this.intro = content + this.intro;
+		return this;
+	},
+
+	remove ( start, end ) {
+		while ( start < 0 ) start += this.original.length;
+		while ( end < 0 ) end += this.original.length;
+
+		if ( start === end ) return this;
+
+		if ( start < 0 || end > this.original.length ) throw new Error( 'Character is out of bounds' );
+		if ( start > end ) throw new Error( 'end must be greater than start' );
+
+		return this.overwrite( start, end, '', false );
+	},
+
+	slice ( start = 0, end = this.original.length ) {
+		while ( start < 0 ) start += this.original.length;
+		while ( end < 0 ) end += this.original.length;
+
+		let result = '';
+
+		// find start chunk
+		let chunk = this.firstChunk;
+		while ( chunk && ( chunk.start > start || chunk.end <= start ) ) {
+
+			// found end chunk before start
+			if ( chunk.start < end && chunk.end >= end ) {
+				return result;
+			}
+
+			chunk = chunk.next;
+		}
+
+		if ( chunk && chunk.edited && chunk.start !== start ) throw new Error(`Cannot use replaced character ${start} as slice start anchor.`);
+
+		let startChunk = chunk;
+		while ( chunk ) {
+			if ( chunk.intro && ( startChunk !== chunk || chunk.start === start ) ) {
+				result += chunk.intro;
+			}
+
+			const containsEnd = chunk.start < end && chunk.end >= end;
+			if ( containsEnd && chunk.edited && chunk.end !== end ) throw new Error(`Cannot use replaced character ${end} as slice end anchor.`);
+
+			const sliceStart = startChunk === chunk ? start - chunk.start : 0;
+			const sliceEnd = containsEnd ? chunk.content.length + end - chunk.end : chunk.content.length;
+
+			result += chunk.content.slice( sliceStart, sliceEnd );
+
+			if ( chunk.outro && ( !containsEnd || chunk.end === end ) ) {
+				result += chunk.outro;
+			}
+
+			if ( containsEnd ) {
+				break;
+			}
+
+			chunk = chunk.next;
+		}
+
+		return result;
+	},
+
+	// TODO deprecate this? not really very useful
+	snip ( start, end ) {
+		const clone = this.clone();
+		clone.remove( 0, start );
+		clone.remove( end, clone.original.length );
+
+		return clone;
+	},
+
+	_split ( index ) {
+		if ( this.byStart[ index ] || this.byEnd[ index ] ) return;
+
+		if ( DEBUG ) this.stats.time( '_split' );
+
+		let chunk = this.lastSearchedChunk;
+		const searchForward = index > chunk.end;
+
+		while ( true ) {
+			if ( chunk.contains( index ) ) return this._splitChunk( chunk, index );
+
+			chunk = searchForward ?
+				this.byStart[ chunk.end ] :
+				this.byEnd[ chunk.start ];
+		}
+	},
+
+	_splitChunk ( chunk, index ) {
+		if ( chunk.edited && chunk.content.length ) { // zero-length edited chunks are a special case (overlapping replacements)
+			const loc = getLocator( this.original )( index );
+			throw new Error( `Cannot split a chunk that has already been edited (${loc.line}:${loc.column} – "${chunk.original}")` );
+		}
+
+		const newChunk = chunk.split( index );
+
+		this.byEnd[ index ] = chunk;
+		this.byStart[ index ] = newChunk;
+		this.byEnd[ newChunk.end ] = newChunk;
+
+		if ( chunk === this.lastChunk ) this.lastChunk = newChunk;
+
+		this.lastSearchedChunk = chunk;
+		if ( DEBUG ) this.stats.timeEnd( '_split' );
+		return true;
+	},
+
+	toString () {
+		let str = this.intro;
+
+		let chunk = this.firstChunk;
+		while ( chunk ) {
+			str += chunk.toString();
+			chunk = chunk.next;
+		}
+
+		return str + this.outro;
+	},
+
+	trimLines () {
+		return this.trim('[\\r\\n]');
+	},
+
+	trim ( charType ) {
+		return this.trimStart( charType ).trimEnd( charType );
+	},
+
+	trimEnd ( charType ) {
+		const rx = new RegExp( ( charType || '\\s' ) + '+$' );
+
+		this.outro = this.outro.replace( rx, '' );
+		if ( this.outro.length ) return this;
+
+		let chunk = this.lastChunk;
+
+		do {
+			const end = chunk.end;
+			const aborted = chunk.trimEnd( rx );
+
+			// if chunk was trimmed, we have a new lastChunk
+			if ( chunk.end !== end ) {
+				this.lastChunk = chunk.next;
+
+				this.byEnd[ chunk.end ] = chunk;
+				this.byStart[ chunk.next.start ] = chunk.next;
+			}
+
+			if ( aborted ) return this;
+			chunk = chunk.previous;
+		} while ( chunk );
+
+		return this;
+	},
+
+	trimStart ( charType ) {
+		const rx = new RegExp( '^' + ( charType || '\\s' ) + '+' );
+
+		this.intro = this.intro.replace( rx, '' );
+		if ( this.intro.length ) return this;
+
+		let chunk = this.firstChunk;
+
+		do {
+			const end = chunk.end;
+			const aborted = chunk.trimStart( rx );
+
+			if ( chunk.end !== end ) {
+				// special case...
+				if ( chunk === this.lastChunk ) this.lastChunk = chunk.next;
+
+				this.byEnd[ chunk.end ] = chunk;
+				this.byStart[ chunk.next.start ] = chunk.next;
+			}
+
+			if ( aborted ) return this;
+			chunk = chunk.next;
+		} while ( chunk );
+
+		return this;
+	}
+};
diff --git a/src/index-legacy.js b/src/index-legacy.js
new file mode 100644
index 0000000..16d6f4d
--- /dev/null
+++ b/src/index-legacy.js
@@ -0,0 +1,6 @@
+import MagicString from './MagicString.js';
+import Bundle from './Bundle.js';
+
+MagicString.Bundle = Bundle;
+
+export default MagicString;
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..594d8db
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,4 @@
+import MagicString from './MagicString.js';
+
+export default MagicString;
+export { default as Bundle } from './Bundle.js';
diff --git a/src/utils/SourceMap.js b/src/utils/SourceMap.js
new file mode 100644
index 0000000..f3dde82
--- /dev/null
+++ b/src/utils/SourceMap.js
@@ -0,0 +1,21 @@
+import btoa from './btoa.js';
+
+export default function SourceMap ( properties ) {
+	this.version = 3;
+
+	this.file           = properties.file;
+	this.sources        = properties.sources;
+	this.sourcesContent = properties.sourcesContent;
+	this.names          = properties.names;
+	this.mappings       = properties.mappings;
+}
+
+SourceMap.prototype = {
+	toString () {
+		return JSON.stringify( this );
+	},
+
+	toUrl () {
+		return 'data:application/json;charset=utf-8;base64,' + btoa( this.toString() );
+	}
+};
diff --git a/src/utils/Stats.js b/src/utils/Stats.js
new file mode 100644
index 0000000..46d4c0c
--- /dev/null
+++ b/src/utils/Stats.js
@@ -0,0 +1,18 @@
+export default class Stats {
+	constructor () {
+		Object.defineProperties( this, {
+			startTimes: { value: {} }
+		});
+	}
+
+	time ( label ) {
+		this.startTimes[ label ] = process.hrtime();
+	}
+
+	timeEnd ( label ) {
+		const elapsed = process.hrtime( this.startTimes[ label ] );
+
+		if ( !this[ label ] ) this[ label ] = 0;
+		this[ label ] += elapsed[0] * 1e3 + elapsed[1] * 1e-6;
+	}
+}
diff --git a/src/utils/btoa.js b/src/utils/btoa.js
new file mode 100644
index 0000000..889bf81
--- /dev/null
+++ b/src/utils/btoa.js
@@ -0,0 +1,13 @@
+let _btoa;
+
+if ( typeof window !== 'undefined' && typeof window.btoa === 'function' ) {
+	_btoa = window.btoa;
+} else if ( typeof Buffer === 'function' ) {
+	_btoa = str => new Buffer( str ).toString( 'base64' );
+} else {
+	_btoa = () => {
+		throw new Error( 'Unsupported environment: `window.btoa` or `Buffer` should be supported.' );
+	};
+}
+
+export default _btoa;
diff --git a/src/utils/encodeMappings.js b/src/utils/encodeMappings.js
new file mode 100644
index 0000000..b96efca
--- /dev/null
+++ b/src/utils/encodeMappings.js
@@ -0,0 +1,137 @@
+import { encode } from 'vlq';
+import getSemis from './getSemis.js';
+import getLocator from './getLocator.js';
+
+const nonWhitespace = /\S/;
+
+export default function encodeMappings ( original, intro, outro, chunk, hires, sourcemapLocations, sourceIndex, offsets, names ) {
+	let rawLines = [];
+
+	let generatedCodeLine = intro.split( '\n' ).length - 1;
+	let rawSegments = rawLines[ generatedCodeLine ] = [];
+
+	let generatedCodeColumn = 0;
+
+	const locate = getLocator( original );
+
+	function addEdit ( content, original, loc, nameIndex, i ) {
+		if ( i || ( content.length && nonWhitespace.test( content ) ) ) {
+			rawSegments.push({
+				generatedCodeLine,
+				generatedCodeColumn,
+				sourceCodeLine: loc.line,
+				sourceCodeColumn: loc.column,
+				sourceCodeName: nameIndex,
+				sourceIndex
+			});
+		}
+
+		let lines = content.split( '\n' );
+		let lastLine = lines.pop();
+
+		if ( lines.length ) {
+			generatedCodeLine += lines.length;
+			rawLines[ generatedCodeLine ] = rawSegments = [];
+			generatedCodeColumn = lastLine.length;
+		} else {
+			generatedCodeColumn += lastLine.length;
+		}
+
+		lines = original.split( '\n' );
+		lastLine = lines.pop();
+
+		if ( lines.length ) {
+			loc.line += lines.length;
+			loc.column = lastLine.length;
+		} else {
+			loc.column += lastLine.length;
+		}
+	}
+
+	function addUneditedChunk ( chunk, loc ) {
+		let originalCharIndex = chunk.start;
+		let first = true;
+
+		while ( originalCharIndex < chunk.end ) {
+			if ( hires || first || sourcemapLocations[ originalCharIndex ] ) {
+				rawSegments.push({
+					generatedCodeLine,
+					generatedCodeColumn,
+					sourceCodeLine: loc.line,
+					sourceCodeColumn: loc.column,
+					sourceCodeName: -1,
+					sourceIndex
+				});
+			}
+
+			if ( original[ originalCharIndex ] === '\n' ) {
+				loc.line += 1;
+				loc.column = 0;
+				generatedCodeLine += 1;
+				rawLines[ generatedCodeLine ] = rawSegments = [];
+				generatedCodeColumn = 0;
+			} else {
+				loc.column += 1;
+				generatedCodeColumn += 1;
+			}
+
+			originalCharIndex += 1;
+			first = false;
+		}
+	}
+
+	let hasContent = false;
+
+	while ( chunk ) {
+		let loc = locate( chunk.start );
+
+		if ( chunk.intro.length ) {
+			addEdit( chunk.intro, '', loc, -1, hasContent );
+		}
+
+		if ( chunk.edited ) {
+			addEdit( chunk.content, chunk.original, loc, chunk.storeName ? names.indexOf( chunk.original ) : -1, hasContent );
+		} else {
+			addUneditedChunk( chunk, loc );
+		}
+
+		if ( chunk.outro.length ) {
+			addEdit( chunk.outro, '', loc, -1, hasContent );
+		}
+
+		if ( chunk.content || chunk.intro || chunk.outro ) hasContent = true;
+
+		const nextChunk = chunk.next;
+		chunk = nextChunk;
+	}
+
+	offsets.sourceIndex = offsets.sourceIndex || 0;
+	offsets.sourceCodeLine = offsets.sourceCodeLine || 0;
+	offsets.sourceCodeColumn = offsets.sourceCodeColumn || 0;
+	offsets.sourceCodeName = offsets.sourceCodeName || 0;
+
+	return rawLines.map( segments => {
+		let generatedCodeColumn = 0;
+
+		return segments.map( segment => {
+			let arr = [
+				segment.generatedCodeColumn - generatedCodeColumn,
+				segment.sourceIndex - offsets.sourceIndex,
+				segment.sourceCodeLine - offsets.sourceCodeLine,
+				segment.sourceCodeColumn - offsets.sourceCodeColumn
+			];
+
+			generatedCodeColumn = segment.generatedCodeColumn;
+			offsets.sourceIndex = segment.sourceIndex;
+			offsets.sourceCodeLine = segment.sourceCodeLine;
+			offsets.sourceCodeColumn = segment.sourceCodeColumn;
+
+			if ( ~segment.sourceCodeName ) {
+				arr.push( segment.sourceCodeName - offsets.sourceCodeName );
+				offsets.sourceCodeName = segment.sourceCodeName;
+			}
+
+			return encode( arr );
+		}).join( ',' );
+	}).join( ';' ) + getSemis(outro);
+}
diff --git a/src/utils/getLocator.js b/src/utils/getLocator.js
new file mode 100644
index 0000000..d2ea142
--- /dev/null
+++ b/src/utils/getLocator.js
@@ -0,0 +1,35 @@
+export default function getLocator ( source ) {
+	let originalLines = source.split( '\n' );
+
+	let start = 0;
+	let lineRanges = originalLines.map( ( line, i ) => {
+		const end = start + line.length + 1;
+		const range = { start, end, line: i };
+
+		start = end;
+		return range;
+	});
+
+	let i = 0;
+
+	function rangeContains ( range, index ) {
+		return range.start <= index && index < range.end;
+	}
+
+	function getLocation ( range, index ) {
+		return { line: range.line, column: index - range.start };
+	}
+
+	return function locate ( index ) {
+		let range = lineRanges[i];
+
+		const d = index >= range.end ? 1 : -1;
+
+		while ( range ) {
+			if ( rangeContains( range, index ) ) return getLocation( range, index );
+
+			i += d;
+			range = lineRanges[i];
+		}
+	};
+}
diff --git a/src/utils/getRelativePath.js b/src/utils/getRelativePath.js
new file mode 100644
index 0000000..a05cb19
--- /dev/null
+++ b/src/utils/getRelativePath.js
@@ -0,0 +1,18 @@
+export default function getRelativePath ( from, to ) {
+	let fromParts = from.split( /[\/\\]/ );
+	let toParts = to.split( /[\/\\]/ );
+
+	fromParts.pop(); // get dirname
+
+	while ( fromParts[0] === toParts[0] ) {
+		fromParts.shift();
+		toParts.shift();
+	}
+
+	if ( fromParts.length ) {
+		let i = fromParts.length;
+		while ( i-- ) fromParts[i] = '..';
+	}
+
+	return fromParts.concat( toParts ).join( '/' );
+}
diff --git a/src/utils/getSemis.js b/src/utils/getSemis.js
new file mode 100644
index 0000000..4ead76e
--- /dev/null
+++ b/src/utils/getSemis.js
@@ -0,0 +1,3 @@
+export default function getSemis ( str ) {
+	return new Array( str.split( '\n' ).length ).join( ';' );
+}
diff --git a/src/utils/guessIndent.js b/src/utils/guessIndent.js
new file mode 100644
index 0000000..ce2e8d6
--- /dev/null
+++ b/src/utils/guessIndent.js
@@ -0,0 +1,25 @@
+export default function guessIndent ( code ) {
+	const lines = code.split( '\n' );
+
+	const tabbed = lines.filter( line => /^\t+/.test( line ) );
+	const spaced = lines.filter( line => /^ {2,}/.test( line ) );
+
+	if ( tabbed.length === 0 && spaced.length === 0 ) {
+		return null;
+	}
+
+	// More lines tabbed than spaced? Assume tabs, and
+	// default to tabs in the case of a tie (or nothing
+	// to go on)
+	if ( tabbed.length >= spaced.length ) {
+		return '\t';
+	}
+
+	// Otherwise, we need to guess the multiple
+	const min = spaced.reduce( ( previous, current ) => {
+		const numSpaces = /^ +/.exec( current )[0].length;
+		return Math.min( numSpaces, previous );
+	}, Infinity );
+
+	return new Array( min + 1 ).join( ' ' );
+}
diff --git a/src/utils/hasOwnProp.js b/src/utils/hasOwnProp.js
new file mode 100644
index 0000000..54db1c2
--- /dev/null
+++ b/src/utils/hasOwnProp.js
@@ -0,0 +1 @@
+export default Object.prototype.hasOwnProperty;
\ No newline at end of file
diff --git a/src/utils/isObject.js b/src/utils/isObject.js
new file mode 100644
index 0000000..3660761
--- /dev/null
+++ b/src/utils/isObject.js
@@ -0,0 +1,5 @@
+const toString = Object.prototype.toString;
+
+export default function isObject ( thing ) {
+	return toString.call( thing ) === '[object Object]';
+}
diff --git a/test/index.js b/test/index.js
new file mode 100644
index 0000000..5305692
--- /dev/null
+++ b/test/index.js
@@ -0,0 +1,1684 @@
+/*global require, describe, it, console */
+var assert = require( 'assert' );
+var SourceMapConsumer = require( 'source-map' ).SourceMapConsumer;
+var MagicString = require( '../' );
+
+require( 'source-map-support' ).install();
+require( 'console-group' ).install();
+
+describe( 'MagicString', function () {
+	describe( 'options', function () {
+		it( 'stores source file information', function () {
+			var s = new MagicString( 'abc', {
+				filename: 'foo.js'
+			});
+
+			assert.equal( s.filename, 'foo.js' );
+		});
+	});
+
+	describe( 'append', function () {
+		it( 'should append content', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+
+			s.append( 'xyz' );
+			assert.equal( s.toString(), 'abcdefghijklxyz' );
+
+			s.append( 'xyz' );
+			assert.equal( s.toString(), 'abcdefghijklxyzxyz' );
+		});
+
+		it( 'should return this', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+			assert.strictEqual( s.append( 'xyz' ), s );
+		});
+
+		it( 'should throw when given non-string content', function () {
+			var s = new MagicString( '' );
+			assert.throws(
+				function () { s.append( [] ); },
+				TypeError
+			);
+		});
+	});
+
+	describe( 'clone', function () {
+		it( 'should clone a magic string', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+
+			s.overwrite( 3, 9, 'XYZ' );
+			var c = s.clone();
+
+			assert.notEqual( s, c );
+			assert.equal( c.toString(), 'abcXYZjkl' );
+		});
+
+		it( 'should clone filename info', function () {
+			var s = new MagicString( 'abcdefghijkl', { filename: 'foo.js' });
+			var c = s.clone();
+
+			assert.equal( c.filename, 'foo.js' );
+		});
+
+		it( 'should clone indentExclusionRanges', function () {
+			var array = [ 3, 6 ];
+			var source = new MagicString( 'abcdefghijkl', {
+				filename: 'foo.js',
+				indentExclusionRanges: array
+			});
+
+			var clone = source.clone();
+
+			assert.notStrictEqual( source.indentExclusionRanges, clone.indentExclusionRanges );
+			assert.deepEqual( source.indentExclusionRanges, clone.indentExclusionRanges );
+		});
+
+		it( 'should clone sourcemapLocations', function () {
+			var source = new MagicString( 'abcdefghijkl', {
+				filename: 'foo.js'
+			});
+
+			source.addSourcemapLocation( 3 );
+
+			var clone = source.clone();
+
+			assert.notStrictEqual( source.sourcemapLocations, clone.sourcemapLocations );
+			assert.deepEqual( source.sourcemapLocations, clone.sourcemapLocations );
+		});
+	});
+
+	describe( 'generateMap', function () {
+		it( 'should generate a sourcemap', function () {
+			var s = new MagicString( 'abcdefghijkl' ).remove( 3, 9 );
+
+			var map = s.generateMap({
+				file: 'output.md',
+				source: 'input.md',
+				includeContent: true,
+				hires: true
+			});
+
+			assert.equal( map.version, 3 );
+			assert.equal( map.file, 'output.md' );
+			assert.deepEqual( map.sources, [ 'input.md' ]);
+			assert.deepEqual( map.sourcesContent, [ 'abcdefghijkl' ]);
+			assert.equal( map.mappings, 'AAAA,CAAC,CAAC,CAAC,AAAM,CAAC,CAAC' );
+
+			assert.equal( map.toString(), '{"version":3,"file":"output.md","sources":["input.md"],"sourcesContent":["abcdefghijkl"],"names":[],"mappings":"AAAA,CAAC,CAAC,CAAC,AAAM,CAAC,CAAC"}' );
+			assert.equal( map.toUrl(), 'data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3V0cHV0Lm1kIiwic291cmNlcyI6WyJpbnB1dC5tZCJdLCJzb3VyY2VzQ29udGVudCI6WyJhYmNkZWZnaGlqa2wiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsQ0FBQyxDQUFDLENBQUMsQUFBTSxDQUFDLENBQUMifQ==' );
+
+			var smc = new SourceMapConsumer( map );
+			var loc;
+
+			loc = smc.originalPositionFor({ line: 1, column: 0 });
+			assert.equal( loc.line, 1 );
+			assert.equal( loc.column, 0 );
+
+			loc = smc.originalPositionFor({ line: 1, column: 1 });
+			assert.equal( loc.line, 1 );
+			assert.equal( loc.column, 1 );
+
+			loc = smc.originalPositionFor({ line: 1, column: 4 });
+			assert.equal( loc.line, 1 );
+			assert.equal( loc.column, 10 );
+		});
+
+		it( 'should generate a correct sourcemap for indented content', function () {
+			var s, map, smc, originLoc;
+
+			s = new MagicString( 'var answer = 42;\nconsole.log("the answer is %s", answer);' );
+
+			s.prepend( "'use strict';\n\n" );
+			s.indent( '\t' ).prepend( '(function () {\n' ).append( '\n}).call(global);' );
+
+			map = s.generateMap({
+				source: 'input.md',
+				includeContent: true,
+				hires: true
+			});
+
+			smc = new SourceMapConsumer( map );
+
+			originLoc = smc.originalPositionFor({ line: 5, column: 1 });
+			assert.equal( originLoc.line, 2 );
+			assert.equal( originLoc.column, 0 );
+		});
+
+		it( 'should generate a sourcemap using specified locations', function () {
+			var s, map, smc, loc;
+
+			s = new MagicString( 'abcdefghijkl' );
+
+			s.addSourcemapLocation( 0 );
+			s.addSourcemapLocation( 3 );
+			s.addSourcemapLocation( 10 );
+
+			s.remove( 6, 9 );
+			map = s.generateMap({
+				file: 'output.md',
+				source: 'input.md',
+				includeContent: true
+			});
+
+			assert.equal( map.version, 3 );
+			assert.equal( map.file, 'output.md' );
+			assert.deepEqual( map.sources, [ 'input.md' ]);
+			assert.deepEqual( map.sourcesContent, [ 'abcdefghijkl' ]);
+
+			assert.equal( map.toString(), '{"version":3,"file":"output.md","sources":["input.md"],"sourcesContent":["abcdefghijkl"],"names":[],"mappings":"AAAA,GAAG,GAAG,AAAG,CAAC"}' );
+			assert.equal( map.toUrl(), 'data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3V0cHV0Lm1kIiwic291cmNlcyI6WyJpbnB1dC5tZCJdLCJzb3VyY2VzQ29udGVudCI6WyJhYmNkZWZnaGlqa2wiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsR0FBRyxHQUFHLEFBQUcsQ0FBQyJ9' );
+
+			smc = new SourceMapConsumer( map );
+
+			loc = smc.originalPositionFor({ line: 1, column: 0 });
+			assert.equal( loc.line, 1 );
+			assert.equal( loc.column, 0 );
+
+			loc = smc.originalPositionFor({ line: 1, column: 3 });
+			assert.equal( loc.line, 1 );
+			assert.equal( loc.column, 3 );
+
+			loc = smc.originalPositionFor({ line: 1, column: 7 });
+			assert.equal( loc.line, 1 );
+			assert.equal( loc.column, 10 );
+		});
+
+		it( 'should correctly map inserted content', function () {
+			var s = new MagicString( 'function Foo () {}' );
+
+			s.overwrite( 9, 12, 'Bar' );
+
+			map = s.generateMap({
+				file: 'output.js',
+				source: 'input.js',
+				includeContent: true
+			});
+
+			smc = new SourceMapConsumer( map );
+
+			loc = smc.originalPositionFor({ line: 1, column: 9 });
+			assert.equal( loc.line, 1 );
+			assert.equal( loc.column, 9 );
+		});
+
+		it( 'should yield consistent results between insertLeft and insertRight', function () {
+			var s1 = new MagicString( 'abcdefghijkl' );
+			s1.insertLeft( 6, 'X' );
+
+			var s2 = new MagicString( 'abcdefghijkl' );
+			s2.insertRight( 6, 'X' );
+
+			var m1 = s1.generateMap({ file: 'output', source: 'input', includeContent: true });
+			var m2 = s2.generateMap({ file: 'output', source: 'input', includeContent: true });
+
+			assert.deepEqual( m1, m2 );
+		});
+
+		it( 'should recover original names', function () {
+			var s = new MagicString( 'function Foo () {}' );
+
+			s.overwrite( 9, 12, 'Bar', true );
+
+			map = s.generateMap({
+				file: 'output.js',
+				source: 'input.js',
+				includeContent: true
+			});
+
+			smc = new SourceMapConsumer( map );
+
+			loc = smc.originalPositionFor({ line: 1, column: 9 });
+			assert.equal( loc.name, 'Foo' );
+		});
+
+		it( 'should generate one segment per replacement', function () {
+			var s = new MagicString( 'var answer = 42' );
+			s.overwrite( 4, 10, 'number', true );
+
+			map = s.generateMap({
+				file: 'output.js',
+				source: 'input.js',
+				includeContent: true
+			});
+
+			smc = new SourceMapConsumer( map );
+
+			var numMappings = 0;
+			smc.eachMapping( function ( mapping ) {
+				numMappings += 1;
+			});
+
+			assert.equal( numMappings, 3 ); // one at 0, one at the edit, one afterwards
+		});
+
+		it( 'should generate a sourcemap that correctly locates moved content', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+			s.move( 3, 6, 9 );
+
+			var result = s.toString();
+			var map = s.generateMap({
+				file: 'output.js',
+				source: 'input.js',
+				includeContent: true,
+				hires: true
+			});
+
+			var smc = new SourceMapConsumer( map );
+
+			'abcdefghijkl'.split( '' ).forEach( function ( letter, i ) {
+				var column = result.indexOf( letter );
+				var loc = smc.originalPositionFor({ line: 1, column: column });
+
+				assert.equal( loc.line, 1 );
+				assert.equal( loc.column, i );
+			});
+		});
+
+		it( 'generates a map with trimmed content (#53)', function () {
+			var s = new MagicString( 'abcdefghijkl ' ).trim();
+			var map = s.generateMap({
+				file: 'output',
+				source: 'input',
+				includeContent: true,
+				hires: true
+			});
+
+			var smc = new SourceMapConsumer( map );
+			var loc = smc.originalPositionFor({ line: 1, column: 11 });
+
+			assert.equal( loc.column, 11 );
+
+			s = new MagicString( ' abcdefghijkl' ).trim();
+			map = s.generateMap({
+				file: 'output',
+				source: 'input',
+				includeContent: true,
+				hires: true
+			});
+
+			smc = new SourceMapConsumer( map );
+			loc = smc.originalPositionFor({ line: 1, column: 1 });
+
+			assert.equal( loc.column, 2 );
+		});
+
+		it( 'skips empty segments at the start', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+			s.remove( 0, 3 ).remove( 3, 6 );
+
+			var map = s.generateMap();
+			var smc = new SourceMapConsumer( map );
+			var loc = smc.originalPositionFor({ line: 1, column: 6 });
+
+			assert.equal( loc.column, 6 );
+		});
+
+		it( 'skips indentation at the start', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+			s.indent( '    ' );
+
+			var map = s.generateMap();
+			assert.equal( map.mappings, 'IAAA' );
+		});
+	});
+
+	describe( 'getIndentString', function () {
+		it( 'should guess the indent string', function () {
+			var s = new MagicString( 'abc\n  def\nghi' );
+			assert.equal( s.getIndentString(), '  ' );
+		});
+
+		it( 'should return a tab if no lines are indented', function () {
+			var s = new MagicString( 'abc\ndef\nghi' );
+			assert.equal( s.getIndentString(), '\t' );
+		});
+	});
+
+	describe( 'indent', function () {
+		it( 'should indent content with a single tab character by default', function () {
+			var s = new MagicString( 'abc\ndef\nghi\njkl' );
+
+			s.indent();
+			assert.equal( s.toString(), '\tabc\n\tdef\n\tghi\n\tjkl' );
+
+			s.indent();
+			assert.equal( s.toString(), '\t\tabc\n\t\tdef\n\t\tghi\n\t\tjkl' );
+		});
+
+		it( 'should indent content, using existing indentation as a guide', function () {
+			var s = new MagicString( 'abc\n  def\n    ghi\n  jkl' );
+
+			s.indent();
+			assert.equal( s.toString(), '  abc\n    def\n      ghi\n    jkl' );
+
+			s.indent();
+			assert.equal( s.toString(), '    abc\n      def\n        ghi\n      jkl' );
+		});
+
+		it( 'should disregard single-space indentation when auto-indenting', function () {
+			var s = new MagicString( 'abc\n/**\n *comment\n */' );
+
+			s.indent();
+			assert.equal( s.toString(), '\tabc\n\t/**\n\t *comment\n\t */' );
+		});
+
+		it( 'should indent content using the supplied indent string', function () {
+			var s = new MagicString( 'abc\ndef\nghi\njkl' );
+
+			s.indent( '  ');
+			assert.equal( s.toString(), '  abc\n  def\n  ghi\n  jkl' );
+
+			s.indent( '>>' );
+			assert.equal( s.toString(), '>>  abc\n>>  def\n>>  ghi\n>>  jkl' );
+		});
+
+		it( 'should indent content using the empty string if specified (i.e. noop)', function () {
+			var s = new MagicString( 'abc\ndef\nghi\njkl' );
+
+			s.indent( '');
+			assert.equal( s.toString(), 'abc\ndef\nghi\njkl' );
+		});
+
+		it( 'should prevent excluded characters from being indented', function () {
+			var s = new MagicString( 'abc\ndef\nghi\njkl' );
+
+			s.indent( '  ', { exclude: [ 7, 15 ] });
+			assert.equal( s.toString(), '  abc\n  def\nghi\njkl' );
+
+			s.indent( '>>', { exclude: [ 7, 15 ] });
+			assert.equal( s.toString(), '>>  abc\n>>  def\nghi\njkl' );
+		});
+
+		it( 'should not add characters to empty lines', function () {
+			var s = new MagicString( '\n\nabc\ndef\n\nghi\njkl' );
+
+			s.indent();
+			assert.equal( s.toString(), '\n\n\tabc\n\tdef\n\n\tghi\n\tjkl' );
+
+			s.indent();
+			assert.equal( s.toString(), '\n\n\t\tabc\n\t\tdef\n\n\t\tghi\n\t\tjkl' );
+		});
+
+		it( 'should not add characters to empty lines, even on Windows', function () {
+			var s = new MagicString( '\r\n\r\nabc\r\ndef\r\n\r\nghi\r\njkl' );
+
+			s.indent();
+			assert.equal( s.toString(), '\r\n\r\n\tabc\r\n\tdef\r\n\r\n\tghi\r\n\tjkl' );
+
+			s.indent();
+			assert.equal( s.toString(), '\r\n\r\n\t\tabc\r\n\t\tdef\r\n\r\n\t\tghi\r\n\t\tjkl' );
+		});
+
+		it( 'should indent content with removals', function () {
+			var s = new MagicString( '/* remove this line */\nvar foo = 1;' );
+
+			s.remove( 0, 23 );
+			s.indent();
+
+			assert.equal( s.toString(), '\tvar foo = 1;' );
+		});
+
+		it( 'should not indent patches in the middle of a line', function () {
+			var s = new MagicString( 'class Foo extends Bar {}' );
+
+			s.overwrite( 18, 21, 'Baz' );
+			assert.equal( s.toString(), 'class Foo extends Baz {}' );
+
+			s.indent();
+			assert.equal( s.toString(), '\tclass Foo extends Baz {}' );
+		});
+
+		it( 'should return this', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+			assert.strictEqual( s.indent(), s );
+		});
+
+		it( 'should return this on noop', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+			assert.strictEqual( s.indent( '' ), s );
+		});
+	});
+
+	describe( 'insert', function () {
+		it( 'is deprecated', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+			assert.throws( function () { s.insert( 6, 'X' ); }, /deprecated/ );
+		});
+
+		// TODO move this into insertRight and insertLeft tests
+
+		// it( 'should insert characters in the correct location', function () {
+		// 	var s = new MagicString( 'abcdefghijkl' );
+		//
+		// 	s.insert( 0, '>>>' );
+		// 	s.insert( 6, '***' );
+		// 	s.insert( 12, '<<<' );
+		//
+		// 	assert.equal( s.toString(), '>>>abcdef***ghijkl<<<' );
+		// });
+		//
+		// it( 'should return this', function () {
+		// 	var s = new MagicString( 'abcdefghijkl' );
+		// 	assert.strictEqual( s.insert( 0, 'a' ), s );
+		// });
+		//
+		// it( 'should insert repeatedly at the same position correctly', function () {
+		// 	var s = new MagicString( 'ab' );
+		// 	assert.equal( s.insert(1, '1').toString(), 'a1b' );
+		// 	assert.equal( s.insert(1, '2').toString(), 'a12b' );
+		// });
+		//
+		// it( 'should insert repeatedly at the beginning correctly', function () {
+		// 	var s = new MagicString( 'ab' );
+		// 	assert.equal( s.insert(0, '1').toString(), '1ab' );
+		// 	assert.equal( s.insert(0, '2').toString(), '12ab' );
+		// });
+		//
+		// it( 'should throw when given non-string content', function () {
+		// 	var s = new MagicString( '' );
+		// 	assert.throws(
+		// 		function () { s.insert( 0, [] ); },
+		// 		TypeError
+		// 	);
+		// });
+		//
+		// it( 'should allow inserting after removed range', function () {
+		// 	var s = new MagicString( 'abcd' );
+		// 	s.remove( 1, 2 );
+		// 	s.insert( 2, 'z' );
+		// 	assert.equal( s.toString(), 'azcd' );
+		// });
+	});
+
+	describe( 'insertLeft', function () {
+		it( 'inserts repeatedly in correct order', function () {
+			var s = new MagicString( 'ab' );
+			assert.equal( s.insertLeft(1, '1').toString(), 'a1b' );
+			assert.equal( s.insertLeft(1, '2').toString(), 'a12b' );
+		});
+
+		it( 'should return this', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+			assert.strictEqual( s.insertLeft( 0, 'a' ), s );
+		});
+	});
+
+	describe( 'insertRight', function () {
+		it( 'inserts repeatedly in correct order', function () {
+			var s = new MagicString( 'ab' );
+			assert.equal( s.insertRight(1, '1').toString(), 'a1b' );
+			assert.equal( s.insertRight(1, '2').toString(), 'a21b' );
+		});
+
+		it( 'should return this', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+			assert.strictEqual( s.insertLeft( 0, 'a' ), s );
+		});
+	});
+
+	describe( 'move', function () {
+		it( 'moves content from the start', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+			s.move( 0, 3, 6 );
+
+			assert.equal( s.toString(), 'defabcghijkl' );
+		});
+
+		it( 'moves content to the start', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+			s.move( 3, 6, 0 );
+
+			assert.equal( s.toString(), 'defabcghijkl' );
+		});
+
+		it( 'moves content from the end', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+			s.move( 9, 12, 6 );
+
+			assert.equal( s.toString(), 'abcdefjklghi' );
+		});
+
+		it( 'moves content to the end', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+			s.move( 6, 9, 12 );
+
+			assert.equal( s.toString(), 'abcdefjklghi' );
+		});
+
+		it( 'ignores redundant move', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+			s.insertRight( 9, 'X' );
+			s.move( 9, 12, 6 );
+			s.insertLeft( 12, 'Y' );
+			s.move( 6, 9, 12 ); // this is redundant – [6,9] is already after [9,12]
+
+			assert.equal( s.toString(), 'abcdefXjklYghi' );
+		});
+
+		it( 'moves content to the middle', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+			s.move( 3, 6, 9 );
+
+			assert.equal( s.toString(), 'abcghidefjkl' );
+		});
+
+		it( 'handles multiple moves of the same snippet', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+
+			s.move( 0, 3, 6 );
+			assert.equal( s.toString(), 'defabcghijkl' );
+
+			s.move( 0, 3, 9 );
+			assert.equal( s.toString(), 'defghiabcjkl' );
+		});
+
+		it( 'handles moves of adjacent snippets', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+
+			s.move( 0, 2, 6 );
+			assert.equal( s.toString(), 'cdefabghijkl' );
+
+			s.move( 2, 4, 6 );
+			assert.equal( s.toString(), 'efabcdghijkl' );
+		});
+
+		it( 'handles moves to same index', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+			s.move( 0, 2, 6 ).move( 3, 5, 6 );
+
+			assert.equal( s.toString(), 'cfabdeghijkl' );
+		});
+
+		it( 'refuses to move a selection to inside itself', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+
+			assert.throws( function () {
+				s.move( 3, 6, 3 );
+			}, /Cannot move a selection inside itself/ );
+
+			assert.throws( function () {
+				s.move( 3, 6, 4 );
+			}, /Cannot move a selection inside itself/ );
+
+			assert.throws( function () {
+				s.move( 3, 6, 6 );
+			}, /Cannot move a selection inside itself/ );
+		});
+
+		it( 'allows edits of moved content', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+
+			s.move( 3, 6, 9 );
+			s.overwrite( 3, 6, 'DEF' );
+
+			assert.equal( s.toString(), 'abcghiDEFjkl' );
+
+			s = new MagicString( 'abcdefghijkl' );
+
+			s.move( 3, 6, 9 );
+			s.overwrite( 4, 5, 'E' );
+
+			assert.equal( s.toString(), 'abcghidEfjkl' );
+		});
+
+		// it( 'move follows inserts', function () {
+		// 	var s = new MagicString( 'abcdefghijkl' );
+		//
+		// 	s.insertLeft( 3, 'X' ).move( 6, 9, 3 );
+		// 	assert.equal( s.toString(), 'abcXghidefjkl' );
+		// });
+		//
+		// it( 'inserts follow move', function () {
+		// 	var s = new MagicString( 'abcdefghijkl' );
+		//
+		// 	s.insert( 3, 'X' ).move( 6, 9, 3 ).insert( 3, 'Y' );
+		// 	assert.equal( s.toString(), 'abcXghiYdefjkl' );
+		// });
+		//
+		// it( 'discards inserts at end of move by default', function () {
+		// 	var s = new MagicString( 'abcdefghijkl' );
+		//
+		// 	s.insert( 6, 'X' ).move( 3, 6, 9 );
+		// 	assert.equal( s.toString(), 'abcXghidefjkl' );
+		// });
+
+		it( 'moves content inserted at end of range', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+
+			s.insertLeft( 6, 'X' ).move( 3, 6, 9 );
+			assert.equal( s.toString(), 'abcghidefXjkl' );
+		});
+
+		it( 'returns this', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+			assert.strictEqual( s.move( 3, 6, 9 ), s );
+		});
+	});
+
+	describe( 'overwrite', function () {
+		it( 'should replace characters', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+
+			s.overwrite( 5, 8, 'FGH' );
+			assert.equal( s.toString(), 'abcdeFGHijkl' );
+		});
+
+		it( 'should throw an error if overlapping replacements are attempted', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+
+			s.overwrite( 7, 11, 'xx' );
+
+			assert.throws( function () {
+				s.overwrite( 8, 12, 'yy' );
+			}, /Cannot split a chunk that has already been edited/ );
+
+			assert.equal( s.toString(), 'abcdefgxxl' );
+
+			s.overwrite( 6, 12, 'yes' );
+			assert.equal( s.toString(), 'abcdefyes' );
+		});
+
+		it( 'should allow contiguous but non-overlapping replacements', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+
+			s.overwrite( 3, 6, 'DEF' );
+			assert.equal( s.toString(), 'abcDEFghijkl' );
+
+			s.overwrite( 6, 9, 'GHI' );
+			assert.equal( s.toString(), 'abcDEFGHIjkl' );
+
+			s.overwrite( 0, 3, 'ABC' );
+			assert.equal( s.toString(), 'ABCDEFGHIjkl' );
+
+			s.overwrite( 9, 12, 'JKL' );
+			assert.equal( s.toString(), 'ABCDEFGHIJKL' );
+		});
+
+		it( 'does not replace zero-length inserts at overwrite start location', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+
+			s.remove( 0, 6 );
+			s.insertLeft( 6, 'DEF' );
+			s.overwrite( 6, 9, 'GHI' );
+			assert.equal( s.toString(), 'DEFGHIjkl' );
+		});
+
+		it( 'replaces zero-length inserts inside overwrite', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+
+			s.insertLeft( 6, 'XXX' );
+			s.overwrite( 3, 9, 'DEFGHI' );
+			assert.equal( s.toString(), 'abcDEFGHIjkl' );
+		});
+
+		it( 'replaces non-zero-length inserts inside overwrite', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+
+			s.overwrite( 3, 4, 'XXX' );
+			s.overwrite( 3, 5, 'DE' );
+			assert.equal( s.toString(), 'abcDEfghijkl' );
+
+			s.overwrite( 7, 8, 'YYY' );
+			s.overwrite( 6, 8, 'GH' );
+			assert.equal( s.toString(), 'abcDEfGHijkl' );
+		});
+
+		it( 'should return this', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+			assert.strictEqual( s.overwrite( 3, 4, 'D' ), s );
+		});
+
+		it( 'should disallow overwriting zero-length ranges', function () {
+			var s = new MagicString( 'x' );
+			assert.throws( function () {
+				s.overwrite( 0, 0, 'anything' );
+			}, /Cannot overwrite a zero-length range – use insertLeft or insertRight instead/ );
+		});
+
+		it( 'should throw when given non-string content', function () {
+			var s = new MagicString( '' );
+			assert.throws(
+				function () { s.overwrite( 0, 1, [] ); },
+				TypeError
+			);
+		});
+
+		it ( 'replaces interior inserts', function() {
+			var s = new MagicString( 'abcdefghijkl' );
+
+			s.insertLeft( 1, '&' );
+			s.insertRight( 1, '^' );
+			s.insertLeft( 3, '!' );
+			s.insertRight( 3, '?' );
+			s.overwrite( 1, 3, '...' );
+			assert.equal( s.toString(), 'a&...?defghijkl' );
+		})
+	});
+
+	describe( 'prepend', function () {
+		it( 'should prepend content', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+
+			s.prepend( 'xyz' );
+			assert.equal( s.toString(), 'xyzabcdefghijkl' );
+
+			s.prepend( '123' );
+			assert.equal( s.toString(), '123xyzabcdefghijkl' );
+		});
+
+		it( 'should return this', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+			assert.strictEqual( s.prepend( 'xyz' ), s );
+		});
+	});
+
+	describe( 'remove', function () {
+		it( 'should remove characters from the original string', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+
+			s.remove( 1, 5 );
+			assert.equal( s.toString(), 'afghijkl' );
+
+			s.remove( 9, 12 );
+			assert.equal( s.toString(), 'afghi' );
+		});
+
+		it( 'should remove from the start', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+
+			s.remove( 0, 6 );
+			assert.equal( s.toString(), 'ghijkl' );
+		});
+
+		it( 'should remove from the end', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+
+			s.remove( 6, 12 );
+			assert.equal( s.toString(), 'abcdef' );
+		});
+
+		it( 'should treat zero-length removals as a no-op', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+
+			s.remove( 0, 0 ).remove( 6, 6 ).remove( 9, -3 );
+			assert.equal( s.toString(), 'abcdefghijkl' );
+		});
+
+		it( 'should remove overlapping ranges', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+
+			s.remove( 3, 7 ).remove( 5, 9 );
+			assert.equal( s.toString(), 'abcjkl' );
+
+			s = new MagicString( 'abcdefghijkl' );
+
+			s.remove( 3, 7 ).remove( 4, 6 );
+			assert.equal( s.toString(), 'abchijkl' );
+		});
+
+		it( 'should remove overlapping ranges, redux', function () {
+			var s = new MagicString( 'abccde' );
+
+			s.remove( 2, 3 ); // c
+			s.remove( 1, 3 ); // bc
+			assert.equal( s.toString(), 'acde' );
+		});
+
+		it( 'should remove modified ranges', function () {
+			var s = new MagicString( 'abcdefghi' );
+
+			s.overwrite( 3, 6, 'DEF' );
+			s.remove( 2, 7 ); // cDEFg
+			assert.equal( s.slice( 1, 8 ), 'bh' );
+			assert.equal( s.toString(), 'abhi' );
+		});
+
+		it( 'should not remove content inserted after the end of removed range', function () {
+			var s = new MagicString( 'ab.c;' );
+
+			s.insertRight( 0, '(' );
+			s.insertRight( 4, ')' );
+			s.remove( 2, 4 );
+			assert.equal( s.toString(), '(ab);' );
+		});
+
+		it( 'should remove interior inserts', function () {
+			var s = new MagicString( 'abc;' );
+
+			s.insertLeft( 1, '[' );
+			s.insertRight( 1, '(' );
+			s.insertLeft( 2, ')' );
+			s.insertRight( 2, ']' );
+			s.remove( 1, 2 );
+			assert.equal( s.toString(), 'a[]c;' );
+		});
+
+		it( 'should provide a useful error when illegal removals are attempted', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+
+			s.overwrite( 5, 7, 'XX' );
+
+			assert.throws( function () {
+				s.remove( 4, 6 );
+			}, /Cannot split a chunk that has already been edited/ );
+		});
+
+		it( 'should return this', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+			assert.strictEqual( s.remove( 3, 4 ), s );
+		});
+	});
+
+	describe( 'slice', function () {
+		it( 'should return the generated content between the specified original characters', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+
+			assert.equal( s.slice( 3, 9 ), 'defghi' );
+			s.overwrite( 4, 8, 'XX' );
+			assert.equal( s.slice( 3, 9 ), 'dXXi' );
+			s.overwrite( 2, 10, 'ZZ' );
+			assert.equal( s.slice( 1, 11 ), 'bZZk' );
+			assert.equal( s.slice( 2, 10 ), 'ZZ' );
+
+			assert.throws( function () {
+				s.slice( 3, 9 );
+			});
+		});
+
+		it( 'defaults `end` to the original string length', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+			assert.equal( s.slice( 3 ), 'defghijkl' );
+		});
+
+		it( 'allows negative numbers as arguments', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+			assert.equal( s.slice( -3 ), 'jkl' );
+			assert.equal( s.slice( 0, -3 ), 'abcdefghi' );
+		});
+
+		it( 'includes inserted characters, respecting insertion direction', function () {
+			var s = new MagicString( 'abefij' );
+
+			s.insertRight( 2, 'cd' );
+			s.insertLeft( 4, 'gh' );
+
+			assert.equal( s.slice(), 'abcdefghij' );
+			assert.equal( s.slice( 1, 5 ), 'bcdefghi' );
+			assert.equal( s.slice( 2, 4 ), 'cdefgh' );
+			assert.equal( s.slice( 3, 4 ), 'fgh' );
+			assert.equal( s.slice( 0, 2 ), 'ab' );
+			assert.equal( s.slice( 0, 3 ), 'abcde' );
+			assert.equal( s.slice( 4, 6 ), 'ij' );
+			assert.equal( s.slice( 3, 6 ), 'fghij' );
+		});
+
+		it( 'supports characters moved outward', function () {
+			var s = new MagicString( 'abcdEFghIJklmn' );
+
+			s.move( 4, 6, 2 );
+			s.move( 8, 10, 12 );
+			assert.equal( s.toString(), 'abEFcdghklIJmn' );
+
+			assert.equal( s.slice( 1, -1 ), 'bEFcdghklIJm' );
+			assert.equal( s.slice( 2, -2 ), 'cdghkl' );
+			assert.equal( s.slice( 3, -3 ), 'dghk' );
+			assert.equal( s.slice( 4, -4 ), 'EFcdghklIJ' );
+			assert.equal( s.slice( 5, -5 ), 'FcdghklI' );
+			assert.equal( s.slice( 6, -6 ), 'gh' );
+		});
+
+		it( 'supports characters moved inward', function () {
+			var s = new MagicString( 'abCDefghijKLmn' );
+
+			s.move( 2, 4, 6 );
+			s.move( 10, 12, 8 );
+			assert.equal( s.toString(), 'abefCDghKLijmn' );
+
+			assert.equal( s.slice( 1, -1 ), 'befCDghKLijm' );
+			assert.equal( s.slice( 2, -2 ), 'CDghKL' );
+			assert.equal( s.slice( 3, -3 ), 'DghK' );
+			assert.equal( s.slice( 4, -4 ), 'efCDghKLij' );
+			assert.equal( s.slice( 5, -5 ), 'fCDghKLi' );
+			assert.equal( s.slice( 6, -6 ), 'gh' );
+		});
+
+		it( 'supports characters moved opposing', function () {
+			var s = new MagicString( 'abCDefghIJkl' );
+
+			s.move( 2, 4, 8 );
+			s.move( 8, 10, 4 );
+			assert.equal( s.toString(), 'abIJefghCDkl' );
+
+			assert.equal( s.slice( 1, -1 ), 'bIJefghCDk' );
+			assert.equal( s.slice( 2, -2 ), '' );
+			assert.equal( s.slice( 3, -3 ), '' );
+			assert.equal( s.slice( -3, 3 ), 'JefghC' );
+			assert.equal( s.slice( 4, -4 ), 'efgh' );
+			assert.equal( s.slice( 0, 3 ), 'abIJefghC' );
+			assert.equal( s.slice( 3 ), 'Dkl' );
+			assert.equal( s.slice( 0, -3 ), 'abI' );
+			assert.equal( s.slice( -3 ), 'JefghCDkl' );
+		});
+
+		it( 'errors if replaced characters are used as slice anchors', function () {
+			var s = new MagicString( 'abcdef' );
+			s.overwrite( 2, 4, 'CD' );
+
+			assert.throws( function () {
+				s.slice( 2, 3 );
+			}, /slice end anchor/ );
+
+			assert.throws( function () {
+				s.slice( 3, 4 );
+			}, /slice start anchor/ );
+
+			assert.throws( function () {
+				s.slice( 3, 5 );
+			}, /slice start anchor/ );
+
+			assert.equal( s.slice( 1, 5 ), 'bCDe' );
+		});
+
+		it( 'does not error if slice is after removed characters', function () {
+			var s = new MagicString( 'abcdef' );
+
+			s.remove( 0, 2 );
+
+			assert.equal( s.slice( 2, 4 ), 'cd' );
+		});
+	});
+
+	describe( 'snip', function () {
+		it( 'should return a clone with content outside `start` and `end` removed', function () {
+			var s = new MagicString( 'abcdefghijkl', {
+				filename: 'foo.js'
+			});
+
+			s.overwrite( 6, 9, 'GHI' );
+
+			var snippet = s.snip( 3, 9 );
+			assert.equal( snippet.toString(), 'defGHI' );
+			assert.equal( snippet.filename, 'foo.js' );
+		});
+
+		it( 'should snip from the start', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+			var snippet = s.snip( 0, 6 );
+
+			assert.equal( snippet.toString(), 'abcdef' );
+		});
+
+		it( 'should snip from the end', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+			var snippet = s.snip( 6, 12 );
+
+			assert.equal( snippet.toString(), 'ghijkl' );
+		});
+
+		it( 'should respect original indices', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+			var snippet = s.snip( 3, 9 );
+
+			snippet.overwrite( 6, 9, 'GHI' );
+			assert.equal( snippet.toString(), 'defGHI' );
+		});
+	});
+
+	describe( 'trim', function () {
+		it( 'should trim original content', function () {
+			assert.equal( new MagicString( '   abcdefghijkl   ' ).trim().toString(), 'abcdefghijkl' );
+			assert.equal( new MagicString( '   abcdefghijkl' ).trim().toString(), 'abcdefghijkl' );
+			assert.equal( new MagicString( 'abcdefghijkl   ' ).trim().toString(), 'abcdefghijkl' );
+		});
+
+		it( 'should trim replaced content', function () {
+			var s = new MagicString( 'abcdefghijkl' );
+
+			s.overwrite( 0, 3, '   ' ).overwrite( 9, 12, '   ' ).trim();
+			assert.equal( s.toString(), 'defghi' );
+		});
+
+		it( 'should trim original content before replaced content', function () {
+			var s = new MagicString( 'abc   def' );
+
+			s.remove( 6, 9 );
+			assert.equal( s.toString(), 'abc   ' );
+
+			s.trim();
+			assert.equal( s.toString(), 'abc' );
+		});
+
+		it( 'should trim original content after replaced content', function () {
+			var s = new MagicString( 'abc   def' );
+
+			s.remove( 0, 3 );
+			assert.equal( s.toString(), '   def' );
+
+			s.trim();
+			assert.equal( s.toString(), 'def' );
+		});
+
+		it( 'should trim original content before and after replaced content', function () {
+			var s = new MagicString( 'abc   def   ghi' );
+
+			s.remove( 0, 3 );
+			s.remove( 12, 15 );
+			assert.equal( s.toString(), '   def   ' );
+
+			s.trim();
+			assert.equal( s.toString(), 'def' );
+		});
+
+		it( 'should trim appended/prepended content', function () {
+			var s = new MagicString( ' abcdefghijkl ' );
+
+			s.prepend( '  ' ).append( '  ' ).trim();
+			assert.equal( s.toString(), 'abcdefghijkl' );
+		});
+
+		it( 'should trim empty string', function () {
+			var s = new MagicString( '   ' );
+
+			assert.equal( s.trim().toString(), '' );
+		});
+
+		it( 'should return this', function () {
+			var s = new MagicString( '  abcdefghijkl  ' );
+			assert.strictEqual( s.trim(), s );
+		});
+	});
+
+	describe( 'trimLines', function () {
+		it( 'should trim original content', function () {
+			var s = new MagicString( '\n\n   abcdefghijkl   \n\n' );
+
+			s.trimLines();
+			assert.equal( s.toString(), '   abcdefghijkl   ' );
+		});
+	});
+});
+
+describe( 'MagicString.Bundle', function () {
+	describe( 'addSource', function () {
+		it( 'should return this', function () {
+			var b = new MagicString.Bundle();
+			var source = new MagicString( 'abcdefghijkl' );
+
+			assert.strictEqual( b.addSource({ content: source }), b );
+		});
+
+		it( 'should accept MagicString instance as a single argument', function () {
+			var b = new MagicString.Bundle();
+			var array = [];
+			var source = new MagicString( 'abcdefghijkl', {
+				filename: 'foo.js',
+				indentExclusionRanges: array
+			});
+
+			b.addSource( source );
+			assert.strictEqual( b.sources[0].content, source );
+			assert.strictEqual( b.sources[0].filename, 'foo.js' );
+			assert.strictEqual( b.sources[0].indentExclusionRanges, array );
+		});
+
+		it( 'respects MagicString init options with { content: source }', function () {
+			var b = new MagicString.Bundle();
+			var array = [];
+			var source = new MagicString( 'abcdefghijkl', {
+				filename: 'foo.js',
+				indentExclusionRanges: array
+			});
+
+			b.addSource({ content: source });
+			assert.strictEqual( b.sources[0].content, source );
+			assert.strictEqual( b.sources[0].filename, 'foo.js' );
+			assert.strictEqual( b.sources[0].indentExclusionRanges, array );
+		});
+	});
+
+	describe( 'append', function () {
+		it( 'should append content', function () {
+			var b = new MagicString.Bundle();
+
+			b.addSource({ content: new MagicString( '*' ) });
+
+			b.append( '123' ).append( '456' );
+			assert.equal( b.toString(), '*123456' );
+		});
+
+		it( 'should append content before subsequent sources', function () {
+			var b = new MagicString.Bundle();
+
+			b.addSource( new MagicString( '*' ) );
+
+			b.append( '123' ).addSource( new MagicString( '-' ) ).append( '456' );
+			assert.equal( b.toString(), '*123\n-456' );
+		});
+
+		it( 'should return this', function () {
+			var b = new MagicString.Bundle();
+			assert.strictEqual( b.append( 'x' ), b );
+		});
+	});
+
+	describe( 'clone', function () {
+		it( 'should clone a bundle', function () {
+			var b = new MagicString.Bundle(),
+				s1 = new MagicString( 'abcdef' ),
+				s2 = new MagicString( 'ghijkl' ),
+				clone;
+
+			b
+			.addSource({
+				content: s1
+			})
+			.addSource({
+				content: s2
+			})
+			.prepend( '>>>' )
+			.append( '<<<' );
+
+			clone = b.clone();
+
+			assert.equal( clone.toString(), '>>>abcdef\nghijkl<<<' );
+
+			s1.overwrite( 2, 4, 'XX' );
+			assert.equal( b.toString(), '>>>abXXef\nghijkl<<<' );
+			assert.equal( clone.toString(), '>>>abcdef\nghijkl<<<' );
+		});
+	});
+
+	describe( 'generateMap', function () {
+		it( 'should generate a sourcemap', function () {
+			var b, map, smc, loc;
+
+			b = new MagicString.Bundle();
+
+			b.addSource({
+				filename: 'foo.js',
+				content: new MagicString( 'var answer = 42;' )
+			});
+
+			b.addSource({
+				filename: 'bar.js',
+				content: new MagicString( 'console.log( answer );' )
+			});
+
+			map = b.generateMap({
+				file: 'bundle.js',
+				includeContent: true,
+				hires: true
+			});
+
+			assert.equal( map.version, 3 );
+			assert.equal( map.file, 'bundle.js' );
+			assert.deepEqual( map.sources, [ 'foo.js', 'bar.js' ]);
+			assert.deepEqual( map.sourcesContent, [ 'var answer = 42;', 'console.log( answer );' ]);
+
+			assert.equal( map.toString(), '{"version":3,"file":"bundle.js","sources":["foo.js","bar.js"],"sourcesContent":["var answer = 42;","console.log( answer );"],"names":[],"mappings":"AAAA,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;ACAf,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC"}' );
+			assert.equal( map.toUrl(), 'data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYnVuZGxlLmpzIiwic291cmNlcyI6WyJmb28uanMiLCJiYXIuanMiXSwic291cmNlc0NvbnRlbnQiOlsidmFyIGFuc3dlciA9IDQyOyIsImNvbnNvbGUubG9nKCBhbnN3ZXIgKTsiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7QUNBZixDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLE [...]
+
+			smc = new SourceMapConsumer( map );
+
+			loc = smc.originalPositionFor({ line: 1, column: 0 });
+			assert.equal( loc.line, 1 );
+			assert.equal( loc.column, 0 );
+			assert.equal( loc.source, 'foo.js' );
+
+			loc = smc.originalPositionFor({ line: 1, column: 1 });
+			assert.equal( loc.line, 1 );
+			assert.equal( loc.column, 1 );
+			assert.equal( loc.source, 'foo.js' );
+
+			loc = smc.originalPositionFor({ line: 2, column: 0 });
+			assert.equal( loc.line, 1 );
+			assert.equal( loc.column, 0 );
+			assert.equal( loc.source, 'bar.js' );
+
+			loc = smc.originalPositionFor({ line: 2, column: 1 });
+			assert.equal( loc.line, 1 );
+			assert.equal( loc.column, 1 );
+			assert.equal( loc.source, 'bar.js' );
+		});
+
+		it( 'should handle Windows-style paths', function () {
+			var b, map, smc, loc;
+
+			b = new MagicString.Bundle();
+
+			b.addSource({
+				filename: 'path\\to\\foo.js',
+				content: new MagicString( 'var answer = 42;' )
+			});
+
+			b.addSource({
+				filename: 'path\\to\\bar.js',
+				content: new MagicString( 'console.log( answer );' )
+			});
+
+			map = b.generateMap({
+				file: 'bundle.js',
+				includeContent: true,
+				hires: true
+			});
+
+			assert.equal( map.version, 3 );
+			assert.equal( map.file, 'bundle.js' );
+			assert.deepEqual( map.sources, [ 'path/to/foo.js', 'path/to/bar.js' ]);
+			assert.deepEqual( map.sourcesContent, [ 'var answer = 42;', 'console.log( answer );' ]);
+
+			assert.equal( map.toString(), '{"version":3,"file":"bundle.js","sources":["path/to/foo.js","path/to/bar.js"],"sourcesContent":["var answer = 42;","console.log( answer );"],"names":[],"mappings":"AAAA,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;ACAf,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC"}' );
+
+			smc = new SourceMapConsumer( map );
+
+			loc = smc.originalPositionFor({ line: 1, column: 0 });
+			assert.equal( loc.line, 1 );
+			assert.equal( loc.column, 0 );
+			assert.equal( loc.source, 'path/to/foo.js' );
+
+			loc = smc.originalPositionFor({ line: 1, column: 1 });
+			assert.equal( loc.line, 1 );
+			assert.equal( loc.column, 1 );
+			assert.equal( loc.source, 'path/to/foo.js' );
+
+			loc = smc.originalPositionFor({ line: 2, column: 0 });
+			assert.equal( loc.line, 1 );
+			assert.equal( loc.column, 0 );
+			assert.equal( loc.source, 'path/to/bar.js' );
+
+			loc = smc.originalPositionFor({ line: 2, column: 1 });
+			assert.equal( loc.line, 1 );
+			assert.equal( loc.column, 1 );
+			assert.equal( loc.source, 'path/to/bar.js' );
+		});
+
+		it( 'should handle edge case with intro content', function () {
+			var b, map, smc, loc;
+
+			b = new MagicString.Bundle();
+
+			b.addSource({
+				filename: 'foo.js',
+				content: new MagicString( 'var answer = 42;' )
+			});
+
+			b.addSource({
+				filename: 'bar.js',
+				content: new MagicString( '\nconsole.log( answer );' )
+			});
+
+			b.indent().prepend( '(function () {\n' ).append( '\n}());' );
+
+			map = b.generateMap({
+				file: 'bundle.js',
+				includeContent: true,
+				hires: true
+			});
+
+			smc = new SourceMapConsumer( map );
+
+			loc = smc.originalPositionFor({ line: 2, column: 1 });
+			assert.equal( loc.line, 1 );
+			assert.equal( loc.column, 0 );
+			assert.equal( loc.source, 'foo.js' );
+
+			loc = smc.originalPositionFor({ line: 2, column: 2 });
+			assert.equal( loc.line, 1 );
+			assert.equal( loc.column, 1 );
+			assert.equal( loc.source, 'foo.js' );
+
+			loc = smc.originalPositionFor({ line: 4, column: 1 });
+			assert.equal( loc.line, 2 );
+			assert.equal( loc.column, 0 );
+			assert.equal( loc.source, 'bar.js' );
+
+			loc = smc.originalPositionFor({ line: 4, column: 2 });
+			assert.equal( loc.line, 2 );
+			assert.equal( loc.column, 1 );
+			assert.equal( loc.source, 'bar.js' );
+		});
+
+		it( 'should allow missing file option when generating map', function () {
+			var b, map;
+
+			b = new MagicString.Bundle();
+
+			b.addSource({
+				filename: 'foo.js',
+				content: new MagicString( 'var answer = 42;' )
+			});
+
+			map = b.generateMap({
+				includeContent: true,
+				hires: true
+			});
+		});
+
+		it( 'should handle repeated sources', function () {
+			var b = new MagicString.Bundle();
+
+			var foo = new MagicString( 'var one = 1;\nvar three = 3;', {
+				filename: 'foo.js'
+			});
+
+			var bar = new MagicString( 'var two = 2;\nvar four = 4;', {
+				filename: 'bar.js'
+			});
+
+			b.addSource( foo.snip( 0, 12 ) );
+			b.addSource( bar.snip( 0, 12 ) );
+			b.addSource( foo.snip( 13, 27 ) );
+			b.addSource( bar.snip( 13, 26 ) );
+
+			var code = b.toString();
+			assert.equal( code, 'var one = 1;\nvar two = 2;\nvar three = 3;\nvar four = 4;' );
+
+			var map = b.generateMap({
+				includeContent: true,
+				hires: true
+			});
+
+			assert.equal( map.sources.length, 2 );
+			assert.equal( map.sourcesContent.length, 2 );
+
+			var smc = new SourceMapConsumer( map );
+			var loc;
+
+			loc = smc.originalPositionFor({ line: 1, column: 0 });
+			assert.equal( loc.line, 1 );
+			assert.equal( loc.column, 0 );
+			assert.equal( loc.source, 'foo.js' );
+
+			loc = smc.originalPositionFor({ line: 2, column: 0 });
+			assert.equal( loc.line, 1 );
+			assert.equal( loc.column, 0 );
+			assert.equal( loc.source, 'bar.js' );
+
+			loc = smc.originalPositionFor({ line: 3, column: 0 });
+			assert.equal( loc.line, 2 );
+			assert.equal( loc.column, 0 );
+			assert.equal( loc.source, 'foo.js' );
+
+			loc = smc.originalPositionFor({ line: 4, column: 0 });
+			assert.equal( loc.line, 2 );
+			assert.equal( loc.column, 0 );
+			assert.equal( loc.source, 'bar.js' );
+		});
+
+		it( 'should recover original names', function () {
+			var b = new MagicString.Bundle();
+
+			var one = new MagicString( 'function one () {}', { filename: 'one.js' });
+			var two = new MagicString( 'function two () {}', { filename: 'two.js' });
+
+			one.overwrite( 9, 12, 'three', true );
+			two.overwrite( 9, 12, 'four', true );
+
+			b.addSource( one );
+			b.addSource( two );
+
+			map = b.generateMap({
+				file: 'output.js',
+				source: 'input.js',
+				includeContent: true
+			});
+
+			smc = new SourceMapConsumer( map );
+
+			loc = smc.originalPositionFor({ line: 1, column: 9 });
+			assert.equal( loc.name, 'one' );
+
+			loc = smc.originalPositionFor({ line: 2, column: 9 });
+			assert.equal( loc.name, 'two' );
+		});
+
+		it( 'should exclude sources without filename from sourcemap', function () {
+			var b = new MagicString.Bundle();
+
+			var one = new MagicString( 'function one () {}', { filename: 'one.js' });
+			var two = new MagicString( 'function two () {}', { filename: null });
+			var three = new MagicString( 'function three () {}', { filename: 'three.js' });
+
+			b.addSource( one );
+			b.addSource( two );
+			b.addSource( three );
+
+			map = b.generateMap({
+				file: 'output.js',
+				source: 'input.js',
+				includeContent: true
+			});
+
+			smc = new SourceMapConsumer( map );
+
+			loc = smc.originalPositionFor({ line: 1, column: 9 });
+			assert.equal( loc.source, 'one.js' );
+
+			loc = smc.originalPositionFor({ line: 2, column: 9 });
+			assert.equal( loc.source, null );
+
+			loc = smc.originalPositionFor({ line: 3, column: 9 });
+			assert.equal( loc.source, 'three.js' );
+		});
+
+		it( 'handles prepended content', function () {
+			var b = new MagicString.Bundle();
+
+			var one = new MagicString( 'function one () {}', { filename: 'one.js' });
+			var two = new MagicString( 'function two () {}', { filename: 'two.js' });
+			two.prepend( 'function oneAndAHalf() {}\n' );
+
+			b.addSource( one );
+			b.addSource( two );
+
+			map = b.generateMap({
+				file: 'output.js',
+				source: 'input.js',
+				includeContent: true
+			});
+
+			smc = new SourceMapConsumer( map );
+
+			loc = smc.originalPositionFor({ line: 1, column: 9 });
+			assert.equal( loc.source, 'one.js' );
+
+			loc = smc.originalPositionFor({ line: 3, column: 9 });
+			assert.equal( loc.source, 'two.js' );
+		});
+
+		it( 'handles appended content', function () {
+			var b = new MagicString.Bundle();
+
+			var one = new MagicString( 'function one () {}', { filename: 'one.js' });
+			one.append( '\nfunction oneAndAHalf() {}' );
+			var two = new MagicString( 'function two () {}', { filename: 'two.js' });
+
+			b.addSource( one );
+			b.addSource( two );
+
+			map = b.generateMap({
+				file: 'output.js',
+				source: 'input.js',
+				includeContent: true
+			});
+
+			smc = new SourceMapConsumer( map );
+
+			loc = smc.originalPositionFor({ line: 1, column: 9 });
+			assert.equal( loc.source, 'one.js' );
+
+			loc = smc.originalPositionFor({ line: 3, column: 9 });
+			assert.equal( loc.source, 'two.js' );
+		});
+	});
+
+	describe( 'indent', function () {
+		it( 'should indent a bundle', function () {
+			var b = new MagicString.Bundle();
+
+			b.addSource({ content: new MagicString( 'abcdef' ) });
+			b.addSource({ content: new MagicString( 'ghijkl' ) });
+
+			b.indent().prepend( '>>>\n' ).append( '\n<<<' );
+			assert.equal( b.toString(), '>>>\n\tabcdef\n\tghijkl\n<<<' );
+		});
+
+		it( 'should ignore non-indented sources when guessing indentation', function () {
+			var b = new MagicString.Bundle();
+
+			b.addSource({ content: new MagicString( 'abcdef' ) });
+			b.addSource({ content: new MagicString( 'ghijkl' ) });
+			b.addSource({ content: new MagicString( '  mnopqr' ) });
+
+			b.indent();
+			assert.equal( b.toString(), '  abcdef\n  ghijkl\n    mnopqr' );
+		});
+
+		it( 'should respect indent exclusion ranges', function () {
+			var b = new MagicString.Bundle();
+
+			b.addSource({
+				content: new MagicString( 'abc\ndef\nghi\njkl' ),
+				indentExclusionRanges: [ 7, 15 ]
+			});
+
+			b.indent( '  ' );
+			assert.equal( b.toString(), '  abc\n  def\nghi\njkl' );
+
+			b.indent( '>>' );
+			assert.equal( b.toString(), '>>  abc\n>>  def\nghi\njkl' );
+		});
+
+		it( 'does not indent sources with no preceding newline, i.e. append()', function () {
+			var b = new MagicString.Bundle();
+
+			b.addSource( new MagicString( 'abcdef' ) );
+			b.addSource( new MagicString( 'ghijkl' ) );
+
+			b.prepend( '>>>' ).append( '<<<' ).indent();
+			assert.equal( b.toString(), '\t>>>abcdef\n\tghijkl<<<' );
+		});
+
+		it( 'should noop with an empty string', function () {
+			var b = new MagicString.Bundle();
+
+			b.addSource( new MagicString( 'abcdef' ) );
+			b.addSource( new MagicString( 'ghijkl' ) );
+
+			b.indent( '' );
+			assert.equal( b.toString(), 'abcdef\nghijkl' );
+		});
+
+		it( 'indents prepended content', function () {
+			var b = new MagicString.Bundle();
+			b.prepend( 'a\nb' ).indent();
+
+			assert.equal( b.toString(), '\ta\n\tb' );
+		});
+
+		it( 'indents content immediately following intro with trailing newline', function () {
+			var b = new MagicString.Bundle({ separator: '\n\n' });
+
+			var s = new MagicString( '2' );
+			b.addSource({ content: s });
+
+			b.prepend( '1\n' );
+
+			assert.equal( b.indent().toString(), '\t1\n\t2' );
+		});
+
+		it( 'should return this', function () {
+			var b = new MagicString.Bundle();
+			assert.strictEqual( b.indent(), b );
+		});
+
+		it( 'should return this on noop', function () {
+			var b = new MagicString.Bundle();
+			assert.strictEqual( b.indent( '' ), b );
+		});
+	});
+
+	describe( 'prepend', function () {
+		it( 'should append content', function () {
+			var b = new MagicString.Bundle();
+
+			b.addSource({ content: new MagicString( '*' ) });
+
+			b.prepend( '123' ).prepend( '456' );
+			assert.equal( b.toString(), '456123*' );
+		});
+
+		it( 'should return this', function () {
+			var b = new MagicString.Bundle();
+			assert.strictEqual( b.prepend( 'x' ), b );
+		});
+	});
+
+	describe( 'trim', function () {
+		it( 'should trim bundle', function () {
+			var b = new MagicString.Bundle();
+
+			b.addSource({
+				content: new MagicString( '   abcdef   ' )
+			});
+
+			b.addSource({
+				content: new MagicString( '   ghijkl   ' )
+			});
+
+			b.trim();
+			assert.equal( b.toString(), 'abcdef   \n   ghijkl' );
+		});
+
+		it( 'should handle funky edge cases', function () {
+			var b = new MagicString.Bundle();
+
+			b.addSource({
+				content: new MagicString( '   abcdef   ' )
+			});
+
+			b.addSource({
+				content: new MagicString( '   x   ' )
+			});
+
+			b.prepend( '\n>>>\n' ).append( '   ' );
+
+			b.trim();
+			assert.equal( b.toString(), '>>>\n   abcdef   \n   x' );
+		});
+
+		it( 'should return this', function () {
+			var b = new MagicString.Bundle();
+			assert.strictEqual( b.trim(), b );
+		});
+	});
+
+	describe( 'toString', function () {
+		it( 'should separate with a newline by default', function () {
+			var b = new MagicString.Bundle();
+
+			b.addSource( new MagicString( 'abc' ) );
+			b.addSource( new MagicString( 'def' ) );
+
+			assert.strictEqual( b.toString(), 'abc\ndef' );
+		});
+
+		it( 'should accept separator option', function () {
+			var b = new MagicString.Bundle({ separator: '==' });
+
+			b.addSource( new MagicString( 'abc' ) );
+			b.addSource( new MagicString( 'def' ) );
+
+			assert.strictEqual( b.toString(), 'abc==def' );
+		});
+
+		it( 'should accept empty string separator option', function () {
+			var b = new MagicString.Bundle({ separator: '' });
+
+			b.addSource( new MagicString( 'abc' ) );
+			b.addSource( new MagicString( 'def' ) );
+
+			assert.strictEqual( b.toString(), 'abcdef' );
+		});
+	});
+});

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



More information about the Pkg-javascript-commits mailing list