diff --git a/CHANGELOG.md b/CHANGELOG.md index b19d478..4fb0ca4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # changelog +## 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 diff --git a/README.md b/README.md index 7fad524..ef6eadd 100644 --- a/README.md +++ b/README.md @@ -52,8 +52,6 @@ var s = new MagicString( 'problems = 99' ); s.overwrite( 0, 8, 'answer' ); s.toString(); // 'answer = 99' -s.locate( 9 ); // 7 - the character originally at index 9 ('=') is now at index 7 -s.locateOrigin( 7 ); // 9 s.overwrite( 11, 13, '42' ); // character indices always refer to the original string s.toString(); // 'answer = 42' @@ -126,11 +124,11 @@ Inserts the specified `content` at the `index` in the original string. Returns ` ### s.locate( index ) -Finds the location, in the generated string, of the character at `index` in the original string. Returns `null` if the character in question has been removed or overwritten. +**DEPRECATED** since 0.10 – see [#30](https://github.com/Rich-Harris/magic-string/pull/30) ### s.locateOrigin( index ) -The opposite of `s.locate()`. Returns `null` if the character in question was inserted with `s.append()`, `s.prepend()` or `s.overwrite()`. +**DEPRECATED** since 0.10 – see [#30](https://github.com/Rich-Harris/magic-string/pull/30) ### s.overwrite( start, end, content[, storeName] ) diff --git a/package.json b/package.json index dd6c4f9..4347b62 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "magic-string", "description": "Modify strings, generate sourcemaps", "author": "Rich Harris", - "version": "0.9.1", + "version": "0.10.0", "repository": "https://github.com/rich-harris/magic-string", "main": "dist/magic-string.cjs.js", "jsnext:main": "dist/magic-string.es6.js", @@ -13,6 +13,7 @@ "devDependencies": { "babel-preset-es2015-rollup": "^1.0.0", "codecov.io": "^0.1.6", + "console-group": "^0.1.2", "es6-promise": "^3.0.2", "eslint": "^1.10.3", "istanbul": "^0.4.0", @@ -22,7 +23,8 @@ "rollup": "^0.22.0", "rollup-plugin-babel": "^2.2.0", "rollup-plugin-npm": "^1.1.0", - "source-map": "^0.5.3" + "source-map": "^0.5.3", + "source-map-support": "^0.4.0" }, "keywords": [ "string", @@ -33,13 +35,16 @@ ], "scripts": { "test": "mocha", - "pretest": "npm run build", - "pretest-coverage": "npm run build", + "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": "rm -rf dist && rollup -c -f cjs -o dist/magic-string.cjs.js && rollup -c -f es6 -o dist/magic-string.es6.js && export DEPS=true && rollup -c -f umd -o dist/magic-string.umd.js", - "prepublish": "npm test", + "build:cjs": "rollup -c -f cjs -o dist/magic-string.cjs.js", + "build:es6": "rollup -c -f es6 -o dist/magic-string.es6.js", + "build:umd": "export DEPS=true && rollup -c -f umd -o dist/magic-string.umd.js", + "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" }, "files": [ diff --git a/src/Bundle.js b/src/Bundle.js index 830b26a..e5d9090 100644 --- a/src/Bundle.js +++ b/src/Bundle.js @@ -6,7 +6,6 @@ import isObject from './utils/isObject.js'; export default function Bundle ( options = {} ) { this.intro = options.intro || ''; - this.outro = options.outro || ''; this.separator = options.separator !== undefined ? options.separator : '\n'; this.sources = []; @@ -65,7 +64,6 @@ Bundle.prototype = { clone () { const bundle = new Bundle({ intro: this.intro, - outro: this.outro, separator: this.separator }); @@ -85,8 +83,7 @@ Bundle.prototype = { let names = []; this.sources.forEach( source => { - Object.keys( source.content.nameLocations ).forEach( location => { - const name = source.content.nameLocations[ location ]; + Object.keys( source.content.storedNames ).forEach( name => { if ( !~names.indexOf( name ) ) names.push( name ); }); }); @@ -106,8 +103,7 @@ Bundle.prototype = { } return prefix + mappings; - }).join( '' ) + - getSemis( this.outro ) + }).join( '' ) ); return new SourceMap({ @@ -158,7 +154,8 @@ Bundle.prototype = { indentStart//: trailingNewline || /\r?\n$/.test( separator ) //true///\r?\n/.test( separator ) }); - trailingNewline = source.content.str.slice( 0, -1 ) === '\n'; + // TODO this is a very slow way to determine this + trailingNewline = source.content.toString().slice( 0, -1 ) === '\n'; }); if ( this.intro ) { @@ -167,8 +164,6 @@ Bundle.prototype = { }); } - this.outro = this.outro.replace( /^[^\n]/gm, indentStr + '$&' ); - return this; }, @@ -185,7 +180,7 @@ Bundle.prototype = { return str; }).join( '' ); - return this.intro + body + this.outro; + return this.intro + body; }, trimLines () { @@ -201,20 +196,19 @@ Bundle.prototype = { this.intro = this.intro.replace( rx, '' ); if ( !this.intro ) { - let source; // TODO put inside loop if safe + let source; let i = 0; do { source = this.sources[i]; if ( !source ) { - this.outro = this.outro.replace( rx, '' ); break; } - source.content.trimStart(); + source.content.trimStart( charType ); i += 1; - } while ( source.content.str === '' ); + } while ( source.content.toString() === '' ); // TODO faster way to determine non-empty source? } return this; @@ -222,24 +216,21 @@ Bundle.prototype = { trimEnd ( charType ) { const rx = new RegExp( ( charType || '\\s' ) + '+$' ); - this.outro = this.outro.replace( rx, '' ); - if ( !this.outro ) { - let source; - let i = this.sources.length - 1; + let source; + let i = this.sources.length - 1; - do { - source = this.sources[i]; + do { + source = this.sources[i]; - if ( !source ) { - this.intro = this.intro.replace( rx, '' ); - break; - } + if ( !source ) { + this.intro = this.intro.replace( rx, '' ); + break; + } - source.content.trimEnd(charType); - i -= 1; - } while ( source.content.str === '' ); - } + source.content.trimEnd( charType ); + i -= 1; + } while ( source.content.toString() === '' ); // TODO faster way to determine non-empty source? return this; } diff --git a/src/MagicString.js b/src/MagicString.js index f3c6fe0..367ace7 100644 --- a/src/MagicString.js +++ b/src/MagicString.js @@ -1,3 +1,4 @@ +import Patch from './Patch.js'; import SourceMap from './utils/SourceMap.js'; import guessIndent from './utils/guessIndent.js'; import encodeMappings from './utils/encodeMappings.js'; @@ -9,12 +10,13 @@ let warned = false; export default function MagicString ( string, options = {} ) { Object.defineProperties( this, { original: { writable: true, value: string }, - str: { writable: true, value: string }, - mappings: { writable: true, value: initMappings( string.length ) }, + outro: { writable: true, value: '' }, + intro: { writable: true, value: '' }, + patches: { writable: true, value: [] }, filename: { writable: true, value: options.filename }, indentExclusionRanges: { writable: true, value: options.indentExclusionRanges }, sourcemapLocations: { writable: true, value: {} }, - nameLocations: { writable: true, value: {} }, + storedNames: { writable: true, value: {} }, indentStr: { writable: true, value: guessIndent( string ) } }); } @@ -25,22 +27,16 @@ MagicString.prototype = { }, append ( content ) { - if ( typeof content !== 'string' ) { - throw new TypeError( 'appended content must be a string' ); - } + if ( typeof content !== 'string' ) throw new TypeError( 'outro content must be a string' ); - this.str += content; + this.outro += content; return this; }, clone () { let cloned = new MagicString( this.original, { filename: this.filename }); - cloned.str = this.str; - let i = cloned.mappings.length; - while ( i-- ) { - cloned.mappings[i] = this.mappings[i]; - } + cloned.patches = this.patches.map( patch => patch.clone() ); if ( this.indentExclusionRanges ) { cloned.indentExclusionRanges = typeof this.indentExclusionRanges[0] === 'number' ? @@ -58,11 +54,7 @@ MagicString.prototype = { generateMap ( options ) { options = options || {}; - let names = []; - Object.keys( this.nameLocations ).forEach( location => { - const name = this.nameLocations[ location ]; - if ( !~names.indexOf( name ) ) names.push( name ); - }); + const names = Object.keys( this.storedNames ); return new SourceMap({ file: ( options.file ? options.file.split( /[\/\\]/ ).pop() : null ), @@ -78,12 +70,10 @@ MagicString.prototype = { }, getMappings ( hires, sourceIndex, offsets, names ) { - return encodeMappings( this.original, this.str, this.mappings, hires, this.sourcemapLocations, sourceIndex, offsets, names, this.nameLocations ); + return encodeMappings( this.original, this.intro, this.patches, hires, this.sourcemapLocations, sourceIndex, offsets, names ); }, indent ( indentStr, options ) { - const mappings = this.mappings; - const reverseMappings = reverse( mappings, this.str.length ); const pattern = /^[^\r\n]/gm; if ( isObject( indentStr ) ) { @@ -98,93 +88,69 @@ MagicString.prototype = { options = options || {}; // Process exclusion ranges - let exclusions; + let isExcluded = {}; if ( options.exclude ) { - exclusions = typeof options.exclude[0] === 'number' ? [ options.exclude ] : options.exclude; - - exclusions = exclusions.map( range => { - const rangeStart = this.locate( range[0] ); - const rangeEnd = this.locate( range[1] ); - - if ( rangeStart === null || rangeEnd === null ) { - throw new Error( 'Cannot use indices of replaced characters as exclusion ranges' ); + 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; } - - return [ rangeStart, rangeEnd ]; }); + } - exclusions.sort( ( a, b ) => a[0] - b[0] ); + let shouldIndentNextCharacter = options.indentStart !== false; + const replacer = match => { + if ( shouldIndentNextCharacter ) return `${indentStr}${match}`; + shouldIndentNextCharacter = true; + return match; + }; - // check for overlaps - lastEnd = -1; - exclusions.forEach( range => { - if ( range[0] < lastEnd ) { - throw new Error( 'Exclusion ranges cannot overlap' ); - } + this.intro = this.intro.replace( pattern, replacer ); - lastEnd = range[1]; - }); - } + let charIndex = 0; + let patchIndex = 0; - const indentStart = options.indentStart !== false; - let inserts = []; + const indentUntil = end => { + while ( charIndex < end ) { + if ( !isExcluded[ charIndex ] ) { + const char = this.original[ charIndex ]; - if ( !exclusions ) { - this.str = this.str.replace( pattern, ( match, index ) => { - if ( !indentStart && index === 0 ) { - return match; - } + if ( char === '\n' ) { + shouldIndentNextCharacter = true; + } else if ( char !== '\r' && shouldIndentNextCharacter ) { + this.patches.splice( patchIndex, 0, new Patch( charIndex, charIndex, indentStr, '', false ) ); + shouldIndentNextCharacter = false; - inserts.push( index ); - return indentStr + match; - }); - } else { - this.str = this.str.replace( pattern, ( match, index ) => { - if ( ( !indentStart && index === 0 ) || isExcluded( index - 1 ) ) { - return match; + patchIndex += 1; + } } - inserts.push( index ); - return indentStr + match; - }); - } + charIndex += 1; + } + }; - const adjustments = inserts.map( index => { - let origin; + for ( ; patchIndex < this.patches.length; patchIndex += 1 ) { // can't cache this.patches.length, it may change + const patch = this.patches[ patchIndex ]; - do { - origin = reverseMappings[ index++ ]; - } while ( !~origin && index < this.str.length ); + indentUntil( patch.start ); - return origin; - }); - - let i = adjustments.length; - let lastEnd = this.mappings.length; - while ( i-- ) { - adjust( this.mappings, adjustments[i], lastEnd, ( ( i + 1 ) * indentStr.length ) ); - lastEnd = adjustments[i]; - } + if ( !isExcluded[ charIndex ] ) { + patch.content = patch.content.replace( pattern, replacer ); - return this; + if ( patch.content.length ) { + shouldIndentNextCharacter = patch.content[ patch.content.length - 1 ] === '\n'; + } + } - function isExcluded ( index ) { - let i = exclusions.length; - let range; + charIndex = patch.end; + } - while ( i-- ) { - range = exclusions[i]; + indentUntil( this.original.length ); - if ( range[1] < index ) { - return false; - } + this.outro = this.outro.replace( pattern, replacer ); - if ( range[0] <= index ) { - return true; - } - } - } + return this; }, insert ( index, content ) { @@ -192,45 +158,17 @@ MagicString.prototype = { throw new TypeError( 'inserted content must be a string' ); } - if ( index === this.original.length ) { - this.append( content ); - } else { - const mapped = this.locate( index ); - - if ( mapped === null ) { - throw new Error( 'Cannot insert at replaced character index: ' + index ); - } - - this.str = this.str.substr( 0, mapped ) + content + this.str.substr( mapped ); - adjust( this.mappings, index, this.mappings.length, content.length ); - } - + this.patch( index, index, content ); return this; }, // get current location of character in original string locate ( character ) { - if ( character < 0 || character > this.mappings.length ) { - throw new Error( 'Character is out of bounds' ); - } - - const loc = this.mappings[ character ]; - return ~loc ? loc : null; + throw new Error( 'magicString.locate is deprecated' ); }, locateOrigin ( character ) { - if ( character < 0 || character >= this.str.length ) { - throw new Error( 'Character is out of bounds' ); - } - - let i = this.mappings.length; - while ( i-- ) { - if ( this.mappings[i] === character ) { - return i; - } - } - - return null; + throw new Error( 'magicString.locateOrigin is deprecated' ); }, overwrite ( start, end, content, storeName ) { @@ -238,60 +176,61 @@ MagicString.prototype = { throw new TypeError( 'replacement content must be a string' ); } - const firstChar = this.locate( start ); - const lastChar = this.locate( end - 1 ); + this.patch( start, end, content, storeName ); + return this; + }, - if ( firstChar === null || lastChar === null ) { - throw new Error( `Cannot overwrite the same content twice: '${this.original.slice(start, end).replace(/\n/g, '\\n')}'` ); - } + patch ( start, end, content, storeName ) { + const original = this.original.slice( start, end ); + if ( storeName ) this.storedNames[ original ] = true; - if ( firstChar > lastChar + 1 ) { - throw new Error( - 'BUG! First character mapped to a position after the last character: ' + - '[' + start + ', ' + end + '] -> [' + firstChar + ', ' + ( lastChar + 1 ) + ']' - ); - } + let i = this.patches.length; + while ( i-- ) { + const previous = this.patches[i]; - if ( storeName ) { - this.nameLocations[ start ] = this.original.slice( start, end ); - } + // TODO can we tidy this up? - this.str = this.str.substr( 0, firstChar ) + content + this.str.substring( lastChar + 1 ); + // if this completely covers previous patch, remove it + if ( start !== end && start <= previous.start && end >= previous.end ) { + this.patches.splice( i, 1 ); + } - const d = content.length - ( lastChar + 1 - firstChar ); + // if it overlaps, throw error + else if ( start < previous.end && end > previous.start ) { + // special case – it's okay to remove overlapping ranges + if ( !previous.content.length && !content.length ) { + previous.start = Math.min( start, previous.start ); + previous.end = Math.max( end, previous.end ); + return; + } - blank( this.mappings, start, end ); - adjust( this.mappings, end, this.mappings.length, d ); - return this; + throw new Error( `Cannot overwrite the same content twice: '${original}'` ); + } + + // if this precedes previous patch, stop search + else if ( start >= previous.end ) { + break; + } + } + + const patch = new Patch( start, end, content, original, storeName ); + this.patches.splice( i + 1, 0, patch ); + return patch; }, prepend ( content ) { - this.str = content + this.str; - adjust( this.mappings, 0, this.mappings.length, content.length ); + if ( typeof content !== 'string' ) throw new TypeError( 'outro content must be a string' ); + + this.intro = content + this.intro; return this; }, remove ( start, end ) { - if ( start < 0 || end > this.mappings.length ) { + if ( start < 0 || end > this.original.length ) { throw new Error( 'Character is out of bounds' ); } - let currentStart = -1; - let currentEnd = -1; - for ( let i = start; i < end; i += 1 ) { - const loc = this.mappings[i]; - - if ( ~loc ) { - if ( !~currentStart ) currentStart = loc; - - currentEnd = loc + 1; - this.mappings[i] = -1; - } - } - - this.str = this.str.slice( 0, currentStart ) + this.str.slice( currentEnd ); - - adjust( this.mappings, end, this.mappings.length, currentStart - currentEnd ); + this.patch( start, end, '' ); return this; }, @@ -308,14 +247,41 @@ MagicString.prototype = { while ( start < 0 ) start += this.original.length; while ( end < 0 ) end += this.original.length; - const firstChar = this.locate( start ); - const lastChar = this.locate( end - 1 ); + let firstPatchIndex = 0; + let lastPatchIndex = this.patches.length; + + while ( lastPatchIndex-- ) { + const patch = this.patches[ lastPatchIndex ]; + if ( end >= patch.start && end < patch.end ) throw new Error( `Cannot use replaced characters (${start}, ${end}) as slice anchors` ); - if ( firstChar === null || lastChar === null ) { - throw new Error( 'Cannot use replaced characters as slice anchors' ); + // TODO this is weird, rewrite it + if ( patch.start > end ) continue; + break; } - return this.str.slice( firstChar, lastChar + 1 ); + for ( firstPatchIndex = 0; firstPatchIndex <= lastPatchIndex; firstPatchIndex += 1 ) { + const patch = this.patches[ firstPatchIndex ]; + if ( start > patch.start && start <= patch.end ) throw new Error( `Cannot use replaced characters (${start}, ${end}) as slice anchors` ); + + if ( start <= patch.start ) { + break; + } + } + + let result = ''; + let lastIndex = start; + + for ( let i = firstPatchIndex; i <= lastPatchIndex; i += 1 ) { + const patch = this.patches[i]; + result += this.original.slice( lastIndex, patch.start ); + result += patch.content; + + lastIndex = patch.end; + } + + result += this.original.slice( lastIndex, end ); + + return result; }, snip ( start, end ) { @@ -327,113 +293,91 @@ MagicString.prototype = { }, toString () { - return this.str; + return this.intro + this.slice( 0, this.original.length ) + this.outro; }, trimLines () { return this.trim('[\\r\\n]'); }, - trim (charType) { - return this.trimStart(charType).trimEnd(charType); + trim ( charType ) { + return this.trimStart( charType ).trimEnd( charType ); }, - trimEnd (charType) { + trimEnd ( charType ) { const rx = new RegExp( ( charType || '\\s' ) + '+$' ); - this.str = this.str.replace( rx, ( trailing, index, str ) => { - const strLength = str.length; - const length = trailing.length; + this.outro = this.outro.replace( rx, '' ); + if ( this.outro.length ) return this; - let chars = []; + let charIndex = this.original.length; + let i = this.patches.length; - let i = strLength; - while ( i-- > strLength - length ) { - chars.push( this.locateOrigin( i ) ); - } + while ( i-- ) { + const patch = this.patches[i]; + + if ( charIndex > patch.end ) { + const slice = this.original.slice( patch.end, charIndex ); + + const match = rx.exec( slice ); + if ( match ) { + this.patch( charIndex - match[0].length, charIndex, '' ); + } - i = chars.length; - while ( i-- ) { - if ( chars[i] !== null ) { - this.mappings[ chars[i] ] = -1; + if ( !match || match[0].length < slice.length ) { + // there is non-whitespace after the patch + return this; } } - return ''; - }); + patch.content = patch.content.replace( rx, '' ); + if ( patch.content ) return this; + + charIndex = patch.start; + } + + const slice = this.original.slice( 0, charIndex ); + + const match = rx.exec( slice ); + if ( match ) this.patch( charIndex - match[0].length, charIndex, '' ); return this; }, - trimStart (charType) { + trimStart ( charType ) { const rx = new RegExp( '^' + ( charType || '\\s' ) + '+' ); - this.str = this.str.replace( rx, leading => { - const length = leading.length; - - let chars = []; - let adjustmentStart = 0; + this.intro = this.intro.replace( rx, '' ); + if ( this.intro.length ) return this; - let i = length; - while ( i-- ) { - chars.push( this.locateOrigin( i ) ); - } + let charIndex = 0; - i = chars.length; - while ( i-- ) { - if ( chars[i] !== null ) { - this.mappings[ chars[i] ] = -1; - adjustmentStart += 1; - } - } + for ( let i = 0; i < this.patches.length; i += 1 ) { + const patch = this.patches[i]; - adjust( this.mappings, adjustmentStart, this.mappings.length, -length ); + if ( charIndex < patch.start ) { + const slice = this.original.slice( charIndex, patch.start ); - return ''; - }); + const match = rx.exec( slice ); + if ( match ) this.patch( charIndex, charIndex + match[0].length, '' ); - return this; - } -} + if ( !match || match[0].length < slice.length ) { + // there is non-whitespace before the patch + return this; + } + } -function adjust ( mappings, start, end, d ) { - if ( !d ) return; // replacement is same length as replaced string + patch.content = patch.content.replace( rx, '' ); + if ( patch.content ) return this; - let i = end; - while ( i-- > start ) { - if ( ~mappings[i] ) { - mappings[i] += d; + charIndex = patch.end; } - } -} - -function initMappings ( i ) { - let mappings = new Uint32Array( i ); - while ( i-- ) mappings[i] = i; - return mappings; -} - -function blank ( mappings, start, i ) { - while ( i-- > start ) mappings[i] = -1; -} + const slice = this.original.slice( charIndex, this.original.length ); -function reverse ( mappings, i ) { - let result = new Uint32Array( i ); + const match = rx.exec( slice ); + if ( match ) this.patch( charIndex, charIndex + match[0].length, '' ); - while ( i-- ) { - result[i] = -1; - } - - let location; - i = mappings.length; - while ( i-- ) { - location = mappings[i]; - - if ( ~location ) { - result[ location ] = i; - } + return this; } - - return result; } diff --git a/src/Patch.js b/src/Patch.js new file mode 100644 index 0000000..1c333b3 --- /dev/null +++ b/src/Patch.js @@ -0,0 +1,13 @@ +export default function Patch ( start, end, content, original, storeName ) { + this.start = start; + this.end = end; + this.content = content; + this.original = original; + this.storeName = storeName; +} + +Patch.prototype = { + clone () { + return new Patch( this.start, this.end, this.content, this.original, this.storeName ); + } +}; diff --git a/src/utils/encodeMappings.js b/src/utils/encodeMappings.js index 83190ab..404ca18 100644 --- a/src/utils/encodeMappings.js +++ b/src/utils/encodeMappings.js @@ -1,72 +1,97 @@ import { encode } from 'vlq'; -export default function encodeMappings ( original, str, mappings, hires, sourcemapLocations, sourceIndex, offsets, names, nameLocations ) { - // store locations, for fast lookup - let lineStart = 0; - const locations = original.split( '\n' ).map( line => { - const start = lineStart; - lineStart += line.length + 1; // +1 for the newline - - return start; - }); - - const inverseMappings = invert( str, mappings ); - - let charOffset = 0; - const lines = str.split( '\n' ).map( line => { - let segments = []; - - let char; // TODO put these inside loop, once we've determined it's safe to do so transpilation-wise - let origin; - let lastOrigin = -1; - let location; - let nameIndex; - - let i; +export default function encodeMappings ( original, intro, patches, hires, sourcemapLocations, sourceIndex, offsets, names ) { + let rawLines = []; + + let generatedCodeLine = intro.split( '\n' ).length - 1; + let rawSegments = rawLines[ generatedCodeLine ] = []; + + let originalCharIndex = 0; + + let generatedCodeColumn = 0; + let sourceCodeLine = 0; + let sourceCodeColumn = 0; + + function addSegmentsUntil ( end ) { + let first = true; + + while ( originalCharIndex < end ) { + if ( hires || first || sourcemapLocations[ originalCharIndex ] ) { + rawSegments.push({ + generatedCodeLine, + generatedCodeColumn, + sourceCodeLine, + sourceCodeColumn, + sourceCodeName: -1, + sourceIndex + }); + } - const len = line.length; - for ( i = 0; i < len; i += 1 ) { - char = i + charOffset; - origin = inverseMappings[ char ]; + if ( original[ originalCharIndex ] === '\n' ) { + sourceCodeLine += 1; + sourceCodeColumn = 0; + generatedCodeLine += 1; + rawLines[ generatedCodeLine ] = rawSegments = []; + generatedCodeColumn = 0; + } else { + sourceCodeColumn += 1; + generatedCodeColumn += 1; + } - nameIndex = -1; - location = null; + originalCharIndex += 1; + first = false; + } + } - // if this character has no mapping, but the last one did, - // create a new segment - if ( !~origin && ~lastOrigin ) { - location = getLocation( locations, lastOrigin + 1 ); + for ( let i = 0; i < patches.length; i += 1 ) { + const patch = patches[i]; + const addSegmentForPatch = patch.storeName || patch.start > originalCharIndex; + + addSegmentsUntil( patch.start ); + + if ( addSegmentForPatch ) { + rawSegments.push({ + generatedCodeLine, + generatedCodeColumn, + sourceCodeLine, + sourceCodeColumn, + sourceCodeName: patch.storeName ? names.indexOf( patch.original ) : -1, + sourceIndex + }); + } - if ( ( lastOrigin + 1 ) in nameLocations ) nameIndex = names.indexOf( nameLocations[ lastOrigin + 1 ] ); - } + let lines = patch.content.split( '\n' ); + let lastLine = lines.pop(); - else if ( ~origin && ( hires || ( ~lastOrigin && origin !== lastOrigin + 1 ) || sourcemapLocations[ origin ] ) ) { - location = getLocation( locations, origin ); - } + if ( lines.length ) { + generatedCodeLine += lines.length; + rawLines[ generatedCodeLine ] = rawSegments = []; + generatedCodeColumn = lastLine.length; + } else { + generatedCodeColumn += lastLine.length; + } - if ( location ) { - segments.push({ - generatedCodeColumn: i, - sourceIndex, - sourceCodeLine: location.line, - sourceCodeColumn: location.column, - sourceCodeName: nameIndex - }); - } + lines = patch.original.split( '\n' ); + lastLine = lines.pop(); - lastOrigin = origin; + if ( lines.length ) { + sourceCodeLine += lines.length; + sourceCodeColumn = lastLine.length; + } else { + sourceCodeColumn += lastLine.length; } - charOffset += line.length + 1; - return segments; - }); + originalCharIndex = patch.end; + } + + addSegmentsUntil( original.length ); offsets.sourceIndex = offsets.sourceIndex || 0; offsets.sourceCodeLine = offsets.sourceCodeLine || 0; offsets.sourceCodeColumn = offsets.sourceCodeColumn || 0; offsets.sourceCodeName = offsets.sourceCodeName || 0; - const encoded = lines.map( segments => { + const encoded = rawLines.map( segments => { let generatedCodeColumn = 0; return segments.map( segment => { @@ -93,38 +118,3 @@ export default function encodeMappings ( original, str, mappings, hires, sourcem return encoded; } - - -function invert ( str, mappings ) { - let inverted = new Uint32Array( str.length ); - - // initialise everything to -1 - let i = str.length; - while ( i-- ) { - inverted[i] = -1; - } - - // then apply the actual mappings - i = mappings.length; - while ( i-- ) { - if ( ~mappings[i] ) { - inverted[ mappings[i] ] = i; - } - } - - return inverted; -} - -function getLocation ( locations, char ) { - let i = locations.length; - while ( i-- ) { - if ( locations[i] <= char ) { - return { - line: i, - column: char - locations[i] - }; - } - } - - throw new Error( 'Character out of bounds' ); -} diff --git a/test/index.js b/test/index.js index 9ca5fc9..fb81ec4 100644 --- a/test/index.js +++ b/test/index.js @@ -3,6 +3,9 @@ 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 () { @@ -49,7 +52,6 @@ describe( 'MagicString', function () { assert.notEqual( s, c ); assert.equal( c.toString(), 'abcXYZjkl' ); - assert.equal( c.locate( 9 ), 6 ); }); it( 'should clone filename info', function () { @@ -105,8 +107,8 @@ describe( 'MagicString', function () { 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,CAAC,CAAC,CAAO,CAAC,CAAC"}' ); - assert.equal( map.toUrl(), 'data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3V0cHV0Lm1kIiwic291cmNlcyI6WyJpbnB1dC5tZCJdLCJzb3VyY2VzQ29udGVudCI6WyJhYmNkZWZnaGlqa2wiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsQ0FBQyxDQUFDLENBQU8sQ0FBQyxDQUFDIn0=' ); + 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==' ); smc = new SourceMapConsumer( map ); @@ -118,10 +120,6 @@ describe( 'MagicString', function () { assert.equal( loc.line, 1 ); assert.equal( loc.column, 1 ); - loc = smc.originalPositionFor({ line: 1, column: 3 }); - assert.equal( loc.line, 1 ); - assert.equal( loc.column, 9 ); - loc = smc.originalPositionFor({ line: 1, column: 4 }); assert.equal( loc.line, 1 ); assert.equal( loc.column, 10 ); @@ -169,8 +167,8 @@ describe( 'MagicString', function () { 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,GAAM,CAAC"}' ); - assert.equal( map.toUrl(), 'data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3V0cHV0Lm1kIiwic291cmNlcyI6WyJpbnB1dC5tZCJdLCJzb3VyY2VzQ29udGVudCI6WyJhYmNkZWZnaGlqa2wiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsR0FBRyxHQUFNLENBQUMifQ==' ); + 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 ); @@ -182,10 +180,6 @@ describe( 'MagicString', function () { assert.equal( loc.line, 1 ); assert.equal( loc.column, 3 ); - loc = smc.originalPositionFor({ line: 1, column: 6 }); - assert.equal( loc.line, 1 ); - assert.equal( loc.column, 9 ); - loc = smc.originalPositionFor({ line: 1, column: 7 }); assert.equal( loc.line, 1 ); assert.equal( loc.column, 10 ); @@ -243,7 +237,7 @@ describe( 'MagicString', function () { numMappings += 1; }); - assert.equal( numMappings, 1 ); + assert.equal( numMappings, 3 ); // one at 0, one at the edit, one afterwards }); }); @@ -334,6 +328,25 @@ describe( 'MagicString', function () { 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 ); @@ -354,10 +367,6 @@ describe( 'MagicString', function () { s.insert( 12, '<<<' ); assert.equal( s.toString(), '>>>abcdef***ghijkl<<<' ); - assert.equal( s.locate( 0 ), 3 ); - assert.equal( s.locate( 5 ), 8 ); - assert.equal( s.locate( 6 ), 12 ); - assert.equal( s.locate( 11 ), 17 ); }); it( 'should return this', function () { @@ -387,138 +396,147 @@ describe( 'MagicString', function () { }); describe( 'locate', function () { - it( 'should correctly locate characters in an unmodified string', function () { - var s = new MagicString( 'abcdefghijkl' ); - assert.equal( s.locate( 0 ), 0 ); - assert.equal( s.locate( 6 ), 6 ); - assert.equal( s.locate( 11 ), 11 ); - }); - - it ( 'should throw an error if character is out of bounds', function () { - var s = new MagicString( 'abcdefghijkl' ); - - assert.throws( function () { s.locate( -1 ); }); - assert.throws( function () { s.locate( 13 ); }); - }); - - it( 'should correctly locate characters in a string with characters removed', function () { - var s = new MagicString( 'abcdefghijkl' ); - - s.remove( 1, 5 ); - assert.equal( s.locate( 0 ), 0 ); - assert.equal( s.locate( 1 ), null ); - assert.equal( s.locate( 2 ), null ); - assert.equal( s.locate( 5 ), 1 ); - assert.equal( s.locate( 11 ), 7 ); - }); - - it( 'should correctly locate characters in a string with characters replaced', function () { - var s = new MagicString( 'abcdefghijkl' ); - - s.overwrite( 5, 8, 'FGH' ); - assert.equal( s.locate( 0 ), 0 ); - assert.equal( s.locate( 4 ), 4 ); - assert.equal( s.locate( 5 ), null ); - assert.equal( s.locate( 7 ), null ); - assert.equal( s.locate( 8 ), 8 ); - - s.overwrite( 1, 4, 'X' ); - assert.equal( s.toString(), 'aXeFGHijkl' ); - assert.equal( s.locate( 2 ), null ); - assert.equal( s.locate( 4 ), 2 ); - assert.equal( s.locate( 8 ), 6 ); - }); - - it( 'should correctly locate characters in a string that has had content prepended', function () { - var s = new MagicString( 'abcdefghijkl' ); - - s.prepend( 'xyz' ); - assert.equal( s.locate( 0 ), 3 ); - assert.equal( s.locate( 11 ), 14 ); - - s.prepend( 'xyz' ); - assert.equal( s.locate( 0 ), 6 ); - assert.equal( s.locate( 11 ), 17 ); - }); - - it( 'should correctly locate characters in a string that has had content appended', function () { - var s = new MagicString( 'abcdefghijkl' ); - - s.append( 'xyz' ); - assert.equal( s.locate( 0 ), 0 ); - assert.equal( s.locate( 11 ), 11 ); - - s.append( 'xyz' ); - assert.equal( s.locate( 0 ), 0 ); - assert.equal( s.locate( 11 ), 11 ); - }); - - it( 'should correctly locate characters in indented code', function () { - var s = new MagicString( 'abc\ndef\nghi\njkl' ); - - s.indent(); - - assert.equal( s.locate( 0 ), 1 ); - assert.equal( s.locate( 4 ), 6 ); - assert.equal( s.locate( 5 ), 7 ); - assert.equal( s.locate( 8 ), 11 ); - assert.equal( s.locate( 12 ), 16 ); - assert.equal( s.locate( 13 ), 17 ); - - s.indent(); - - assert.equal( s.locate( 0 ), 2 ); - assert.equal( s.locate( 4 ), 8 ); - assert.equal( s.locate( 8 ), 14 ); - assert.equal( s.locate( 12 ), 20 ); - }); - - it( 'should correctly locate characters in trimmed original content', function () { - var s = new MagicString( ' abcdefghijkl ' ); - - s.trim(); - assert.equal( s.locate( 0 ), null ); - assert.equal( s.locate( 2 ), null ); - assert.equal( s.locate( 3 ), 0 ); - assert.equal( s.locate( 14 ), 11 ); - assert.equal( s.locate( 15 ), null ); - }); - - it( 'should correctly locate characters in trimmed replaced content', function () { + it( 'is deprecated', function () { var s = new MagicString( 'abcdefghijkl' ); - - s.overwrite( 0, 3, ' ' ).overwrite( 9, 12, ' ' ).trim(); - assert.equal( s.locate( 0 ), null ); - assert.equal( s.locate( 2 ), null ); - assert.equal( s.locate( 3 ), 0 ); - - assert.equal( s.locate( 8 ), 5 ); - assert.equal( s.locate( 9 ), null ); - }); - - it( 'should correctly locate characters in trimmed appended/prepended content', function () { - var s = new MagicString( ' abcdefghijkl ' ); - - s.prepend( ' ' ).append( ' ' ).trim(); - assert.equal( s.locate( 0 ), null ); - assert.equal( s.locate( 1 ), 0 ); - assert.equal( s.locate( 12 ), 11 ); - assert.equal( s.locate( 13 ), null ); - }); + assert.throws( function () { s.locate( 6 ); }, /deprecated/ ); + }); + // it( 'should correctly locate characters in an unmodified string', function () { + // var s = new MagicString( 'abcdefghijkl' ); + // assert.equal( s.locate( 0 ), 0 ); + // assert.equal( s.locate( 6 ), 6 ); + // assert.equal( s.locate( 11 ), 11 ); + // }); + // + // it ( 'should throw an error if character is out of bounds', function () { + // var s = new MagicString( 'abcdefghijkl' ); + // + // assert.throws( function () { s.locate( -1 ); }); + // assert.throws( function () { s.locate( 13 ); }); + // }); + // + // it( 'should correctly locate characters in a string with characters removed', function () { + // var s = new MagicString( 'abcdefghijkl' ); + // + // s.remove( 1, 5 ); + // assert.equal( s.locate( 0 ), 0 ); + // assert.equal( s.locate( 1 ), null ); + // assert.equal( s.locate( 2 ), null ); + // assert.equal( s.locate( 5 ), 1 ); + // assert.equal( s.locate( 11 ), 7 ); + // }); + // + // it( 'should correctly locate characters in a string with characters replaced', function () { + // var s = new MagicString( 'abcdefghijkl' ); + // + // s.overwrite( 5, 8, 'FGH' ); + // assert.equal( s.locate( 0 ), 0 ); + // assert.equal( s.locate( 4 ), 4 ); + // assert.equal( s.locate( 5 ), null ); + // assert.equal( s.locate( 7 ), null ); + // assert.equal( s.locate( 8 ), 8 ); + // + // s.overwrite( 1, 4, 'X' ); + // assert.equal( s.toString(), 'aXeFGHijkl' ); + // assert.equal( s.locate( 2 ), null ); + // assert.equal( s.locate( 4 ), 2 ); + // assert.equal( s.locate( 8 ), 6 ); + // }); + // + // it( 'should correctly locate characters in a string that has had content prepended', function () { + // var s = new MagicString( 'abcdefghijkl' ); + // + // s.prepend( 'xyz' ); + // assert.equal( s.locate( 0 ), 3 ); + // assert.equal( s.locate( 11 ), 14 ); + // + // s.prepend( 'xyz' ); + // assert.equal( s.locate( 0 ), 6 ); + // assert.equal( s.locate( 11 ), 17 ); + // }); + // + // it( 'should correctly locate characters in a string that has had content appended', function () { + // var s = new MagicString( 'abcdefghijkl' ); + // + // s.append( 'xyz' ); + // assert.equal( s.locate( 0 ), 0 ); + // assert.equal( s.locate( 11 ), 11 ); + // + // s.append( 'xyz' ); + // assert.equal( s.locate( 0 ), 0 ); + // assert.equal( s.locate( 11 ), 11 ); + // }); + // + // it( 'should correctly locate characters in indented code', function () { + // var s = new MagicString( 'abc\ndef\nghi\njkl' ); + // + // s.indent(); + // + // assert.equal( s.locate( 0 ), 1 ); + // assert.equal( s.locate( 4 ), 6 ); + // assert.equal( s.locate( 5 ), 7 ); + // assert.equal( s.locate( 8 ), 11 ); + // assert.equal( s.locate( 12 ), 16 ); + // assert.equal( s.locate( 13 ), 17 ); + // + // s.indent(); + // + // assert.equal( s.locate( 0 ), 2 ); + // assert.equal( s.locate( 4 ), 8 ); + // assert.equal( s.locate( 8 ), 14 ); + // assert.equal( s.locate( 12 ), 20 ); + // }); + // + // it( 'should correctly locate characters in trimmed original content', function () { + // var s = new MagicString( ' abcdefghijkl ' ); + // + // s.trim(); + // assert.equal( s.locate( 0 ), null ); + // assert.equal( s.locate( 2 ), null ); + // assert.equal( s.locate( 3 ), 0 ); + // assert.equal( s.locate( 14 ), 11 ); + // assert.equal( s.locate( 15 ), null ); + // }); + // + // it( 'should correctly locate characters in trimmed replaced content', function () { + // var s = new MagicString( 'abcdefghijkl' ); + // + // s.overwrite( 0, 3, ' ' ).overwrite( 9, 12, ' ' ).trim(); + // assert.equal( s.locate( 0 ), null ); + // assert.equal( s.locate( 2 ), null ); + // assert.equal( s.locate( 3 ), 0 ); + // + // assert.equal( s.locate( 8 ), 5 ); + // assert.equal( s.locate( 9 ), null ); + // }); + // + // it( 'should correctly locate characters in trimmed appended/prepended content', function () { + // var s = new MagicString( ' abcdefghijkl ' ); + // + // s.prepend( ' ' ).append( ' ' ).trim(); + // assert.equal( s.locate( 0 ), null ); + // assert.equal( s.locate( 1 ), 0 ); + // assert.equal( s.locate( 12 ), 11 ); + // assert.equal( s.locate( 13 ), null ); + // }); }); describe( 'locateOrigin', function () { - it( 'should locate the origin of characters in the generated string', function () { + it( 'is deprecated', function () { var s = new MagicString( 'abcdefghijkl' ); - - assert.equal( s.locateOrigin( 4 ), 4 ); - assert.equal( s.locateOrigin( 11 ), 11 ); - - s.remove( 1, 3 ); - assert.equal( s.locateOrigin( 0 ), 0 ); - assert.equal( s.locateOrigin( 1 ), 3 ); - assert.equal( s.locateOrigin( 2 ), 4 ); - }); + assert.throws( function () { s.locateOrigin( 6 ); }, /deprecated/ ); + }); + + // it( 'should locate the origin of characters in the generated string', function () { + // var s = new MagicString( 'abcdefghijkl' ); + // + // assert.equal( s.locateOrigin( 4 ), 4 ); + // assert.equal( s.locateOrigin( 11 ), 11 ); + // + // s.remove( 1, 3 ); + // assert.equal( s.locateOrigin( 0 ), 0 ); + // assert.equal( s.locateOrigin( 1 ), 3 ); + // assert.equal( s.locateOrigin( 2 ), 4 ); + // }); }); describe( 'overwrite', function () { @@ -651,9 +669,10 @@ describe( 'MagicString', function () { 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( 2, 10 ); + s.slice( 3, 9 ); }); }); @@ -670,22 +689,18 @@ describe( 'MagicString', function () { it( 'errors if replaced characters are used as slice anchors', function () { var s = new MagicString( 'abcdef' ); - s.replace( 2, 4, 'CD' ); + s.overwrite( 2, 4, 'CD' ); assert.throws( function () { s.slice( 2, 3 ); }, /slice anchors/ ); assert.throws( function () { - s.slice( 2, 4 ); - }, /slice anchors/ ); - - assert.throws( function () { - s.slice( 1, 4 ); + s.slice( 3, 4 ); }, /slice anchors/ ); assert.throws( function () { - s.slice( 2, 5 ); + s.slice( 3, 5 ); }, /slice anchors/ ); assert.equal( s.slice( 1, 5 ), 'bCDe' ); @@ -702,7 +717,6 @@ describe( 'MagicString', function () { var snippet = s.snip( 3, 9 ); assert.equal( snippet.toString(), 'defGHI' ); - assert.equal( snippet.locate( 0, 3 ) ); assert.equal( snippet.filename, 'foo.js' ); }); @@ -717,10 +731,9 @@ describe( 'MagicString', function () { describe( 'trim', function () { it( 'should trim original content', function () { - var s = new MagicString( ' abcdefghijkl ' ); - - s.trim(); - assert.equal( s.toString(), 'abcdefghijkl' ); + 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 () { @@ -730,6 +743,34 @@ describe( 'MagicString', function () { assert.equal( s.toString(), 'defghi' ); }); + it( 'should trim original content before or after replaced content', function () { + var s = new MagicString( 'abc defghi' ); + + s.remove( 6, 9 ); + s.remove( 9, 12 ); + assert.equal( s.toString(), 'abc ' ); + + s.trim(); + assert.equal( s.toString(), 'abc' ); + + s = new MagicString( 'xyz abc defghi' ); + + s.remove( 0, 3 ); + s.remove( 12, 18 ); + assert.equal( s.toString(), ' abc ' ); + + s.trim(); + assert.equal( s.toString(), 'abc' ); + + s = new MagicString( 'xyz abc' ); + + s.remove( 0, 3 ); + assert.equal( s.toString(), ' abc' ); + + s.trim(); + assert.equal( s.toString(), 'abc' ); + }); + it( 'should trim appended/prepended content', function () { var s = new MagicString( ' abcdefghijkl ' );