@uiwwsw/hangulscript 0.5.0 → 0.6.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 +148 -83
- package/README.md +37 -46
- package/dist/cli.js +2 -2
- package/dist/expressions.js +59 -0
- package/dist/index.js +1 -1
- package/dist/runtime.js +45 -0
- package/dist/sentences.js +124 -89
- package/dist/transformer.js +4 -335
- package/examples/calculator-ui.hgs +0 -1
- package/examples/calculator.hgs +0 -1
- package/examples/discount-ui.hgs +0 -1
- package/examples/discount.hgs +0 -1
- package/examples/hello.hgs +4 -19
- package/examples/math-library.hgs +1 -2
- package/examples/scores.hgs +0 -1
- package/examples/statistics-ui.hgs +0 -1
- package/examples/statistics.hgs +0 -1
- package/examples/text-ui.hgs +0 -1
- package/examples/text.hgs +0 -1
- package/package.json +1 -1
package/LANGUAGE.md
CHANGED
|
@@ -1,107 +1,172 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 한글스크립트 0.6 문법
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
문장형이 유일한 기본 문법입니다. 시작 표기 없이 첫 문장부터 작성합니다.
|
|
4
|
+
키워드형과 `.ks` 입력은 제거됐습니다. 소스는 `.hgs`, 기본 빌드는 JavaScript와 `.d.ts`입니다.
|
|
5
|
+
문장 블록은 공백 들여쓰기를 사용합니다. 탭과 여러 줄 표현식은 지원하지 않습니다.
|
|
6
|
+
`#`로 시작하는 행은 주석입니다. 행 끝 주석은 지원하지 않습니다.
|
|
6
7
|
|
|
7
|
-
##
|
|
8
|
+
## 값·함수·조건·반복
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
변수 선언·각 반복·함수 정의 문장으로도 자동 감지한다.
|
|
11
|
-
기존 `.hgs` 키워드형은 계속 지원한다. 같은 파일 안에서는 하나의 방식을 사용한다.
|
|
12
|
-
공백 들여쓰기 폭은 자유지만 같은 블록은 동일해야 하고 탭은 허용하지 않는다.
|
|
13
|
-
`#`는 행 전체 주석이다. 문장 끝 주석은 현재 지원하지 않는다.
|
|
14
|
-
|
|
15
|
-
```ks
|
|
16
|
-
문장으로 시작
|
|
10
|
+
```text
|
|
17
11
|
이름은 "하늘"이다
|
|
18
|
-
|
|
12
|
+
점수: 숫자는 90이다
|
|
13
|
+
기준은 80으로 고정한다
|
|
19
14
|
|
|
20
|
-
인사(
|
|
15
|
+
인사(누구: 문자열)를 정의한다:
|
|
21
16
|
돌려주기 "안녕, " + 누구
|
|
22
17
|
|
|
23
|
-
각
|
|
24
|
-
만약
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
동안 횟수 > 0:
|
|
32
|
-
횟수 = 횟수 - 1
|
|
33
|
-
만약 횟수 == 1 이라면:
|
|
18
|
+
각 수를 범위(3)에서:
|
|
19
|
+
만약 수 == 1 이라면:
|
|
20
|
+
계속하기
|
|
21
|
+
출력(인사(이름), 수)
|
|
22
|
+
|
|
23
|
+
동안 점수 < 100:
|
|
24
|
+
점수 = 점수 + 1
|
|
25
|
+
만약 점수 == 95 이라면:
|
|
34
26
|
멈추기
|
|
35
27
|
```
|
|
36
28
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
29
|
+
| 기능 | 문장 |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| 변수 | `이름은 값이다`, 재대입은 `이름 = 값` |
|
|
32
|
+
| 타입 있는 변수 | `점수: 숫자는 90이다` |
|
|
33
|
+
| 고정값 | `기준은 80으로 고정한다` |
|
|
34
|
+
| 함수 | `더함(가: 숫자, 나: 숫자)를 정의한다:` |
|
|
35
|
+
| 반환 | `돌려주기 값`, 값 없이도 가능 |
|
|
36
|
+
| 조건 | `만약 식 이라면:` / `아니고 식 이라면:` / `아니면:` |
|
|
37
|
+
| 반복 | `각 항목을 목록에서:` / `동안 식:` |
|
|
38
|
+
| 반복 제어 | `멈추기` / `계속하기` |
|
|
39
|
+
|
|
40
|
+
은·는과 을·를은 둘 다 허용합니다. 타입 없는 인자는 `any`, 빈 목록 선언은 `any[]`가 됩니다.
|
|
41
|
+
타입은 `숫자`, `문자열`, `논리`(또는 `불리언`), `모름`, `아무거나`, `무효`를 사용하거나 TypeScript 타입 표현식을 작성할 수 있습니다.
|
|
42
|
+
표현식은 JavaScript 연산자·배열·객체·정규식·템플릿 문자열을 사용합니다.
|
|
43
|
+
`참`, `거짓`, `비어있음`, `정의안됨`은 리터럴입니다. 문자열·주석·속성 이름은 치환하지 않습니다.
|
|
44
|
+
JS 전역 `Math`, `JSON`, `Map`, `Set`, `Date`, `Error` 등은 원래 이름으로 사용합니다.
|
|
45
|
+
임의의 JS/TS 선언·중괄호 블록을 소스로 작성하는 문법은 지원하지 않습니다.
|
|
46
|
+
|
|
47
|
+
## 선택과 한 번 이상 반복
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
상태는 "완료"이다
|
|
51
|
+
상태에 따라:
|
|
52
|
+
"준비"인 경우:
|
|
53
|
+
출력("준비 중")
|
|
54
|
+
"완료"인 경우:
|
|
55
|
+
출력("끝")
|
|
56
|
+
그 밖의 경우:
|
|
57
|
+
출력("알 수 없음")
|
|
58
|
+
|
|
59
|
+
횟수는 0이다
|
|
60
|
+
먼저 실행한다:
|
|
61
|
+
횟수 = 횟수 + 1
|
|
62
|
+
횟수 < 3인 동안 반복한다
|
|
63
|
+
```
|
|
47
64
|
|
|
48
|
-
|
|
65
|
+
선택 분기는 다음 분기로 넘어가지 않습니다. `그 밖의 경우:`는 기본 분기입니다.
|
|
49
66
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
| `포함(목록, 값)` | 포함 여부 |
|
|
59
|
-
| `나누기(문자열, 구분자)` | 문자열 분리 |
|
|
60
|
-
| `잇기(목록, 구분자)` | 기본 구분자 빈 문자열로 연결 |
|
|
67
|
+
## 모듈
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
"./math.js"에서 { 더함 }을 가져온다
|
|
71
|
+
"./settings.js"에서 설정을 가져온다
|
|
72
|
+
|
|
73
|
+
계산(값: 숫자)를 정의한다:
|
|
74
|
+
돌려주기 더함(값, 설정.기준)
|
|
61
75
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
76
|
+
계산을 공개한다
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
여러 이름은 `{ 계산, 더함 }을 공개한다`, 기본 내보내기는 `계산을 기본으로 공개한다`로 작성합니다.
|
|
80
|
+
가져오기·공개 문장은 최상위에서만 사용합니다. CLI는 `.hgs` 모듈 그래프를 묶거나 경로를 바꾸지 않으므로 미리 빌드한 `.js` 경로를 사용합니다.
|
|
81
|
+
React·Vue의 Vite 플러그인은 `.hgs` import를 빌드할 때 처리합니다.
|
|
82
|
+
|
|
83
|
+
## 비동기·생성기·오류 처리
|
|
65
84
|
|
|
66
|
-
|
|
85
|
+
```text
|
|
86
|
+
비동기로 계산(값: 숫자)를 정의한다:
|
|
87
|
+
결과는 기다리기 Promise.resolve(값 * 2)이다
|
|
88
|
+
돌려주기 결과
|
|
67
89
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
2
|
|
71
|
-
TypeScript 타입 진단은 현재 확장된 생성 코드 위치를 사용한다.
|
|
72
|
-
3. **표준 라이브러리 계약**: 문자열/목록/사전/날짜를 같은 함수 체계로 확장하고
|
|
73
|
-
Unicode 문자 수·동등성·정렬·오류 모델을 명시한다.
|
|
74
|
-
4. **VS Code/LSP**: 강조, 자동 완성, 한글 오류 설명, 포맷터를 추가한다.
|
|
75
|
-
5. **웹 프로젝트 통합**: Vite 플러그인과 .hgs 모듈 그래프를 구현한다.
|
|
76
|
-
현재 CLI는 파일 단위 변환이며 .hgs import 경로를 재작성하지 않는다.
|
|
90
|
+
차례로 목록()를 정의한다:
|
|
91
|
+
내주기 1
|
|
92
|
+
내주기 2
|
|
77
93
|
|
|
78
|
-
|
|
79
|
-
|
|
94
|
+
시도한다:
|
|
95
|
+
오류를 일으킨다 새 Error("입력을 확인하세요")
|
|
96
|
+
오류를 잡으면:
|
|
97
|
+
출력(String(오류))
|
|
98
|
+
마지막으로:
|
|
99
|
+
출력("마침")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`기다리기`는 비동기 함수의 식 맨 앞에 사용합니다. `내주기`는 `차례로` 정의한 함수 안에서 사용합니다.
|
|
103
|
+
브라우저 데모는 네트워크·외부 모듈을 제공하지 않습니다. 모듈과 외부 API 연동은 로컬 프로젝트에서 사용하세요.
|
|
104
|
+
|
|
105
|
+
## 클래스·자료형·제네릭
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
사람 클래스를 정의한다:
|
|
109
|
+
이름: 문자열은 ""이다
|
|
110
|
+
생성할 때(이름: 문자열):
|
|
111
|
+
자신.이름 = 이름
|
|
112
|
+
소개()를 정의한다:
|
|
113
|
+
돌려주기 자신.이름
|
|
114
|
+
별명을 읽으면:
|
|
115
|
+
돌려주기 자신.이름
|
|
116
|
+
별명을 바꾸면(값: 문자열):
|
|
117
|
+
자신.이름 = 값
|
|
118
|
+
|
|
119
|
+
사람을 이어받아 학생 클래스를 정의한다:
|
|
120
|
+
생성할 때(이름: 문자열):
|
|
121
|
+
부모(이름)
|
|
122
|
+
|
|
123
|
+
사용자는 새 학생("하늘")이다
|
|
124
|
+
출력(사용자.소개())
|
|
125
|
+
|
|
126
|
+
상자<T> 자료형을 정의한다:
|
|
127
|
+
값: T
|
|
128
|
+
숫자상자 타입은 상자<숫자>이다
|
|
129
|
+
상태 열거값을 정의한다:
|
|
130
|
+
준비
|
|
131
|
+
완료
|
|
132
|
+
복사<T>(값: T)를 정의한다:
|
|
133
|
+
돌려주기 값
|
|
134
|
+
```
|
|
80
135
|
|
|
81
|
-
|
|
136
|
+
자료형·타입 별칭·열거값은 최상위에 작성합니다. 클래스의 추상·접근 제한자·데코레이터 및 전체 TypeScript 문법은 지원 범위가 아닙니다.
|
|
82
137
|
|
|
83
|
-
|
|
84
|
-
`ksc run 파일.hgs`는 생성 파일 없이 타입 검사 후 Node.js에서 실행한다.
|
|
85
|
-
JS 출력은 ESM이며, 프로젝트는 `type: module` 또는 ESM 번들러를 사용한다.
|
|
86
|
-
공개 함수에 `내보내기 { 함수이름 }`을 쓰면 해당 타입을 .d.ts로 배포할 수 있다.
|
|
138
|
+
## 기본 함수 15개
|
|
87
139
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
140
|
+
| 함수 | 의미 |
|
|
141
|
+
| --- | --- |
|
|
142
|
+
| `출력(...값)` | 콘솔 출력 |
|
|
143
|
+
| `길이(값)` | 문자열 UTF-16 코드 단위 또는 목록 항목 수 |
|
|
144
|
+
| `추가(목록, 값)` | 원본 목록에 추가 |
|
|
145
|
+
| `포함(목록, 값)` | 포함 여부 |
|
|
146
|
+
| `나누기(글, 구분자)` / `잇기(목록, 구분자)` | 문자열 분리·연결 |
|
|
147
|
+
| `다듬기(글)` | 양 끝 공백 제거 |
|
|
148
|
+
| `숫자로(값)` | 빈 문자열·비유한 숫자는 오류 |
|
|
149
|
+
| `합계(목록)` / `평균(목록)` | 합계·평균, 빈 평균은 오류 |
|
|
150
|
+
| `최솟값(목록)` / `최댓값(목록)` | 빈 목록은 오류 |
|
|
151
|
+
| `반올림(값, 자리=0)` | JS Math.round, 자리 -10~10 |
|
|
152
|
+
| `정렬(목록)` | 원본을 복사해 숫자 오름차순 정렬 |
|
|
153
|
+
| `범위(끝)` / `범위(시작, 끝, 간격)` | 끝 제외, 음수 간격 지원, 최대 10만 항목 |
|
|
92
154
|
|
|
93
|
-
|
|
155
|
+
기본 함수 이름은 사용자 선언 이름으로 사용하지 않습니다. JS 부동소수점·반올림 규칙을 따르며 Python 호환·임의 정밀도 십진 연산을 의미하지 않습니다.
|
|
94
156
|
|
|
95
|
-
|
|
96
|
-
기본 소스 확장자는 `.hgs`이며 기존 `.ks` 파일도 CLI에서 계속 지원합니다.
|
|
97
|
-
이전 패키지에서 이동하려면 `npm uninstall @uiwwsw/koreanscript` 후 새 패키지를 설치하세요.
|
|
157
|
+
## 화면 함수 5개
|
|
98
158
|
|
|
159
|
+
`숫자입력("이름", 0)`, `글자입력("이름", "")`, `선택입력("이름", ["하나", "둘"], "하나")`로 입력값을 읽습니다.
|
|
160
|
+
`"계산하기" 버튼을 누르면:` 뒤의 블록은 해당 버튼을 눌렀을 때 실행합니다.
|
|
161
|
+
`버튼("이름")`도 같은 조건을 반환하며 `보여주기("이름", 값)`은 결과 카드를 만듭니다.
|
|
162
|
+
버튼을 누를 때마다 현재 입력값으로 전체 프로그램을 다시 실행합니다. 일반 변수는 실행 사이에 유지되지 않습니다.
|
|
163
|
+
이름은 중복할 수 없으며 한 실행당 최대 40개 화면 요소를 제공합니다.
|
|
164
|
+
화면 함수는 데모 UI 실행 환경이 필요하며 Node CLI 단독 실행이나 React·Vue 컴포넌트 자동 생성 기능은 아닙니다.
|
|
99
165
|
|
|
100
|
-
##
|
|
166
|
+
## 실행·진단 범위
|
|
101
167
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
화면 함수는 Node CLI 단독 실행에서 지원되지 않습니다.
|
|
168
|
+
`ksc build`는 타입 검사 후 JS와 .d.ts를 생성합니다. `ksc run`은 신뢰하는 로컬 코드를 Node.js에서 실행합니다.
|
|
169
|
+
`--typescript`는 중간 TypeScript 확인용 출력입니다. 별도의 소스 문법 모드가 아닙니다.
|
|
170
|
+
웹 데모와 Vite 플러그인은 문법 변환·JS 구문 진단을 제공하며 전체 타입 검사는 하지 않습니다.
|
|
171
|
+
문장 오류는 원본 행 번호를 표시합니다. TypeScript 타입 진단은 생성 코드 위치를 사용하며 원본 소스맵은 아직 제공하지 않습니다.
|
|
172
|
+
임의의 한국어를 해석하는 AI 실행기·Python 호환 언어·전체 JavaScript/TypeScript 대체 언어가 아닙니다.
|
package/README.md
CHANGED
|
@@ -8,10 +8,9 @@
|
|
|
8
8
|
Python의 들여쓰기와 익숙한 기본 함수, JavaScript의 표현식과 웹 생태계,
|
|
9
9
|
TypeScript의 타입 검사를 결합합니다. 소스 확장자는 **`.hgs`**입니다.
|
|
10
10
|
|
|
11
|
-
[온라인에서 실행하기](https://hangulscript.vercel.app/) · [문법 명세](LANGUAGE.md) · [릴리스](https://github.com/uiwwsw/hangulscript/releases) · [의견 보내기](https://github.com/uiwwsw/hangulscript/issues)
|
|
11
|
+
[온라인에서 실행하기](https://hangulscript.vercel.app/) · [React·Vue 사용법](docs/frameworks.md) · [문법 명세](LANGUAGE.md) · [릴리스](https://github.com/uiwwsw/hangulscript/releases) · [의견 보내기](https://github.com/uiwwsw/hangulscript/issues)
|
|
12
12
|
|
|
13
|
-
```
|
|
14
|
-
문장으로 시작
|
|
13
|
+
```text
|
|
15
14
|
점수들은 [75, 92, 88]이다
|
|
16
15
|
각 점수를 점수들에서:
|
|
17
16
|
만약 점수 >= 80 이라면:
|
|
@@ -34,8 +33,8 @@ Python 소스 호환이나 전체 TypeScript 문법 지원을 뜻하지 않습
|
|
|
34
33
|
| [타입 있는 함수 모듈](examples/math-library.hgs) | 함수·타입 | JS + .d.ts 라이브러리 빌드 |
|
|
35
34
|
|
|
36
35
|
데모에서 예제를 선택하고 입력값을 바꾼 다음 **실행하기**를 누르세요. 변환 결과는 기본적으로 JavaScript로 표시하며, 선택 메뉴에서 TypeScript와 비교할 수 있습니다. 실행 결과 패널은 데모를 스크롤하는 동안 화면 하단에 붙어 있습니다. CLI 빌드는 JavaScript와 `.d.ts`를 함께 생성합니다.
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
화면 예제에서는 입력값을 바꾸고 도구 안의 버튼을 누르면 현재 값으로 프로그램을 다시 실행합니다.
|
|
37
|
+
데모는 입력창·선택창·버튼·결과 카드를 제공합니다. 데이터베이스·외부 API는 제공하지 않습니다.
|
|
39
38
|
생성한 JS를 기존 웹/Node 프로젝트에 연결해 화면과 서비스를 확장할 수 있습니다.
|
|
40
39
|
|
|
41
40
|
## 설치
|
|
@@ -79,7 +78,7 @@ npx ksc --typescript examples/math-library.hgs --out-dir output
|
|
|
79
78
|
```
|
|
80
79
|
|
|
81
80
|
**0.2에서 변경:** CLI의 기본 출력이 `.ts`에서 **`.js` + `.d.ts`**로 바뀝니다.
|
|
82
|
-
|
|
81
|
+
중간 TypeScript를 확인하려면 `--typescript`를 추가하세요. 소스는 문장형 하나만 지원합니다.
|
|
83
82
|
`compileSource`·`compileString` API는 기존처럼 TypeScript 문자열을 반환합니다.
|
|
84
83
|
|
|
85
84
|
`run`은 신뢰하는 코드를 로컬 Node.js에서 실행합니다. 온라인 데모와 달리 격리·2초 제한이 없습니다.
|
|
@@ -87,11 +86,10 @@ npx ksc --typescript examples/math-library.hgs --out-dir output
|
|
|
87
86
|
|
|
88
87
|
### 배포 산출물 사용
|
|
89
88
|
|
|
90
|
-
```
|
|
91
|
-
문장으로 시작
|
|
89
|
+
```text
|
|
92
90
|
더함(가: 숫자, 나: 숫자)를 정의한다:
|
|
93
91
|
돌려주기 가 + 나
|
|
94
|
-
|
|
92
|
+
더함을 공개한다
|
|
95
93
|
```
|
|
96
94
|
|
|
97
95
|
생성한 `.js`에는 실행 코드가, `.d.ts`에는 다음과 같은 타입 계약이 담깁니다.
|
|
@@ -114,8 +112,9 @@ ESM을 지원하는 번들러를 사용하세요. JS와 .d.ts를 함께 배포
|
|
|
114
112
|
|
|
115
113
|
## 문장형 문법
|
|
116
114
|
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
문장형이 유일한 기본 문법입니다. 별도의 시작 문장 없이 첫 줄부터 작성합니다.
|
|
116
|
+
|
|
117
|
+
```text
|
|
119
118
|
이름은 "하늘"이다
|
|
120
119
|
횟수는 3이다
|
|
121
120
|
|
|
@@ -136,16 +135,6 @@ ESM을 지원하는 번들러를 사용하세요. JS와 .d.ts를 함께 배포
|
|
|
136
135
|
- 블록은 콜론 뒤에 공백으로 들여씁니다. 탭은 허용하지 않습니다.
|
|
137
136
|
- 행 전체 주석은 `#`입니다. 표현식은 JS의 연산자·배열·객체를 사용합니다.
|
|
138
137
|
|
|
139
|
-
기존 키워드형도 사용할 수 있습니다.
|
|
140
|
-
|
|
141
|
-
```ks
|
|
142
|
-
상수 이름 = "철수";
|
|
143
|
-
함수 인사(사람: 문자열): 문자열 {
|
|
144
|
-
반환 "안녕, " + 사람;
|
|
145
|
-
}
|
|
146
|
-
콘솔.출력(인사(이름));
|
|
147
|
-
```
|
|
148
|
-
|
|
149
138
|
지원 범위와 제약은 [LANGUAGE.md](LANGUAGE.md)에 정리했습니다.
|
|
150
139
|
|
|
151
140
|
## 한글 기본 함수
|
|
@@ -189,17 +178,11 @@ ESM을 지원하는 번들러를 사용하세요. JS와 .d.ts를 함께 배포
|
|
|
189
178
|
|
|
190
179
|
- **컴파일 전용 Worker:** TypeScript 파싱·변환·출력 정리를 UI 스레드 밖에서 실행.
|
|
191
180
|
- **180ms 입력 지연과 결과 재사용:** 키를 누를 때마다 변환하지 않고, 같은 소스는 다시 컴파일하지 않음.
|
|
192
|
-
-
|
|
193
|
-
- **불필요한 재처리 생략:** 기본 함수 코드를 다시 치환하지 않고, 클래스가 없으면 클래스 보정도 생략.
|
|
181
|
+
- **단일 문장형 파서:** 모드 자동 감지·키워드 치환·클래스 사후 보정을 제거하고 문장 블록을 직접 생성.
|
|
194
182
|
- **실행 출력 제한:** 부모 페이지에서도 최대 100행·행당 1,000자로 제한.
|
|
195
183
|
- **범위 제한:** `범위`는 최대 100,000개 항목. 최솟값·최댓값은 인자 펼치기 없이 순회.
|
|
196
184
|
|
|
197
|
-
|
|
198
|
-
변환 중앙값은 변경 전 약 **4.45ms**, 변경 후 약 **0.65ms**였습니다.
|
|
199
|
-
개선 후 20회 측정 p95는 약 **1.88ms**였고, 작은 코드의 타입 검사·JS/.d.ts 생성은 약 **168ms**였습니다.
|
|
200
|
-
서로 다른 기기·입력·브라우저에서 같은 수치를 보장하는 벤치마크는 아닙니다.
|
|
201
|
-
|
|
202
|
-
화면 JS는 약 **10.4KB**, 컴파일러 Worker는 약 **3.58MB**입니다(압축 전).
|
|
185
|
+
성능은 `npm run benchmark`로 1,000행 문장형 선언의 변환 시간과 현재 번들 크기를 측정할 수 있습니다.
|
|
203
186
|
Worker 분리는 UI의 파싱·실행 부담을 분리하지만 전체 다운로드를 제거하지 않습니다.
|
|
204
187
|
첫 로딩은 네트워크와 기기에 따라 느릴 수 있습니다.
|
|
205
188
|
|
|
@@ -218,7 +201,7 @@ npm run benchmark
|
|
|
218
201
|
```js
|
|
219
202
|
import { buildSource, lintSource } from '@uiwwsw/hangulscript';
|
|
220
203
|
|
|
221
|
-
const source = '
|
|
204
|
+
const source = '출력(합계([1, 2, 3]))';
|
|
222
205
|
const { javascript, declaration } = buildSource(source);
|
|
223
206
|
const { hasErrors } = lintSource(source);
|
|
224
207
|
```
|
|
@@ -227,7 +210,7 @@ Node 파일 API 없이 변환만 필요하면:
|
|
|
227
210
|
|
|
228
211
|
```js
|
|
229
212
|
import { compileSource } from '@uiwwsw/hangulscript/browser';
|
|
230
|
-
const typescript = compileSource('
|
|
213
|
+
const typescript = compileSource('출력("안녕")');
|
|
231
214
|
```
|
|
232
215
|
|
|
233
216
|
이 진입점도 TypeScript 스캐너를 포함합니다. 브라우저에서는 데모처럼 Worker에서 사용하세요.
|
|
@@ -246,7 +229,7 @@ npm pack --dry-run
|
|
|
246
229
|
Vercel은 GitHub main과 연결되어 `site-dist/`를 배포합니다.
|
|
247
230
|
`SITE_URL`에 HTTPS origin을 지정하면 canonical·Open Graph·robots·sitemap 주소를 바꿀 수 있습니다.
|
|
248
231
|
|
|
249
|
-
|
|
232
|
+
Vite 연동을 제공합니다. 독립 AST·원본 `.hgs` 소스맵·LSP·모듈 그래프는 향후 과제입니다.
|
|
250
233
|
|
|
251
234
|
## 라이선스
|
|
252
235
|
|
|
@@ -255,7 +238,7 @@ Vercel은 GitHub main과 연결되어 `site-dist/`를 배포합니다.
|
|
|
255
238
|
## 이름과 확장자 이전 (0.4)
|
|
256
239
|
|
|
257
240
|
제품명은 한글스크립트(HangulScript), 패키지는 `@uiwwsw/hangulscript`입니다.
|
|
258
|
-
|
|
241
|
+
0.6부터 소스 확장자는 `.hgs`만 지원하며 키워드형과 시작 표기를 제거했습니다. 기존 코드는 [문법 명세](LANGUAGE.md)의 문장형으로 옮겨야 합니다.
|
|
259
242
|
이전 패키지에서 이동하려면 `npm uninstall @uiwwsw/koreanscript` 후 새 패키지를 설치하세요.
|
|
260
243
|
|
|
261
244
|
|
|
@@ -264,7 +247,6 @@ Vercel은 GitHub main과 연결되어 `site-dist/`를 배포합니다.
|
|
|
264
247
|
온라인 데모의 계산기·할인·통계·문자열 예제는 코드에 선언한 실제 화면을 제공합니다.
|
|
265
248
|
|
|
266
249
|
```text
|
|
267
|
-
문장으로 시작
|
|
268
250
|
가격은 숫자입력("상품 가격", 10000)이다
|
|
269
251
|
할인율은 숫자입력("할인율", 20)이다
|
|
270
252
|
|
|
@@ -285,7 +267,11 @@ HTML·이벤트 핸들러 문자열을 삽입하지 않습니다.
|
|
|
285
267
|
|
|
286
268
|
## React·Vue에서 .hgs 가져오기
|
|
287
269
|
|
|
288
|
-
Vite
|
|
270
|
+
**0.6.0 이상**의 공통 Vite 플러그인을 사용합니다. React/Vue의 화면과 상태 관리에 한글스크립트 계산·검증 함수를 연결할 수 있습니다.
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
npm install --save-dev @uiwwsw/hangulscript@^0.6.0
|
|
274
|
+
```
|
|
289
275
|
|
|
290
276
|
```js
|
|
291
277
|
// vite.config.js — React
|
|
@@ -296,21 +282,26 @@ import hangulscript from '@uiwwsw/hangulscript/vite';
|
|
|
296
282
|
export default defineConfig({ plugins: [hangulscript(), react()] });
|
|
297
283
|
```
|
|
298
284
|
|
|
299
|
-
Vue에서는
|
|
285
|
+
Vue에서는 `@vitejs/plugin-vue`에서 `vue`를 가져와 `[hangulscript(), vue()]`로 설정합니다.
|
|
300
286
|
|
|
301
287
|
```text
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
288
|
+
결제금액(가격: number, 할인율: number)를 정의한다:
|
|
289
|
+
돌려주기 가격 - 반올림(가격 * 할인율 / 100)
|
|
290
|
+
결제금액을 공개한다
|
|
306
291
|
```
|
|
307
292
|
|
|
308
293
|
```js
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
console.log(할인(10000, 20)); // 8000
|
|
294
|
+
import { 결제금액 } from './discount.hgs';
|
|
295
|
+
console.log(결제금액(10000, 20)); // 8000
|
|
312
296
|
```
|
|
313
297
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
298
|
+
완성된 **입력창 → 버튼 → 계산 결과** 예제와 React JSX·Vue SFC 전체 코드를 제공합니다.
|
|
299
|
+
|
|
300
|
+
- [React·Vue 단계별 사용 안내](docs/frameworks.md): 설치·설정·한글 함수·컴포넌트 연결·TypeScript 사용법
|
|
301
|
+
- [실행 가능한 React 예제](examples/react-vite): 해당 폴더에서 `npm ci` → `npm run dev`
|
|
302
|
+
- [실행 가능한 Vue 예제](examples/vue-vite): 해당 폴더에서 `npm ci` → `npm run dev`
|
|
303
|
+
- [온라인 연동 안내](https://hangulscript.vercel.app/#frameworks)
|
|
304
|
+
|
|
305
|
+
예제는 Node.js `^20.19.0 || >=22.12.0`이 필요하며 Node.js 24에서 검증했습니다.
|
|
306
|
+
Vite 플러그인은 타입 검사·선언 생성·소스맵을 제공하지 않습니다. TypeScript 프로젝트는 `ksc build`로 생성한 `.js`와 `.d.ts`를 가져오는 방식이 적합합니다.
|
|
307
|
+
데모의 UI 문장을 React/Vue 컴포넌트로 자동 변환하는 기능은 포함하지 않습니다.
|
package/dist/cli.js
CHANGED
|
@@ -12,7 +12,7 @@ function usage() {
|
|
|
12
12
|
옵션:
|
|
13
13
|
-o, --out-dir <경로> 출력 디렉터리 (기본: 입력 파일 옆)
|
|
14
14
|
--check 타입 검사만 실행
|
|
15
|
-
--typescript
|
|
15
|
+
--typescript 변환된 TypeScript 확인용 출력
|
|
16
16
|
-h, --help 도움말
|
|
17
17
|
|
|
18
18
|
문장형은 실험판입니다. run은 신뢰하는 로컬 코드만 실행하세요.`);
|
|
@@ -34,7 +34,7 @@ async function main() {
|
|
|
34
34
|
} else if (arg === '--check') check = true;
|
|
35
35
|
else if (arg === '--typescript') typescript = true;
|
|
36
36
|
else if (arg.startsWith('-')) throw new Error(`알 수 없는 옵션: ${arg}`);
|
|
37
|
-
else if (!(/\.
|
|
37
|
+
else if (!(/\.hgs$/.test(arg))) throw new Error('입력 파일은 .hgs 확장자여야 합니다.');
|
|
38
38
|
else inputs.push(arg);
|
|
39
39
|
}
|
|
40
40
|
if (!inputs.length) throw new Error('입력 .hgs 파일을 지정하세요.');
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
|
|
3
|
+
const literals = new Map([['참', 'true'], ['거짓', 'false'], ['비어있음', 'null'], ['정의안됨', 'undefined'], ['자신', 'this'], ['부모', 'super']]);
|
|
4
|
+
const types = new Map([['숫자', 'number'], ['문자열', 'string'], ['논리', 'boolean'], ['불리언', 'boolean'], ['아무거나', 'any'], ['모름', 'unknown'], ['무효', 'void']]);
|
|
5
|
+
|
|
6
|
+
function identifiers(source, replacements) {
|
|
7
|
+
const file = ts.createSourceFile('expression.ts', source, ts.ScriptTarget.Latest, true);
|
|
8
|
+
const edits = [];
|
|
9
|
+
const visit = node => {
|
|
10
|
+
if (ts.isIdentifier(node) && replacements.has(node.text)) {
|
|
11
|
+
const p = node.parent;
|
|
12
|
+
const property = (ts.isPropertyAccessExpression(p) && p.name === node) ||
|
|
13
|
+
((ts.isPropertyAssignment(p) || ts.isMethodDeclaration(p) || ts.isPropertySignature(p)) && p.name === node);
|
|
14
|
+
if (!property) edits.push([node.getStart(file), node.end, replacements.get(node.text)]);
|
|
15
|
+
}
|
|
16
|
+
ts.forEachChild(node, visit);
|
|
17
|
+
};
|
|
18
|
+
visit(file);
|
|
19
|
+
for (const [start, end, value] of edits.reverse()) source = source.slice(0, start) + value + source.slice(end);
|
|
20
|
+
return source;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function expression(source) {
|
|
24
|
+
source = source.replace(/^새\s+/, 'new ').replace(/^기다리기\s+/, 'await ');
|
|
25
|
+
if (/[참거비정자부]/.test(source)) {
|
|
26
|
+
const prefix = 'const __expression = (';
|
|
27
|
+
source = identifiers(prefix + source + ');', literals).slice(prefix.length, -2);
|
|
28
|
+
}
|
|
29
|
+
return source;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function typeExpression(source) {
|
|
33
|
+
const prefix = 'type __Type = ';
|
|
34
|
+
return identifiers(prefix + source, types).slice(prefix.length);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function signature(source, name = '__fn') {
|
|
38
|
+
const file = ts.createSourceFile('signature.ts', `function ${name}${source} {}`, ts.ScriptTarget.Latest, true);
|
|
39
|
+
const fn = file.statements[0];
|
|
40
|
+
if (file.parseDiagnostics.length || !ts.isFunctionDeclaration(fn)) throw new SyntaxError('함수 이름·매개변수·타입을 확인하세요.');
|
|
41
|
+
return fn.parameters.map(param => {
|
|
42
|
+
let printed = param.getText(file);
|
|
43
|
+
const start = param.getStart(file);
|
|
44
|
+
const edits = [];
|
|
45
|
+
if (param.type) edits.push([param.type.getStart(file) - start, param.type.end - start, typeExpression(param.type.getText(file))]);
|
|
46
|
+
if (param.initializer) edits.push([param.initializer.getStart(file) - start, param.initializer.end - start, expression(param.initializer.getText(file))]);
|
|
47
|
+
for (const [from, to, value] of edits.sort((a, b) => b[0] - a[0])) printed = printed.slice(0, from) + value + printed.slice(to);
|
|
48
|
+
if (!param.type && !param.initializer) printed += param.dotDotDotToken ? ': any[]' : ': any';
|
|
49
|
+
return printed;
|
|
50
|
+
}).join(', ');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function statement(source) {
|
|
54
|
+
const file = ts.createSourceFile('statement.ts', source, ts.ScriptTarget.Latest, true);
|
|
55
|
+
if (file.parseDiagnostics.length || file.statements.length !== 1 || !ts.isExpressionStatement(file.statements[0])) {
|
|
56
|
+
throw new SyntaxError('문장형 구문을 사용하세요. 선언은 「이름은 값이다」, 함수는 「이름(...)를 정의한다:」로 작성합니다.');
|
|
57
|
+
}
|
|
58
|
+
return source.replace(/;$/, '') + ';';
|
|
59
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -141,7 +141,7 @@ export { compileSource };
|
|
|
141
141
|
/** Compile through TypeScript once, emitting executable JS and declarations in memory. */
|
|
142
142
|
export function buildSource(source, filePath = 'input.hgs') {
|
|
143
143
|
const ts = loadTypeScriptModule();
|
|
144
|
-
const virtualPath = path.resolve(filePath).replace(/\.
|
|
144
|
+
const virtualPath = path.resolve(filePath).replace(/\.hgs$/i, '') + '.ts';
|
|
145
145
|
const compiled = compileSource(source);
|
|
146
146
|
const options = {
|
|
147
147
|
target: ts.ScriptTarget.ES2022,
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { uiRuntime } from './ui.js';
|
|
2
|
+
export const sentenceRuntime = `${uiRuntime}
|
|
3
|
+
|
|
4
|
+
function 출력(...값: unknown[]): void { console.log(...값); }
|
|
5
|
+
function 길이(값: string | readonly unknown[]): number { return 값.length; }
|
|
6
|
+
function 추가<T>(목록: T[], 값: T): void { 목록.push(값); }
|
|
7
|
+
function 포함<T>(목록: readonly T[], 값: T): boolean { return 목록.includes(값); }
|
|
8
|
+
function 나누기(글: string, 구분: string): string[] { return 글.split(구분); }
|
|
9
|
+
function 잇기(목록: readonly unknown[], 구분 = ""): string { return 목록.join(구분); }
|
|
10
|
+
function 합계(값: readonly number[]): number { return 값.reduce((합, 항목) => 합 + 항목, 0); }
|
|
11
|
+
function 숫자로(값: string | number): number {
|
|
12
|
+
if (typeof 값 === "string" && 값.trim() === "") throw new Error("빈 문자열은 숫자로 바꿀 수 없습니다.");
|
|
13
|
+
const 결과 = Number(값);
|
|
14
|
+
if (!Number.isFinite(결과)) throw new Error("유한한 숫자를 입력하세요.");
|
|
15
|
+
return 결과;
|
|
16
|
+
}
|
|
17
|
+
function 다듬기(글: string): string { return 글.trim(); }
|
|
18
|
+
function 반올림(값: number, 자리 = 0): number {
|
|
19
|
+
if (!Number.isFinite(값) || !Number.isInteger(자리) || 자리 < -10 || 자리 > 10) throw new Error("유한한 수와 -10~10 사이의 정수 자리가 필요합니다.");
|
|
20
|
+
return Math.round(값 * 10 ** 자리) / 10 ** 자리;
|
|
21
|
+
}
|
|
22
|
+
function 평균(목록: readonly number[]): number {
|
|
23
|
+
if (!목록.length) throw new Error("빈 목록의 평균을 구할 수 없습니다.");
|
|
24
|
+
return 합계(목록) / 목록.length;
|
|
25
|
+
}
|
|
26
|
+
function 최솟값(목록: readonly number[]): number {
|
|
27
|
+
if (!목록.length) throw new Error("빈 목록의 최솟값을 구할 수 없습니다.");
|
|
28
|
+
let 결과 = 목록[0]; for (const 항목 of 목록) if (항목 < 결과) 결과 = 항목;
|
|
29
|
+
return 결과;
|
|
30
|
+
}
|
|
31
|
+
function 최댓값(목록: readonly number[]): number {
|
|
32
|
+
if (!목록.length) throw new Error("빈 목록의 최댓값을 구할 수 없습니다.");
|
|
33
|
+
let 결과 = 목록[0]; for (const 항목 of 목록) if (항목 > 결과) 결과 = 항목;
|
|
34
|
+
return 결과;
|
|
35
|
+
}
|
|
36
|
+
function 정렬(값: readonly number[]): number[] { return [...값].sort((가, 나) => 가 - 나); }
|
|
37
|
+
function 범위(시작: number, 끝?: number, 간격 = 1): number[] {
|
|
38
|
+
if (끝 === undefined) { 끝 = 시작; 시작 = 0; }
|
|
39
|
+
if (!Number.isFinite(시작) || !Number.isFinite(끝) || !Number.isFinite(간격) || 간격 === 0) throw new Error("범위에는 유한한 수와 0이 아닌 간격이 필요합니다.");
|
|
40
|
+
const 개수 = Math.max(0, Math.ceil((끝 - 시작) / 간격));
|
|
41
|
+
if (개수 > 100000) throw new Error("범위는 최대 100000개까지 만들 수 있습니다.");
|
|
42
|
+
return Array.from({ length: 개수 }, (_, 순서) => 시작 + 순서 * 간격);
|
|
43
|
+
}
|
|
44
|
+
`;
|
|
45
|
+
|
package/dist/sentences.js
CHANGED
|
@@ -1,103 +1,138 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export
|
|
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));
|
|
5
|
-
}
|
|
1
|
+
import { sentenceRuntime } from './runtime.js';
|
|
2
|
+
import { expression, typeExpression, signature, statement } from './expressions.js';
|
|
3
|
+
export { sentenceRuntime } from './runtime.js';
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
function 길이(값: string | readonly unknown[]): number { return 값.length; }
|
|
11
|
-
function 추가<T>(목록: T[], 값: T): void { 목록.push(값); }
|
|
12
|
-
function 포함<T>(목록: readonly T[], 값: T): boolean { return 목록.includes(값); }
|
|
13
|
-
function 나누기(글: string, 구분: string): string[] { return 글.split(구분); }
|
|
14
|
-
function 잇기(목록: readonly unknown[], 구분 = ""): string { return 목록.join(구분); }
|
|
15
|
-
function 합계(값: readonly number[]): number { return 값.reduce((합, 항목) => 합 + 항목, 0); }
|
|
16
|
-
function 숫자로(값: string | number): number {
|
|
17
|
-
if (typeof 값 === "string" && 값.trim() === "") throw new Error("빈 문자열은 숫자로 바꿀 수 없습니다.");
|
|
18
|
-
const 결과 = Number(값);
|
|
19
|
-
if (!Number.isFinite(결과)) throw new Error("유한한 숫자를 입력하세요.");
|
|
20
|
-
return 결과;
|
|
21
|
-
}
|
|
22
|
-
function 다듬기(글: string): string { return 글.trim(); }
|
|
23
|
-
function 반올림(값: number, 자리 = 0): number {
|
|
24
|
-
if (!Number.isFinite(값) || !Number.isInteger(자리) || 자리 < -10 || 자리 > 10) throw new Error("유한한 수와 -10~10 사이의 정수 자리가 필요합니다.");
|
|
25
|
-
return Math.round(값 * 10 ** 자리) / 10 ** 자리;
|
|
26
|
-
}
|
|
27
|
-
function 평균(목록: readonly number[]): number {
|
|
28
|
-
if (!목록.length) throw new Error("빈 목록의 평균을 구할 수 없습니다.");
|
|
29
|
-
return 합계(목록) / 목록.length;
|
|
30
|
-
}
|
|
31
|
-
function 최솟값(목록: readonly number[]): number {
|
|
32
|
-
if (!목록.length) throw new Error("빈 목록의 최솟값을 구할 수 없습니다.");
|
|
33
|
-
let 결과 = 목록[0]; for (const 항목 of 목록) if (항목 < 결과) 결과 = 항목;
|
|
34
|
-
return 결과;
|
|
35
|
-
}
|
|
36
|
-
function 최댓값(목록: readonly number[]): number {
|
|
37
|
-
if (!목록.length) throw new Error("빈 목록의 최댓값을 구할 수 없습니다.");
|
|
38
|
-
let 결과 = 목록[0]; for (const 항목 of 목록) if (항목 > 결과) 결과 = 항목;
|
|
39
|
-
return 결과;
|
|
40
|
-
}
|
|
41
|
-
function 정렬(값: readonly number[]): number[] { return [...값].sort((가, 나) => 가 - 나); }
|
|
42
|
-
function 범위(시작: number, 끝?: number, 간격 = 1): number[] {
|
|
43
|
-
if (끝 === undefined) { 끝 = 시작; 시작 = 0; }
|
|
44
|
-
if (!Number.isFinite(시작) || !Number.isFinite(끝) || !Number.isFinite(간격) || 간격 === 0) throw new Error("범위에는 유한한 수와 0이 아닌 간격이 필요합니다.");
|
|
45
|
-
const 개수 = Math.max(0, Math.ceil((끝 - 시작) / 간격));
|
|
46
|
-
if (개수 > 100000) throw new Error("범위는 최대 100000개까지 만들 수 있습니다.");
|
|
47
|
-
return Array.from({ length: 개수 }, (_, 순서) => 시작 + 순서 * 간격);
|
|
48
|
-
}
|
|
49
|
-
`;
|
|
5
|
+
const identifier = /^[\p{L}_$][\p{L}\p{N}_$]*$/u;
|
|
6
|
+
const functionName = /^[\p{L}_$][\p{L}\p{N}_$]*(?:<.+>)?$/u;
|
|
7
|
+
const obsolete = /^(?:상수|변수|함수|반환|클래스|가져오기|내보내기|반복|비동기함수|스위치|선택|케이스|경우|인터페이스|열거형|시도|잡기|마침내|던지기|선언|추상)(?:\s|[({*])|^콘솔\.출력/u;
|
|
50
8
|
|
|
51
9
|
export function compileSentences(source, { includeRuntime = true } = {}) {
|
|
52
10
|
const output = includeRuntime ? [sentenceRuntime] : [];
|
|
53
|
-
const
|
|
54
|
-
let
|
|
11
|
+
const frames = [{ indent: 0, kind: 'root' }];
|
|
12
|
+
let pending = null;
|
|
55
13
|
const lines = source.split(/\r?\n/);
|
|
14
|
+
const inside = kind => {
|
|
15
|
+
for (let i = frames.length - 1; i > 0; i--) {
|
|
16
|
+
if (kind.includes(frames[i].kind)) return true;
|
|
17
|
+
if (['function', 'method', 'generator', 'async'].includes(frames[i].kind)) return false;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
};
|
|
56
21
|
for (let index = 0; index < lines.length; index++) {
|
|
57
22
|
const raw = lines[index];
|
|
58
23
|
const text = raw.trim();
|
|
59
|
-
if (!text || text
|
|
24
|
+
if (!text || text.startsWith('#')) continue;
|
|
60
25
|
const fail = message => { throw new SyntaxError(`${index + 1}행: ${message}`); };
|
|
26
|
+
if (/^문장으로 시작[.]?$/.test(text)) fail('시작 표기는 사용하지 않습니다. 첫 문장부터 작성하세요.');
|
|
61
27
|
if (/^\s*\t/.test(raw)) fail('들여쓰기는 탭 대신 공백을 사용하세요.');
|
|
62
28
|
const indent = raw.length - raw.trimStart().length;
|
|
63
|
-
if (
|
|
64
|
-
if (indent <=
|
|
65
|
-
|
|
66
|
-
} else if (indent >
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
29
|
+
if (pending) {
|
|
30
|
+
if (indent <= frames.at(-1).indent) fail('블록 안의 문장을 들여써 주세요.');
|
|
31
|
+
frames.push({ ...pending, indent }); pending = null;
|
|
32
|
+
} else if (indent > frames.at(-1).indent) fail('예상하지 못한 들여쓰기입니다.');
|
|
33
|
+
let closed;
|
|
34
|
+
while (indent < frames.at(-1).indent) {
|
|
35
|
+
closed = frames.pop();
|
|
36
|
+
output.push(closed.kind === 'case' ? 'break; }' : '}');
|
|
37
|
+
}
|
|
38
|
+
if (indent !== frames.at(-1).indent) fail('이전 블록과 들여쓰기를 맞춰 주세요.');
|
|
39
|
+
const parent = frames.at(-1).kind;
|
|
40
|
+
const open = (code, kind) => { output.push(code + ' {'); pending = { kind }; };
|
|
41
|
+
const emit = code => output.push(code);
|
|
42
|
+
const exp = value => expression(value);
|
|
43
|
+
const top = () => { if (parent !== 'root') fail('가져오기·공개·타입 선언은 파일의 최상위에 작성하세요.'); };
|
|
44
|
+
let m;
|
|
45
|
+
try {
|
|
46
|
+
if (closed?.kind === 'do' && !/^.+인 동안 반복한다$/.test(text)) fail('먼저 실행한다 블록 뒤에 「조건인 동안 반복한다」가 필요합니다.');
|
|
47
|
+
if (obsolete.test(text)) fail('키워드형 문법은 제거됐습니다. 문장형으로 작성하세요.');
|
|
48
|
+
if ((m = text.match(/^(비동기로 |차례로 )?(.+?)\((.*)\)를 정의한다:$/u))) {
|
|
49
|
+
if (!functionName.test(m[2])) fail('함수 이름을 확인하세요.');
|
|
50
|
+
const method = parent === 'class';
|
|
51
|
+
const kind = m[1] === '비동기로 ' ? 'async' : m[1] === '차례로 ' ? 'generator' : method ? 'method' : 'function';
|
|
52
|
+
const prefix = (kind === 'async' ? 'async ' : '') + (method ? (kind === 'generator' ? '*' : '') : (kind === 'generator' ? 'function* ' : 'function '));
|
|
53
|
+
open(`${prefix}${m[2]}(${signature('(' + m[3] + ')', m[2])})`, kind);
|
|
54
|
+
} else if ((m = text.match(/^생성할 때\((.*)\):$/u))) {
|
|
55
|
+
if (parent !== 'class') fail('생성할 때는 클래스 안에서 사용하세요.');
|
|
56
|
+
open(`constructor(${signature('(' + m[1] + ')')})`, 'method');
|
|
57
|
+
} else if ((m = text.match(/^(.+?)을 읽으면:$/u))) {
|
|
58
|
+
if (parent !== 'class' || !identifier.test(m[1])) fail('읽기 속성은 클래스 안에 작성하세요.');
|
|
59
|
+
open(`get ${m[1]}()`, 'method');
|
|
60
|
+
} else if ((m = text.match(/^(.+?)을 바꾸면\((.*)\):$/u))) {
|
|
61
|
+
if (parent !== 'class' || !identifier.test(m[1])) fail('쓰기 속성은 클래스 안에 작성하세요.');
|
|
62
|
+
open(`set ${m[1]}(${signature('(' + m[2] + ')')})`, 'method');
|
|
63
|
+
} else if ((m = text.match(/^(?:(.+?)을 이어받아 )?(.+?) 클래스를 정의한다:$/u))) {
|
|
64
|
+
if (!functionName.test(m[2])) fail('클래스 이름을 확인하세요.');
|
|
65
|
+
open(`class ${m[2]}${m[1] ? ' extends ' + m[1] : ''}`, 'class');
|
|
66
|
+
} else if ((m = text.match(/^(.+?) 자료형을 정의한다:$/u))) {
|
|
67
|
+
top(); if (!functionName.test(m[1])) fail('자료형 이름을 확인하세요.'); open(`interface ${m[1]}`, 'interface');
|
|
68
|
+
} else if ((m = text.match(/^(.+?) 타입은 (.+)이다$/u))) {
|
|
69
|
+
top(); if (!functionName.test(m[1])) fail('타입 이름을 확인하세요.'); emit(`type ${m[1]} = ${typeExpression(m[2])};`);
|
|
70
|
+
} else if ((m = text.match(/^(.+?) 열거값을 정의한다:$/u))) {
|
|
71
|
+
top(); if (!identifier.test(m[1])) fail('열거값 이름을 확인하세요.'); open(`enum ${m[1]}`, 'enum');
|
|
72
|
+
} else if (parent === 'enum') {
|
|
73
|
+
if (!identifier.test(text)) fail('열거값 항목에는 이름 하나를 작성하세요.'); emit(text + ',');
|
|
74
|
+
} else if ((m = text.match(/^(.+)에서 (.+?)(?:을|를) 가져온다$/u))) {
|
|
75
|
+
top(); if (!/^(?:".*"|'.*')$/.test(m[1])) fail('모듈 경로를 따옴표로 감싸세요.');
|
|
76
|
+
const binding = m[2].replace(/^모두를 (.+)로$/, '* as $1'); emit(`import ${binding} from ${m[1]};`);
|
|
77
|
+
} else if ((m = text.match(/^(.+?)(?:을|를) 기본으로 공개한다$/u))) {
|
|
78
|
+
top(); emit(`export default ${exp(m[1])};`);
|
|
79
|
+
} else if ((m = text.match(/^(.+?)(?:을|를) 공개한다$/u))) {
|
|
80
|
+
top(); emit(`export ${m[1].startsWith('{') ? m[1] : '{ ' + m[1] + ' }'};`);
|
|
81
|
+
} else if ((m = text.match(/^("(?:[^"\\]|\\.)*") 버튼을 누르면:$/u))) {
|
|
82
|
+
open(`if (버튼(${m[1]}))`, 'if');
|
|
83
|
+
} else if ((m = text.match(/^각 (.+?)(?:을|를) (.+)에서:$/u))) {
|
|
84
|
+
if (!identifier.test(m[1])) fail('반복 변수 이름을 확인하세요.'); open(`for (const ${m[1]} of ${exp(m[2])})`, 'loop');
|
|
85
|
+
} else if ((m = text.match(/^만약 (.+) (?:이)?라면:$/u))) {
|
|
86
|
+
open(`if (${exp(m[1])})`, 'if');
|
|
87
|
+
} else if ((m = text.match(/^아니고 (.+) (?:이)?라면:$/u))) {
|
|
88
|
+
if (!['if', 'elseif'].includes(closed?.kind)) fail('아니고 앞에는 조건 블록이 필요합니다.'); open(`else if (${exp(m[1])})`, 'elseif');
|
|
89
|
+
} else if (text === '아니면:') {
|
|
90
|
+
if (!['if', 'elseif'].includes(closed?.kind)) fail('아니면 앞에는 조건 블록이 필요합니다.'); open('else', 'else');
|
|
91
|
+
} else if ((m = text.match(/^동안 (.+):$/u))) {
|
|
92
|
+
open(`while (${exp(m[1])})`, 'loop');
|
|
93
|
+
} else if (text === '먼저 실행한다:') {
|
|
94
|
+
open('do', 'do');
|
|
95
|
+
} else if ((m = text.match(/^(.+)인 동안 반복한다$/u))) {
|
|
96
|
+
if (closed?.kind !== 'do') fail('먼저 실행한다 블록 뒤에 작성하세요.'); emit(`while (${exp(m[1])});`);
|
|
97
|
+
} else if ((m = text.match(/^(.+)에 따라:$/u))) {
|
|
98
|
+
open(`switch (${exp(m[1])})`, 'switch');
|
|
99
|
+
} else if ((m = text.match(/^(.+)인 경우:$/u))) {
|
|
100
|
+
if (parent !== 'switch') fail('경우는 값에 따라 블록 안에 작성하세요.'); open(`case ${exp(m[1])}:`, 'case');
|
|
101
|
+
} else if (text === '그 밖의 경우:') {
|
|
102
|
+
if (parent !== 'switch') fail('그 밖의 경우는 값에 따라 블록 안에 작성하세요.'); open('default:', 'case');
|
|
103
|
+
} else if (text === '시도한다:') {
|
|
104
|
+
open('try', 'try');
|
|
105
|
+
} else if ((m = text.match(/^(.+?)(?:을|를) 잡으면:$/u))) {
|
|
106
|
+
if (closed?.kind !== 'try' || !identifier.test(m[1])) fail('잡으면 앞에는 시도한다 블록이 필요합니다.'); open(`catch (${m[1]})`, 'catch');
|
|
107
|
+
} else if (text === '마지막으로:') {
|
|
108
|
+
if (!['try', 'catch'].includes(closed?.kind)) fail('마지막으로 앞에는 시도한다 또는 잡으면 블록이 필요합니다.'); open('finally', 'finally');
|
|
109
|
+
} else if ((m = text.match(/^오류를 일으킨다 (.+)$/u))) {
|
|
110
|
+
emit(`throw ${exp(m[1])};`);
|
|
111
|
+
} else if ((m = text.match(/^(.+?)(?:은|는) (.+?)(이다|으로 고정한다)$/u))) {
|
|
112
|
+
const binding = m[1].match(/^([\p{L}_$][\p{L}\p{N}_$]*)(?::\s*(.+))?$/u);
|
|
113
|
+
if (!binding) fail('변수 이름과 타입을 확인하세요.');
|
|
114
|
+
const type = binding[2] ? ': ' + typeExpression(binding[2]) : m[2] === '[]' ? ': any[]' : '';
|
|
115
|
+
emit(`${parent === 'class' ? '' : m[3] === '으로 고정한다' ? 'const ' : 'let '}${binding[1]}${type} = ${exp(m[2])};`);
|
|
116
|
+
} else if (['class', 'interface'].includes(parent) && (m = text.match(/^([\p{L}_$][\p{L}\p{N}_$]*\??): (.+)$/u))) {
|
|
117
|
+
emit(`${m[1]}: ${typeExpression(m[2])};`);
|
|
118
|
+
} else if ((m = text.match(/^돌려주기(?: (.+))?$/u))) {
|
|
119
|
+
if (!frames.some(f => ['function', 'method', 'async', 'generator'].includes(f.kind))) fail('돌려주기는 함수 안에서 사용하세요.'); emit(`return ${m[1] ? exp(m[1]) : ''};`);
|
|
120
|
+
} else if ((m = text.match(/^내주기 (.+)$/u))) {
|
|
121
|
+
if (!inside(['generator'])) fail('내주기는 차례로 정의한 함수 안에서 사용하세요.'); emit(`yield ${exp(m[1])};`);
|
|
122
|
+
} else if (text === '멈추기' || text === '계속하기') {
|
|
123
|
+
if (!inside(text === '멈추기' ? ['loop', 'do', 'case'] : ['loop', 'do'])) fail('반복 블록 안에서 사용하세요.'); emit(text === '멈추기' ? 'break;' : 'continue;');
|
|
124
|
+
} else if (text.endsWith(':')) fail('지원하지 않는 블록 문장입니다.');
|
|
125
|
+
else emit(statement(exp(text)));
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error.message.startsWith(`${index + 1}행:`)) throw error;
|
|
128
|
+
fail(error.message);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (pending) throw new SyntaxError('마지막 블록에 실행할 문장이 필요합니다.');
|
|
132
|
+
while (frames.length > 1) {
|
|
133
|
+
const frame = frames.pop();
|
|
134
|
+
if (frame.kind === 'do') throw new SyntaxError('먼저 실행한다 블록 뒤에 반복 조건이 필요합니다.');
|
|
135
|
+
output.push(frame.kind === 'case' ? 'break; }' : '}');
|
|
99
136
|
}
|
|
100
|
-
if (needsIndent) throw new SyntaxError('마지막 블록에 실행할 문장이 필요합니다.');
|
|
101
|
-
while (indents.length > 1) { indents.pop(); output.push('}'); }
|
|
102
137
|
return output.join('\n');
|
|
103
138
|
}
|
package/dist/transformer.js
CHANGED
|
@@ -1,338 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { compileSentences, isSentenceSource, sentenceRuntime } from './sentences.js';
|
|
3
|
-
|
|
4
|
-
const identifierCharRegex = /[\p{L}\p{N}_$]/u;
|
|
5
|
-
|
|
6
|
-
const KEYWORD_TRANSFORMS = [
|
|
7
|
-
{ match: "내보내기 기본내보내기", replacement: "export default", requiresBoundary: true },
|
|
8
|
-
{ match: "비동기함수", replacement: "async function", requiresBoundary: true },
|
|
9
|
-
{ match: "상수", replacement: "const", requiresBoundary: true },
|
|
10
|
-
{ match: "변수", replacement: "let", requiresBoundary: true },
|
|
11
|
-
{ match: "함수", replacement: "function", requiresBoundary: true },
|
|
12
|
-
{ match: "반환", replacement: "return", requiresBoundary: true },
|
|
13
|
-
{ match: "만약", replacement: "if", requiresBoundary: true },
|
|
14
|
-
{ match: "아니면", replacement: "else", requiresBoundary: true },
|
|
15
|
-
{ match: "반복", replacement: "for", requiresBoundary: true },
|
|
16
|
-
{ match: "동안", replacement: "while", requiresBoundary: true },
|
|
17
|
-
{ match: "실행", replacement: "do", requiresBoundary: true },
|
|
18
|
-
{ match: "참", replacement: "true", requiresBoundary: true },
|
|
19
|
-
{ match: "거짓", replacement: "false", requiresBoundary: true },
|
|
20
|
-
{ match: "비어있음", replacement: "null", requiresBoundary: true },
|
|
21
|
-
{ match: "정의안됨", replacement: "undefined", requiresBoundary: true },
|
|
22
|
-
{ match: "가져오기", replacement: "import", requiresBoundary: true },
|
|
23
|
-
{ match: "에서", replacement: "from", requiresBoundary: true },
|
|
24
|
-
{ match: "프롬", replacement: "from", requiresBoundary: true },
|
|
25
|
-
{ match: "오브", replacement: "of", requiresBoundary: true },
|
|
26
|
-
{ match: "안에", replacement: "in", requiresBoundary: true },
|
|
27
|
-
{ match: "내보내기", replacement: "export", requiresBoundary: true },
|
|
28
|
-
{ match: "스위치", replacement: "switch", requiresBoundary: true },
|
|
29
|
-
{ match: "선택", replacement: "switch", requiresBoundary: true },
|
|
30
|
-
{ match: "케이스", replacement: "case", requiresBoundary: true },
|
|
31
|
-
{ match: "경우", replacement: "case", requiresBoundary: true },
|
|
32
|
-
{ match: "디폴트", replacement: "default", requiresBoundary: true },
|
|
33
|
-
{ match: "기본", replacement: "default", requiresBoundary: true },
|
|
34
|
-
{ match: "중단", replacement: "break", requiresBoundary: true },
|
|
35
|
-
{ match: "멈춤", replacement: "break", requiresBoundary: true },
|
|
36
|
-
{ match: "계속", replacement: "continue", requiresBoundary: true },
|
|
37
|
-
{ match: "마침내", replacement: "finally", requiresBoundary: true },
|
|
38
|
-
{ match: "타입", replacement: "type", requiresBoundary: true },
|
|
39
|
-
{ match: "인터페이스", replacement: "interface", requiresBoundary: true },
|
|
40
|
-
{ match: "구현", replacement: "implements", requiresBoundary: true },
|
|
41
|
-
{ match: "확장", replacement: "extends", requiresBoundary: true },
|
|
42
|
-
{ match: "열거형", replacement: "enum", requiresBoundary: true },
|
|
43
|
-
{ match: "클래스", replacement: "class", requiresBoundary: true },
|
|
44
|
-
{ match: "추상", replacement: "abstract", requiresBoundary: true },
|
|
45
|
-
{ match: "공개", replacement: "public", requiresBoundary: true },
|
|
46
|
-
{ match: "보호", replacement: "protected", requiresBoundary: true },
|
|
47
|
-
{ match: "비공개", replacement: "private", requiresBoundary: true },
|
|
48
|
-
{ match: "정적", replacement: "static", requiresBoundary: true },
|
|
49
|
-
{ match: "읽기전용", replacement: "readonly", requiresBoundary: true },
|
|
50
|
-
{ match: "얻기", replacement: "get", requiresBoundary: true },
|
|
51
|
-
{ match: "설정", replacement: "set", requiresBoundary: true },
|
|
52
|
-
{ match: "선언", replacement: "declare", requiresBoundary: true },
|
|
53
|
-
{ match: "생성자", replacement: "constructor", requiresBoundary: true },
|
|
54
|
-
{ match: "상위", replacement: "super", requiresBoundary: true },
|
|
55
|
-
{ match: "이것", replacement: "this", requiresBoundary: true },
|
|
56
|
-
{ match: "새로운", replacement: "new", requiresBoundary: true },
|
|
57
|
-
{ match: "대기", replacement: "await", requiresBoundary: true },
|
|
58
|
-
{ match: "시도", replacement: "try", requiresBoundary: true },
|
|
59
|
-
{ match: "잡기", replacement: "catch", requiresBoundary: true },
|
|
60
|
-
{ match: "던지기", replacement: "throw", requiresBoundary: true },
|
|
61
|
-
{ match: "양보", replacement: "yield", requiresBoundary: true },
|
|
62
|
-
{ match: "삭제", replacement: "delete", requiresBoundary: true },
|
|
63
|
-
{ match: "타입오브", replacement: "typeof", requiresBoundary: true },
|
|
64
|
-
{ match: "키오브", replacement: "keyof", requiresBoundary: true },
|
|
65
|
-
{ match: "인퍼", replacement: "infer", requiresBoundary: true },
|
|
66
|
-
{ match: "만족", replacement: "satisfies", requiresBoundary: true },
|
|
67
|
-
{ match: "애즈", replacement: "as", requiresBoundary: true },
|
|
68
|
-
{ match: "처럼", replacement: "as", requiresBoundary: true },
|
|
69
|
-
{ match: "이즈", replacement: "is", requiresBoundary: true },
|
|
70
|
-
{ match: "이다", replacement: "is", requiresBoundary: true },
|
|
71
|
-
{ match: "무효", replacement: "void", requiresBoundary: true },
|
|
72
|
-
{ match: "문자열", replacement: "string", requiresBoundary: true },
|
|
73
|
-
{ match: "숫자", replacement: "number", requiresBoundary: true },
|
|
74
|
-
{ match: "불리언", replacement: "boolean", requiresBoundary: true },
|
|
75
|
-
{ match: "아무거나", replacement: "any", requiresBoundary: true },
|
|
76
|
-
{ match: "배열", replacement: "Array", requiresBoundary: true },
|
|
77
|
-
{ match: "레코드", replacement: "Record", requiresBoundary: true },
|
|
78
|
-
{ match: "맵", replacement: "Map", requiresBoundary: true },
|
|
79
|
-
{ match: "세트", replacement: "Set", requiresBoundary: true },
|
|
80
|
-
{ match: "약한맵", replacement: "WeakMap", requiresBoundary: true },
|
|
81
|
-
{ match: "약한세트", replacement: "WeakSet", requiresBoundary: true },
|
|
82
|
-
{ match: "프로미스", replacement: "Promise", requiresBoundary: true },
|
|
83
|
-
{ match: "객체", replacement: "Object", requiresBoundary: true },
|
|
84
|
-
{ match: "날짜", replacement: "Date", requiresBoundary: true },
|
|
85
|
-
{ match: "오류", replacement: "Error", requiresBoundary: true },
|
|
86
|
-
{ match: "정규식", replacement: "RegExp", requiresBoundary: true },
|
|
87
|
-
{ match: "제이슨", replacement: "JSON", requiresBoundary: true },
|
|
88
|
-
{ match: "수학", replacement: "Math", requiresBoundary: true },
|
|
89
|
-
{ match: "콘솔.출력", replacement: "console.log", requiresBoundary: true }
|
|
90
|
-
];
|
|
91
|
-
|
|
92
|
-
const transformsByFirst = new Map();
|
|
93
|
-
for (const transform of KEYWORD_TRANSFORMS) {
|
|
94
|
-
const first = transform.match[0];
|
|
95
|
-
if (!transformsByFirst.has(first)) transformsByFirst.set(first, []);
|
|
96
|
-
transformsByFirst.get(first).push(transform);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function isIdentifierBoundary(source, index) {
|
|
100
|
-
if (index <= 0 || index >= source.length) {
|
|
101
|
-
return true;
|
|
102
|
-
}
|
|
103
|
-
const char = source[index - 1];
|
|
104
|
-
return !identifierCharRegex.test(char);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function isBoundaryAfter(source, index) {
|
|
108
|
-
if (index >= source.length) {
|
|
109
|
-
return true;
|
|
110
|
-
}
|
|
111
|
-
const char = source[index];
|
|
112
|
-
return !identifierCharRegex.test(char);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function tryApplyTransform(source, index) {
|
|
116
|
-
for (const transform of transformsByFirst.get(source[index]) ?? []) {
|
|
117
|
-
if (!source.startsWith(transform.match, index)) {
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
if (transform.requiresBoundary) {
|
|
121
|
-
const beforeOk = isIdentifierBoundary(source, index);
|
|
122
|
-
const afterOk = isBoundaryAfter(source, index + transform.match.length);
|
|
123
|
-
if (!beforeOk || !afterOk) {
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return {
|
|
128
|
-
replacement: transform.replacement,
|
|
129
|
-
length: transform.match.length
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function transformKeywords(source) {
|
|
136
|
-
let result = "";
|
|
137
|
-
let index = 0;
|
|
138
|
-
const modeStack = ["code"];
|
|
139
|
-
const templateExpressionStack = [];
|
|
140
|
-
|
|
141
|
-
while (index < source.length) {
|
|
142
|
-
const mode = modeStack[modeStack.length - 1];
|
|
143
|
-
const char = source[index];
|
|
144
|
-
|
|
145
|
-
if (mode === "code" || mode === "templateExpression") {
|
|
146
|
-
if (mode === "templateExpression") {
|
|
147
|
-
if (char === "}") {
|
|
148
|
-
const info = templateExpressionStack[templateExpressionStack.length - 1];
|
|
149
|
-
info.depth -= 1;
|
|
150
|
-
result += char;
|
|
151
|
-
index += 1;
|
|
152
|
-
if (info.depth === 0) {
|
|
153
|
-
templateExpressionStack.pop();
|
|
154
|
-
modeStack.pop();
|
|
155
|
-
}
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
if (char === "{") {
|
|
159
|
-
const info = templateExpressionStack[templateExpressionStack.length - 1];
|
|
160
|
-
info.depth += 1;
|
|
161
|
-
result += char;
|
|
162
|
-
index += 1;
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (char === "\"") {
|
|
168
|
-
modeStack.push("doubleQuote");
|
|
169
|
-
result += char;
|
|
170
|
-
index += 1;
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
if (char === "'") {
|
|
174
|
-
modeStack.push("singleQuote");
|
|
175
|
-
result += char;
|
|
176
|
-
index += 1;
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
if (char === "`") {
|
|
180
|
-
modeStack.push("template");
|
|
181
|
-
result += char;
|
|
182
|
-
index += 1;
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
if (char === "/") {
|
|
186
|
-
const next = source[index + 1];
|
|
187
|
-
if (next === "/") {
|
|
188
|
-
modeStack.push("lineComment");
|
|
189
|
-
result += "//";
|
|
190
|
-
index += 2;
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
if (next === "*") {
|
|
194
|
-
modeStack.push("blockComment");
|
|
195
|
-
result += "/*";
|
|
196
|
-
index += 2;
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const transform = tryApplyTransform(source, index);
|
|
202
|
-
if (transform) {
|
|
203
|
-
result += transform.replacement;
|
|
204
|
-
index += transform.length;
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
result += char;
|
|
209
|
-
index += 1;
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (mode === "singleQuote") {
|
|
214
|
-
result += char;
|
|
215
|
-
index += 1;
|
|
216
|
-
if (char === "\\") {
|
|
217
|
-
if (index < source.length) {
|
|
218
|
-
result += source[index];
|
|
219
|
-
index += 1;
|
|
220
|
-
}
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
if (char === "'") {
|
|
224
|
-
modeStack.pop();
|
|
225
|
-
}
|
|
226
|
-
continue;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (mode === "doubleQuote") {
|
|
230
|
-
result += char;
|
|
231
|
-
index += 1;
|
|
232
|
-
if (char === "\\") {
|
|
233
|
-
if (index < source.length) {
|
|
234
|
-
result += source[index];
|
|
235
|
-
index += 1;
|
|
236
|
-
}
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
if (char === "\"") {
|
|
240
|
-
modeStack.pop();
|
|
241
|
-
}
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (mode === "template") {
|
|
246
|
-
result += char;
|
|
247
|
-
index += 1;
|
|
248
|
-
if (char === "\\") {
|
|
249
|
-
if (index < source.length) {
|
|
250
|
-
result += source[index];
|
|
251
|
-
index += 1;
|
|
252
|
-
}
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
|
-
if (char === "`") {
|
|
256
|
-
modeStack.pop();
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
if (char === "$" && source[index] === "{") {
|
|
260
|
-
result += "{";
|
|
261
|
-
modeStack.push("templateExpression");
|
|
262
|
-
templateExpressionStack.push({ depth: 1 });
|
|
263
|
-
index += 1;
|
|
264
|
-
}
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (mode === "lineComment") {
|
|
269
|
-
result += char;
|
|
270
|
-
index += 1;
|
|
271
|
-
if (char === "\n") {
|
|
272
|
-
modeStack.pop();
|
|
273
|
-
}
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (mode === "blockComment") {
|
|
278
|
-
result += char;
|
|
279
|
-
index += 1;
|
|
280
|
-
if (char === "*" && source[index] === "/") {
|
|
281
|
-
result += "/";
|
|
282
|
-
index += 1;
|
|
283
|
-
modeStack.pop();
|
|
284
|
-
}
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
return result;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Scan tokens so literals, comments, and nested function bodies stay untouched.
|
|
293
|
-
function fixClassMethods(source) {
|
|
294
|
-
if (!source.includes('class')) return source;
|
|
295
|
-
const scanner = ts.createScanner(ts.ScriptTarget.Latest, false, ts.LanguageVariant.Standard, source);
|
|
296
|
-
const stack = [];
|
|
297
|
-
const edits = [];
|
|
298
|
-
let pendingClass = false;
|
|
299
|
-
let previous = ts.SyntaxKind.Unknown;
|
|
300
|
-
const templateDepths = [];
|
|
301
|
-
let braces = 0;
|
|
302
|
-
for (let token = scanner.scan(); token !== ts.SyntaxKind.EndOfFileToken; token = scanner.scan()) {
|
|
303
|
-
if (token === ts.SyntaxKind.CloseBraceToken && templateDepths.at(-1) === braces) {
|
|
304
|
-
token = scanner.reScanTemplateToken(false);
|
|
305
|
-
if (token === ts.SyntaxKind.TemplateTail) templateDepths.pop();
|
|
306
|
-
continue;
|
|
307
|
-
}
|
|
308
|
-
if (token === ts.SyntaxKind.TemplateHead) { templateDepths.push(braces); continue; }
|
|
309
|
-
if (token === ts.SyntaxKind.SingleLineCommentTrivia || token === ts.SyntaxKind.MultiLineCommentTrivia ||
|
|
310
|
-
token === ts.SyntaxKind.WhitespaceTrivia || token === ts.SyntaxKind.NewLineTrivia) continue;
|
|
311
|
-
if (token === ts.SyntaxKind.ClassKeyword) pendingClass = true;
|
|
312
|
-
if (token === ts.SyntaxKind.OpenBraceToken) {
|
|
313
|
-
stack.push(pendingClass ? 'class' : 'block');
|
|
314
|
-
pendingClass = false;
|
|
315
|
-
braces++;
|
|
316
|
-
} else if (token === ts.SyntaxKind.CloseBraceToken) {
|
|
317
|
-
stack.pop(); braces--;
|
|
318
|
-
} else if (token === ts.SyntaxKind.FunctionKeyword && stack.at(-1) === 'class' &&
|
|
319
|
-
[ts.SyntaxKind.OpenBraceToken, ts.SyntaxKind.CloseBraceToken, ts.SyntaxKind.SemicolonToken,
|
|
320
|
-
ts.SyntaxKind.AsyncKeyword, ts.SyntaxKind.PublicKeyword, ts.SyntaxKind.PrivateKeyword,
|
|
321
|
-
ts.SyntaxKind.ProtectedKeyword, ts.SyntaxKind.StaticKeyword].includes(previous)) {
|
|
322
|
-
const start = scanner.getTokenPos();
|
|
323
|
-
let end = scanner.getTextPos();
|
|
324
|
-
while (source[end] === ' ' || source[end] === '\t') end++;
|
|
325
|
-
edits.push([start, end]);
|
|
326
|
-
}
|
|
327
|
-
previous = token;
|
|
328
|
-
}
|
|
329
|
-
for (const [start, end] of edits.reverse()) source = source.slice(0, start) + source.slice(end);
|
|
330
|
-
return source;
|
|
331
|
-
}
|
|
1
|
+
import { compileSentences } from './sentences.js';
|
|
332
2
|
|
|
3
|
+
/** HangulScript has one syntax: indentation-based Korean sentences. */
|
|
333
4
|
export function compileSource(source) {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const transformed = fixClassMethods(transformKeywords(source));
|
|
337
|
-
return sentenceMode ? sentenceRuntime + '\n' + transformed : transformed;
|
|
5
|
+
if (typeof source !== 'string') throw new TypeError('한글스크립트 소스는 문자열이어야 합니다.');
|
|
6
|
+
return compileSentences(source);
|
|
338
7
|
}
|
package/examples/calculator.hgs
CHANGED
package/examples/discount-ui.hgs
CHANGED
package/examples/discount.hgs
CHANGED
package/examples/hello.hgs
CHANGED
|
@@ -1,21 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
변수 나이: 숫자 = 12;
|
|
1
|
+
이름은 "세상"이다
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
3
|
+
인사(누구: 문자열)를 정의한다:
|
|
4
|
+
돌려주기 "안녕, " + 누구 + "!"
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
이름: 문자열;
|
|
10
|
-
나이: 숫자;
|
|
11
|
-
생성자(이름: 문자열, 나이: 숫자) {
|
|
12
|
-
이것.이름 = 이름;
|
|
13
|
-
이것.나이 = 나이;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
함수 소개(): 문자열 {
|
|
17
|
-
반환 `${이것.이름}(${이것.나이}) 입니다`;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
콘솔.출력(인사(이름));
|
|
6
|
+
출력(인사(이름))
|
package/examples/scores.hgs
CHANGED
package/examples/statistics.hgs
CHANGED
package/examples/text-ui.hgs
CHANGED
package/examples/text.hgs
CHANGED