@junnyontop-pixel/neo-app 1.1.8 → 1.1.10
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 +38 -49
- package/compiler/index.js +9 -19
- package/core/NeoCore.js +3 -11
- package/package.json +1 -1
- package/src/App.js +6 -7
package/compiler/NeoParser.js
CHANGED
|
@@ -1,70 +1,59 @@
|
|
|
1
1
|
export class NeoParser {
|
|
2
2
|
static parse(source) {
|
|
3
|
-
const lines = source.split('\n');
|
|
4
|
-
let root = null;
|
|
5
3
|
let scriptContent = "";
|
|
6
|
-
const stack = [];
|
|
7
|
-
|
|
8
4
|
// 1. 스크립트 영역 추출 (@Script { ... })
|
|
9
5
|
const scriptMatch = source.match(/@Script\s*\{([\s\S]*?)\}/);
|
|
10
6
|
if (scriptMatch) {
|
|
11
7
|
scriptContent = scriptMatch[1].trim();
|
|
12
8
|
}
|
|
13
9
|
|
|
14
|
-
// 2. 태그
|
|
15
|
-
|
|
10
|
+
// 2. 태그 블록 분석 로직
|
|
11
|
+
// 정규식 설명: @아이디:태그 { 내부내용 } 을 줄바꿈 상관없이([\\s\\S]*?) 추출
|
|
12
|
+
const tagRegex = /@(\w+):(\w+)\s*\{([\s\S]*?)\}/g;
|
|
16
13
|
let match;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// 소스에서 태그 블록들을 찾아내기 위한 단순화된 로직
|
|
20
|
-
const tokens = source.split(/(@\w+:\w+\s*\{)/).filter(t => t.trim());
|
|
14
|
+
let root = null;
|
|
15
|
+
const stack = [];
|
|
21
16
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (tagMatch) {
|
|
26
|
-
const node = {
|
|
27
|
-
id: tagMatch[1],
|
|
28
|
-
tag: tagMatch[2],
|
|
29
|
-
styles: {},
|
|
30
|
-
events: {},
|
|
31
|
-
innerHtml: "",
|
|
32
|
-
children: []
|
|
33
|
-
};
|
|
17
|
+
// source 전체를 훑으며 태그 블록을 하나씩 찾습니다.
|
|
18
|
+
while ((match = tagRegex.exec(source)) !== null) {
|
|
19
|
+
const [_, id, tag, content] = match;
|
|
34
20
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
21
|
+
const node = {
|
|
22
|
+
id: id,
|
|
23
|
+
tag: tag,
|
|
24
|
+
styles: {},
|
|
25
|
+
events: {},
|
|
26
|
+
innerHtml: "",
|
|
27
|
+
children: []
|
|
28
|
+
};
|
|
40
29
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
30
|
+
// 💡 [핵심] 블록 내부(content)에서 줄바꿈 무시하고 속성 추출
|
|
31
|
+
// Innerhtml 추출 (i 플래그로 대소문자 무시)
|
|
32
|
+
const htmlMatch = content.match(/Innerhtml:\s*"(.*?)"/i);
|
|
33
|
+
if (htmlMatch) {
|
|
34
|
+
node.innerHtml = htmlMatch[1];
|
|
35
|
+
}
|
|
46
36
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
37
|
+
// Style 추출 (si 플래그로 줄바꿈 허용 및 대소문자 무시)
|
|
38
|
+
const styleMatch = content.match(/Style\((.*?)\)/si);
|
|
39
|
+
if (styleMatch && styleMatch[1]) {
|
|
40
|
+
node.styles = this.parseKV(styleMatch[1]);
|
|
41
|
+
}
|
|
52
42
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
stack.push(node);
|
|
43
|
+
// Event 추출 (si 플래그)
|
|
44
|
+
const eventMatch = content.match(/Event\((.*?)\)/si);
|
|
45
|
+
if (eventMatch && eventMatch[1]) {
|
|
46
|
+
node.events = this.parseKV(eventMatch[1]);
|
|
59
47
|
}
|
|
60
48
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
49
|
+
// 3. 계층 구조 형성 (단순화된 방식: 첫 번째가 root, 나머지는 root의 자식)
|
|
50
|
+
// ※ 중첩 구조가 깊어질 경우 스택 로직이 필요하지만, 현재 요구사항엔 이 방식이 가장 확실합니다.
|
|
51
|
+
if (!root) {
|
|
52
|
+
root = node;
|
|
53
|
+
} else {
|
|
54
|
+
root.children.push(node);
|
|
66
55
|
}
|
|
67
|
-
}
|
|
56
|
+
}
|
|
68
57
|
|
|
69
58
|
return { root, scriptContent };
|
|
70
59
|
}
|
package/compiler/index.js
CHANGED
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
3
|
+
import path from 'path';
|
|
4
4
|
import { NeoParser } from './NeoParser.js';
|
|
5
5
|
|
|
6
6
|
const inputFile = process.argv[2];
|
|
7
7
|
|
|
8
|
-
// 인자가 없을 경우 에러 처리
|
|
9
8
|
if (!inputFile) {
|
|
10
|
-
console.error("❌ 컴파일할 .neo 파일을 입력해주세요.
|
|
9
|
+
console.error("❌ 컴파일할 .neo 파일을 입력해주세요.");
|
|
11
10
|
process.exit(1);
|
|
12
11
|
}
|
|
13
12
|
|
|
14
13
|
const source = fs.readFileSync(inputFile, 'utf8');
|
|
15
14
|
const { root, scriptContent } = NeoParser.parse(source);
|
|
16
15
|
|
|
17
|
-
// compiler/index.js 내부 generateCode 함수 수정
|
|
18
|
-
|
|
19
16
|
function generateCode(node, indent = " ") {
|
|
20
17
|
const childrenCode = node.children
|
|
21
18
|
.map(child => generateCode(child, indent + " "))
|
|
@@ -27,42 +24,35 @@ function generateCode(node, indent = " ") {
|
|
|
27
24
|
let processedAction = action;
|
|
28
25
|
|
|
29
26
|
if (action.includes('++')) processedAction = `state.${action}`;
|
|
30
|
-
|
|
31
27
|
if (processedAction.includes('(') && !processedAction.includes(')')) {
|
|
32
28
|
processedAction += ')';
|
|
33
29
|
}
|
|
34
|
-
// Proxy가 있으므로 renderApp() 호출은 빼도 됩니다.
|
|
35
30
|
eventProps[propName] = `() => { ${processedAction} }`;
|
|
36
31
|
}
|
|
37
32
|
|
|
38
33
|
const eventString = Object.entries(eventProps)
|
|
39
34
|
.map(([k, v]) => `${k}: ${v}`)
|
|
40
|
-
.join('
|
|
35
|
+
.join(',\n' + indent + ' ');
|
|
41
36
|
|
|
42
|
-
// 💡
|
|
43
|
-
// index.js의 generateCode 함수 리턴 부분
|
|
37
|
+
// 💡 생성 키값을 innerHTML로 고정 (중요)
|
|
44
38
|
return `${indent}h('${node.tag}', {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
${indent} id: '${node.id}',
|
|
40
|
+
${indent} style: ${JSON.stringify(node.styles)},
|
|
41
|
+
${indent} innerHTML: \`${node.innerHtml.replace(/\$(\w+)/g, '${state.$1}')}\`${eventString ? ',\n' + indent + ' ' + eventString : ''}
|
|
42
|
+
${indent}}, [${childrenCode ? '\n' + childrenCode + '\n' + indent : ''}])`;
|
|
49
43
|
}
|
|
50
44
|
|
|
51
|
-
// compiler/index.js 내부
|
|
52
45
|
const finalJS = `
|
|
53
|
-
// 상대 경로 대신 패키지 이름을 직접 사용합니다.
|
|
54
|
-
// Vite가 node_modules에서 알아서 찾아줍니다.
|
|
55
46
|
import { h } from '@junnyontop-pixel/neo-app/core/NeoCore.js';
|
|
56
47
|
|
|
57
48
|
// [User Script]
|
|
58
49
|
${scriptContent}
|
|
59
50
|
|
|
60
51
|
export default function render(state) {
|
|
61
|
-
return ${generateCode(root)};
|
|
52
|
+
return ${generateCode(root).trimStart()};
|
|
62
53
|
}
|
|
63
54
|
`.trim();
|
|
64
55
|
|
|
65
|
-
// 파일 생성 위치를 입력 파일과 동일한 위치의 .js로 지정
|
|
66
56
|
const outputPath = inputFile.replace('.neo', '.js');
|
|
67
57
|
fs.writeFileSync(outputPath, finalJS);
|
|
68
58
|
|
package/core/NeoCore.js
CHANGED
|
@@ -3,11 +3,10 @@ export class NeoCore {
|
|
|
3
3
|
this.container = document.getElementById(containerId);
|
|
4
4
|
this.rootRenderFn = rootRenderFn;
|
|
5
5
|
|
|
6
|
-
// Proxy를 사용하여 state가 바뀔 때마다 자동으로 mount() 호출
|
|
7
6
|
this.state = new Proxy(state, {
|
|
8
7
|
set: (target, key, value) => {
|
|
9
8
|
target[key] = value;
|
|
10
|
-
this.mount();
|
|
9
|
+
this.mount();
|
|
11
10
|
return true;
|
|
12
11
|
}
|
|
13
12
|
});
|
|
@@ -15,28 +14,24 @@ export class NeoCore {
|
|
|
15
14
|
|
|
16
15
|
mount() {
|
|
17
16
|
if (!this.container) return;
|
|
18
|
-
this.container.innerHTML = '';
|
|
17
|
+
this.container.innerHTML = '';
|
|
19
18
|
const domTree = this.rootRenderFn(this.state);
|
|
20
19
|
this.container.appendChild(domTree);
|
|
21
20
|
}
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
// 가상 노드를 실제 DOM 요소로 변환하는 함수
|
|
25
23
|
export function h(tag, props, children = []) {
|
|
26
|
-
// 1. 태그 생성
|
|
27
24
|
const el = document.createElement(tag);
|
|
28
25
|
|
|
29
|
-
// 2. 속성 설정
|
|
30
26
|
if (props.id) el.id = props.id;
|
|
31
27
|
if (props.style) Object.assign(el.style, props.style);
|
|
32
28
|
|
|
33
|
-
//
|
|
29
|
+
// 컴파일러가 주는 innerHTML과 파서가 주는 innerHtml 모두 대응
|
|
34
30
|
const content = props.innerHTML || props.innerHtml;
|
|
35
31
|
if (content !== undefined) {
|
|
36
32
|
el.innerHTML = content;
|
|
37
33
|
}
|
|
38
34
|
|
|
39
|
-
// 3. 이벤트 연결
|
|
40
35
|
Object.keys(props).forEach(key => {
|
|
41
36
|
if (key.startsWith('on') && typeof props[key] === 'function') {
|
|
42
37
|
const eventType = key.toLowerCase().substring(2);
|
|
@@ -44,16 +39,13 @@ export function h(tag, props, children = []) {
|
|
|
44
39
|
}
|
|
45
40
|
});
|
|
46
41
|
|
|
47
|
-
// 4. 자식 요소 추가 (배열 중첩 및 텍스트 노드 대응 강화)
|
|
48
42
|
const flattenChildren = Array.isArray(children) ? children.flat() : [children];
|
|
49
43
|
|
|
50
44
|
flattenChildren.forEach(child => {
|
|
51
45
|
if (child === null || child === undefined) return;
|
|
52
|
-
|
|
53
46
|
if (child instanceof HTMLElement) {
|
|
54
47
|
el.appendChild(child);
|
|
55
48
|
} else {
|
|
56
|
-
// 숫자나 문자열 등은 텍스트 노드로 변환하여 추가
|
|
57
49
|
el.appendChild(document.createTextNode(String(child)));
|
|
58
50
|
}
|
|
59
51
|
});
|
package/package.json
CHANGED
package/src/App.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// 상대 경로 대신 패키지 이름을 직접 사용합니다.
|
|
2
|
-
// Vite가 node_modules에서 알아서 찾아줍니다.
|
|
3
1
|
import { h } from '@junnyontop-pixel/neo-app/core/NeoCore.js';
|
|
4
2
|
|
|
5
3
|
// [User Script]
|
|
@@ -7,9 +5,10 @@ function sayHello() {
|
|
|
7
5
|
alert("안녕하세요!");
|
|
8
6
|
|
|
9
7
|
export default function render(state) {
|
|
10
|
-
return
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
return h('button', {
|
|
9
|
+
id: 'Btn',
|
|
10
|
+
style: {},
|
|
11
|
+
innerHTML: `인사하기`,
|
|
12
|
+
onClick: () => { sayHello() }
|
|
13
|
+
}, []);
|
|
15
14
|
}
|