UNPKG

leetkick

Version:

A CLI tool for scaffolding LeetCode exercises with language-specific testing setups

330 lines 12.6 kB
import { readFile, writeFile, mkdir } from 'fs/promises'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; // @ts-ignore import TurndownService from 'turndown'; const __dirname = dirname(fileURLToPath(import.meta.url)); const TEMPLATES_DIR = join(__dirname, '../../../templates'); export async function createProblemFiles(problem, language) { 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; let testDir; 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, replacements) { let result = template; for (const [key, value] of Object.entries(replacements)) { result = result.replace(new RegExp(key, 'g'), value); } return result; } function convertHtmlToMarkdown(htmlContent) { // @ts-ignore const turndownService = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced', }); return turndownService.turndown(htmlContent).trim(); } function formatProblemName(title) { // 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) { // 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) { // 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) { const slugMap = { typescript: 'typescript', javascript: 'javascript', python: 'python3', java: 'java', cpp: 'cpp', go: 'golang', rust: 'rust', kotlin: 'kotlin', }; return slugMap[language] || language; } function getFileExtension(language) { const extMap = { typescript: 'ts', javascript: 'js', python: 'py', java: 'java', cpp: 'cpp', go: 'go', rust: 'rs', kotlin: 'kt', }; return extMap[language] || 'txt'; } function getExerciseFileName(className, snakeCaseName, language, paddedId) { 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, snakeCaseName, language, paddedId) { 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, snakeCaseName, language) { 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, title) { 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, moduleName) { 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) { // 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) { // 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; } //# sourceMappingURL=file-operations.js.map