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}