@uiwwsw/hangulscript 0.4.0 → 0.5.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/LANGUAGE.md CHANGED
@@ -95,3 +95,13 @@ JS 출력은 ESM이며, 프로젝트는 `type: module` 또는 ESM 번들러를
95
95
  제품명은 한글스크립트(HangulScript), 패키지는 `@uiwwsw/hangulscript`입니다.
96
96
  기본 소스 확장자는 `.hgs`이며 기존 `.ks` 파일도 CLI에서 계속 지원합니다.
97
97
  이전 패키지에서 이동하려면 `npm uninstall @uiwwsw/koreanscript` 후 새 패키지를 설치하세요.
98
+
99
+
100
+ ## 화면 함수와 버튼 블록
101
+
102
+ UI 실행 환경에서는 `숫자입력("이름", 0)`, `글자입력("이름", "")`,
103
+ `선택입력("이름", ["하나", "둘"], "하나")`로 현재 입력값을 읽습니다.
104
+ `"계산하기" 버튼을 누르면:` 뒤의 들여쓴 블록은 해당 버튼을 눌렀을 때만 실행합니다.
105
+ `보여주기("결과 이름", 값)`은 결과 카드를 생성합니다. `버튼("이름")`도 같은 조건을 반환합니다.
106
+ 버튼 실행은 현재 입력값을 사용한 전체 프로그램 재실행이며, 지속되는 변수나 DOM 이벤트 클로저가 아닙니다.
107
+ 화면 함수는 Node CLI 단독 실행에서 지원되지 않습니다.
package/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # 한글스크립트 · HangulScript
2
2
 
