diff-parser.ts
1import type { DiffFile, DiffHunk } from '$lib/types/index';
2
3export function parseDiff(raw: string): DiffFile[] {
4 if (!raw.trim()) return [];
5
6 const files: DiffFile[] = [];
7 const fileChunks = raw.split(/^diff --git /m).filter(Boolean);
8
9 for (const chunk of fileChunks) {
10 const lines = chunk.split('\n');
11 let path = '';
12 let oldPath: string | undefined;
13
14 // Parse file header
15 const headerMatch = lines[0]?.match(/a\/(.+?) b\/(.+)/);
16 if (headerMatch) {
17 oldPath = headerMatch[1];
18 path = headerMatch[2];
19 if (oldPath === path) oldPath = undefined;
20 }
21
22 const language = getLanguage(path);
23 const hunks: DiffHunk[] = [];
24 let currentHunk: DiffHunk | null = null;
25 let oldLineNum = 0;
26 let newLineNum = 0;
27
28 for (const line of lines) {
29 const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)/);
30 if (hunkMatch) {
31 const oldStart = parseInt(hunkMatch[1], 10);
32 const oldLines = hunkMatch[2] ? parseInt(hunkMatch[2], 10) : 1;
33 const newStart = parseInt(hunkMatch[3], 10);
34 const newLines = hunkMatch[4] ? parseInt(hunkMatch[4], 10) : 1;
35 currentHunk = {
36 header: line,
37 oldStart,
38 oldLines,
39 newStart,
40 newLines,
41 lines: []
42 };
43 hunks.push(currentHunk);
44 oldLineNum = oldStart;
45 newLineNum = newStart;
46 continue;
47 }
48
49 if (!currentHunk) continue;
50
51 if (line.startsWith('+')) {
52 currentHunk.lines.push({
53 type: 'add',
54 content: line.substring(1),
55 newNumber: newLineNum++
56 });
57 } else if (line.startsWith('-')) {
58 currentHunk.lines.push({
59 type: 'del',
60 content: line.substring(1),
61 oldNumber: oldLineNum++
62 });
63 } else if (line.startsWith(' ') || line === '') {
64 // Only treat as context if we're inside a hunk and it looks like a context line
65 if (line.startsWith(' ')) {
66 currentHunk.lines.push({
67 type: 'context',
68 content: line.substring(1),
69 oldNumber: oldLineNum++,
70 newNumber: newLineNum++
71 });
72 }
73 } else if (line.startsWith('\\')) {
74 // "No newline at end of file" marker — skip
75 }
76 }
77
78 if (path) {
79 files.push({ path, oldPath, hunks, language });
80 }
81 }
82
83 return files;
84}
85
86export function getLanguage(filePath: string): string {
87 const ext = filePath.split('.').pop()?.toLowerCase() || '';
88 const map: Record<string, string> = {
89 ts: 'typescript',
90 tsx: 'tsx',
91 js: 'javascript',
92 jsx: 'jsx',
93 py: 'python',
94 rb: 'ruby',
95 go: 'go',
96 rs: 'rust',
97 java: 'java',
98 kt: 'kotlin',
99 kts: 'kotlin',
100 swift: 'swift',
101 cs: 'csharp',
102 fs: 'fsharp',
103 php: 'php',
104 r: 'r',
105 lua: 'lua',
106 dart: 'dart',
107 scala: 'scala',
108 groovy: 'groovy',
109 pl: 'perl',
110 pm: 'perl',
111 ex: 'elixir',
112 exs: 'elixir',
113 erl: 'erlang',
114 hs: 'haskell',
115 clj: 'clojure',
116 css: 'css',
117 scss: 'scss',
118 less: 'less',
119 html: 'html',
120 htm: 'html',
121 xml: 'xml',
122 svg: 'xml',
123 svelte: 'svelte',
124 vue: 'vue',
125 json: 'json',
126 jsonc: 'jsonc',
127 yaml: 'yaml',
128 yml: 'yaml',
129 toml: 'toml',
130 ini: 'ini',
131 md: 'markdown',
132 mdx: 'mdx',
133 sh: 'bash',
134 bash: 'bash',
135 zsh: 'bash',
136 fish: 'fish',
137 ps1: 'powershell',
138 sql: 'sql',
139 graphql: 'graphql',
140 gql: 'graphql',
141 proto: 'proto',
142 c: 'c',
143 cpp: 'cpp',
144 cc: 'cpp',
145 cxx: 'cpp',
146 h: 'c',
147 hpp: 'cpp',
148 m: 'objective-c',
149 mm: 'objective-cpp',
150 zig: 'zig',
151 nim: 'nim',
152 tf: 'hcl',
153 dockerfile: 'dockerfile',
154 makefile: 'makefile',
155 cmake: 'cmake'
156 };
157 // Also check filename (no extension)
158 const filename = filePath.split('/').pop()?.toLowerCase() || '';
159 const filenameMap: Record<string, string> = {
160 dockerfile: 'dockerfile',
161 makefile: 'makefile',
162 gemfile: 'ruby',
163 rakefile: 'ruby',
164 podfile: 'ruby'
165 };
166 return map[ext] || filenameMap[filename] || 'text';
167}