export enum ChangeType { Delete = 'Delete', Insert = 'Insert', } export interface StringDeletion { type: ChangeType.Delete; /** * Place in the original text to start deleting characters */ start: number; /** * Number of characters to delete */ length: number; } export interface StringInsertion { type: ChangeType.Insert; /** * Text to insert into the original text */ text: string; /** * Place in the original text to insert new text */ index: number; } /** * A change to be made to a string */ export type StringChange = StringInsertion | StringDeletion; /** * Applies a list of changes to a string's original value. * * This is useful when working with ASTs. * * For Example, to rename a property in a method's options: * * ``` * const code = `bootstrap({ * target: document.querySelector('#app') * })`; * * const indexOfPropertyName = 13; // Usually determined by analyzing an AST. * const updatedCode = applyChangesToString(code, [ * { * type: ChangeType.Insert, * index: indexOfPropertyName, * text: 'element' * }, * { * type: ChangeType.Delete, * start: indexOfPropertyName, * length: 6 * }, * ]); * * bootstrap({ * element: document.querySelector('#app') * }); * ``` */ export function applyChangesToString( text: string, changes: StringChange[] ): string { assertChangesValid(changes); const sortedChanges = changes.sort((a, b) => { const diff = getChangeIndex(a) - getChangeIndex(b); if (diff === 0) { if (a.type === b.type) { return 0; } else { // When at the same place, Insert before Delete return a.type === ChangeType.Insert ? -1 : 1; } } return diff; }); let offset = 0; for (const change of sortedChanges) { const index = getChangeIndex(change) + offset; switch (change.type) { case ChangeType.Insert: { text = text.substr(0, index) + change.text + text.substr(index); offset += change.text.length; break; } case ChangeType.Delete: { text = text.substr(0, index) + text.substr(index + change.length); offset -= change.length; break; } } } return text; } function assertChangesValid(changes: Array) { for (const change of changes) { switch (change.type) { case ChangeType.Delete: { if (!Number.isInteger(change.start)) { throw new TypeError(`${change.start} must be an integer.`); } if (change.start < 0) { throw new Error(`${change.start} must be a positive integer.`); } if (!Number.isInteger(change.length)) { throw new TypeError(`${change.length} must be an integer.`); } if (change.length < 0) { throw new Error(`${change.length} must be a positive integer.`); } break; } case ChangeType.Insert: { if (!Number.isInteger(change.index)) { throw new TypeError(`${change.index} must be an integer.`); } if (change.index < 0) { throw new Error(`${change.index} must be a positive integer.`); } if (typeof change.text !== 'string') { throw new Error(`${change.text} must be a string.`); } } break; } } } function getChangeIndex(change: StringInsertion | StringDeletion) { switch (change.type) { case ChangeType.Insert: { return change.index; } case ChangeType.Delete: { return change.start; } } }