leetkick
Version:
A CLI tool for scaffolding LeetCode exercises with language-specific testing setups
426 lines (385 loc) • 12.4 kB
text/typescript
import {readFile, writeFile, mkdir} from 'fs/promises';
import {join, dirname} from 'path';
import {fileURLToPath} from 'url';
import type {Problem} from '../types/leetcode.js';
// @ts-ignore
import TurndownService from 'turndown';
const __dirname = dirname(fileURLToPath(import.meta.url));
const TEMPLATES_DIR = join(__dirname, '../../../templates');
export async function createProblemFiles(
problem: Problem,
language: string,
): Promise<void> {
const templateDir = join(TEMPLATES_DIR, language);
const paddedId = problem.questionFrontendId.padStart(4, '0');
const problemDirName = `problem_${paddedId}`;
const languageDir = join(process.cwd(), language);
// For Kotlin and Java, we need to create src/main/{lang} and src/test/{lang} structure
// For Rust, we need src/ directory for Cargo
// For Python, we need src/{problem_pkg} and tests/{problem_pkg} structure
let problemDir: string;
let testDir: string;
if (language === 'kotlin' || language === 'java') {
const problemPackage = `problem${paddedId}`;
problemDir = join(languageDir, 'src', 'main', language, problemPackage);
testDir = join(languageDir, 'src', 'test', language, problemPackage);
await mkdir(problemDir, {recursive: true});
await mkdir(testDir, {recursive: true});
} else if (language === 'python') {
problemDir = join(languageDir, 'src', problemDirName);
testDir = join(languageDir, 'tests', problemDirName);
await mkdir(problemDir, {recursive: true});
await mkdir(testDir, {recursive: true});
} else if (language === 'rust') {
problemDir = join(languageDir, 'src');
testDir = problemDir;
await mkdir(problemDir, {recursive: true});
} else if (language === 'go') {
problemDir = join(languageDir, problemDirName);
testDir = problemDir;
await mkdir(problemDir, {recursive: true});
} else {
problemDir = join(languageDir, problemDirName);
testDir = problemDir;
await mkdir(problemDir, {recursive: true});
}
// Find the code snippet for this language
const codeSnippet = problem.codeSnippets.find(
snippet => snippet.langSlug === getLanguageSlug(language),
);
let defaultCode =
codeSnippet?.code || getDefaultCodeForLanguage(language, problem.title);
// Process JavaScript code to make it exportable
if (language === 'javascript' && codeSnippet?.code) {
defaultCode = processJavaScriptCode(defaultCode);
}
// Generate clean names based on language conventions
const className = formatClassName(problem.title);
const snakeCaseName = formatSnakeCase(problem.title);
const camelCaseName = formatProblemName(problem.title);
// Extract function name from the code snippet
const functionName = extractFunctionName(defaultCode) || camelCaseName;
// Template replacements
const replacements = {
__PROBLEM_ID__: problem.questionFrontendId,
__PROBLEM_TITLE__: problem.title,
__PROBLEM_DESC_MARKDOWN__: convertHtmlToMarkdown(problem.content),
__PROBLEM_DIFFICULTY__: problem.difficulty,
__PROBLEM_DEFAULT_CODE__: defaultCode,
__PROBLEM_NAME_FORMATTED__: functionName,
__PROBLEM_NAME_SNAKE_CASE__: snakeCaseName,
__CLASS_NAME__: className,
__SNAKE_CASE_NAME__: snakeCaseName,
__PROBLEM_PACKAGE__:
language === 'kotlin' || language === 'java'
? `problem${paddedId}`
: language === 'go'
? `problem_${paddedId}`
: language === 'python'
? `problem_${paddedId}`
: '',
__PROBLEM_CLASS_NAME__: className,
__EXERCISE_FILE_NAME__: getExerciseFileName(
className,
snakeCaseName,
language,
paddedId,
),
__EXERCISE_FILE_NAME_NO_EXT__: getExerciseFileNameNoExt(
className,
snakeCaseName,
language,
paddedId,
),
};
// Create exercise file
const exerciseTemplate = await readFile(
join(templateDir, 'exercise_template.' + getFileExtension(language)),
'utf-8',
);
const exerciseContent = replaceTemplateVars(exerciseTemplate, replacements);
await writeFile(
join(
problemDir,
getExerciseFileName(className, snakeCaseName, language, paddedId),
),
exerciseContent,
);
// Create README.md file
const readmeTemplate = await readFile(
join(TEMPLATES_DIR, 'README_template.md'),
'utf-8',
);
const readmeContent = replaceTemplateVars(readmeTemplate, replacements);
await writeFile(join(problemDir, 'README.md'), readmeContent);
// For Rust, add module declaration to lib.rs
if (language === 'rust') {
await addModuleToLibRs(languageDir, `problem_${paddedId}`);
}
// Python uses implicit namespace packages (no __init__.py needed)
// Create test file (skip for Rust as tests are in the same file)
if (language !== 'rust') {
const testTemplate = await readFile(
join(templateDir, 'test_template.' + getFileExtension(language)),
'utf-8',
);
const testContent = replaceTemplateVars(testTemplate, replacements);
await writeFile(
join(testDir, getTestFileName(className, snakeCaseName, language)),
testContent,
);
}
}
function replaceTemplateVars(
template: string,
replacements: Record<string, string>,
): string {
let result = template;
for (const [key, value] of Object.entries(replacements)) {
result = result.replace(new RegExp(key, 'g'), value);
}
return result;
}
function convertHtmlToMarkdown(htmlContent: string): string {
// @ts-ignore
const turndownService = new TurndownService({
headingStyle: 'atx',
codeBlockStyle: 'fenced',
});
return turndownService.turndown(htmlContent).trim();
}
function formatProblemName(title: string): string {
// Convert "Two Sum" to "twoSum" for function names
return title
.replace(/[^a-zA-Z0-9\s]/g, '')
.split(' ')
.map((word, index) =>
index === 0
? word.toLowerCase()
: word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
)
.join('');
}
function formatClassName(title: string): string {
// Convert "Two Sum" to "TwoSum" for class names
return title
.replace(/[^a-zA-Z0-9\s]/g, '')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join('');
}
function formatSnakeCase(title: string): string {
// Convert "Two Sum" to "two_sum" for C++ files
return title
.replace(/[^a-zA-Z0-9\s]/g, '')
.toLowerCase()
.split(/\s+/)
.join('_');
}
function getLanguageSlug(language: string): string {
const slugMap: Record<string, string> = {
typescript: 'typescript',
javascript: 'javascript',
python: 'python3',
java: 'java',
cpp: 'cpp',
go: 'golang',
rust: 'rust',
kotlin: 'kotlin',
};
return slugMap[language] || language;
}
function getFileExtension(language: string): string {
const extMap: Record<string, string> = {
typescript: 'ts',
javascript: 'js',
python: 'py',
java: 'java',
cpp: 'cpp',
go: 'go',
rust: 'rs',
kotlin: 'kt',
};
return extMap[language] || 'txt';
}
function getExerciseFileName(
className: string,
snakeCaseName: string,
language: string,
paddedId?: string,
): string {
const ext = getFileExtension(language);
switch (language) {
case 'typescript':
case 'javascript':
return `${className}.${ext}`;
case 'cpp':
case 'c':
return `${snakeCaseName}.${ext}`;
case 'kotlin':
case 'java':
return `${className}.${ext}`;
case 'rust':
return `problem_${paddedId || '0001'}.rs`;
case 'go':
return `${snakeCaseName}.${ext}`;
case 'python':
return `${snakeCaseName}.${ext}`;
default:
return `${className}.${ext}`;
}
}
function getExerciseFileNameNoExt(
className: string,
snakeCaseName: string,
language: string,
paddedId?: string,
): string {
switch (language) {
case 'typescript':
case 'javascript':
return className;
case 'cpp':
case 'c':
return snakeCaseName;
case 'kotlin':
case 'java':
return className;
case 'rust':
return `problem_${paddedId || '0001'}`;
case 'go':
return 'solution';
case 'python':
return snakeCaseName;
default:
return className;
}
}
function getTestFileName(
className: string,
snakeCaseName: string,
language: string,
): string {
const ext = getFileExtension(language);
switch (language) {
case 'typescript':
case 'javascript':
return `${className}.test.${ext}`;
case 'cpp':
case 'c':
return `${snakeCaseName}.test.${ext}`;
case 'kotlin':
return `${className}Test.${ext}`;
case 'java':
return `${className}Test.${ext}`;
case 'python':
return `test_${snakeCaseName}.${ext}`;
case 'go':
return `${snakeCaseName}_test.${ext}`;
case 'rust':
return 'lib.rs'; // Tests are in the same file for Rust
default:
return `${className}.test.${ext}`;
}
}
function getDefaultCodeForLanguage(language: string, title: string): string {
switch (language) {
case 'kotlin':
return `class Solution {
// TODO: Implement solution for ${title}
}`;
case 'java':
return `class Solution {
// TODO: Implement solution for ${title}
}`;
case 'typescript':
case 'javascript':
return `// TODO: Implement solution for ${title}`;
case 'cpp':
return `class Solution {
public:
// TODO: Implement solution for ${title}
};`;
case 'go':
return `// TODO: Implement solution for ${title}`;
default:
return `// TODO: Implement solution for ${title}`;
}
}
async function addModuleToLibRs(
languageDir: string,
moduleName: string,
): Promise<void> {
const libPath = join(languageDir, 'src', 'lib.rs');
try {
let libContent = await readFile(libPath, 'utf-8');
// Check if module is already declared
const moduleDeclaration = `pub mod ${moduleName};`;
if (libContent.includes(moduleDeclaration)) {
return; // Module already declared
}
// Add module declaration at the end
libContent += `\npub mod ${moduleName};\n`;
await writeFile(libPath, libContent);
} catch (error) {
throw new Error(`Failed to update lib.rs: ${error}`);
}
}
function processJavaScriptCode(code: string): string {
// Convert var declarations to export const/function
if (code.includes('var ') && code.includes(' = function(')) {
// Handle: var twoSum = function(nums, target) { ... };
code = code.replace(
/var\s+(\w+)\s*=\s*function\s*\(/g,
'export function $1(',
);
code = code.replace(/;\s*$/, ''); // Remove trailing semicolon
} else if (code.includes('function ')) {
// Handle: function twoSum(nums, target) { ... }
code = code.replace(/^(\s*)function\s+/m, '$1export function ');
} else if (code.includes('const ') && code.includes(' = (')) {
// Handle: const twoSum = (nums, target) => { ... };
code = code.replace(/const\s+(\w+)\s*=/g, 'export const $1 =');
}
return code;
}
function extractFunctionName(code: string): string | null {
// Extract function name from C++ code (class method) - handle templates like vector<int>
const cppMethodMatch = code.match(/[\w<>]+\s+(\w+)\s*\([^)]*\)\s*\{/);
if (cppMethodMatch) {
return cppMethodMatch[1];
}
// Extract function name from Java code (public method in class)
const javaMethodMatch = code.match(
/public\s+[\w<>[\]]+\s+(\w+)\s*\([^)]*\)\s*\{/,
);
if (javaMethodMatch) {
return javaMethodMatch[1];
}
// Extract function name from Kotlin code
const kotlinFunMatch = code.match(/fun\s+(\w+)\s*\(/);
if (kotlinFunMatch) {
return kotlinFunMatch[1];
}
// Extract function name from TypeScript/JavaScript code
const functionMatch = code.match(/function\s+(\w+)\s*\(/);
if (functionMatch) {
return functionMatch[1];
}
// Extract function name from arrow function or method
const arrowMatch = code.match(/(\w+)\s*[=:]\s*\(/);
if (arrowMatch) {
return arrowMatch[1];
}
// Extract function name from Go code
const goFuncMatch = code.match(/func\s+(\w+)\s*\(/);
if (goFuncMatch) {
return goFuncMatch[1];
}
// Extract function name from Python code (def function or class method)
const pythonFuncMatch = code.match(/def\s+(\w+)\s*\(/);
if (pythonFuncMatch) {
return pythonFuncMatch[1];
}
return null;
}