@junnyontop-pixel/neo-app 1.0.1
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/README.md +5 -0
- package/compiler/NeoParser.js +54 -0
- package/compiler/index.js +61 -0
- package/core/NeoCore.js +52 -0
- package/index.html +19 -0
- package/package.json +22 -0
- package/src/App.js +14 -0
- package/src/App.neo +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export class NeoParser {
|
|
2
|
+
static parse(code) {
|
|
3
|
+
// 1. @Script 블록 추출 로직 개선
|
|
4
|
+
// 괄호를 포함해서 캡처한 뒤, 앞뒤 껍데기만 날리는 방식이 가장 안전합니다.
|
|
5
|
+
const scriptMatch = code.match(/@Script\s*\{([\s\S]*?)\}(?=\s*@|$)/);
|
|
6
|
+
let scriptContent = "";
|
|
7
|
+
|
|
8
|
+
if (scriptMatch) {
|
|
9
|
+
scriptContent = scriptMatch[1].trim();
|
|
10
|
+
// 만약 사용자가 적은 마지막 } 가 잘려 나갔다면 복구하거나,
|
|
11
|
+
// 원본에서 블록 전체를 가져오는 로직으로 보강
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// 2. UI 요소 추출 (이 부분은 동일)
|
|
15
|
+
const uiCode = code.replace(/@Script\s*\{[\s\S]*?\}\s*/, "");
|
|
16
|
+
const tokenRegex = /(@\w+:\w+)|(Innerhtml:\s*".*?")|(Style\([\s\S]*?\))|(Event\([\s\S]*?\))|(\{)|(\})/g;
|
|
17
|
+
const tokens = uiCode.match(tokenRegex);
|
|
18
|
+
|
|
19
|
+
const stack = [];
|
|
20
|
+
let root = null;
|
|
21
|
+
|
|
22
|
+
tokens.forEach(token => {
|
|
23
|
+
if (token.startsWith('@')) {
|
|
24
|
+
const [_, id, tag] = token.match(/@(\w+):(\w+)/);
|
|
25
|
+
const node = { id, tag, innerHtml: "", styles: {}, events: {}, children: [] };
|
|
26
|
+
if (stack.length > 0) stack[stack.length - 1].children.push(node);
|
|
27
|
+
else if (!root) root = node;
|
|
28
|
+
stack.push(node);
|
|
29
|
+
} else if (token === '}') {
|
|
30
|
+
stack.pop();
|
|
31
|
+
} else if (token.startsWith('Innerhtml')) {
|
|
32
|
+
stack[stack.length - 1].innerHtml = token.match(/"(.*?)"/)[1];
|
|
33
|
+
} else if (token.startsWith('Style')) {
|
|
34
|
+
stack[stack.length - 1].styles = this.parseKV(token.match(/Style\((.*?)\)/)[1]);
|
|
35
|
+
} else if (token.startsWith('Event')) {
|
|
36
|
+
stack[stack.length - 1].events = this.parseKV(token.match(/Event\((.*?)\)/)[1]);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return { root, scriptContent };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static parseKV(str) {
|
|
44
|
+
const obj = {};
|
|
45
|
+
str.split(';').forEach(line => {
|
|
46
|
+
const [k, v] = line.split(':').map(s => s?.trim());
|
|
47
|
+
if (k && v) {
|
|
48
|
+
const camelK = k.replace(/-([a-z])/g, g => g[1].toUpperCase());
|
|
49
|
+
obj[camelK] = v;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
return obj;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path'; // 경로 처리를 위해 추가
|
|
4
|
+
import { NeoParser } from './NeoParser.js';
|
|
5
|
+
|
|
6
|
+
const inputFile = process.argv[2];
|
|
7
|
+
|
|
8
|
+
// 인자가 없을 경우 에러 처리
|
|
9
|
+
if (!inputFile) {
|
|
10
|
+
console.error("❌ 컴파일할 .neo 파일을 입력해주세요. (예: node index.js App.neo)");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const source = fs.readFileSync(inputFile, 'utf8');
|
|
15
|
+
const { root, scriptContent } = NeoParser.parse(source);
|
|
16
|
+
|
|
17
|
+
function generateCode(node) {
|
|
18
|
+
const childrenCode = node.children.map(child => generateCode(child)).join(', ');
|
|
19
|
+
|
|
20
|
+
const eventProps = {};
|
|
21
|
+
for (const [evt, action] of Object.entries(node.events)) {
|
|
22
|
+
const propName = `on${evt.charAt(0).toUpperCase() + evt.slice(1)}`;
|
|
23
|
+
|
|
24
|
+
let processedAction = action;
|
|
25
|
+
if (action.includes('++')) processedAction = `state.${action}`;
|
|
26
|
+
|
|
27
|
+
if (processedAction.includes('(') && !processedAction.includes(')')) {
|
|
28
|
+
processedAction += ')';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
eventProps[propName] = `() => { ${processedAction} }`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const eventString = Object.entries(eventProps)
|
|
35
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
36
|
+
.join(', ');
|
|
37
|
+
|
|
38
|
+
return `h('${node.tag}', {
|
|
39
|
+
id: '${node.id}',
|
|
40
|
+
style: ${JSON.stringify(node.styles)},
|
|
41
|
+
innerHtml: \`${node.innerHtml.replace(/\$(\w+)/g, '${state.$1}')}\`${eventString ? ', ' + eventString : ''}
|
|
42
|
+
}, [${childrenCode}])`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// compiler/index.js 내부
|
|
46
|
+
const finalJS = `
|
|
47
|
+
import { h } from './node_modules/@junnyontop-pixel/neo-app/core/NeoCore.js';
|
|
48
|
+
|
|
49
|
+
// [User Script]
|
|
50
|
+
${scriptContent}
|
|
51
|
+
|
|
52
|
+
export default function render(state) {
|
|
53
|
+
return ${generateCode(root)};
|
|
54
|
+
}
|
|
55
|
+
`.trim();
|
|
56
|
+
|
|
57
|
+
// 파일 생성 위치를 입력 파일과 동일한 위치의 .js로 지정
|
|
58
|
+
const outputPath = inputFile.replace('.neo', '.js');
|
|
59
|
+
fs.writeFileSync(outputPath, finalJS);
|
|
60
|
+
|
|
61
|
+
console.log(`✅ [컴파일 완료] -> ${outputPath}`);
|
package/core/NeoCore.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export class NeoCore {
|
|
2
|
+
constructor(state, rootRenderFn, containerId) {
|
|
3
|
+
this.container = document.getElementById(containerId);
|
|
4
|
+
this.rootRenderFn = rootRenderFn;
|
|
5
|
+
this.state = new Proxy(state, {
|
|
6
|
+
set: (target, key, value) => {
|
|
7
|
+
target[key] = value;
|
|
8
|
+
this.mount(); // 상태 변화 시 리렌더링
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
mount() {
|
|
15
|
+
if (!this.container) return;
|
|
16
|
+
this.container.innerHTML = '';
|
|
17
|
+
const domTree = this.rootRenderFn(this.state);
|
|
18
|
+
this.container.appendChild(domTree);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 가상 노드를 DOM 요소로 바꾸는 함수
|
|
23
|
+
export function h(tag, props, children = []) {
|
|
24
|
+
const el = document.createElement(tag);
|
|
25
|
+
|
|
26
|
+
// 1. ID 설정
|
|
27
|
+
if (props.id) el.id = props.id;
|
|
28
|
+
|
|
29
|
+
// 2. 스타일 설정
|
|
30
|
+
if (props.style) Object.assign(el.style, props.style);
|
|
31
|
+
|
|
32
|
+
// 3. 내용물 설정
|
|
33
|
+
if (props.innerHtml) el.innerHTML = props.innerHtml;
|
|
34
|
+
|
|
35
|
+
// 💡 4. 이벤트 연결 (이 부분이 빠져있을 거예요!)
|
|
36
|
+
// props에 on으로 시작하는 속성(onClick 등)이 있다면 이벤트를 등록합니다.
|
|
37
|
+
Object.keys(props).forEach(key => {
|
|
38
|
+
if (key.startsWith('on') && typeof props[key] === 'function') {
|
|
39
|
+
const eventType = key.toLowerCase().substring(2); // 'onClick' -> 'click'
|
|
40
|
+
el.addEventListener(eventType, props[key]);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// 5. 자식 요소 추가
|
|
45
|
+
children.forEach(child => {
|
|
46
|
+
if (child instanceof HTMLElement) {
|
|
47
|
+
el.appendChild(child);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return el;
|
|
52
|
+
}
|
package/index.html
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<body>
|
|
4
|
+
<div id="app"></div>
|
|
5
|
+
<script type="module">
|
|
6
|
+
import { NeoCore } from './core/NeoCore.js';
|
|
7
|
+
import render from './src/App.js'; // 컴파일러가 만든 결과물!
|
|
8
|
+
|
|
9
|
+
const state = { count: 0, name: "NeoVGC" };
|
|
10
|
+
const neo = new NeoCore(state, render, 'app');
|
|
11
|
+
neo.mount();
|
|
12
|
+
|
|
13
|
+
// 버튼 클릭 이벤트 (임시 연결)
|
|
14
|
+
document.addEventListener('click', (e) => {
|
|
15
|
+
if(e.target.id === 'Btn') neo.state.count++;
|
|
16
|
+
});
|
|
17
|
+
</script>
|
|
18
|
+
</body>
|
|
19
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@junnyontop-pixel/neo-app",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "나만의 커스텀 프레임워크 Neo",
|
|
5
|
+
"main": "core/NeoCore.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"neoc": "./compiler/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"prepare": "node compiler/index.js src/App.neo",
|
|
12
|
+
"build": "node compiler/index.js src/App.neo",
|
|
13
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"framework",
|
|
17
|
+
"neo",
|
|
18
|
+
"dsl"
|
|
19
|
+
],
|
|
20
|
+
"author": "junnyontop-pixel",
|
|
21
|
+
"license": "MIT"
|
|
22
|
+
}
|
package/src/App.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { h } from './node_modules/@junnyontop-pixel/neo-app/core/NeoCore.js';
|
|
2
|
+
|
|
3
|
+
// [User Script]
|
|
4
|
+
function sayHello() {
|
|
5
|
+
alert("안녕하세요!");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function render(state) {
|
|
9
|
+
return h('button', {
|
|
10
|
+
id: 'Btn',
|
|
11
|
+
style: {},
|
|
12
|
+
innerHtml: `인사하기`, onClick: () => { sayHello() }
|
|
13
|
+
}, []);
|
|
14
|
+
}
|