UNPKG

ember-introjs

Version:
629 lines (462 loc) 16.6 kB
import Chunk from './Chunk.js'; import SourceMap from './utils/SourceMap.js'; import guessIndent from './utils/guessIndent.js'; import getRelativePath from './utils/getRelativePath.js'; import isObject from './utils/isObject.js'; import getLocator from './utils/getLocator.js'; import Mappings from './utils/Mappings.js'; import Stats from './utils/Stats.js'; const warned = { insertLeft: false, insertRight: false, storeName: false }; 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; }, appendLeft ( index, content ) { if ( typeof content !== 'string' ) throw new TypeError( 'inserted content must be a string' ); if ( DEBUG ) this.stats.time( 'appendLeft' ); this._split( index ); const chunk = this.byEnd[ index ]; if ( chunk ) { chunk.appendLeft( content ); } else { this.intro += content; } if ( DEBUG ) this.stats.timeEnd( 'appendLeft' ); return this; }, appendRight ( index, content ) { if ( typeof content !== 'string' ) throw new TypeError( 'inserted content must be a string' ); if ( DEBUG ) this.stats.time( 'appendRight' ); this._split( index ); const chunk = this.byStart[ index ]; if ( chunk ) { chunk.appendRight( content ); } else { this.outro += content; } if ( DEBUG ) this.stats.timeEnd( 'appendRight' ); return this; }, clone () { const 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 = this.indentExclusionRanges.slice(); } Object.keys( this.sourcemapLocations ).forEach( loc => { cloned.sourcemapLocations[ loc ] = true; }); return cloned; }, generateMap ( options ) { options = options || {}; const sourceIndex = 0; const names = Object.keys( this.storedNames ); const mappings = new Mappings( options.hires ); const locate = getLocator( this.original ); if ( this.intro ) { mappings.advance( this.intro ); } this.firstChunk.eachNext( chunk => { const loc = locate( chunk.start ); if ( chunk.intro.length ) mappings.advance( chunk.intro ); if ( chunk.edited ) { mappings.addEdit( sourceIndex, chunk.content, chunk.original, loc, chunk.storeName ? names.indexOf( chunk.original ) : -1 ); } else { mappings.addUneditedChunk( sourceIndex, chunk, this.original, loc, this.sourcemapLocations ); } if ( chunk.outro.length ) mappings.advance( chunk.outro ); }); 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: mappings.encode() }); if ( DEBUG ) this.stats.timeEnd( 'generateMap' ); return map; }, getIndentString () { return this.indentStr === null ? '\t' : this.indentStr; }, 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 const isExcluded = {}; if ( options.exclude ) { const 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.prependRight( indentStr ); } else { this._splitChunk( chunk, charIndex ); chunk = chunk.next; chunk.prependRight( indentStr ); } } } 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 prependRight(...) or appendLeft(...)' ); }, insertLeft ( index, content ) { if ( !warned.insertLeft ) { console.warn( 'magicString.insertLeft(...) is deprecated. Use magicString.appendLeft(...) instead' ); // eslint-disable-line no-console warned.insertLeft = true; } return this.appendLeft( index, content ); }, insertRight ( index, content ) { if ( !warned.insertRight ) { console.warn( 'magicString.insertRight(...) is deprecated. Use magicString.prependRight(...) instead' ); // eslint-disable-line no-console warned.insertRight = true; } return this.prependRight( index, content ); }, 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 || null; if ( !newLeft ) this.firstChunk = first; if ( !newRight ) this.lastChunk = last; if ( DEBUG ) this.stats.timeEnd( 'move' ); return this; }, overwrite ( start, end, content, options ) { 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 appendLeft or prependRight instead' ); if ( DEBUG ) this.stats.time( 'overwrite' ); this._split( start ); this._split( end ); if ( options === true ) { if ( !warned.storeName ) { console.warn( 'The final argument to magicString.overwrite(...) should be an options object. See https://github.com/rich-harris/magic-string' ); // eslint-disable-line no-console warned.storeName = true; } options = { storeName: true }; } const storeName = options !== undefined ? options.storeName : false; const contentOnly = options !== undefined ? options.contentOnly : false; 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 ) { if ( end > first.end && first.next !== this.byStart[ first.end ] ) { throw new Error( 'Cannot overwrite across a split point' ); } first.edit( content, storeName, contentOnly ); 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; }, prependLeft ( 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.byEnd[ index ]; if ( chunk ) { chunk.prependLeft( content ); } else { this.intro = content + this.intro; } if ( DEBUG ) this.stats.timeEnd( 'insertRight' ); return this; }, prependRight ( 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.prependRight( content ); } else { this.outro = content + this.outro; } if ( DEBUG ) this.stats.timeEnd( 'insertRight' ); 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' ); if ( DEBUG ) this.stats.time( 'remove' ); this._split( start ); this._split( end ); let chunk = this.byStart[ start ]; while ( chunk ) { chunk.intro = ''; chunk.outro = ''; chunk.edit( '' ); chunk = end > chunk.end ? this.byStart[ chunk.end ] : null; } if ( DEBUG ) this.stats.timeEnd( 'remove' ); return this; }, 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.`); const 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 ) { if ( this.lastChunk === chunk ) { this.lastChunk = chunk.next; } this.byEnd[ chunk.end ] = chunk; this.byStart[ chunk.next.start ] = chunk.next; this.byEnd[ chunk.next.end ] = 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; this.byEnd[ chunk.next.end ] = chunk.next; } if ( aborted ) return this; chunk = chunk.next; } while ( chunk ); return this; } };