@junnyontop-pixel/neo-app 2.5.2 → 3.0.0
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 +23 -0
- package/compiler/main.js +1 -0
- package/core/NeoCore.js +52 -15
- package/index.html +1 -2
- package/package.json +1 -1
- package/src/App.neo +28 -30
- package/src/store.js +31 -0
- package/src/actions.js +0 -3
- package/src/state.js +0 -9
package/compiler/NeoParser.js
CHANGED
|
@@ -54,6 +54,29 @@ export class NeoParser {
|
|
|
54
54
|
rest = rest.slice(0, start) + " ".repeat(nextIndex - start) + rest.slice(nextIndex);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
while (rest.includes('::for')) {
|
|
58
|
+
const start = rest.indexOf('::for');
|
|
59
|
+
|
|
60
|
+
const forMatch = rest.slice(start).match(/::for\s*\((.*?)\s+in\s+(.*?)\)/);
|
|
61
|
+
|
|
62
|
+
const itemName = forMatch ? forMatch[1].trim() : "item"; // "item" 추출
|
|
63
|
+
const listPath = forMatch ? forMatch[2].trim() : "[]"; // "Store.items" 추출
|
|
64
|
+
|
|
65
|
+
const { block, nextIndex } = extractBlock(rest, start);
|
|
66
|
+
const dummyParsed = this.parse(`@for-container:div { ${block} }`);
|
|
67
|
+
|
|
68
|
+
result.children.push({
|
|
69
|
+
type: "forBlock",
|
|
70
|
+
itemName: itemName,
|
|
71
|
+
listPath: listPath,
|
|
72
|
+
children: dummyParsed.children,
|
|
73
|
+
_pos: start
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// rest = rest.slice(0, start) + rest.slice(nextIndex);
|
|
77
|
+
rest = rest.slice(0, start) + " ".repeat(nextIndex - start) + rest.slice(nextIndex);
|
|
78
|
+
}
|
|
79
|
+
|
|
57
80
|
// ✅ 1) children 태그들 파싱
|
|
58
81
|
while (rest.includes('@')) {
|
|
59
82
|
const start = rest.indexOf('@');
|
package/compiler/main.js
CHANGED
package/core/NeoCore.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
export class NeoCore {
|
|
2
|
-
static create(data) {
|
|
2
|
+
static create(data, loopContext = {}) {
|
|
3
3
|
|
|
4
4
|
if (data.type === "ifBlock") {
|
|
5
5
|
let isTrue = false;
|
|
6
6
|
try {
|
|
7
7
|
// "$Store.state" -> "false" 로 변환
|
|
8
|
-
const conditionStr = NeoCore.renderTemplate(data.condition);
|
|
8
|
+
const conditionStr = NeoCore.renderTemplate(data.condition, loopContext);
|
|
9
9
|
// "return false" 를 자바스크립트로 실행해서 진짜 false로 만듦
|
|
10
10
|
isTrue = new Function(`return ${conditionStr}`)();
|
|
11
11
|
} catch (e) {
|
|
12
12
|
console.warn("Neo if condition error:", e);
|
|
13
|
-
}
|
|
13
|
+
}
|
|
14
14
|
|
|
15
15
|
if (isTrue) {
|
|
16
16
|
// 참일 때: 껍데기(div) 없이 알맹이만 DocumentFragment로 묶어서 반환
|
|
17
17
|
const frag = document.createDocumentFragment();
|
|
18
18
|
if (Array.isArray(data.children)) {
|
|
19
19
|
data.children.forEach(child => {
|
|
20
|
-
frag.appendChild(NeoCore.create(child));
|
|
20
|
+
frag.appendChild(NeoCore.create(child, loopContext));
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
return frag;
|
|
@@ -27,6 +27,29 @@ export class NeoCore {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
if (data.type === "forBlock") {
|
|
31
|
+
let list = [];
|
|
32
|
+
try {
|
|
33
|
+
const listPath = data.listPath;
|
|
34
|
+
list = listPath
|
|
35
|
+
.split('.')
|
|
36
|
+
.reduce((obj, key) => obj?.[key], window) ?? [];
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.warn("Neo for list error:", e);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const frag = document.createDocumentFragment();
|
|
42
|
+
if (Array.isArray(list)) {
|
|
43
|
+
list.forEach((item, index) => {
|
|
44
|
+
data.children.forEach(child => {
|
|
45
|
+
const loopContext = { [data.itemName]: item, index: index };
|
|
46
|
+
frag.appendChild(NeoCore.create(child, loopContext));
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return frag;
|
|
51
|
+
}
|
|
52
|
+
|
|
30
53
|
const el = document.createElement(data.tag || 'div');
|
|
31
54
|
|
|
32
55
|
if (data.id) el.id = data.id;
|
|
@@ -38,7 +61,7 @@ export class NeoCore {
|
|
|
38
61
|
for (const [key, rawValue] of Object.entries(data.attrs)) {
|
|
39
62
|
|
|
40
63
|
// ⭐ 핵심: 먼저 렌더링
|
|
41
|
-
const value = NeoCore.renderTemplate(String(rawValue));
|
|
64
|
+
const value = NeoCore.renderTemplate(String(rawValue), loopContext);
|
|
42
65
|
|
|
43
66
|
// boolean attribute
|
|
44
67
|
if (value === 'true' || value === 'false') {
|
|
@@ -65,27 +88,34 @@ export class NeoCore {
|
|
|
65
88
|
|
|
66
89
|
// 1️⃣ 텍스트 먼저
|
|
67
90
|
if (data.innerHTML) {
|
|
68
|
-
el.innerHTML = NeoCore.renderTemplate(data.innerHTML);
|
|
91
|
+
el.innerHTML = NeoCore.renderTemplate(data.innerHTML, loopContext);
|
|
69
92
|
}
|
|
70
93
|
|
|
71
94
|
// 2️⃣ ⭐ 자식 태그 렌더링 (이게 핵심)
|
|
72
95
|
if (Array.isArray(data.children)) {
|
|
73
96
|
data.children.forEach(child => {
|
|
74
|
-
const childEl = NeoCore.create(child);
|
|
97
|
+
const childEl = NeoCore.create(child, loopContext);
|
|
75
98
|
el.appendChild(childEl);
|
|
76
99
|
});
|
|
77
100
|
}
|
|
78
101
|
|
|
79
102
|
(data.events || []).forEach(evt => {
|
|
80
|
-
el.addEventListener(evt.type, () => {
|
|
103
|
+
el.addEventListener(evt.type, (e) => {
|
|
81
104
|
try {
|
|
82
|
-
|
|
105
|
+
const keys = Object.keys(loopContext);
|
|
106
|
+
const values = Object.values(loopContext);
|
|
107
|
+
|
|
108
|
+
// 1. 함수 생성
|
|
109
|
+
const runner = new Function(...keys, evt.action);
|
|
110
|
+
|
|
111
|
+
// 2. ⭐ 핵심: .call()을 사용해서 'this'를 현재 엘리먼트(el)로 고정!
|
|
112
|
+
runner.call(el, ...values);
|
|
83
113
|
|
|
84
114
|
if (window.__neoRender) {
|
|
85
115
|
window.__neoRender();
|
|
86
116
|
}
|
|
87
|
-
} catch (
|
|
88
|
-
console.error("Neo Event Error:",
|
|
117
|
+
} catch (err) {
|
|
118
|
+
console.error("Neo Event Error:", err);
|
|
89
119
|
}
|
|
90
120
|
});
|
|
91
121
|
});
|
|
@@ -93,11 +123,18 @@ export class NeoCore {
|
|
|
93
123
|
return el;
|
|
94
124
|
}
|
|
95
125
|
|
|
96
|
-
static renderTemplate(template = "") {
|
|
126
|
+
static renderTemplate(template = "", loopContext = {}) {
|
|
97
127
|
return template.replace(/\$([\w.]+)/g, (_, path) => {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
128
|
+
const keys = path.split('.');
|
|
129
|
+
const firstKey = keys[0];
|
|
130
|
+
|
|
131
|
+
// 1. 만약 loopContext(예: todo)에 첫 번째 키가 있다면 거기서 찾기
|
|
132
|
+
if (loopContext.hasOwnProperty(firstKey)) {
|
|
133
|
+
return keys.reduce((obj, key) => obj?.[key], loopContext) ?? '';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 2. 없다면 전역 window(Store)에서 찾기
|
|
137
|
+
return keys.reduce((obj, key) => obj?.[key], window) ?? '';
|
|
101
138
|
});
|
|
102
139
|
}
|
|
103
140
|
}
|
package/index.html
CHANGED
package/package.json
CHANGED
package/src/App.neo
CHANGED
|
@@ -1,38 +1,36 @@
|
|
|
1
|
-
@
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
@Header:div [mb-4, p-3, bg-white, rounded] {
|
|
6
|
-
|
|
7
|
-
@Title:h1 [text-2xl, font-bold] {
|
|
8
|
-
innerHTML: "Header Title"
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
@Sub:p [text-gray-500] {
|
|
12
|
-
innerHTML: "This is a nested paragraph"
|
|
13
|
-
}
|
|
1
|
+
@TodoApp:div [p-6, bg-gray-50] {
|
|
2
|
+
@Header:h1 [text-2xl, font-bold, mb-4] {
|
|
3
|
+
innerHTML: "$Store.user.name 님의 오늘 할 일"
|
|
14
4
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@Counter:p [mb-2] {
|
|
19
|
-
innerHTML: "Count: $Store.count"
|
|
5
|
+
@ListContainer:div [bg-white, shadow, rounded-lg] {
|
|
6
|
+
::attrs {
|
|
7
|
+
"data-version": "2.6.0"
|
|
20
8
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
9
|
+
::for(todo in Store.todoList) {
|
|
10
|
+
@Task:div [flex, items-center, p-4, border-b] {
|
|
11
|
+
@Checkbox:input {
|
|
12
|
+
::attrs {
|
|
13
|
+
type: "checkbox",
|
|
14
|
+
checked: "$todo.completed"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
on:change: todo.completed = this.checked
|
|
18
|
+
}
|
|
19
|
+
@Title:span [ml-3, text-gray-700] {
|
|
20
|
+
innerHTML: "$todo.text"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
25
23
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
}
|
|
25
|
+
@Footer:p [mt-4, text-sm, text-gray-400] {
|
|
26
|
+
::if($Store.remainingCount > 0) {
|
|
27
|
+
@Status:span {
|
|
28
|
+
innerHTML: "아직 $Store.remainingCount개의 할 일이 남았습니다."
|
|
31
29
|
}
|
|
32
30
|
}
|
|
33
|
-
::if($Store.
|
|
34
|
-
@
|
|
35
|
-
innerHTML: "
|
|
31
|
+
::if($Store.remainingCount === 0) {
|
|
32
|
+
@Status:span [text-green-500, font-bold] {
|
|
33
|
+
innerHTML: "🎉 모든 할 일을 끝냈습니다!"
|
|
36
34
|
}
|
|
37
35
|
}
|
|
38
36
|
}
|
package/src/store.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const Store = {
|
|
2
|
+
// 1. 유저 정보 ($Store.user.name)
|
|
3
|
+
user: {
|
|
4
|
+
name: "Neo"
|
|
5
|
+
},
|
|
6
|
+
|
|
7
|
+
// 2. 할 일 목록 (::for(todo in Store.todoList) 에서 쓰임)
|
|
8
|
+
todoList: [
|
|
9
|
+
{ text: "Neo 프레임워크 v2.6.0 파서 완성", completed: true },
|
|
10
|
+
{ text: "::for 루프 데이터 바인딩 로직 구현", completed: false },
|
|
11
|
+
{ text: "할 일 추가 기능 구현", completed: false }
|
|
12
|
+
],
|
|
13
|
+
|
|
14
|
+
// 3. 할 일을 추가하는 기능 (나중에 버튼 만들면 쓸 수 있게!)
|
|
15
|
+
addTodo(text) {
|
|
16
|
+
this.todoList.push({ text, completed: false });
|
|
17
|
+
// 여기서 리렌더링 트리거를 해주면 최고!
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
get remainingCount() {
|
|
21
|
+
return this.todoList.filter(todo => !todo.completed).length;
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
update() {
|
|
25
|
+
if (window.__neoRender) {
|
|
26
|
+
window.__neoRender();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
window.Store = Store; // 전역에서 Store 접근 가능하도록
|
package/src/actions.js
DELETED