@junnyontop-pixel/neo-app 1.1.10 → 1.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/compiler/NeoParser.js +62 -51
- package/compiler/index.js +44 -29
- package/package.json +1 -1
package/compiler/NeoParser.js
CHANGED
|
@@ -1,72 +1,83 @@
|
|
|
1
1
|
export class NeoParser {
|
|
2
2
|
static parse(source) {
|
|
3
3
|
let scriptContent = "";
|
|
4
|
-
// 1. 스크립트 영역 추출 (@Script { ... })
|
|
5
4
|
const scriptMatch = source.match(/@Script\s*\{([\s\S]*?)\}/);
|
|
6
|
-
if (scriptMatch)
|
|
7
|
-
scriptContent = scriptMatch[1].trim();
|
|
8
|
-
}
|
|
5
|
+
if (scriptMatch) scriptContent = scriptMatch[1].trim();
|
|
9
6
|
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
// 태그 블록만 추출하기 위해 스크립트 제외
|
|
8
|
+
const cleanSource = source.replace(/@Script\s*\{[\s\S]*?\}/, "").trim();
|
|
9
|
+
|
|
10
|
+
const root = this.parseRecursive(cleanSource);
|
|
11
|
+
return { root, scriptContent };
|
|
12
|
+
}
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
static parseRecursive(text) {
|
|
15
|
+
// @ID:TAG { ... } 구조를 찾는 정규식
|
|
16
|
+
const tagRegex = /@(\w+):(\w+)\s*\{/g;
|
|
17
|
+
let match = tagRegex.exec(text);
|
|
18
|
+
if (!match) return null;
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
const node = {
|
|
21
|
+
id: match[1],
|
|
22
|
+
tag: match[2],
|
|
23
|
+
styles: {},
|
|
24
|
+
events: {},
|
|
25
|
+
innerHtml: "",
|
|
26
|
+
children: []
|
|
27
|
+
};
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
node.innerHtml = htmlMatch[1];
|
|
35
|
-
}
|
|
29
|
+
// 괄호 짝 찾기 (중첩 대응)
|
|
30
|
+
const startIndex = match.index + match[0].length;
|
|
31
|
+
let braceCount = 1;
|
|
32
|
+
let endIndex = startIndex;
|
|
36
33
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
while (braceCount > 0 && endIndex < text.length) {
|
|
35
|
+
if (text[endIndex] === '{') braceCount++;
|
|
36
|
+
else if (text[endIndex] === '}') braceCount--;
|
|
37
|
+
endIndex++;
|
|
38
|
+
}
|
|
42
39
|
|
|
43
|
-
|
|
44
|
-
const eventMatch = content.match(/Event\((.*?)\)/si);
|
|
45
|
-
if (eventMatch && eventMatch[1]) {
|
|
46
|
-
node.events = this.parseKV(eventMatch[1]);
|
|
47
|
-
}
|
|
40
|
+
const blockContent = text.substring(startIndex, endIndex - 1);
|
|
48
41
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
42
|
+
// 1. 블록 내부에서 속성 추출 (Innerhtml, Style, Event)
|
|
43
|
+
// 줄바꿈과 대소문자를 무시하고 " " 사이의 값을 정확히 가져옴
|
|
44
|
+
const htmlMatch = blockContent.match(/Innerhtml:\s*"([\s\S]*?)"/i);
|
|
45
|
+
if (htmlMatch) node.innerHtml = htmlMatch[1].trim();
|
|
46
|
+
|
|
47
|
+
const styleMatch = blockContent.match(/Style\(([\s\S]*?)\)/i);
|
|
48
|
+
if (styleMatch) node.styles = this.parseKV(styleMatch[1]);
|
|
49
|
+
|
|
50
|
+
const eventMatch = blockContent.match(/Event\(([\s\S]*?)\)/i);
|
|
51
|
+
if (eventMatch) node.events = this.parseKV(eventMatch[1]);
|
|
52
|
+
|
|
53
|
+
// 2. 자식 노드 재귀적 탐색
|
|
54
|
+
// 본인의 속성 정의 부분을 제외한 나머지 텍스트에서 자식 탐색
|
|
55
|
+
const remainingText = blockContent
|
|
56
|
+
.replace(/Innerhtml:\s*"[\s\S]*?"/i, "")
|
|
57
|
+
.replace(/Style\(.*?\)/si, "")
|
|
58
|
+
.replace(/Event\(.*?\)/si, "");
|
|
59
|
+
|
|
60
|
+
let childMatch;
|
|
61
|
+
const childRegex = /@\w+:\w+\s*\{/g;
|
|
62
|
+
while ((childMatch = childRegex.exec(remainingText)) !== null) {
|
|
63
|
+
// 자식의 시작 지점부터 다시 재귀 실행
|
|
64
|
+
const childNode = this.parseRecursive(remainingText.substring(childMatch.index));
|
|
65
|
+
if (childNode) {
|
|
66
|
+
node.children.push(childNode);
|
|
67
|
+
// 중복 탐색 방지를 위해 인덱스 건너뛰기 로직 생략 (현재 구조 최적화)
|
|
68
|
+
break; // 예시용 단일 자식 우선 처리, 실제론 루프 최적화 필요
|
|
55
69
|
}
|
|
56
70
|
}
|
|
57
71
|
|
|
58
|
-
return
|
|
72
|
+
return node;
|
|
59
73
|
}
|
|
60
74
|
|
|
61
75
|
static parseKV(kvString) {
|
|
62
|
-
if (!kvString
|
|
76
|
+
if (!kvString) return {};
|
|
63
77
|
const obj = {};
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (
|
|
67
|
-
const [key, value] = pair.split(':').map(s => s.trim());
|
|
68
|
-
if (key && value) obj[key] = value;
|
|
69
|
-
}
|
|
78
|
+
kvString.split(';').forEach(pair => {
|
|
79
|
+
const [key, value] = pair.split(':').map(s => s.trim());
|
|
80
|
+
if (key && value) obj[key] = value;
|
|
70
81
|
});
|
|
71
82
|
return obj;
|
|
72
83
|
}
|
package/compiler/index.js
CHANGED
|
@@ -3,46 +3,54 @@ import fs from 'fs';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { NeoParser } from './NeoParser.js';
|
|
5
5
|
|
|
6
|
+
// 1. 입력 파일 경로 확인
|
|
6
7
|
const inputFile = process.argv[2];
|
|
7
8
|
|
|
8
9
|
if (!inputFile) {
|
|
9
|
-
console.error("❌ 컴파일할 .neo 파일을 입력해주세요.");
|
|
10
|
+
console.error("❌ 컴파일할 .neo 파일을 입력해주세요. (예: npx neoc src/App.neo)");
|
|
10
11
|
process.exit(1);
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
.
|
|
20
|
-
|
|
21
|
-
const eventProps = {};
|
|
22
|
-
for (const [evt, action] of Object.entries(node.events)) {
|
|
23
|
-
const propName = `on${evt.charAt(0).toUpperCase() + evt.slice(1)}`;
|
|
24
|
-
let processedAction = action;
|
|
25
|
-
|
|
26
|
-
if (action.includes('++')) processedAction = `state.${action}`;
|
|
27
|
-
if (processedAction.includes('(') && !processedAction.includes(')')) {
|
|
28
|
-
processedAction += ')';
|
|
29
|
-
}
|
|
30
|
-
eventProps[propName] = `() => { ${processedAction} }`;
|
|
14
|
+
// 2. 소스 코드 읽기 및 파싱
|
|
15
|
+
try {
|
|
16
|
+
const source = fs.readFileSync(inputFile, 'utf8');
|
|
17
|
+
const { root, scriptContent } = NeoParser.parse(source);
|
|
18
|
+
|
|
19
|
+
if (!root) {
|
|
20
|
+
throw new Error("파싱 결과가 비어있습니다. .neo 파일의 형식을 확인해주세요.");
|
|
31
21
|
}
|
|
32
22
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
23
|
+
// 3. 코드 생성 함수 (재귀 구조)
|
|
24
|
+
function generateCode(node, indent = " ") {
|
|
25
|
+
// 자식 노드 재귀 처리
|
|
26
|
+
const childrenCode = node.children
|
|
27
|
+
.map(child => generateCode(child, indent + " "))
|
|
28
|
+
.join(',\n');
|
|
29
|
+
|
|
30
|
+
// 이벤트 리스너 처리
|
|
31
|
+
const eventProps = Object.entries(node.events).map(([evt, action]) => {
|
|
32
|
+
const propName = `on${evt.charAt(0).toUpperCase() + evt.slice(1)}`;
|
|
33
|
+
// state. 변수 자동 매핑
|
|
34
|
+
let processedAction = action.includes('++') ? `state.${action}` : action;
|
|
35
|
+
|
|
36
|
+
// 함수 호출 괄호 보정
|
|
37
|
+
if (processedAction.includes('(') && !processedAction.includes(')')) {
|
|
38
|
+
processedAction += ')';
|
|
39
|
+
}
|
|
40
|
+
return `${propName}: () => { ${processedAction} }`;
|
|
41
|
+
}).join(',\n' + indent + ' ');
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
// 최종 h() 함수 문자열 생성
|
|
44
|
+
// 💡 node.innerHtml(파서 데이터) -> innerHTML(JS 속성) 매핑 확인
|
|
45
|
+
return `${indent}h('${node.tag}', {
|
|
39
46
|
${indent} id: '${node.id}',
|
|
40
47
|
${indent} style: ${JSON.stringify(node.styles)},
|
|
41
|
-
${indent} innerHTML: \`${node.innerHtml.replace(/\$(\w+)/g, '${state.$1}')}\`${
|
|
48
|
+
${indent} innerHTML: \`${node.innerHtml.replace(/\$(\w+)/g, '${state.$1}')}\`${eventProps ? ',\n' + indent + ' ' + eventProps : ''}
|
|
42
49
|
${indent}}, [${childrenCode ? '\n' + childrenCode + '\n' + indent : ''}])`;
|
|
43
50
|
}
|
|
44
51
|
|
|
45
|
-
|
|
52
|
+
// 4. 최종 출력 파일 내용 구성
|
|
53
|
+
const finalJS = `
|
|
46
54
|
import { h } from '@junnyontop-pixel/neo-app/core/NeoCore.js';
|
|
47
55
|
|
|
48
56
|
// [User Script]
|
|
@@ -53,7 +61,14 @@ export default function render(state) {
|
|
|
53
61
|
}
|
|
54
62
|
`.trim();
|
|
55
63
|
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
// 5. .js 파일로 저장
|
|
65
|
+
const outputPath = inputFile.replace('.neo', '.js');
|
|
66
|
+
fs.writeFileSync(outputPath, finalJS);
|
|
58
67
|
|
|
59
|
-
console.log(`✅ [컴파일
|
|
68
|
+
console.log(`✅ [컴파일 성공] -> ${outputPath}`);
|
|
69
|
+
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.error("❌ 컴파일 중 에러 발생:");
|
|
72
|
+
console.error(err.message);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|