nx/packages/devkit/src/utils/string-change.ts
2021-01-19 16:45:44 -05:00

147 lines
3.5 KiB
TypeScript

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<StringInsertion | StringDeletion>) {
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;
}
}
}