UNPKG

4.94 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
14 const normal = (line) => {
15 currentChunk?.changes.push({
16 type: "normal",
17 normal: true,
18 ln1: deletedLineCounter++,
19 ln2: addedLineCounter++,
20 content: line,
21 });
22 };
23
24 const start = (line) => {
25 const [fromFileName, toFileName] = parseFiles(line) ?? [];
26
27 currentFile = {
28 chunks: [],
29 deletions: 0,
30 additions: 0,
31 from: fromFileName,
32 to: toFileName,
33 };
34
35 files.push(currentFile);
36 };
37
38 const restart = () => {
39 if (!currentFile || currentFile.chunks.length) start();
40 };
41
42 const newFile = () => {
43 restart();
44 currentFile.new = true;
45 currentFile.from = "/dev/null";
46 };
47
48 const deletedFile = () => {
49 restart();
50 currentFile.deleted = true;
51 currentFile.to = "/dev/null";
52 };
53
54 const index = (line) => {
55 restart();
56 currentFile.index = line.split(" ").slice(1);
57 };
58
59 const fromFile = (line) => {
60 restart();
61 currentFile.from = parseOldOrNewFile(line);
62 };
63
64 const toFile = (line) => {
65 restart();
66 currentFile.to = parseOldOrNewFile(line);
67 };
68
69 const chunk = (line, match) => {
70 if (!currentFile) return;
71
72 const [oldStart, oldNumLines, newStart, newNumLines] = match.slice(1);
73
74 deletedLineCounter = +oldStart;
75 addedLineCounter = +newStart;
76 currentChunk = {
77 content: line,
78 changes: [],
79 oldStart: +oldStart,
80 oldLines: +(oldNumLines || 1),
81 newStart: +newStart,
82 newLines: +(newNumLines || 1),
83 };
84 currentFile.chunks.push(currentChunk);
85 };
86
87 const del = (line) => {
88 if (!currentChunk) return;
89
90 currentChunk.changes.push({
91 type: "del",
92 del: true,
93 ln: deletedLineCounter++,
94 content: line,
95 });
96 currentFile.deletions++;
97 };
98
99 const add = (line) => {
100 if (!currentChunk) return;
101
102 currentChunk.changes.push({
103 type: "add",
104 add: true,
105 ln: addedLineCounter++,
106 content: line,
107 });
108 currentFile.additions++;
109 };
110
111 const eof = (line) => {
112 if (!currentChunk) return;
113
114 const [mostRecentChange] = currentChunk.changes.slice(-1);
115
116 currentChunk.changes.push({
117 type: mostRecentChange.type,
118 [mostRecentChange.type]: true,
119 ln1: mostRecentChange.ln1,
120 ln2: mostRecentChange.ln2,
121 ln: mostRecentChange.ln,
122 content: line,
123 });
124 };
125
126 const schema = [
127 // TODO: better regexp to avoid detect normal line starting with diff
128 [/^\s+/, normal],
129 [/^diff\s/, start],
130 [/^new file mode \d+$/, newFile],
131 [/^deleted file mode \d+$/, deletedFile],
132 [/^index\s[\da-zA-Z]+\.\.[\da-zA-Z]+(\s(\d+))?$/, index],
133 [/^---\s/, fromFile],
134 [/^\+\+\+\s/, toFile],
135 [/^@@\s+-(\d+),?(\d+)?\s+\+(\d+),?(\d+)?\s@@/, chunk],
136 [/^-/, del],
137 [/^\+/, add],
138 [/^\\ No newline at end of file$/, eof],
139 ];
140
141 const parseLine = (line) => {
142 for (const [pattern, handler] of schema) {
143 const match = line.match(pattern);
144 if (match) {
145 handler(line, match);
146 return true;
147 }
148 }
149 return false;
150 };
151
152 for (const line of lines) parseLine(line);
153
154 return files;
155};
156
157const fileNameDiffRegex = /a\/.*(?=["']? ["']?b\/)|b\/.*$/g;
158const gitFileHeaderRegex = /^(a|b)\//;
159const parseFiles = (line) => {
160 let fileNames = line?.match(fileNameDiffRegex);
161 return fileNames?.map((fileName) =>
162 fileName.replace(gitFileHeaderRegex, "").replace(/("|')$/, "")
163 );
164};
165
166const qoutedFileNameRegex = /^\\?['"]|\\?['"]$/g;
167const parseOldOrNewFile = (line) => {
168 let fileName = leftTrimChars(line, "-+").trim();
169 fileName = removeTimeStamp(fileName);
170 return fileName
171 .replace(qoutedFileNameRegex, "")
172 .replace(gitFileHeaderRegex, "");
173};
174
175const leftTrimChars = (string, trimmingChars) => {
176 string = makeString(string);
177 if (!trimmingChars && String.prototype.trimLeft) return string.trimLeft();
178
179 let trimmingString = formTrimmingString(trimmingChars);
180
181 return string.replace(new RegExp(`^${trimmingString}+`), "");
182};
183
184const timeStampRegex = /\t.*|\d{4}-\d\d-\d\d\s\d\d:\d\d:\d\d(.\d+)?\s(\+|-)\d\d\d\d/;
185const removeTimeStamp = (string) => {
186 const timeStamp = timeStampRegex.exec(string);
187 if (timeStamp) {
188 string = string.substring(0, timeStamp.index).trim();
189 }
190 return string;
191};
192
193const formTrimmingString = (trimmingChars) => {
194 if (trimmingChars === null || trimmingChars === undefined) return "\\s";
195 else if (trimmingChars instanceof RegExp) return trimmingChars.source;
196 return `[${makeString(trimmingChars).replace(
197 /([.*+?^=!:${}()|[\]/\\])/g,
198 "\\$1"
199 )}]`;
200};
201
202const makeString = (itemToConvert) => (itemToConvert ?? "") + "";