'use strict'; // Detect either spaces or tabs but not both to properly handle tabs for indentation and spaces for alignment const INDENT_REGEX = /^(?:( )+|\t+)/; const INDENT_TYPE_SPACE = 'space'; const INDENT_TYPE_TAB = 'tab'; // Make a Map that counts how many indents/unindents have occurred for a given size and how many lines follow a given indentation. // The key is a concatenation of the indentation type (s = space and t = tab) and the size of the indents/unindents. // // indents = { // t3: [1, 0], // t4: [1, 5], // s5: [1, 0], // s12: [1, 0], // } function makeIndentsMap(string, ignoreSingleSpaces) { const indents = new Map(); // Remember the size of previous line's indentation let previousSize = 0; let previousIndentType; // Indents key (ident type + size of the indents/unindents) let key; for (const line of string.split(/\n/g)) { if (!line) { // Ignore empty lines continue; } let indent; let indentType; let weight; let entry; const matches = line.match(INDENT_REGEX); if (matches === null) { previousSize = 0; previousIndentType = ''; } else { indent = matches[0].length; if (matches[1]) { indentType = INDENT_TYPE_SPACE; } else { indentType = INDENT_TYPE_TAB; } // Ignore single space unless it's the only indent detected to prevent common false positives if (ignoreSingleSpaces && indentType === INDENT_TYPE_SPACE && indent === 1) { continue; } if (indentType !== previousIndentType) { previousSize = 0; } previousIndentType = indentType; weight = 0; const indentDifference = indent - previousSize; previousSize = indent; // Previous line have same indent? if (indentDifference === 0) { weight++; // We use the key from previous loop } else { const absoluteIndentDifference = indentDifference > 0 ? indentDifference : -indentDifference; key = encodeIndentsKey(indentType, absoluteIndentDifference); } // Update the stats entry = indents.get(key); if (entry === undefined) { entry = [1, 0]; // Init } else { entry = [++entry[0], entry[1] + weight]; } indents.set(key, entry); } } return indents; } // Encode the indent type and amount as a string (e.g. 's4') for use as a compound key in the indents Map. function encodeIndentsKey(indentType, indentAmount) { const typeCharacter = indentType === INDENT_TYPE_SPACE ? 's' : 't'; return typeCharacter + String(indentAmount); } // Extract the indent type and amount from a key of the indents Map. function decodeIndentsKey(indentsKey) { const keyHasTypeSpace = indentsKey[0] === 's'; const type = keyHasTypeSpace ? INDENT_TYPE_SPACE : INDENT_TYPE_TAB; const amount = Number(indentsKey.slice(1)); return {type, amount}; } // Return the key (e.g. 's4') from the indents Map that represents the most common indent, // or return undefined if there are no indents. function getMostUsedKey(indents) { let result; let maxUsed = 0; let maxWeight = 0; for (const [key, [usedCount, weight]] of indents) { if (usedCount > maxUsed || (usedCount === maxUsed && weight > maxWeight)) { maxUsed = usedCount; maxWeight = weight; result = key; } } return result; } function makeIndentString(type, amount) { const indentCharacter = type === INDENT_TYPE_SPACE ? ' ' : '\t'; return indentCharacter.repeat(amount); } module.exports = string => { if (typeof string !== 'string') { throw new TypeError('Expected a string'); } // Identify indents while skipping single space indents to avoid common edge cases (e.g. code comments) // If no indents are identified, run again and include all indents for comprehensive detection let indents = makeIndentsMap(string, true); if (indents.size === 0) { indents = makeIndentsMap(string, false); } const keyOfMostUsedIndent = getMostUsedKey(indents); let type; let amount = 0; let indent = ''; if (keyOfMostUsedIndent !== undefined) { ({type, amount} = decodeIndentsKey(keyOfMostUsedIndent)); indent = makeIndentString(type, amount); } return { amount, type, indent }; };