@junnyontop-pixel/neo-app 1.2.0 → 2.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/README.md CHANGED
@@ -1,5 +1,11 @@
1
- # NeoVGC 사용법
1
+ ## Neo v2
2
2
 
3
- 1. `.neo` 파일을 작성합니다.
4
- 2. `npx neoc src/App.neo`를 실행하여 컴파일합니다.
5
- 3. 생성된 `App.js`를 HTML에서 불러옵니다.
3
+ Neo v2 is a runtime-first DSL framework.
4
+
5
+ - No build step
6
+ - No compiler
7
+ - Runtime parsing
8
+ - Script + UI separation
9
+ - State-driven rendering (full rerender)
10
+
11
+ Nested components and partial reactivity are planned for v2.x+.
@@ -1,101 +1,98 @@
1
1
  export class NeoParser {
2
- static parse(source) {
3
- let scriptContent = "";
4
- const scriptMatch = source.match(/@Script\s*\{([\s\S]*?)\}/);
5
- if (scriptMatch) scriptContent = scriptMatch[1].trim();
6
-
7
- // 태그 블록만 추출하기 위해 스크립트 제외
8
- const cleanSource = source.replace(/@Script\s*\{[\s\S]*?\}/, "").trim();
9
-
10
- const root = this.parseRecursive(cleanSource);
11
- return { root, scriptContent };
2
+ static parse(rawCode) {
3
+ const result = {
4
+ id: "",
5
+ tag: "",
6
+ script: "",
7
+ styles: [],
8
+ innerHTML: "",
9
+ events: []
10
+ };
11
+
12
+ // 0. #Script 블록 분리 (depth 기반)
13
+ const extracted = NeoParser.extractScript(rawCode);
14
+ result.script = extracted.script;
15
+ rawCode = extracted.rest;
16
+
17
+ // 1. @ID:Tag
18
+ const tagMatch = rawCode.match(/@([\w-]+):([\w-]+)/);
19
+ if (tagMatch) {
20
+ result.id = tagMatch[1];
21
+ result.tag = tagMatch[2];
22
+ }
23
+
24
+ // 2. [style, style]
25
+ const styleMatch = rawCode.match(/\[(.*?)\]/);
26
+ if (styleMatch) {
27
+ result.styles = styleMatch[1]
28
+ .split(',')
29
+ .map(s => s.trim())
30
+ .filter(Boolean);
12
31
  }
13
32
 
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;
19
-
20
- const node = {
21
- id: match[1],
22
- tag: match[2],
23
- styles: {},
24
- events: {},
25
- props: {},
26
- innerHtml: "",
27
- children: []
28
- };
29
-
30
- // 괄호 짝 찾기 (중첩 대응)
31
- const startIndex = match.index + match[0].length;
32
- let braceCount = 1;
33
- let endIndex = startIndex;
34
-
35
- while (braceCount > 0 && endIndex < text.length) {
36
- if (text[endIndex] === '{') braceCount++;
37
- else if (text[endIndex] === '}') braceCount--;
38
- endIndex++;
33
+ // 3. { content } (UI 블록)
34
+ const contentMatch = rawCode.match(/\{([\s\S]*?)\}$/);
35
+ if (contentMatch) {
36
+ const lines = contentMatch[1].trim().split('\n');
37
+
38
+ lines.forEach(line => {
39
+ const text = line.trim();
40
+
41
+ if (text.startsWith('innerHTML:')) {
42
+ let value = text.slice('innerHTML:'.length).trim();
43
+
44
+ // ⭐ 문자열 리터럴이면 바깥 따옴표만 제거
45
+ if (
46
+ (value.startsWith('"') && value.endsWith('"')) ||
47
+ (value.startsWith("'") && value.endsWith("'"))
48
+ ) {
49
+ value = value.slice(1, -1);
50
+ }
51
+
52
+ result.innerHTML = value;
39
53
  }
40
54
 
41
- const blockContent = text.substring(startIndex, endIndex - 1);
42
-
43
- const contentWithoutChildren = blockContent.replace(/@\w+:\w+\s*\{[\s\S]*?\}/g, "");
44
-
45
- // 1. 블록 내부에서 속성 추출 (Innerhtml, Style, Event)
46
- // 줄바꿈과 대소문자를 무시하고 " " 사이의 값을 정확히 가져옴
47
- const htmlMatch = blockContent.match(/innerHTML:\s*"([\s\S]*?)"/i);
48
- if (htmlMatch) node.innerHtml = htmlMatch[1].trim();
49
-
50
- const styleMatch = blockContent.match(/Style\(([\s\S]*?)\)/i);
51
- if (styleMatch) node.styles = this.parseKV(styleMatch[1]);
52
-
53
- const eventMatch = contentWithoutChildren.match(/Event\(([\s\S]*?)\)/i);
54
- if (eventMatch) node.events = this.parseKV(eventMatch[1]);
55
-
56
- const propsMatch = blockContent.match(/Props\(([\s\S]*?)\)/i);
57
- if (propsMatch) node.props = this.parseKV(propsMatch[1]);
58
-
59
- // 2. 자식 노드 재귀적 탐색
60
- // 본인의 속성 정의 부분을 제외한 나머지 텍스트에서 자식 탐색
61
- const remainingText = blockContent
62
- .replace(/innerHTML:\s*"[\s\S]*?"/i, "")
63
- .replace(/Style\(.*?\)/si, "")
64
- .replace(/Event\(.*?\)/si, "");
65
-
66
- let childMatch;
67
- const childRegex = /@\w+:\w+\s*\{/g;
68
- while ((childMatch = childRegex.exec(remainingText)) !== null) {
69
- // 자식의 시작 지점부터 다시 재귀 실행
70
- const childNode = this.parseRecursive(remainingText.substring(childMatch.index));
71
- if (childNode) {
72
- node.children.push(childNode);
73
-
74
- // 자식 노드의 끝 지점을 찾아서 다음 검색 위치를 조정
75
- let braceCount = 1;
76
- let i = remainingText.indexOf('{', childMatch.index) + 1;
77
-
78
- while (braceCount > 0 && i < remainingText.length) {
79
- if (remainingText[i] === '{') braceCount++;
80
- else if (remainingText[i] === '}') braceCount--;
81
- i++;
82
- }
83
-
84
- // 정규식의 다음 검색 시작 위치를 자식 노드가 끝난 지점(i)으로 옮겨줘
85
- childRegex.lastIndex = i;
86
- }
55
+ if (text.startsWith('on:')) {
56
+ const [, type, ...rest] = text.split(':');
57
+ result.events.push({
58
+ type: type.trim(),
59
+ action: rest.join(':').trim()
60
+ });
87
61
  }
62
+ });
63
+ }
64
+
65
+ return result;
66
+ }
88
67
 
89
- return node;
68
+ // ⭐ 핵심: #Script depth 파서
69
+ static extractScript(rawCode) {
70
+ const start = rawCode.indexOf('#Script');
71
+ if (start === -1) {
72
+ return { script: '', rest: rawCode };
90
73
  }
91
74
 
92
- static parseKV(kvString) {
93
- if (!kvString) return {};
94
- const obj = {};
95
- kvString.split(';').forEach(pair => {
96
- const [key, value] = pair.split(':').map(s => s.trim());
97
- if (key && value) obj[key] = value;
98
- });
99
- return obj;
75
+ const open = rawCode.indexOf('{', start);
76
+ if (open === -1) {
77
+ return { script: '', rest: rawCode };
100
78
  }
79
+
80
+ let depth = 1;
81
+ let i = open + 1;
82
+
83
+ while (i < rawCode.length && depth > 0) {
84
+ if (rawCode[i] === '{') depth++;
85
+ if (rawCode[i] === '}') depth--;
86
+ i++;
87
+ }
88
+
89
+ const script = rawCode.slice(open + 1, i - 1);
90
+ const rest =
91
+ rawCode.slice(0, start) + rawCode.slice(i);
92
+
93
+ return {
94
+ script: script.trim(),
95
+ rest: rest.trim()
96
+ };
97
+ }
101
98
  }
@@ -0,0 +1,50 @@
1
+ import { NeoParser } from './NeoParser.js';
2
+ import { NeoCore } from '../core/NeoCore.js';
3
+
4
+ // 현재 페이지
5
+ let currentPath = 'App.neo';
6
+ let pageCtx = null; // ⭐ 추가
7
+
8
+ async function render() {
9
+ const app = document.getElementById('app');
10
+
11
+ try {
12
+ const response = await fetch(`../src/${currentPath}`);
13
+ if (!response.ok) {
14
+ throw new Error(`Failed to load ${currentPath}`);
15
+ }
16
+
17
+ const rawCode = await response.text();
18
+
19
+ // Parser
20
+ const parsedData = NeoParser.parse(rawCode);
21
+
22
+ // ⭐ 최초 1회만 Script 실행
23
+ if (!pageCtx) {
24
+ pageCtx = NeoCore.createScriptContext(parsedData.script);
25
+ console.log("CTX (init):", pageCtx);
26
+ }
27
+
28
+ // UI 생성
29
+ const ui = NeoCore.create(parsedData, pageCtx);
30
+
31
+ // 화면 교체
32
+ app.replaceChildren(ui);
33
+
34
+ console.log(`🚀 ${currentPath} 렌더링 완료`);
35
+ } catch (err) {
36
+ console.error("렌더링 중 에러 발생:", err);
37
+ app.innerHTML = `<pre style="color:red">${err.message}</pre>`;
38
+ }
39
+ }
40
+
41
+ // 전역 네비게이션
42
+ window.neoNavigate = (path) => {
43
+ if (path === currentPath) return;
44
+ currentPath = path;
45
+ render();
46
+ };
47
+
48
+ // 최초 렌더
49
+ window.__neoRender = render;
50
+ render();
package/core/NeoCore.js CHANGED
@@ -1,61 +1,85 @@
1
1
  export class NeoCore {
2
- constructor(state, rootRenderFn, containerId) {
3
- this.container = document.getElementById(containerId);
4
- this.rootRenderFn = rootRenderFn;
5
-
6
- this.state = new Proxy(state, {
7
- set: (target, key, value) => {
8
- target[key] = value;
9
- this.mount();
10
- return true;
11
- }
12
- });
13
- }
2
+ static create(data, ctx) {
3
+ const tag = data.tag && data.tag.trim() ? data.tag : 'div';
4
+ const el = document.createElement(tag);
14
5
 
15
- mount() {
16
- if (!this.container) return;
17
- this.container.innerHTML = '';
18
- const domTree = this.rootRenderFn(this.state);
19
- this.container.appendChild(domTree);
6
+ if (data.id) el.id = data.id;
7
+ if (Array.isArray(data.styles) && data.styles.length > 0) {
8
+ el.className = data.styles.join(" ");
20
9
  }
21
- }
22
10
 
23
- export function h(tag, props, children = []) {
24
- const el = document.createElement(tag);
11
+ // 상태 삽입 ($template)
12
+ el.innerHTML = NeoCore.renderTemplate(data.innerHTML, ctx);
25
13
 
26
- if (props.id) el.id = props.id;
27
- if (props.style) Object.assign(el.style, props.style);
28
-
29
- // 컴파일러가 주는 innerHTML과 파서가 주는 innerHtml 모두 대응
30
- // const content = props.innerHTML || props.innerHtml;
31
- // if (content !== undefined) {
32
- // el.innerHTML = content;
33
- // }
34
-
35
- Object.keys(props).forEach(key => {
36
- const reserved = ['id', 'style', 'innerHTML', 'innerHtml'];
37
-
38
- // 이벤트도 아니고, 예약어도 아닌 'type', 'placeholder' 같은 애들이라면?
39
- if (!reserved.includes(key) && !key.startsWith('on')) {
40
- el.setAttribute(key, props[key]);
41
- }
42
-
43
- if (key.startsWith('on') && typeof props[key] === 'function') {
44
- const eventType = key.toLowerCase().substring(2);
45
- el.addEventListener(eventType, props[key]);
46
- }
47
- });
14
+ (data.events || []).forEach(evt => {
15
+ el.addEventListener(evt.type, () => {
16
+ try {
17
+ // ctx 기준 실행
18
+ const run = new Function('ctx', `
19
+ with (ctx) {
20
+ ${evt.action}
21
+ }
22
+ `);
23
+ run(ctx);
48
24
 
49
- const flattenChildren = Array.isArray(children) ? children.flat() : [children];
50
-
51
- flattenChildren.forEach(child => {
52
- if (child === null || child === undefined) return;
53
- if (child instanceof HTMLElement) {
54
- el.appendChild(child);
55
- } else {
56
- el.appendChild(document.createTextNode(String(child)));
25
+ if (window.__neoRender) {
26
+ window.__neoRender();
27
+ }
28
+ } catch (e) {
29
+ console.error("이벤트 실행 에러:", e);
57
30
  }
31
+ });
58
32
  });
59
33
 
60
34
  return el;
35
+ }
36
+
37
+ static createScriptContext(scriptCode = "") {
38
+ const ctx = {};
39
+
40
+ try {
41
+ const transformed = NeoCore.transformScript(scriptCode);
42
+ console.log("🔁 transformed script:\n", transformed);
43
+
44
+ const runner = new Function('ctx', transformed);
45
+ runner(ctx);
46
+
47
+ console.log("🧠 ctx after script:", ctx);
48
+ } catch (e) {
49
+ console.error("Script 실행 에러:", e);
50
+ }
51
+
52
+ return ctx;
53
+ }
54
+
55
+ static renderTemplate(template, ctx) {
56
+ if (!template) return "";
57
+ if (!ctx) return template;
58
+
59
+ return template.replace(/\$([\w]+)/g, (_, key) =>
60
+ Object.prototype.hasOwnProperty.call(ctx, key)
61
+ ? ctx[key]
62
+ : ""
63
+ );
64
+ }
65
+
66
+ // ⭐ let / const → ctx.xxx 브리지
67
+ static transformScript(code = "") {
68
+ return code
69
+ // let / const → ctx.xxx
70
+ .replace(
71
+ /\b(let|const)\s+([a-zA-Z_$][\w$]*)\s*=/g,
72
+ 'ctx.$2 ='
73
+ )
74
+ // function add() { → ctx.add = function () {
75
+ .replace(
76
+ /\bfunction\s+([a-zA-Z_$][\w$]*)\s*\(\)\s*\{/g,
77
+ 'ctx.$1 = function () {'
78
+ )
79
+ // count++ → ctx.count++
80
+ .replace(
81
+ /\b([a-zA-Z_$][\w$]*)\s*\+\+/g,
82
+ 'ctx.$1++'
83
+ );
84
+ }
61
85
  }
package/index.html CHANGED
@@ -1,19 +1,13 @@
1
1
  <!DOCTYPE html>
2
- <html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Neo App</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ </head>
3
9
  <body>
4
10
  <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>
11
+ <script type="module" src="./compiler/main.js"></script>
18
12
  </body>
19
13
  </html>
package/package.json CHANGED
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "name": "@junnyontop-pixel/neo-app",
3
- "version": "1.2.0",
3
+ "version": "2.0.0",
4
4
  "description": "나만의 커스텀 프레임워크 Neo",
5
5
  "main": "core/NeoCore.js",
6
6
  "type": "module",
7
7
  "bin": {
8
- "neoc": "compiler/index.js",
9
8
  "neoc-init": "scripts/init.js"
10
9
  },
11
10
  "files": [
@@ -17,7 +16,6 @@
17
16
  "README.md"
18
17
  ],
19
18
  "scripts": {
20
- "prepare": "node compiler/index.js src/App.neo",
21
19
  "build": "node compiler/index.js src/App.neo",
22
20
  "test": "echo \"Error: no test specified\" && exit 1"
23
21
  },
package/scripts/init.js CHANGED
@@ -4,35 +4,38 @@ import path from 'path';
4
4
 
5
5
  // 1. 템플릿 정의
6
6
  const htmlContent = `<!DOCTYPE html>
7
- <html>
7
+ <html lang="en">
8
8
  <head>
9
9
  <meta charset="UTF-8">
10
- <title>My Neo App</title>
10
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
11
+ <title>Neo App</title>
12
+ <script src="https://cdn.tailwindcss.com"></script>
11
13
  </head>
12
14
  <body>
13
15
  <div id="app"></div>
14
- <script type="module">
15
- import { NeoCore } from '@junnyontop-pixel/neo-app/core/NeoCore.js';
16
- import render from './src/App.js';
17
-
18
- const state = { title: "Hello Neo!", count: 0 };
19
- new NeoCore(state, render, 'app').mount();
20
- </script>
16
+ <script type="module" src="./compiler/main.js"></script>
21
17
  </body>
22
18
  </html>`;
23
19
 
24
- const neoContent = `@Script {
25
- // Logic here
20
+ const neoContent = `#Script {
21
+ let count = 0;
22
+ const add = () => count++;
26
23
  }
27
24
 
28
- @Main:div {
29
- Innerhtml: "🚀 $title"
30
- Style(padding: 20px; text-align: center; font-family: sans-serif)
25
+ @Main:div [flex, flex-col, items-center, p-10, bg-white] {
26
+
27
+ @Title:h1 [text-3xl, font-bold] {
28
+ innerHTML: "Neo v2 New Syntax"
29
+ }
30
+
31
+ @Counter:p [my-4, text-blue-500] {
32
+ innerHTML: "현재 숫자: $count"
33
+ }
31
34
 
32
- @Counter:button {
33
- Innerhtml: "클릭 수: $count"
34
- Style(background: #4CAF50; color: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer)
35
- Event(click: $count++)
35
+ @UpBtn:button [px-4, py-2, bg-black, text-white, rounded] {
36
+ innerHTML: "증가"
37
+ on:click: add()
38
+ on:mouseover: console.log('hovered!')
36
39
  }
37
40
  }`;
38
41
 
@@ -48,7 +51,7 @@ try {
48
51
  fs.writeFileSync(path.join(targetDir, 'src/App.neo'), neoContent);
49
52
 
50
53
  console.log("✅ [Neo] index.html 및 src/App.neo 생성이 완료되었습니다!");
51
- console.log("👉 이제 'npx neoc src/App.neo'를 실행하여 첫 컴파일을 완료하세요.");
54
+ console.log("👉 Neo v2는 컴파일할 필요가 없습니다!");
52
55
  } catch (err) {
53
56
  console.error("❌ 초기화 중 에러 발생:", err.message);
54
57
  }
package/src/App.neo CHANGED
@@ -1,10 +1,12 @@
1
- @Script {
2
- function sayHello() {
3
- alert("안녕하세요!");
1
+ #Script {
2
+ let count = 0
3
+
4
+ function add() {
5
+ count++
4
6
  }
5
7
  }
6
8
 
7
- @Btn:button {
8
- Innerhtml: "인사하기";
9
- Event( click: sayHello() )
9
+ @Upbtn:button [px-4, py-2, bg-black, text-white, rounded]{
10
+ innerHTML: "현재 숫자: $count"
11
+ on:click: add()
10
12
  }
package/compiler/index.js DELETED
@@ -1,80 +0,0 @@
1
- #!/usr/bin/env node
2
- import fs from 'fs';
3
- import path from 'path';
4
- import { NeoParser } from './NeoParser.js';
5
-
6
- // 1. 입력 파일 경로 확인
7
- const inputFile = process.argv[2];
8
-
9
- if (!inputFile) {
10
- console.error("❌ 컴파일할 .neo 파일을 입력해주세요. (예: npx neoc src/App.neo)");
11
- process.exit(1);
12
- }
13
-
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 파일의 형식을 확인해주세요.");
21
- }
22
-
23
- // 3. 코드 생성 함수 (재귀 구조)
24
- function generateCode(node, indent = " ") {
25
-
26
- const textNode = node.innerHtml
27
- ? `\`${node.innerHtml.replace(/\$(\w+)/g, '${state.$1}')}\``
28
- : null;
29
-
30
- const childrenCode = node.children
31
- .map(child => generateCode(child, indent + " "))
32
- .join(',\n');
33
-
34
- const allChildren = [textNode, childrenCode].filter(Boolean).join(',\n');
35
-
36
- // 이벤트들을 객체 문자열로 변환
37
- const eventProps = Object.entries(node.events).map(([evt, action]) => {
38
- const propName = `on${evt.charAt(0).toUpperCase() + evt.slice(1)}`;
39
- // $count -> state.count로 치환
40
- let processedAction = action.replace(/\$(\w+)/g, 'state.$1');
41
- return `${propName}: () => { ${processedAction} }`;
42
- }).join(`,\n${indent} `);
43
-
44
- const mergedProps = {
45
- id: node.id,
46
- ...node.props
47
- };
48
-
49
- const propsContent = JSON.stringify(mergedProps).slice(1, -1);
50
-
51
- return `${indent}h('${node.tag}', {
52
- ${indent} style: ${JSON.stringify(node.styles)},
53
- ${indent} ${propsContent}${eventProps ? ',\n' + indent + ' ' + eventProps : ''}
54
- ${indent}}, [${allChildren ? '\n' + allChildren + '\n' + indent : ''}])`;
55
- }
56
-
57
- // render 함수 생성 부분
58
- const finalJS = `
59
- import { h } from '@junnyontop-pixel/neo-app/core/NeoCore.js';
60
-
61
- export default function render(state) {
62
- // 값이 없을 때만 초기화 (중요: 매번 0으로 덮어쓰지 않음)
63
- if (state.count === undefined) state.count = 0;
64
- if (state.title === undefined) state.title = "hello, neo";
65
-
66
- return ${generateCode(root).trimStart()};
67
- }
68
- `.trim();
69
-
70
- // 5. .js 파일로 저장
71
- const outputPath = inputFile.replace('.neo', '.js');
72
- fs.writeFileSync(outputPath, finalJS);
73
-
74
- console.log(`✅ [컴파일 성공] -> ${outputPath}`);
75
-
76
- } catch (err) {
77
- console.error("❌ 컴파일 중 에러 발생:");
78
- console.error(err.message);
79
- process.exit(1);
80
- }
package/src/App.js DELETED
@@ -1,15 +0,0 @@
1
- import { h } from '@junnyontop-pixel/neo-app/core/NeoCore.js';
2
-
3
- export default function render(state) {
4
- // 값이 없을 때만 초기화 (중요: 매번 0으로 덮어쓰지 않음)
5
- if (state.count === undefined) state.count = 0;
6
- if (state.title === undefined) state.title = "hello, neo";
7
-
8
- return h('button', {
9
- style: {},
10
- "id":"Btn",
11
- onClick: () => { sayHello( }
12
- }, [
13
- `인사하기`
14
- ]);
15
- }