UNPKG

tiny-essentials

Version:

Collection of small, essential scripts designed to be used across various projects. These simple utilities are crafted for speed, ease of use, and versatility.

256 lines (255 loc) 7.99 kB
/** * Represents the result of a single text segment comparison. * @typedef {Object} DiffResult * @property {string} value - The actual string content of the segment. * @property {"normal"|"added"|"deleted"} type - The status of the segment relative to the comparison. */ /** * A utility class to store a history of text strings and compute the differences * between any two versions, generating a detailed diff output. */ class TinyTextDiffer { /** @type {string[]} */ #history = []; /** @type {boolean} */ #destroyed = false; /** * Retrieves a shallow copy of the current history array. * @returns {string[]} */ get history() { return [...this.#history]; } /** * Overwrites the current history array, ensuring all elements are valid strings. * @param {string[]} history */ set history(history) { if (!Array.isArray(history)) { throw new TypeError('History data must be provided as an array.'); } if (!history.every((item) => typeof item === 'string')) { throw new TypeError('All items in the history array must be strings.'); } /** @type {string[]} */ this.#history = [...history]; } /** * Gets the total number of versions currently stored in the history. * @returns {number} */ get size() { return this.#history.length; } /** * Initializes a new instance of TinyTextDiffer. * @param {string[]} [history=[]] */ constructor(history = []) { this.history = history; } /** * @throws {Error} * @returns {void} */ #checkDestroyed() { if (this.#destroyed) { throw new Error('Cannot perform operations on a destroyed TinyTextDiffer instance.'); } } /** * @param {any} index * @throws {TypeError} * @returns {void} */ #checkIndex(index) { if (typeof index !== 'number') { throw new TypeError('The provided index must be a valid number.'); } } /** * @param {any} text * @throws {TypeError} * @returns {void} */ #checkText(text) { if (typeof text !== 'string') { throw new TypeError('The provided text must be a valid string.'); } } /** * Retrieves the text string at the specified index. * @param {number} index * @returns {string} */ get(index) { this.#checkDestroyed(); this.#checkIndex(index); if (typeof this.#history[index] === 'undefined') { throw new Error(`No text version found at index ${index}.`); } return this.#history[index]; } /** * Checks if a text string exists at the specified index. * @param {number} index * @returns {boolean} */ has(index) { this.#checkDestroyed(); this.#checkIndex(index); return typeof this.#history[index] !== 'undefined'; } /** * Appends a new text string to the end of the history. * @param {string} text * @returns {void} */ add(text) { this.#checkDestroyed(); this.#checkText(text); this.#history.push(text); } /** * Removes and returns the last text string from the history. * @returns {string | undefined} */ remove() { this.#checkDestroyed(); return this.#history.pop(); } /** * Inserts a text string at the specified index position. * @param {number} index * @param {string} text * @returns {void} */ addAt(index, text) { this.#checkDestroyed(); this.#checkIndex(index); this.#checkText(text); this.#history.splice(index, 0, text); } /** * Removes the text string at the specified index position. * @param {number} index * @returns {boolean} */ removeAt(index) { this.#checkDestroyed(); this.#checkIndex(index); const oldSize = this.#history.length; this.#history.splice(index, 1); const newSize = this.#history.length; return oldSize !== newSize; } /** * Empties the entire history. * @returns {void} */ clear() { this.#checkDestroyed(); this.#history = []; } /** * Compares multiple pairs of text strings from the history based on their indices. * Each pair of indices (e.g., index1 and index2) will produce a DiffResult array. * @param {...number} indexes - An even number of indices to be compared in pairs. * @returns {(DiffResult[])[]} An array of DiffResult arrays for each compared pair. */ compare(...indexes) { this.#checkDestroyed(); /** @type {number} */ const totalIndexes = indexes.length; if (totalIndexes === 0 || totalIndexes % 2 !== 0) { throw new Error('The compare method requires an even number of indices to form comparison pairs.'); } /** @type {(DiffResult[])[]} */ const results = []; for (let i = 0; i < totalIndexes; i += 2) { const i2 = i + 1; if (i2 > totalIndexes - 1) continue; this.#checkIndex(indexes[i]); this.#checkIndex(indexes[i2]); /** @type {string} */ const str1 = this.#history[indexes[i]]; /** @type {string} */ const str2 = this.#history[indexes[i2]]; results.push(this._computeDiff(str1, str2)); } return results; } /** * Computes the Longest Common Subsequence (LCS) to generate the diff result. * @private * @param {string} str1 * @param {string} str2 * @returns {DiffResult[]} */ _computeDiff(str1, str2) { /** @type {number} */ const len1 = str1.length; /** @type {number} */ const len2 = str2.length; /** @type {number[][]} */ const matrix = Array(len1 + 1) .fill(null) .map(() => Array(len2 + 1).fill(0)); /** @type {number} */ let i = 1; /** @type {number} */ let j = 1; for (i = 1; i <= len1; i++) { for (j = 1; j <= len2; j++) { if (str1[i - 1] === str2[j - 1]) { matrix[i][j] = matrix[i - 1][j - 1] + 1; } else { matrix[i][j] = Math.max(matrix[i - 1][j], matrix[i][j - 1]); } } } i = len1; j = len2; /** @type {DiffResult[]} */ const result = []; while (i > 0 || j > 0) { if (i > 0 && j > 0 && str1[i - 1] === str2[j - 1]) { result.unshift({ value: str1[i - 1], type: 'normal' }); i--; j--; } else if (j > 0 && (i === 0 || matrix[i][j - 1] >= matrix[i - 1][j])) { result.unshift({ value: str2[j - 1], type: 'added' }); j--; } else if (i > 0 && (j === 0 || matrix[i][j - 1] < matrix[i - 1][j])) { result.unshift({ value: str1[i - 1], type: 'deleted' }); i--; } } return result.reduce((/** @type {DiffResult[]} */ acc, /** @type {DiffResult} */ curr) => { /** @type {number} */ const lastIndex = acc.length - 1; if (acc.length > 0 && acc[lastIndex].type === curr.type) { acc[lastIndex].value += curr.value; } else { acc.push(curr); } return acc; }, []); } /** * Cleans up internal references and marks the instance as destroyed to prevent memory leaks. * @returns {void} */ destroy() { if (this.#destroyed) return; this.#history = []; this.#destroyed = true; } } export default TinyTextDiffer;