UNPKG

5.83 kBJavaScriptView Raw
1module.exports = (input) => {
2 if (!input) return [];
3 if (typeof input !== "string" || input.match(/^\s+$/)) return [];
4
5 const lines = input.split("\n");
6 if (lines.length === 0) return [];
7
8 const files = [];
9 let currentFile = null;
10 let currentChunk = null;
11 let deletedLineCounter = 0;
12 let addedLineCounter = 0;
13 let currentFileChanges = null;
14
15 const normal = (line) => {
16 currentChunk?.changes.push({
17 type: "normal",
18 normal: true,
19 ln1: deletedLineCounter++,
20 ln2: addedLineCounter++,
21 content: line,
22 });
23 currentFileChanges.oldLines--;
24 currentFileChanges.newLines--;
25 };
26
27 const start = (line) => {
28 const [fromFileName, toFileName] = parseFiles(line) ?? [];
29
30 currentFile = {
31 chunks: [],
32 deletions: 0,
33 additions: 0,
34 from: fromFileName,
35 to: toFileName,
36 };
37
38 files.push(currentFile);
39 };
40
41 const restart = () => {
42 if (!currentFile || currentFile.chunks.length) start();
43 };
44
45 const newFile = () => {
46 restart();
47 currentFile.new = true;
48 currentFile.from = "/dev/null";
49 };
50
51 const deletedFile = () => {
52 restart();
53 currentFile.deleted = true;
54 currentFile.to = "/dev/null";
55 };
56
57 const index = (line) => {
58 restart();
59 currentFile.index = line.split(" ").slice(1);
60 };
61
62 const fromFile = (line) => {
63 restart();
64 currentFile.from = parseOldOrNewFile(line);
65 };
66
67 const toFile = (line) => {
68 restart();
69 currentFile.to = parseOldOrNewFile(line);
70 };
71
72 const toNumOfLines = (number) => +(number || 1);
73
74 const chunk = (line, match) => {
75 if (!currentFile) return;
76
77 const [oldStart, oldNumLines, newStart, newNumLines] = match.slice(1);
78
79 deletedLineCounter = +oldStart;
80 addedLineCounter = +newStart;
81 currentChunk = {
82 content: line,
83 changes: [],
84 oldStart: +oldStart,
85 oldLines: toNumOfLines(oldNumLines),
86 newStart: +newStart,
87 newLines: toNumOfLines(newNumLines),
88 };
89 currentFileChanges = {
90 oldLines: toNumOfLines(oldNumLines),
91 newLines: toNumOfLines(newNumLines),
92 };
93 currentFile.chunks.push(currentChunk);
94 };
95
96 const del = (line) => {
97 if (!currentChunk) return;
98
99 currentChunk.changes.push({
100 type: "del",
101 del: true,
102 ln: deletedLineCounter++,
103 content: line,
104 });
105 currentFile.deletions++;
106 currentFileChanges.oldLines--;
107 };
108
109 const add = (line) => {
110 if (!currentChunk) return;
111
112 currentChunk.changes.push({
113 type: "add",
114 add: true,
115 ln: addedLineCounter++,
116 content: line,
117 });
118 currentFile.additions++;
119 currentFileChanges.newLines--;
120 };
121
122 const eof = (line) => {
123 if (!currentChunk) return;
124
125 const [mostRecentChange] = currentChunk.changes.slice(-1);
126
127 currentChunk.changes.push({
128 type: mostRecentChange.type,
129 [mostRecentChange.type]: true,
130 ln1: mostRecentChange.ln1,
131 ln2: mostRecentChange.ln2,
132 ln: mostRecentChange.ln,
133 content: line,
134 });
135 };
136
137 const schemaHeaders = [
138 [/^diff\s/, start],
139 [/^new file mode \d+$/, newFile],
140 [/^deleted file mode \d+$/, deletedFile],
141 [/^index\s[\da-zA-Z]+\.\.[\da-zA-Z]+(\s(\d+))?$/, index],
142 [/^---\s/, fromFile],
143 [/^\+\+\+\s/, toFile],
144 [/^@@\s+-(\d+),?(\d+)?\s+\+(\d+),?(\d+)?\s@@/, chunk],
145 [/^\\ No newline at end of file$/, eof],
146 ];
147
148 const schemaContent = [
149 [/^-/, del],
150 [/^\+/, add],
151 [/^\s+/, normal],
152 ];
153
154 const parseContentLine = (line) => {
155 for (const [pattern, handler] of schemaContent) {
156 const match = line.match(pattern);
157 if (match) {
158 handler(line, match);
159 break;
160 }
161 }
162 if (
163 currentFileChanges.oldLines === 0 &&
164 currentFileChanges.newLines === 0
165 ) {
166 currentFileChanges = null;
167 }
168 };
169
170 const parseHeaderLine = (line) => {
171 for (const [pattern, handler] of schemaHeaders) {
172 const match = line.match(pattern);
173 if (match) {
174 handler(line, match);
175 break;
176 }
177 }
178 };
179
180 const parseLine = (line) => {
181 if (currentFileChanges) {
182 parseContentLine(line);
183 } else {
184 parseHeaderLine(line);
185 }
186 return;
187 };
188
189 for (const line of lines) parseLine(line);
190
191 return files;
192};
193
194const fileNameDiffRegex =
195 /(a|i|w|c|o|1|2)\/.*(?=["']? ["']?(b|i|w|c|o|1|2)\/)|(b|i|w|c|o|1|2)\/.*$/g;
196const gitFileHeaderRegex = /^(a|b|i|w|c|o|1|2)\//;
197const parseFiles = (line) => {
198 let fileNames = line?.match(fileNameDiffRegex);
199 return fileNames?.map((fileName) =>
200 fileName.replace(gitFileHeaderRegex, "").replace(/("|')$/, "")
201 );
202};
203
204const qoutedFileNameRegex = /^\\?['"]|\\?['"]$/g;
205const parseOldOrNewFile = (line) => {
206 let fileName = leftTrimChars(line, "-+").trim();
207 fileName = removeTimeStamp(fileName);
208 return fileName
209 .replace(qoutedFileNameRegex, "")
210 .replace(gitFileHeaderRegex, "");
211};
212
213const leftTrimChars = (string, trimmingChars) => {
214 string = makeString(string);
215 if (!trimmingChars && String.prototype.trimLeft) return string.trimLeft();
216
217 let trimmingString = formTrimmingString(trimmingChars);
218
219 return string.replace(new RegExp(`^${trimmingString}+`), "");
220};
221
222const timeStampRegex =
223 /\t.*|\d{4}-\d\d-\d\d\s\d\d:\d\d:\d\d(.\d+)?\s(\+|-)\d\d\d\d/;
224const removeTimeStamp = (string) => {
225 const timeStamp = timeStampRegex.exec(string);
226 if (timeStamp) {
227 string = string.substring(0, timeStamp.index).trim();
228 }
229 return string;
230};
231
232const formTrimmingString = (trimmingChars) => {
233 if (trimmingChars === null || trimmingChars === undefined) return "\\s";
234 else if (trimmingChars instanceof RegExp) return trimmingChars.source;
235 return `[${makeString(trimmingChars).replace(
236 /([.*+?^=!:${}()|[\]/\\])/g,
237 "\\$1"
238 )}]`;
239};
240
241const makeString = (itemToConvert) => (itemToConvert ?? "") + "";