160 lines
4 KiB
JavaScript
160 lines
4 KiB
JavaScript
'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
|
|
};
|
|
};
|