codebridge-ai
Version:
Complete fullstack AI coding platform with AST-based code integration and local LLM support. Now with comprehensive web technology support (HTML/CSS/JS) plus JavaScript, Python, Rust, and C++.
476 lines (409 loc) • 13.1 kB
JavaScript
const parse5 = require('parse5');
const babel = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
class CodeBridge {
constructor() {
this.htmlParser = parse5;
this.jsParser = babel;
}
/**
* HTML 코드를 파싱하고 처리합니다.
*/
processHTML(originalCode, snippetCode) {
const originalAst = this.htmlParser.parse(originalCode);
const snippetAst = this.htmlParser.parse(snippetCode);
// HTML AST를 순회하면서 필요한 요소를 찾고 수정
this.traverseHTML(originalAst, snippetAst);
return this.htmlParser.serialize(snippetAst);
}
/**
* HTML AST를 순회하면서 노드를 처리합니다.
*/
traverseHTML(originalAst, snippetAst) {
const traverse = (node) => {
if (node.childNodes) {
node.childNodes.forEach(child => traverse(child));
}
};
traverse(originalAst);
traverse(snippetAst);
}
/**
* JavaScript 코드를 파싱하고 처리합니다.
*/
processJS(originalCode, snippetCode) {
try {
const parserOptions = {
sourceType: 'module',
plugins: ['jsx', 'typescript'],
strictMode: false,
allowReturnOutsideFunction: true,
allowAwaitOutsideFunction: true,
errorRecovery: true,
tokens: true
};
const originalAst = this.jsParser.parse(originalCode, parserOptions);
// 스니펫이 메서드만 포함하는 경우를 처리
const processedSnippet = this.preprocessMethodSnippet(snippetCode);
const snippetAst = this.jsParser.parse(processedSnippet, parserOptions);
// JavaScript AST 처리
this.mergeJSNodes(originalAst, snippetAst);
return generate(snippetAst, {
retainLines: true,
compact: false,
comments: true,
concise: false,
quotes: 'single'
}).code;
} catch (error) {
console.error('JavaScript 파싱 오류:', error.message);
if (error.loc) {
console.error(`오류 위치: 라인 ${error.loc.line}, 열 ${error.loc.column}`);
const lines = snippetCode.split('\n');
const errorLine = lines[error.loc.line - 1];
console.error('문제의 코드:', errorLine);
console.error(' '.repeat(error.loc.column) + '^');
}
throw error;
}
}
/**
* JavaScript AST 노드들을 병합합니다.
*/
/**
* 메서드만 포함된 스니펫을 전처리합니다.
*/
preprocessMethodSnippet(snippetCode) {
// 특별한 주석 명령어 처리
const commands = this.extractCommands(snippetCode);
// 메서드 정의만 있는지 확인
const isMethodOnly = snippetCode.trim().match(/^[a-zA-Z0-9_]+\s*\([^)]*\)\s*{/);
if (isMethodOnly) {
// 메서드를 클래스 내부에 래핑
let wrappedCode = snippetCode;
// 접근 제어자 추가
if (commands.access) {
wrappedCode = `${commands.access} ${wrappedCode}`;
}
// 데코레이터 추가
if (commands.decorators && commands.decorators.length > 0) {
const decorators = commands.decorators.map(d => `@${d}`).join('\n');
wrappedCode = `${decorators}\n${wrappedCode}`;
}
return `class TemporaryClass { ${wrappedCode} }`;
}
return snippetCode;
}
/**
* 주석에서 특별 명령어를 추출합니다.
*/
extractCommands(code) {
const commands = {
access: null,
decorators: [],
rename: null,
delete: false,
updateParams: null
};
const commandRegex = /\/\/\s*@([a-zA-Z]+)(?:\s+(.+))?/g;
let match;
while ((match = commandRegex.exec(code)) !== null) {
const [, command, value] = match;
switch (command.toLowerCase()) {
case 'access':
commands.access = value; // public, private, protected
break;
case 'decorator':
commands.decorators.push(value);
break;
case 'rename':
commands.rename = value;
break;
case 'delete':
commands.delete = true;
break;
case 'params':
commands.updateParams = value;
break;
}
}
return commands;
}
/**
* 메서드 이름을 추출합니다.
*/
extractMethodName(methodNode) {
if (t.isIdentifier(methodNode.key)) {
return methodNode.key.name;
}
if (t.isStringLiteral(methodNode.key)) {
return methodNode.key.value;
}
return null;
}
mergeJSNodes(originalAst, snippetAst) {
const originalNodes = new Map();
const self = this; // this 바인딩을 위해 저장
// 원본 코드의 노드 수집
traverse(originalAst, {
ClassDeclaration(path) {
originalNodes.set(path.node.id.name, path.node);
},
FunctionDeclaration(path) {
originalNodes.set(path.node.id.name, path.node);
}
});
// 스니펫 코드에 노드 병합
traverse(snippetAst, {
ClassDeclaration(path) {
const originalNode = originalNodes.get(path.node.id.name);
if (originalNode) {
self.mergeClassMethods(path.node, originalNode);
}
}
});
}
/**
* 클래스 메서드를 병합합니다.
*/
mergeClassMethods(snippetNode, originalNode) {
const methodMap = new Map();
const commands = new Map();
// 원본 메서드 수집
originalNode.body.body.forEach(method => {
if (t.isClassMethod(method)) {
const methodName = this.extractMethodName(method);
if (methodName) {
methodMap.set(methodName, this.cloneDeep(method));
}
}
});
// 스니펫의 메서드와 명령어 처리
snippetNode.body.body.forEach(method => {
if (t.isClassMethod(method)) {
const methodName = this.extractMethodName(method);
if (!methodName) return;
// 주석에서 명령어 추출
const methodCommands = this.extractMethodCommands(method);
commands.set(methodName, methodCommands);
if (methodCommands.delete) {
// 메서드 삭제
methodMap.delete(methodName);
} else {
// 메서드 수정 또는 추가
const processedMethod = this.processMethod(
methodMap.get(methodName) || method,
method,
methodCommands
);
if (methodCommands.rename) {
// 메서드 이름 변경
methodMap.delete(methodName);
methodMap.set(methodCommands.rename, processedMethod);
} else {
methodMap.set(methodName, processedMethod);
}
}
}
});
// 모든 메서드를 snippetNode에 적용
snippetNode.body.body = Array.from(methodMap.values());
}
/**
* 메서드의 주석에서 명령어를 추출합니다.
*/
extractMethodCommands(method) {
const commands = {
access: null,
decorators: [],
rename: null,
delete: false,
updateParams: null
};
if (!method.leadingComments) return commands;
method.leadingComments.forEach(comment => {
if (comment.type === 'CommentLine') {
const commentText = comment.value.trim();
const commandMatch = commentText.match(/@([a-zA-Z]+)(?:\s+(.+))?/);
if (commandMatch) {
const [, command, value = ''] = commandMatch;
switch (command.toLowerCase()) {
case 'access':
commands.access = value;
break;
case 'decorator':
if (value) commands.decorators.push(value);
break;
case 'rename':
if (value) commands.rename = value;
break;
case 'delete':
commands.delete = true;
break;
case 'params':
if (value) commands.updateParams = value;
break;
}
}
}
});
return commands;
}
/**
* 메서드를 처리하여 수정된 버전을 반환합니다.
*/
processMethod(originalMethod, newMethod, commands) {
const processedMethod = this.cloneDeep(newMethod);
// 접근 제어자 처리
if (commands.access) {
if (t.isClassMethod(processedMethod)) {
switch (commands.access.toLowerCase()) {
case 'private':
processedMethod.key.name = `#${processedMethod.key.name}`;
break;
case 'public':
case 'protected':
processedMethod.accessibility = commands.access;
break;
}
}
}
// 데코레이터 처리
if (commands.decorators && commands.decorators.length > 0) {
processedMethod.decorators = commands.decorators.map(decorator => {
const decoratorName = decorator.trim();
return t.decorator(t.identifier(decoratorName));
});
}
// 매개변수 업데이트
if (commands.updateParams) {
const params = commands.updateParams.split(',')
.map(param => param.trim())
.filter(param => param.length > 0)
.map(param => t.identifier(param));
processedMethod.params = params;
}
// 메서드 본문 업데이트
if (newMethod.body && t.isBlockStatement(newMethod.body)) {
processedMethod.body = newMethod.body;
}
return processedMethod;
}
/**
* AST 노드의 깊은 복사본을 생성합니다.
*/
cloneDeep(node) {
return JSON.parse(JSON.stringify(node));
}
/**
* Script 태그 내의 JavaScript 코드를 처리합니다.
*/
processScriptTags(html) {
const ast = this.htmlParser.parse(html);
const scripts = [];
// Script 태그 찾기
const findScripts = (node) => {
if (node.tagName === 'script') {
scripts.push(node);
}
if (node.childNodes) {
node.childNodes.forEach(child => findScripts(child));
}
};
findScripts(ast);
// 각 Script 태그의 내용 처리
scripts.forEach(script => {
if (script.childNodes.length > 0) {
const jsCode = script.childNodes[0].value;
const processedCode = this.processJS(jsCode, jsCode);
script.childNodes[0].value = processedCode;
}
});
return this.htmlParser.serialize(ast);
}
/**
* 파일 확장자에 따라 적절한 처리 방법을 선택합니다.
*/
process(originalCode, snippetCode, fileType) {
switch (fileType) {
case 'html':
return this.processHTML(originalCode, snippetCode);
case 'js':
case 'javascript':
return this.processJS(originalCode, snippetCode);
case 'css':
return this.processCSS(originalCode, snippetCode);
default:
throw new Error(`Unsupported file type: ${fileType}`);
}
}
/**
* CSS 코드 처리
*/
processCSS(originalCode, snippetCode) {
console.log('🎨 CSS 코드 처리 시작');
if (!snippetCode || snippetCode.trim() === '') {
console.log('📝 빈 CSS 스니펫, 원본 반환');
return originalCode;
}
try {
// CSS는 단순 병합으로 처리 (덮어쓰기)
// 향후 CSS 파서를 추가할 수 있음
const processed = this.mergeCSS(originalCode, snippetCode);
console.log('✅ CSS 처리 완료');
return processed;
} catch (error) {
console.error('CSS 처리 오류:', error.message);
// 오류 발생시 향상된 스니펫 반환
return snippetCode;
}
}
/**
* CSS 병합 로직
*/
mergeCSS(originalCSS, newCSS) {
// 기본적으로 새로운 CSS로 대체
// 실제 프로젝트에서는 더 정교한 병합 로직 필요
if (!originalCSS || originalCSS.trim() === '') {
return newCSS;
}
// 선택자 기반 병합 (간단한 구현)
const originalSelectors = this.extractCSSSelectors(originalCSS);
const newSelectors = this.extractCSSSelectors(newCSS);
let merged = originalCSS;
for (const [selector, rules] of Object.entries(newSelectors)) {
if (originalSelectors[selector]) {
// 기존 선택자 교체
const selectorRegex = new RegExp(`${this.escapeRegex(selector)}\\s*\\{[^}]*\\}`, 'g');
merged = merged.replace(selectorRegex, `${selector} {\n${rules}\n}`);
} else {
// 새 선택자 추가
merged += `\n\n${selector} {\n${rules}\n}`;
}
}
return merged;
}
/**
* CSS 선택자 추출
*/
extractCSSSelectors(css) {
const selectors = {};
const regex = /([^{}]+)\{([^}]*)\}/g;
let match;
while ((match = regex.exec(css)) !== null) {
const selector = match[1].trim();
const rules = match[2].trim();
selectors[selector] = rules;
}
return selectors;
}
/**
* 정규식 이스케이프
*/
escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
}
module.exports = CodeBridge;