UNPKG

4 kBJavaScriptView Raw
1// Detect either spaces or tabs but not both to properly handle tabs for indentation and spaces for alignment
2const INDENT_REGEX = /^(?:( )+|\t+)/;
3
4const INDENT_TYPE_SPACE = 'space';
5const INDENT_TYPE_TAB = 'tab';
6
7/**
8Make a Map that counts how many indents/unindents have occurred for a given size and how many lines follow a given indentation.
9
10The key is a concatenation of the indentation type (s = space and t = tab) and the size of the indents/unindents.
11
12```
13indents = {
14 t3: [1, 0],
15 t4: [1, 5],
16 s5: [1, 0],
17 s12: [1, 0],
18}
19```
20*/
21function makeIndentsMap(string, ignoreSingleSpaces) {
22 const indents = new Map();
23
24 // Remember the size of previous line's indentation
25 let previousSize = 0;
26 let previousIndentType;
27
28 // Indents key (ident type + size of the indents/unindents)
29 let key;
30
31 for (const line of string.split(/\n/g)) {
32 if (!line) {
33 // Ignore empty lines
34 continue;
35 }
36
37 let indent;
38 let indentType;
39 let weight;
40 let entry;
41 const matches = line.match(INDENT_REGEX);
42
43 if (matches === null) {
44 previousSize = 0;
45 previousIndentType = '';
46 } else {
47 indent = matches[0].length;
48 indentType = matches[1] ? INDENT_TYPE_SPACE : INDENT_TYPE_TAB;
49
50 // Ignore single space unless it's the only indent detected to prevent common false positives
51 if (ignoreSingleSpaces && indentType === INDENT_TYPE_SPACE && indent === 1) {
52 continue;
53 }
54
55 if (indentType !== previousIndentType) {
56 previousSize = 0;
57 }
58
59 previousIndentType = indentType;
60
61 weight = 0;
62
63 const indentDifference = indent - previousSize;
64 previousSize = indent;
65
66 // Previous line have same indent?
67 if (indentDifference === 0) {
68 weight++;
69 // We use the key from previous loop
70 } else {
71 const absoluteIndentDifference = indentDifference > 0 ? indentDifference : -indentDifference;
72 key = encodeIndentsKey(indentType, absoluteIndentDifference);
73 }
74
75 // Update the stats
76 entry = indents.get(key);
77 entry = entry === undefined ? [1, 0] : [++entry[0], entry[1] + weight];
78
79 indents.set(key, entry);
80 }
81 }
82
83 return indents;
84}
85
86// Encode the indent type and amount as a string (e.g. 's4') for use as a compound key in the indents Map.
87function encodeIndentsKey(indentType, indentAmount) {
88 const typeCharacter = indentType === INDENT_TYPE_SPACE ? 's' : 't';
89 return typeCharacter + String(indentAmount);
90}
91
92// Extract the indent type and amount from a key of the indents Map.
93function decodeIndentsKey(indentsKey) {
94 const keyHasTypeSpace = indentsKey[0] === 's';
95 const type = keyHasTypeSpace ? INDENT_TYPE_SPACE : INDENT_TYPE_TAB;
96
97 const amount = Number(indentsKey.slice(1));
98
99 return {type, amount};
100}
101
102// Return the key (e.g. 's4') from the indents Map that represents the most common indent,
103// or return undefined if there are no indents.
104function getMostUsedKey(indents) {
105 let result;
106 let maxUsed = 0;
107 let maxWeight = 0;
108
109 for (const [key, [usedCount, weight]] of indents) {
110 if (usedCount > maxUsed || (usedCount === maxUsed && weight > maxWeight)) {
111 maxUsed = usedCount;
112 maxWeight = weight;
113 result = key;
114 }
115 }
116
117 return result;
118}
119
120function makeIndentString(type, amount) {
121 const indentCharacter = type === INDENT_TYPE_SPACE ? ' ' : '\t';
122 return indentCharacter.repeat(amount);
123}
124
125export default function detectIndent(string) {
126 if (typeof string !== 'string') {
127 throw new TypeError('Expected a string');
128 }
129
130 // Identify indents while skipping single space indents to avoid common edge cases (e.g. code comments)
131 // If no indents are identified, run again and include all indents for comprehensive detection
132 let indents = makeIndentsMap(string, true);
133 if (indents.size === 0) {
134 indents = makeIndentsMap(string, false);
135 }
136
137 const keyOfMostUsedIndent = getMostUsedKey(indents);
138
139 let type;
140 let amount = 0;
141 let indent = '';
142
143 if (keyOfMostUsedIndent !== undefined) {
144 ({type, amount} = decodeIndentsKey(keyOfMostUsedIndent));
145 indent = makeIndentString(type, amount);
146 }
147
148 return {
149 amount,
150 type,
151 indent,
152 };
153}