3
+ [![CI on main](https://github.com/uiwwsw/hangulscript/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/uiwwsw/hangulscript/actions/workflows/ci.yml)
4
+ [![MIT license](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](https://github.com/uiwwsw/hangulscript/blob/main/LICENSE)
5
+
3
6
  **한글 문장으로 작성하고, JavaScript와 타입 선언으로 배포하는 실험적 프로그래밍 언어.**
4
7
 
5
8
  Python의 들여쓰기와 익숙한 기본 함수, JavaScript의 표현식과 웹 생태계,
@@ -30,7 +33,7 @@ Python 소스 호환이나 전체 TypeScript 문법 지원을 뜻하지 않습
30
33
  | [성적 분석기](examples/scores.hgs) | 점수 목록 | 반복·필터링·정렬 |
31
34
  | [타입 있는 함수 모듈](examples/math-library.hgs) | 함수·타입 | JS + .d.ts 라이브러리 빌드 |
32
35
 
33
- 데모에서 예제를 선택하고 입력값을 바꾼 다음 **실행하기**를 누르세요.
36
+ 데모에서 예제를 선택하고 입력값을 바꾼 다음 **실행하기**를 누르세요. 변환 결과는 기본적으로 JavaScript로 표시하며, 선택 메뉴에서 TypeScript와 비교할 수 있습니다. 실행 결과 패널은 데모를 스크롤하는 동안 화면 하단에 붙어 있습니다. CLI 빌드는 JavaScript와 `.d.ts`를 함께 생성합니다.
34
37
  입력값 변경은 예제 코드를 다시 생성하며, 코드를 직접 편집하면 입력란은 숨겨집니다.
35
38
  독립적인 웹 UI·데이터베이스·외부 API를 언어 자체에서 제공하는 단계는 아닙니다.
36
39
  생성한 JS를 기존 웹/Node 프로젝트에 연결해 화면과 서비스를 확장할 수 있습니다.
@@ -254,3 +257,60 @@ Vercel은 GitHub main과 연결되어 `site-dist/`를 배포합니다.
254
257
  제품명은 한글스크립트(HangulScript), 패키지는 `@uiwwsw/hangulscript`입니다.
255
258
  기본 소스 확장자는 `.hgs`이며 기존 `.ks` 파일도 CLI에서 계속 지원합니다.
256
259
  이전 패키지에서 이동하려면 `npm uninstall @uiwwsw/koreanscript` 후 새 패키지를 설치하세요.
260
+
261
+
262
+ ## 입력창과 버튼으로 도구 만들기
263
+
264
+ 온라인 데모의 계산기·할인·통계·문자열 예제는 코드에 선언한 실제 화면을 제공합니다.
265
+
266
+ ```text
267
+ 문장으로 시작
268
+ 가격은 숫자입력("상품 가격", 10000)이다
269
+ 할인율은 숫자입력("할인율", 20)이다
270
+
271
+ "계산하기" 버튼을 누르면:
272
+ 결과는 가격 * (1 - 할인율 / 100)이다
273
+ 보여주기("결제 금액", 결과)
274
+ ```
275
+
276
+ `숫자입력`, `글자입력`, `선택입력`, `버튼`, `보여주기`를 지원합니다.
277
+ 버튼을 누르면 현재 입력값과 누른 버튼 이름을 전달하여 프로그램을 처음부터 다시 실행합니다.
278
+ 일반 변수는 실행 사이에 유지되지 않으며 입력값은 같은 화면을 사용하는 동안 유지됩니다.
279
+ 화면 요소 이름은 중복할 수 없고, 한 실행당 최대 40개입니다.
280
+ 공유받은 코드는 실행 버튼을 눌러야 시작합니다. 공유 링크에는 코드와 코드의 기본값이 담기며, 미리보기에서 바꾼 입력값은 포함하지 않습니다.
281
+
282
+ 이 화면 함수는 데모의 UI 실행 환경이 필요합니다. Node CLI에서 직접 실행하거나 React/Vue 컴포넌트 안에 넣는 것으로 화면이 생성되지는 않습니다.
283
+ 사용자 코드는 격리된 Worker에서 실행하고, 호스트는 제한된 데이터만 받아 입력창과 결과 카드를 만듭니다.
284
+ HTML·이벤트 핸들러 문자열을 삽입하지 않습니다.
285
+
286
+ ## React·Vue에서 .hgs 가져오기
287
+
288
+ Vite를 사용하는 React와 Vue는 별도 프레임워크 패키지 없이 같은 플러그인을 사용할 수 있습니다.
289
+
290
+ ```js
291
+ // vite.config.js — React
292
+ import { defineConfig } from 'vite';
293
+ import react from '@vitejs/plugin-react';
294
+ import hangulscript from '@uiwwsw/hangulscript/vite';
295
+
296
+ export default defineConfig({ plugins: [hangulscript(), react()] });
297
+ ```
298
+
299
+ Vue에서는 React 플러그인 대신 `@vitejs/plugin-vue`의 `vue()`를 넣습니다.
300
+
301
+ ```text
302
+ // src/discount.hgs
303
+ export function 할인(가격: number, 비율: number) {
304
+ return 가격 * (1 - 비율 / 100);
305
+ }
306
+ ```
307
+
308
+ ```js
309
+ // React 이벤트 핸들러와 Vue setup 등에서 동일하게 사용
310
+ import { 할인 } from './discount.hgs';
311
+ console.log(할인(10000, 20)); // 8000
312
+ ```
313
+
314
+ 이 플러그인은 빌드 시 `.hgs`를 JavaScript 모듈로 변환합니다. React/Vue 컴포넌트는 각 프레임워크로 작성하고 한글스크립트 함수로 계산·검증 로직을 공유합니다.
315
+ 현재 플러그인은 타입 검사·선언 생성·소스맵을 제공하지 않습니다. 엄격한 타입 연동이 필요하면 `ksc build`로 만든 `.js`와 `.d.ts`를 가져오세요.
316
+ 데모의 UI 문장을 React/Vue 컴포넌트로 변환하는 기능은 포함하지 않습니다.
package/dist/sentences.js CHANGED
@@ -1,9 +1,11 @@
1
+ import { uiRuntime } from './ui.js';
1
2
  // 문장형 v0.1: deterministic indentation grammar, not natural-language inference.
2
3
  export function isSentenceSource(source) {
3
4
  return source.split(/\r?\n/).some(line => /^\s*(?:문장으로 시작\s*$|[\p{L}_$][\p{L}\p{N}_$]*(?:은|는)\s+.+이다\s*$|각 .+에서:\s*$|[\p{L}_$][\p{L}\p{N}_$]*\(.*\)를 정의한다:\s*$)/u.test(line));
4
5
  }
5
6
 
6
- export const sentenceRuntime = `
7
+ export const sentenceRuntime = `${uiRuntime}
8
+
7
9
  function 출력(...값: unknown[]): void { console.log(...값); }
8
10
  function 길이(값: string | readonly unknown[]): number { return 값.length; }
9
11
  function 추가<T>(목록: T[], 값: T): void { 목록.push(값); }
@@ -70,6 +72,8 @@ export function compileSentences(source, { includeRuntime = true } = {}) {
70
72
  // Unannotated parameters are intentionally dynamic, like Python.
71
73
  const signature = match[1].replace(/\((.*)\)$/, (_, params) => '(' + params.split(',').filter(x => x.trim()).map(x => x.includes(':') ? x.trim() : `${x.trim()}: any`).join(', ') + ')');
72
74
  output.push(`function ${signature} {`); needsIndent = true;
75
+ } else if ((match = text.match(/^("(?:[^"\\]|\\.)*") 버튼을 누르면:$/u))) {
76
+ output.push(`if (버튼(${match[1]})) {`); needsIndent = true;
73
77
  } else if ((match = text.match(/^각 (.+?)(?:을|를) (.+)에서:$/u))) {
74
78
  if (!/^[\p{L}_$][\p{L}\p{N}_$]*$/u.test(match[1])) fail('반복 변수 이름을 확인하세요.');
75
79
  output.push(`for (const ${match[1]} of ${match[2]}) {`); needsIndent = true;
package/dist/ui.js ADDED
@@ -0,0 +1,49 @@
1
+ // A data-only UI protocol: the host renders descriptors, never user HTML or code.
2
+ export function createUIBridge(values = {}, action = '') {
3
+ const elements = [];
4
+ const labels = new Set();
5
+ const text = value => String(value).slice(0, 2000);
6
+ function add(type, label, extra = {}) {
7
+ label = text(label);
8
+ if (!label || labels.has(label)) throw new Error('화면 요소의 이름은 비어 있거나 중복될 수 없습니다.');
9
+ if (elements.length >= 40) throw new Error('화면에는 최대 40개 요소를 만들 수 있습니다.');
10
+ labels.add(label);
11
+ const item = { type, label, ...extra };
12
+ elements.push(item);
13
+ return item;
14
+ }
15
+ return {
16
+ number(label, initial = 0) {
17
+ const raw = Object.hasOwn(values, label) ? values[label] : initial;
18
+ if (String(raw).trim() === '' || !Number.isFinite(Number(raw))) throw new Error(`${text(label)}: 올바른 숫자를 입력하세요.`);
19
+ const value = Number(raw); add('number', label, { value }); return value;
20
+ },
21
+ text(label, initial = '') {
22
+ const value = text(Object.hasOwn(values, label) ? values[label] : initial);
23
+ add('text', label, { value }); return value;
24
+ },
25
+ select(label, options, initial = options?.[0]) {
26
+ if (!Array.isArray(options) || !options.length || options.length > 30) throw new Error('선택 항목은 1~30개가 필요합니다.');
27
+ options = options.map(text);
28
+ const value = text(Object.hasOwn(values, label) ? values[label] : initial);
29
+ if (!options.includes(value)) throw new Error(`${text(label)}: 목록에서 항목을 선택하세요.`);
30
+ add('select', label, { value, options }); return value;
31
+ },
32
+ button(label) { add('button', label); return action === label; },
33
+ show(label, value) { add('result', label, { value: text(value) }); },
34
+ snapshot() { return elements; }
35
+ };
36
+ }
37
+
38
+ export const uiRuntime = `
39
+ function 화면연결(): { number: (이름: string, 기본값?: number) => number; text: (이름: string, 기본값?: string) => string; select: (이름: string, 항목: string[], 기본값?: string) => string; button: (이름: string) => boolean; show: (이름: string, 값: unknown) => void } {
40
+ const 연결 = (globalThis as any).__hangulUI;
41
+ if (!연결) throw new Error("화면 함수는 한글스크립트 UI 실행 환경에서 사용하세요.");
42
+ return 연결;
43
+ }
44
+ function 숫자입력(이름: string, 기본값 = 0): number { return 화면연결().number(이름, 기본값); }
45
+ function 글자입력(이름: string, 기본값 = ""): string { return 화면연결().text(이름, 기본값); }
46
+ function 선택입력(이름: string, 항목: string[], 기본값 = 항목[0]): string { return 화면연결().select(이름, 항목, 기본값); }
47
+ function 버튼(이름: string): boolean { return 화면연결().button(이름); }
48
+ function 보여주기(이름: string, 값: unknown): void { 화면연결().show(이름, 값); }
49
+ `;
package/dist/vite.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ /** Transform .hgs imports at build time. Place before the React or Vue plugin. */
2
+ export default function hangulscript(): {
3
+ name: string;
4
+ enforce: 'pre';
5
+ transform(source: string, id: string): { code: string; map: null } | null;
6
+ };
package/dist/vite.js ADDED
@@ -0,0 +1,22 @@
1
+ import ts from 'typescript';
2
+ import { compileSource } from './transformer.js';
3
+
4
+ /** Build-time .hgs imports for Vite projects, including React and Vue. */
5
+ export default function hangulscript() {
6
+ return {
7
+ name: 'hangulscript',
8
+ enforce: 'pre',
9
+ transform(source, id) {
10
+ if (!/\.hgs$/.test(id.split('?')[0]) || /[?&](?:raw|url)(?:[=&]|$)/.test(id)) return null;
11
+ const compiled = compileSource(source);
12
+ const result = ts.transpileModule(compiled, {
13
+ fileName: id.replace(/\.hgs(?:\?.*)?$/, '.ts'),
14
+ compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext, sourceMap: false },
15
+ reportDiagnostics: true
16
+ });
17
+ const errors = (result.diagnostics ?? []).filter(d => d.category === ts.DiagnosticCategory.Error);
18
+ if (errors.length) throw new Error(errors.map(d => ts.flattenDiagnosticMessageText(d.messageText, '\n')).join('\n'));
19
+ return { code: result.outputText, map: null };
20
+ }
21
+ };
22
+ }
@@ -0,0 +1,19 @@
1
+ 문장으로 시작
2
+ 첫째는 숫자입력("첫 번째 수", 24)이다
3
+ 둘째는 숫자입력("두 번째 수", 6)이다
4
+ 연산은 선택입력("연산", ["더하기","빼기","곱하기","나누기"], "나누기")이다
5
+
6
+ "계산하기" 버튼을 누르면:
7
+ 계산(가, 나, 방식)를 정의한다:
8
+ 만약 방식 == "더하기" 이라면:
9
+ 돌려주기 가 + 나
10
+ 아니고 방식 == "빼기" 이라면:
11
+ 돌려주기 가 - 나
12
+ 아니고 방식 == "곱하기" 이라면:
13
+ 돌려주기 가 * 나
14
+ 아니면:
15
+ 만약 나 == 0 이라면:
16
+ 돌려주기 "0으로 나눌 수 없습니다"
17
+ 돌려주기 가 / 나
18
+
19
+ 보여주기("계산 결과:", 계산(첫째, 둘째, 연산))
@@ -0,0 +1,11 @@
1
+ 문장으로 시작
2
+ 가격은 숫자입력("가격 (원)", 32000)이다
3
+ 할인율은 숫자입력("할인율 (%)", 15)이다
4
+
5
+ "계산하기" 버튼을 누르면:
6
+ 만약 가격 < 0 || 할인율 < 0 || 할인율 > 100 이라면:
7
+ 보여주기("안내", "가격은 0 이상, 할인율은 0~100 사이로 입력하세요")
8
+ 아니면:
9
+ 할인액은 반올림(가격 * 할인율 / 100)이다
10
+ 보여주기("할인액:", 할인액 + " 원")
11
+ 보여주기("결제 금액:", 가격 - 할인액 + " 원")
@@ -0,0 +1,12 @@
1
+ 문장으로 시작
2
+ 입력값은 글자입력("쉼표로 구분한 숫자", "10, 20, 30, 40")이다
3
+
4
+ "계산하기" 버튼을 누르면:
5
+ 목록은 []이다
6
+ 각 항목을 나누기(입력값, ",")에서:
7
+ 추가(목록, 숫자로(항목))
8
+ 보여주기("개수:", 길이(목록))
9
+ 보여주기("합계:", 합계(목록))
10
+ 보여주기("평균:", 평균(목록))
11
+ 보여주기("최솟값:", 최솟값(목록))
12
+ 보여주기("최댓값:", 최댓값(목록))
@@ -0,0 +1,11 @@
1
+ 문장으로 시작
2
+ 원문은 글자입력("정리할 문장", " 사과, 바나나, 사과, 포도 ")이다
3
+
4
+ "정리하기" 버튼을 누르면:
5
+ 단어들은 []이다
6
+ 각 조각을 나누기(다듬기(원문), ",")에서:
7
+ 단어는 다듬기(조각)이다
8
+ 만약 길이(단어) > 0 && !포함(단어들, 단어) 이라면:
9
+ 추가(단어들, 단어)
10
+ 보여주기("중복 없이:", 잇기(단어들, " · "))
11
+ 보여주기("단어 수:", 길이(단어들))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uiwwsw/hangulscript",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "한글 문장으로 작성하고 JavaScript와 타입 선언으로 배포하는 실험적 프로그래밍 언어",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -13,6 +13,10 @@
13
13
  "./browser": {
14
14
  "types": "./dist/transformer.d.ts",
15
15
  "import": "./dist/transformer.js"
16
+ },
17
+ "./vite": {
18
+ "types": "./dist/vite.d.ts",
19
+ "import": "./dist/vite.js"
16
20
  }
17
21
  },
18
22
  "bin": {