@uiwwsw/hangulscript 0.0.0-stage → 0.4.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 +97 -0
- package/LICENSE +21 -0
- package/README.md +255 -2
- package/dist/cli.js +80 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +181 -0
- package/dist/sentences.js +99 -0
- package/dist/transformer.d.ts +2 -0
- package/dist/transformer.js +338 -0
- package/examples/calculator.hgs +18 -0
- package/examples/discount.hgs +9 -0
- package/examples/hello.hgs +21 -0
- package/examples/math-library.hgs +4 -0
- package/examples/scores.hgs +9 -0
- package/examples/statistics.hgs +10 -0
- package/examples/text.hgs +9 -0
- package/package.json +61 -4
package/LANGUAGE.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# 문장형 HangulScript v0.1 (실험판)
|
|
2
|
+
|
|
3
|
+
`.hgs`를 TypeScript로 변환한다. Python의 들여쓰기·단순한 함수·기본 함수를
|
|
4
|
+
받아들이고, JS의 배열·객체·표현식과 TypeScript 타입을 활용한다.
|
|
5
|
+
Python 소스 호환 언어나 임의의 한국어를 해석하는 AI 실행기가 아니다.
|
|
6
|
+
|
|
7
|
+
## 문법
|
|
8
|
+
|
|
9
|
+
파일 첫 줄 `문장으로 시작`으로 문장형을 명시하는 것을 권장한다.
|
|
10
|
+
변수 선언·각 반복·함수 정의 문장으로도 자동 감지한다.
|
|
11
|
+
기존 `.hgs` 키워드형은 계속 지원한다. 같은 파일 안에서는 하나의 방식을 사용한다.
|
|
12
|
+
공백 들여쓰기 폭은 자유지만 같은 블록은 동일해야 하고 탭은 허용하지 않는다.
|
|
13
|
+
`#`는 행 전체 주석이다. 문장 끝 주석은 현재 지원하지 않는다.
|
|
14
|
+
|
|
15
|
+
```ks
|
|
16
|
+
문장으로 시작
|
|
17
|
+
이름은 "하늘"이다
|
|
18
|
+
횟수는 3이다
|
|
19
|
+
|
|
20
|
+
인사(누구)를 정의한다:
|
|
21
|
+
돌려주기 "안녕, " + 누구
|
|
22
|
+
|
|
23
|
+
각 순서를 범위(횟수)에서:
|
|
24
|
+
만약 순서 == 0 이라면:
|
|
25
|
+
출력("처음")
|
|
26
|
+
아니고 순서 == 1 이라면:
|
|
27
|
+
출력("다음")
|
|
28
|
+
아니면:
|
|
29
|
+
출력(인사(이름))
|
|
30
|
+
|
|
31
|
+
동안 횟수 > 0:
|
|
32
|
+
횟수 = 횟수 - 1
|
|
33
|
+
만약 횟수 == 1 이라면:
|
|
34
|
+
멈추기
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- 선언: `이름은 값이다` / `이름는 값이다`. 은·는은 문법적으로 둘 다 허용한다.
|
|
38
|
+
`let`으로 변환하며 재대입은 `이름 = 값`이다. 같은 범위에서 재선언할 수 없다. 빈 목록은 동적 목록 `any[]`로 생성한다.
|
|
39
|
+
- 함수: `이름(인자)를 정의한다:`. 타입 없는 인자는 `any`로 변환한다.
|
|
40
|
+
`이름(인자: 문자열)를 정의한다:`처럼 타입을 명시할 수 있다.
|
|
41
|
+
- 반환: `돌려주기 값`; 값 없이도 가능하다.
|
|
42
|
+
- 조건: `만약 식 이라면:` / `아니고 식 이라면:` / `아니면:`.
|
|
43
|
+
- 반복: `각 항목을 목록에서:`; 조사 을·를 모두 허용한다.
|
|
44
|
+
- 조건 반복: `동안 식:`; `멈추기`, `계속하기`를 제공한다.
|
|
45
|
+
- 식: JS의 연산자, 배열, 객체를 사용한다. 참·거짓·비어있음은 기존 변환을 따른다.
|
|
46
|
+
Python의 `and`, `or`, `in`, 리스트 컴프리헨션, 음수 인덱스는 제공하지 않는다.
|
|
47
|
+
|
|
48
|
+
## 기본 함수
|
|
49
|
+
|
|
50
|
+
| 함수 | 의미 |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| `출력(...값)` | 콘솔 출력 |
|
|
53
|
+
| `길이(문자열 또는 목록)` | UTF-16 코드 단위 또는 항목 수 |
|
|
54
|
+
| `합계(숫자목록)` | 합산 |
|
|
55
|
+
| `정렬(숫자목록)` | 원본을 바꾸지 않고 오름차순 정렬 |
|
|
56
|
+
| `범위(끝)` / `범위(시작, 끝, 간격)` | 끝 제외, 음수 간격 지원, 최대 10만 항목 |
|
|
57
|
+
| `추가(목록, 값)` | 원본 목록에 추가 |
|
|
58
|
+
| `포함(목록, 값)` | 포함 여부 |
|
|
59
|
+
| `나누기(문자열, 구분자)` | 문자열 분리 |
|
|
60
|
+
| `잇기(목록, 구분자)` | 기본 구분자 빈 문자열로 연결 |
|
|
61
|
+
|
|
62
|
+
위 이름은 문장형 파일에 생성하는 기본 함수이므로 사용자 함수·변수 이름으로 쓰지 않는다.
|
|
63
|
+
생성한 TypeScript에는 기본 함수의 구현이 포함된다. 독립 모듈로 사용하려면
|
|
64
|
+
TypeScript 프로젝트의 moduleDetection 설정 또는 export로 모듈 범위를 지정한다.
|
|
65
|
+
|
|
66
|
+
## 다음 개선 우선순위
|
|
67
|
+
|
|
68
|
+
1. **독립 토큰화와 AST**: 현재 행 기반 문장 파서·키워드 치환기를 통합한다.
|
|
69
|
+
정규식 리터럴, 여러 줄 식, 복잡한 클래스/제네릭 문법은 완전 지원 범위가 아니다.
|
|
70
|
+
2. **원본 위치 진단과 소스맵**: 들여쓰기 오류는 .hgs 행 번호를 표시하지만
|
|
71
|
+
TypeScript 타입 진단은 현재 확장된 생성 코드 위치를 사용한다.
|
|
72
|
+
3. **표준 라이브러리 계약**: 문자열/목록/사전/날짜를 같은 함수 체계로 확장하고
|
|
73
|
+
Unicode 문자 수·동등성·정렬·오류 모델을 명시한다.
|
|
74
|
+
4. **VS Code/LSP**: 강조, 자동 완성, 한글 오류 설명, 포맷터를 추가한다.
|
|
75
|
+
5. **웹 프로젝트 통합**: Vite 플러그인과 .hgs 모듈 그래프를 구현한다.
|
|
76
|
+
현재 CLI는 파일 단위 변환이며 .hgs import 경로를 재작성하지 않는다.
|
|
77
|
+
|
|
78
|
+
문장형은 실험판이다. 웹 데모는 문법 변환·JS 구문 진단만 하며 전체 타입 검사는 하지 않는다.
|
|
79
|
+
DOM과 네트워크가 필요한 결과물은 신뢰하는 로컬 프로젝트에서 생성 TS를 통합해 만든다.
|
|
80
|
+
|
|
81
|
+
## 0.3 실행·출력과 추가 함수
|
|
82
|
+
|
|
83
|
+
CLI의 기본 빌드는 `.js` + `.d.ts`이며 `--typescript`로 기존 TS 출력을 선택한다.
|
|
84
|
+
`ksc run 파일.hgs`는 생성 파일 없이 타입 검사 후 Node.js에서 실행한다.
|
|
85
|
+
JS 출력은 ESM이며, 프로젝트는 `type: module` 또는 ESM 번들러를 사용한다.
|
|
86
|
+
공개 함수에 `내보내기 { 함수이름 }`을 쓰면 해당 타입을 .d.ts로 배포할 수 있다.
|
|
87
|
+
|
|
88
|
+
추가 기본 함수: `숫자로(값)`, `다듬기(문자열)`, `반올림(값, 자리=0)`,
|
|
89
|
+
`평균(목록)`, `최솟값(목록)`, `최댓값(목록)`.
|
|
90
|
+
평균/최솟값/최댓값의 빈 목록과 숫자로의 빈 문자열·비유한 숫자는 오류다.
|
|
91
|
+
반올림은 JS Math.round 규칙이며 십진 임의 정밀도나 Python round와 같은 규칙을 보장하지 않는다.
|
|
92
|
+
|
|
93
|
+
## 이름과 확장자 이전 (0.4)
|
|
94
|
+
|
|
95
|
+
제품명은 한글스크립트(HangulScript), 패키지는 `@uiwwsw/hangulscript`입니다.
|
|
96
|
+
기본 소스 확장자는 `.hgs`이며 기존 `.ks` 파일도 CLI에서 계속 지원합니다.
|
|
97
|
+
이전 패키지에서 이동하려면 `npm uninstall @uiwwsw/koreanscript` 후 새 패키지를 설치하세요.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 uiwwsw
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,256 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 한글스크립트 · HangulScript
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**한글 문장으로 작성하고, JavaScript와 타입 선언으로 배포하는 실험적 프로그래밍 언어.**
|
|
4
|
+
|
|
5
|
+
Python의 들여쓰기와 익숙한 기본 함수, JavaScript의 표현식과 웹 생태계,
|
|
6
|
+
TypeScript의 타입 검사를 결합합니다. 소스 확장자는 **`.hgs`**입니다.
|
|
7
|
+
|
|
8
|
+
[온라인에서 실행하기](https://hangulscript.vercel.app/) · [문법 명세](LANGUAGE.md) · [릴리스](https://github.com/uiwwsw/hangulscript/releases) · [의견 보내기](https://github.com/uiwwsw/hangulscript/issues)
|
|
9
|
+
|
|
10
|
+
```ks
|
|
11
|
+
문장으로 시작
|
|
12
|
+
점수들은 [75, 92, 88]이다
|
|
13
|
+
각 점수를 점수들에서:
|
|
14
|
+
만약 점수 >= 80 이라면:
|
|
15
|
+
출력(점수)
|
|
16
|
+
출력("평균:", 평균(점수들))
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
문장형은 실험판입니다. 정해진 한국어 문법을 파싱하며, 임의의 자연어를 추측해 실행하지 않습니다.
|
|
20
|
+
Python 소스 호환이나 전체 TypeScript 문법 지원을 뜻하지 않습니다.
|
|
21
|
+
|
|
22
|
+
## 지금 만들 수 있는 것
|
|
23
|
+
|
|
24
|
+
| 예제 | 바꿀 수 있는 값 | 사용하는 기능 |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| [사칙연산 계산기](examples/calculator.hgs) | 두 숫자·연산 | 함수·조건·0 나눗셈 처리 |
|
|
27
|
+
| [할인 계산기](examples/discount.hgs) | 가격·할인율 | 범위 검사·반올림 |
|
|
28
|
+
| [숫자 통계](examples/statistics.hgs) | 쉼표로 구분한 숫자 | 합계·평균·최솟값·최댓값 |
|
|
29
|
+
| [문자열 정리기](examples/text.hgs) | 단어 목록 | 공백 정리·중복 제거·연결 |
|
|
30
|
+
| [성적 분석기](examples/scores.hgs) | 점수 목록 | 반복·필터링·정렬 |
|
|
31
|
+
| [타입 있는 함수 모듈](examples/math-library.hgs) | 함수·타입 | JS + .d.ts 라이브러리 빌드 |
|
|
32
|
+
|
|
33
|
+
데모에서 예제를 선택하고 입력값을 바꾼 다음 **실행하기**를 누르세요.
|
|
34
|
+
입력값 변경은 예제 코드를 다시 생성하며, 코드를 직접 편집하면 입력란은 숨겨집니다.
|
|
35
|
+
독립적인 웹 UI·데이터베이스·외부 API를 언어 자체에서 제공하는 단계는 아닙니다.
|
|
36
|
+
생성한 JS를 기존 웹/Node 프로젝트에 연결해 화면과 서비스를 확장할 수 있습니다.
|
|
37
|
+
|
|
38
|
+
## 설치
|
|
39
|
+
|
|
40
|
+
Node.js **20 이상**이 필요합니다. 아래의 JS + .d.ts 빌드와 `run` 명령은 **0.3.0부터** 제공합니다.
|
|
41
|
+
공개된 버전은 [npm](https://www.npmjs.com/package/@uiwwsw/hangulscript) 또는 `npm view @uiwwsw/hangulscript version`으로 확인하세요.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install --save-dev @uiwwsw/hangulscript
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
소스에서 최신 버전을 사용하려면:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
git clone https://github.com/uiwwsw/hangulscript.git
|
|
51
|
+
cd hangulscript
|
|
52
|
+
npm ci
|
|
53
|
+
npm run build
|
|
54
|
+
node dist/cli.js run examples/calculator.hgs
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## 직접 실행과 빌드
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# .hgs를 바로 실행: 중간 파일을 만들지 않음
|
|
61
|
+
npx ksc run examples/calculator.hgs
|
|
62
|
+
|
|
63
|
+
# 기본 출력은 .js + .d.ts
|
|
64
|
+
npx ksc build examples/math-library.hgs --out-dir output
|
|
65
|
+
# output/math-library.js
|
|
66
|
+
# output/math-library.d.ts
|
|
67
|
+
|
|
68
|
+
# build 생략 가능
|
|
69
|
+
npx ksc examples/math-library.hgs --out-dir output
|
|
70
|
+
|
|
71
|
+
# 타입 검사만 수행
|
|
72
|
+
npx ksc --check examples/math-library.hgs
|
|
73
|
+
|
|
74
|
+
# 기존 .ts 출력이 필요할 때
|
|
75
|
+
npx ksc --typescript examples/math-library.hgs --out-dir output
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**0.2에서 변경:** CLI의 기본 출력이 `.ts`에서 **`.js` + `.d.ts`**로 바뀝니다.
|
|
79
|
+
기존 자동화가 `.ts`를 기대한다면 `--typescript`를 추가하세요.
|
|
80
|
+
`compileSource`·`compileString` API는 기존처럼 TypeScript 문자열을 반환합니다.
|
|
81
|
+
|
|
82
|
+
`run`은 신뢰하는 코드를 로컬 Node.js에서 실행합니다. 온라인 데모와 달리 격리·2초 제한이 없습니다.
|
|
83
|
+
프로그램 인자는 `ksc run 파일.hgs -- 인자1 인자2`처럼 전달합니다.
|
|
84
|
+
|
|
85
|
+
### 배포 산출물 사용
|
|
86
|
+
|
|
87
|
+
```ks
|
|
88
|
+
문장으로 시작
|
|
89
|
+
더함(가: 숫자, 나: 숫자)를 정의한다:
|
|
90
|
+
돌려주기 가 + 나
|
|
91
|
+
내보내기 { 더함 }
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
생성한 `.js`에는 실행 코드가, `.d.ts`에는 다음과 같은 타입 계약이 담깁니다.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
declare function 더함(가: number, 나: number): number;
|
|
98
|
+
export { 더함 };
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
import { 더함 } from './output/math-library.js';
|
|
103
|
+
console.log(더함(2, 3)); // 5
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
출력은 **ES Modules**입니다. Node 프로젝트에는 `package.json`의 `"type": "module"`을 설정하거나
|
|
107
|
+
ESM을 지원하는 번들러를 사용하세요. JS와 .d.ts를 함께 배포하면 TypeScript 사용자가 타입을 읽을 수 있습니다.
|
|
108
|
+
`.d.ts`는 실행 파일이 아닙니다. 함수 인자 타입을 생략하면 `any`가 되므로
|
|
109
|
+
공개 라이브러리 함수에는 타입을 명시하는 편이 좋습니다.
|
|
110
|
+
여러 `.hgs` 파일의 import 경로를 자동으로 묶거나 재작성하지는 않습니다.
|
|
111
|
+
|
|
112
|
+
## 문장형 문법
|
|
113
|
+
|
|
114
|
+
```ks
|
|
115
|
+
문장으로 시작
|
|
116
|
+
이름은 "하늘"이다
|
|
117
|
+
횟수는 3이다
|
|
118
|
+
|
|
119
|
+
인사(누구: 문자열)를 정의한다:
|
|
120
|
+
돌려주기 "안녕, " + 누구
|
|
121
|
+
|
|
122
|
+
각 순서를 범위(횟수)에서:
|
|
123
|
+
만약 순서 == 0 이라면:
|
|
124
|
+
출력("첫 인사")
|
|
125
|
+
아니면:
|
|
126
|
+
출력(인사(이름))
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
- 선언: `이름은 값이다` 또는 `이름는 값이다`. 재대입은 `이름 = 값`.
|
|
130
|
+
- 함수: `인사(누구)를 정의한다:`와 `돌려주기 값`.
|
|
131
|
+
- 조건: `만약 식 이라면:` → `아니고 식 이라면:` → `아니면:`.
|
|
132
|
+
- 반복: `각 항목을 목록에서:`와 `동안 식:`. `멈추기`, `계속하기` 지원.
|
|
133
|
+
- 블록은 콜론 뒤에 공백으로 들여씁니다. 탭은 허용하지 않습니다.
|
|
134
|
+
- 행 전체 주석은 `#`입니다. 표현식은 JS의 연산자·배열·객체를 사용합니다.
|
|
135
|
+
|
|
136
|
+
기존 키워드형도 사용할 수 있습니다.
|
|
137
|
+
|
|
138
|
+
```ks
|
|
139
|
+
상수 이름 = "철수";
|
|
140
|
+
함수 인사(사람: 문자열): 문자열 {
|
|
141
|
+
반환 "안녕, " + 사람;
|
|
142
|
+
}
|
|
143
|
+
콘솔.출력(인사(이름));
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
지원 범위와 제약은 [LANGUAGE.md](LANGUAGE.md)에 정리했습니다.
|
|
147
|
+
|
|
148
|
+
## 한글 기본 함수
|
|
149
|
+
|
|
150
|
+
문장형 파일에는 다음 함수가 제공됩니다.
|
|
151
|
+
|
|
152
|
+
| 함수 | 예시 / 의미 |
|
|
153
|
+
| --- | --- |
|
|
154
|
+
| `출력` | `출력("합계:", 30)` |
|
|
155
|
+
| `길이` | 문자열의 UTF-16 코드 단위 수 또는 목록 항목 수 |
|
|
156
|
+
| `추가` | `추가(목록, 값)` — 원본 목록 변경 |
|
|
157
|
+
| `포함` | `포함(목록, 값)` — 포함 여부 |
|
|
158
|
+
| `나누기` | `나누기("가,나", ",")` |
|
|
159
|
+
| `잇기` | `잇기(["가", "나"], " · ")` |
|
|
160
|
+
| `다듬기` | 문자열 양 끝 공백 제거 |
|
|
161
|
+
| `숫자로` | 유한한 숫자로 변환, 빈 문자열·잘못된 값은 오류 |
|
|
162
|
+
| `합계` | 숫자 목록 합산 |
|
|
163
|
+
| `평균` | 숫자 목록 평균, 빈 목록은 오류 |
|
|
164
|
+
| `최솟값` / `최댓값` | 목록을 순회해 계산, 빈 목록은 오류 |
|
|
165
|
+
| `반올림` | `반올림(12.345, 2)` — 자리 -10~10 |
|
|
166
|
+
| `정렬` | 숫자 목록을 복사해 오름차순 정렬 |
|
|
167
|
+
| `범위` | `범위(5)`, `범위(1, 5)`, `범위(5, 0, -1)` — 끝 제외 |
|
|
168
|
+
|
|
169
|
+
계산은 JS의 부동소수점 규칙을 따릅니다. 예를 들어 `0.1 + 0.2`의 정밀도는
|
|
170
|
+
이 언어에서도 달라지지 않습니다. 금액은 원·센트 같은 정수 단위로 다루세요.
|
|
171
|
+
`반올림`은 JS `Math.round` 규칙이며 임의 정밀도 십진 연산이 아닙니다.
|
|
172
|
+
|
|
173
|
+
## 공유
|
|
174
|
+
|
|
175
|
+
1. 데모에서 예제를 수정하고 실행 결과를 확인합니다.
|
|
176
|
+
2. **공유 링크 복사**로 코드를 담은 URL을 전달합니다.
|
|
177
|
+
3. 받는 사람은 코드를 읽고 직접 실행합니다. 링크를 열기만 해서는 실행되지 않습니다.
|
|
178
|
+
4. **.hgs 저장**으로 소스를 내려받아 CLI에서 실행하거나 JS + .d.ts로 배포합니다.
|
|
179
|
+
|
|
180
|
+
코드는 URL의 `#code=` 조각에 담깁니다. 서버 저장소·공개 코드 목록 기능은 없습니다.
|
|
181
|
+
링크를 가진 사람이 코드를 읽을 수 있으므로 비밀번호·토큰·개인정보는 넣지 마세요.
|
|
182
|
+
|
|
183
|
+
## 성능과 실행 제한
|
|
184
|
+
|
|
185
|
+
0.3에서 다음 개선을 적용했습니다.
|
|
186
|
+
|
|
187
|
+
- **컴파일 전용 Worker:** TypeScript 파싱·변환·출력 정리를 UI 스레드 밖에서 실행.
|
|
188
|
+
- **180ms 입력 지연과 결과 재사용:** 키를 누를 때마다 변환하지 않고, 같은 소스는 다시 컴파일하지 않음.
|
|
189
|
+
- **키워드 첫 글자별 탐색:** 모든 문자에서 전체 키워드 목록을 순회하지 않음.
|
|
190
|
+
- **불필요한 재처리 생략:** 기본 함수 코드를 다시 치환하지 않고, 클래스가 없으면 클래스 보정도 생략.
|
|
191
|
+
- **실행 출력 제한:** 부모 페이지에서도 최대 100행·행당 1,000자로 제한.
|
|
192
|
+
- **범위 제한:** `범위`는 최대 100,000개 항목. 최솟값·최댓값은 인자 펼치기 없이 순회.
|
|
193
|
+
|
|
194
|
+
2026-10-22 Node 24.18.0 환경에서 같은 1,000행 선언 코드를 측정했을 때,
|
|
195
|
+
변환 중앙값은 변경 전 약 **4.45ms**, 변경 후 약 **0.65ms**였습니다.
|
|
196
|
+
개선 후 20회 측정 p95는 약 **1.88ms**였고, 작은 코드의 타입 검사·JS/.d.ts 생성은 약 **168ms**였습니다.
|
|
197
|
+
서로 다른 기기·입력·브라우저에서 같은 수치를 보장하는 벤치마크는 아닙니다.
|
|
198
|
+
|
|
199
|
+
화면 JS는 약 **10.4KB**, 컴파일러 Worker는 약 **3.58MB**입니다(압축 전).
|
|
200
|
+
Worker 분리는 UI의 파싱·실행 부담을 분리하지만 전체 다운로드를 제거하지 않습니다.
|
|
201
|
+
첫 로딩은 네트워크와 기기에 따라 느릴 수 있습니다.
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
npm run build:site
|
|
205
|
+
npm run benchmark
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
온라인 실행은 별도 origin의 sandbox iframe과 Worker에서 최대 2초 동안 수행합니다.
|
|
209
|
+
네트워크·DOM·외부 모듈은 제공하지 않으며 입력은 30,000자로 제한합니다.
|
|
210
|
+
이는 완전한 메모리 할당 제한이 아닙니다. 매우 큰 배열 등은 Worker에서도 메모리를 많이 사용할 수 있습니다.
|
|
211
|
+
대용량 작업·프로덕션 서버용 성능을 검증한 단계는 아닙니다.
|
|
212
|
+
|
|
213
|
+
## 프로그래밍 API
|
|
214
|
+
|
|
215
|
+
```js
|
|
216
|
+
import { buildSource, lintSource } from '@uiwwsw/hangulscript';
|
|
217
|
+
|
|
218
|
+
const source = '문장으로 시작\n출력(합계([1, 2, 3]))';
|
|
219
|
+
const { javascript, declaration } = buildSource(source);
|
|
220
|
+
const { hasErrors } = lintSource(source);
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Node 파일 API 없이 변환만 필요하면:
|
|
224
|
+
|
|
225
|
+
```js
|
|
226
|
+
import { compileSource } from '@uiwwsw/hangulscript/browser';
|
|
227
|
+
const typescript = compileSource('문장으로 시작\n출력("안녕")');
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
이 진입점도 TypeScript 스캐너를 포함합니다. 브라우저에서는 데모처럼 Worker에서 사용하세요.
|
|
231
|
+
루트와 브라우저 진입점 모두 패키지의 `.d.ts`를 제공합니다.
|
|
232
|
+
|
|
233
|
+
## 개발·배포
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
npm ci
|
|
237
|
+
npm test
|
|
238
|
+
npm run build
|
|
239
|
+
npm run build:site
|
|
240
|
+
npm pack --dry-run
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Vercel은 GitHub main과 연결되어 `site-dist/`를 배포합니다.
|
|
244
|
+
`SITE_URL`에 HTTPS origin을 지정하면 canonical·Open Graph·robots·sitemap 주소를 바꿀 수 있습니다.
|
|
245
|
+
|
|
246
|
+
향후에는 독립 AST·원본 `.hgs` 소스맵·LSP·모듈 그래프·Vite 연동 순으로 확장할 계획입니다.
|
|
247
|
+
|
|
248
|
+
## 라이선스
|
|
249
|
+
|
|
250
|
+
[MIT](LICENSE)
|
|
251
|
+
|
|
252
|
+
## 이름과 확장자 이전 (0.4)
|
|
253
|
+
|
|
254
|
+
제품명은 한글스크립트(HangulScript), 패키지는 `@uiwwsw/hangulscript`입니다.
|
|
255
|
+
기본 소스 확장자는 `.hgs`이며 기존 `.ks` 파일도 CLI에서 계속 지원합니다.
|
|
256
|
+
이전 패키지에서 이동하려면 `npm uninstall @uiwwsw/koreanscript` 후 새 패키지를 설치하세요.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { compileFileToDefaultDestination, compileFile, lintFile, buildFile, buildSource } from './index.js';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
function usage() {
|
|
8
|
+
console.log(`사용법:
|
|
9
|
+
ksc [build] [옵션] <파일.hgs ...> JavaScript와 .d.ts 생성
|
|
10
|
+
ksc run <파일.hgs> [-- 인자 ...] 파일 없이 Node.js로 직접 실행
|
|
11
|
+
|
|
12
|
+
옵션:
|
|
13
|
+
-o, --out-dir <경로> 출력 디렉터리 (기본: 입력 파일 옆)
|
|
14
|
+
--check 타입 검사만 실행
|
|
15
|
+
--typescript 기존 .ts 출력 모드
|
|
16
|
+
-h, --help 도움말
|
|
17
|
+
|
|
18
|
+
문장형은 실험판입니다. run은 신뢰하는 로컬 코드만 실행하세요.`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function main() {
|
|
22
|
+
const args = process.argv.slice(2);
|
|
23
|
+
if (!args.length) { usage(); process.exitCode = 1; return; }
|
|
24
|
+
if (args.includes('--help') || args.includes('-h')) { usage(); return; }
|
|
25
|
+
const mode = ['run', 'build'].includes(args[0]) ? args.shift() : 'build';
|
|
26
|
+
let outDir, check = false, typescript = false;
|
|
27
|
+
const inputs = [], programArgs = [];
|
|
28
|
+
for (let index = 0; index < args.length; index++) {
|
|
29
|
+
const arg = args[index];
|
|
30
|
+
if (arg === '--' && mode === 'run') { programArgs.push(...args.slice(index + 1)); break; }
|
|
31
|
+
if (arg === '--out-dir' || arg === '-o') {
|
|
32
|
+
outDir = args[++index];
|
|
33
|
+
if (!outDir || outDir.startsWith('-')) throw new Error('--out-dir에는 경로가 필요합니다.');
|
|
34
|
+
} else if (arg === '--check') check = true;
|
|
35
|
+
else if (arg === '--typescript') typescript = true;
|
|
36
|
+
else if (arg.startsWith('-')) throw new Error(`알 수 없는 옵션: ${arg}`);
|
|
37
|
+
else if (!(/\.(?:hgs|ks)$/.test(arg))) throw new Error('입력 파일은 .hgs 확장자여야 합니다.');
|
|
38
|
+
else inputs.push(arg);
|
|
39
|
+
}
|
|
40
|
+
if (!inputs.length) throw new Error('입력 .hgs 파일을 지정하세요.');
|
|
41
|
+
if (check && (outDir || typescript)) throw new Error('--check는 출력 옵션과 함께 사용할 수 없습니다.');
|
|
42
|
+
if (mode === 'run') {
|
|
43
|
+
if (inputs.length !== 1 || outDir || typescript || check) throw new Error('run에는 .hgs 파일 하나와 -- 뒤의 실행 인자만 지정하세요.');
|
|
44
|
+
const input = path.resolve(inputs[0]);
|
|
45
|
+
const { javascript } = buildSource(await readFile(input, 'utf8'), input);
|
|
46
|
+
const child = spawn(process.execPath, ['--input-type=module', '-', ...programArgs], {
|
|
47
|
+
cwd: path.dirname(input), stdio: ['pipe', 'inherit', 'inherit']
|
|
48
|
+
});
|
|
49
|
+
// Preserve imports relative to the source directory. Runtime needs Node.js 20+.
|
|
50
|
+
child.stdin.on('error', () => {});
|
|
51
|
+
child.stdin.end(javascript);
|
|
52
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
53
|
+
child.once('error', reject);
|
|
54
|
+
child.once('exit', (code, signal) => resolve(code ?? (signal ? 1 : 0)));
|
|
55
|
+
});
|
|
56
|
+
process.exitCode = exitCode;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const destinations = new Set();
|
|
60
|
+
for (const input of inputs) {
|
|
61
|
+
const destination = path.resolve(outDir ?? path.dirname(input), path.parse(input).name);
|
|
62
|
+
if (destinations.has(destination)) throw new Error('출력 파일 이름이 겹칩니다. 파일별로 다른 출력 디렉터리를 사용하세요.');
|
|
63
|
+
destinations.add(destination);
|
|
64
|
+
}
|
|
65
|
+
for (const input of inputs) {
|
|
66
|
+
if (check) {
|
|
67
|
+
const result = await lintFile(input);
|
|
68
|
+
if (result.hasErrors) throw new Error(result.formattedDiagnostics);
|
|
69
|
+
console.log(`✅ ${input} 문제 없음`);
|
|
70
|
+
} else if (typescript) {
|
|
71
|
+
const output = outDir ? path.join(outDir, path.parse(input).name + '.ts') : null;
|
|
72
|
+
const written = output ? (await compileFile(input, output), output) : await compileFileToDefaultDestination(input);
|
|
73
|
+
console.log(`✅ ${written} 생성됨`);
|
|
74
|
+
} else {
|
|
75
|
+
const result = await buildFile(input, outDir);
|
|
76
|
+
console.log(`✅ ${result.javascriptPath}\n✅ ${result.declarationPath}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
main().catch(error => { console.error(`오류: ${error.message}`); process.exitCode = 1; });
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type * as ts from 'typescript';
|
|
2
|
+
export { compileSource } from './transformer.js';
|
|
3
|
+
export function compileString(source: string): string;
|
|
4
|
+
export function compileFile(inputPath: string, outputPath: string): Promise<string>;
|
|
5
|
+
export function compileFileToDefaultDestination(inputPath: string): Promise<string>;
|
|
6
|
+
export interface LintResult {
|
|
7
|
+
diagnostics: readonly ts.Diagnostic[];
|
|
8
|
+
formattedDiagnostics: string;
|
|
9
|
+
hasErrors: boolean;
|
|
10
|
+
}
|
|
11
|
+
export function lintSource(source: string, filePath?: string, compilerOptions?: ts.CompilerOptions): LintResult;
|
|
12
|
+
export function lintFile(inputPath: string, compilerOptions?: ts.CompilerOptions): Promise<LintResult>;
|
|
13
|
+
export interface BuildResult { javascript: string; declaration: string; }
|
|
14
|
+
export function buildSource(source: string, filePath?: string): BuildResult;
|
|
15
|
+
export function buildFile(inputPath: string, outputDirectory?: string): Promise<BuildResult & { javascriptPath: string; declarationPath: string }>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { compileSource } from './transformer.js';
|
|
5
|
+
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
let tsModuleCache;
|
|
8
|
+
let defaultCompilerOptions;
|
|
9
|
+
|
|
10
|
+
function loadTypeScriptModule() {
|
|
11
|
+
if (tsModuleCache) {
|
|
12
|
+
return tsModuleCache;
|
|
13
|
+
}
|
|
14
|
+
let typescript;
|
|
15
|
+
try {
|
|
16
|
+
typescript = require('typescript');
|
|
17
|
+
} catch (initialError) {
|
|
18
|
+
const globalCandidate = path.join(process.execPath, '..', '..', 'lib', 'node_modules', 'typescript');
|
|
19
|
+
try {
|
|
20
|
+
typescript = require(globalCandidate);
|
|
21
|
+
} catch (fallbackError) {
|
|
22
|
+
throw new Error('TypeScript 모듈을 불러올 수 없습니다. Bun 또는 Node 환경에서 TypeScript를 설치해주세요.');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
tsModuleCache = typescript.default ?? typescript;
|
|
26
|
+
return tsModuleCache;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getDefaultCompilerOptions(ts) {
|
|
30
|
+
if (!defaultCompilerOptions) {
|
|
31
|
+
defaultCompilerOptions = {
|
|
32
|
+
allowNonTsExtensions: true,
|
|
33
|
+
noEmit: true,
|
|
34
|
+
module: ts.ModuleKind.NodeNext,
|
|
35
|
+
moduleResolution: ts.ModuleResolutionKind.NodeNext,
|
|
36
|
+
target: ts.ScriptTarget.ES2020,
|
|
37
|
+
strict: true,
|
|
38
|
+
esModuleInterop: true
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return defaultCompilerOptions;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function createLintHost(ts, compilerOptions, filePath, sourceText) {
|
|
45
|
+
const resolvedFilePath = path.resolve(filePath);
|
|
46
|
+
const baseHost = ts.createCompilerHost(compilerOptions, true);
|
|
47
|
+
const sourceFile = ts.createSourceFile(
|
|
48
|
+
resolvedFilePath,
|
|
49
|
+
sourceText,
|
|
50
|
+
compilerOptions.target ?? ts.ScriptTarget.ES2020,
|
|
51
|
+
true,
|
|
52
|
+
ts.ScriptKind.TS
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const getSourceFile = baseHost.getSourceFile.bind(baseHost);
|
|
56
|
+
const fileExists = baseHost.fileExists.bind(baseHost);
|
|
57
|
+
const readFile = baseHost.readFile.bind(baseHost);
|
|
58
|
+
|
|
59
|
+
baseHost.getSourceFile = (fileName, languageVersion, onError, shouldCreateNewSourceFile) => {
|
|
60
|
+
if (path.resolve(fileName) === resolvedFilePath) {
|
|
61
|
+
return shouldCreateNewSourceFile ? ts.createSourceFile(
|
|
62
|
+
resolvedFilePath,
|
|
63
|
+
sourceText,
|
|
64
|
+
languageVersion,
|
|
65
|
+
true,
|
|
66
|
+
ts.ScriptKind.TS
|
|
67
|
+
) : sourceFile;
|
|
68
|
+
}
|
|
69
|
+
return getSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
baseHost.fileExists = (fileName) => {
|
|
73
|
+
if (path.resolve(fileName) === resolvedFilePath) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
return fileExists(fileName);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
baseHost.readFile = (fileName) => {
|
|
80
|
+
if (path.resolve(fileName) === resolvedFilePath) {
|
|
81
|
+
return sourceText;
|
|
82
|
+
}
|
|
83
|
+
return readFile(fileName);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return baseHost;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function formatDiagnostics(ts, diagnostics) {
|
|
90
|
+
if (diagnostics.length === 0) {
|
|
91
|
+
return '';
|
|
92
|
+
}
|
|
93
|
+
const formatHost = {
|
|
94
|
+
getCanonicalFileName: (fileName) => fileName,
|
|
95
|
+
getCurrentDirectory: () => process.cwd(),
|
|
96
|
+
getNewLine: () => '\n'
|
|
97
|
+
};
|
|
98
|
+
return ts.formatDiagnosticsWithColorAndContext(diagnostics, formatHost);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function compileString(source) {
|
|
102
|
+
return compileSource(source);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function compileFile(inputPath, outputPath) {
|
|
106
|
+
const source = await readFile(inputPath, 'utf8');
|
|
107
|
+
const compiled = compileSource(source);
|
|
108
|
+
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
109
|
+
await writeFile(outputPath, compiled, 'utf8');
|
|
110
|
+
return compiled;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function compileFileToDefaultDestination(inputPath) {
|
|
114
|
+
const parsed = path.parse(inputPath);
|
|
115
|
+
const outputPath = path.join(parsed.dir, `${parsed.name}.ts`);
|
|
116
|
+
await compileFile(inputPath, outputPath);
|
|
117
|
+
return outputPath;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function lintSource(source, filePath = 'input.hgs', compilerOptions) {
|
|
121
|
+
const ts = loadTypeScriptModule();
|
|
122
|
+
const options = compilerOptions ?? getDefaultCompilerOptions(ts);
|
|
123
|
+
const compiled = compileSource(source);
|
|
124
|
+
const host = createLintHost(ts, options, filePath, compiled);
|
|
125
|
+
const program = ts.createProgram([path.resolve(filePath)], options, host);
|
|
126
|
+
const diagnostics = ts.getPreEmitDiagnostics(program);
|
|
127
|
+
return {
|
|
128
|
+
diagnostics,
|
|
129
|
+
formattedDiagnostics: formatDiagnostics(ts, diagnostics),
|
|
130
|
+
hasErrors: diagnostics.some((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error)
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function lintFile(inputPath, compilerOptions) {
|
|
135
|
+
const source = await readFile(inputPath, 'utf8');
|
|
136
|
+
return lintSource(source, inputPath, compilerOptions);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export { compileSource };
|
|
140
|
+
|
|
141
|
+
/** Compile through TypeScript once, emitting executable JS and declarations in memory. */
|
|
142
|
+
export function buildSource(source, filePath = 'input.hgs') {
|
|
143
|
+
const ts = loadTypeScriptModule();
|
|
144
|
+
const virtualPath = path.resolve(filePath).replace(/\.(?:hgs|ks)$/i, '') + '.ts';
|
|
145
|
+
const compiled = compileSource(source);
|
|
146
|
+
const options = {
|
|
147
|
+
target: ts.ScriptTarget.ES2022,
|
|
148
|
+
module: ts.ModuleKind.ESNext,
|
|
149
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
150
|
+
strict: true,
|
|
151
|
+
declaration: true,
|
|
152
|
+
noEmitOnError: true,
|
|
153
|
+
skipLibCheck: true
|
|
154
|
+
};
|
|
155
|
+
const host = createLintHost(ts, options, virtualPath, compiled);
|
|
156
|
+
const files = new Map();
|
|
157
|
+
host.writeFile = (name, content) => files.set(name, content);
|
|
158
|
+
const program = ts.createProgram([virtualPath], options, host);
|
|
159
|
+
const diagnostics = ts.getPreEmitDiagnostics(program);
|
|
160
|
+
if (diagnostics.some(d => d.category === ts.DiagnosticCategory.Error)) {
|
|
161
|
+
throw new Error(formatDiagnostics(ts, diagnostics));
|
|
162
|
+
}
|
|
163
|
+
const emitted = program.emit();
|
|
164
|
+
if (emitted.emitSkipped) throw new Error(formatDiagnostics(ts, emitted.diagnostics) || '빌드에 실패했습니다.');
|
|
165
|
+
return {
|
|
166
|
+
javascript: files.get(virtualPath.replace(/\.ts$/, '.js')) ?? '',
|
|
167
|
+
declaration: files.get(virtualPath.replace(/\.ts$/, '.d.ts')) ?? ''
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function buildFile(inputPath, outputDirectory = path.dirname(inputPath)) {
|
|
172
|
+
const source = await readFile(inputPath, 'utf8');
|
|
173
|
+
const result = buildSource(source, inputPath);
|
|
174
|
+
const base = path.parse(inputPath).name;
|
|
175
|
+
await mkdir(outputDirectory, { recursive: true });
|
|
176
|
+
const javascriptPath = path.join(outputDirectory, base + '.js');
|
|
177
|
+
const declarationPath = path.join(outputDirectory, base + '.d.ts');
|
|
178
|
+
await writeFile(javascriptPath, result.javascript, 'utf8');
|
|
179
|
+
await writeFile(declarationPath, result.declaration, 'utf8');
|
|
180
|
+
return { ...result, javascriptPath, declarationPath };
|
|
181
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// 문장형 v0.1: deterministic indentation grammar, not natural-language inference.
|
|
2
|
+
export function isSentenceSource(source) {
|
|
3
|
+
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
|
+
|
|
6
|
+
export const sentenceRuntime = `
|
|
7
|
+
function 출력(...값: unknown[]): void { console.log(...값); }
|
|
8
|
+
function 길이(값: string | readonly unknown[]): number { return 값.length; }
|
|
9
|
+
function 추가<T>(목록: T[], 값: T): void { 목록.push(값); }
|
|
10
|
+
function 포함<T>(목록: readonly T[], 값: T): boolean { return 목록.includes(값); }
|
|
11
|
+
function 나누기(글: string, 구분: string): string[] { return 글.split(구분); }
|
|
12
|
+
function 잇기(목록: readonly unknown[], 구분 = ""): string { return 목록.join(구분); }
|
|
13
|
+
function 합계(값: readonly number[]): number { return 값.reduce((합, 항목) => 합 + 항목, 0); }
|
|
14
|
+
function 숫자로(값: string | number): number {
|
|
15
|
+
if (typeof 값 === "string" && 값.trim() === "") throw new Error("빈 문자열은 숫자로 바꿀 수 없습니다.");
|
|
16
|
+
const 결과 = Number(값);
|
|
17
|
+
if (!Number.isFinite(결과)) throw new Error("유한한 숫자를 입력하세요.");
|
|
18
|
+
return 결과;
|
|
19
|
+
}
|
|
20
|
+
function 다듬기(글: string): string { return 글.trim(); }
|
|
21
|
+
function 반올림(값: number, 자리 = 0): number {
|
|
22
|
+
if (!Number.isFinite(값) || !Number.isInteger(자리) || 자리 < -10 || 자리 > 10) throw new Error("유한한 수와 -10~10 사이의 정수 자리가 필요합니다.");
|
|
23
|
+
return Math.round(값 * 10 ** 자리) / 10 ** 자리;
|
|
24
|
+
}
|
|
25
|
+
function 평균(목록: readonly number[]): number {
|
|
26
|
+
if (!목록.length) throw new Error("빈 목록의 평균을 구할 수 없습니다.");
|
|
27
|
+
return 합계(목록) / 목록.length;
|
|
28
|
+
}
|
|
29
|
+
function 최솟값(목록: readonly number[]): number {
|
|
30
|
+
if (!목록.length) throw new Error("빈 목록의 최솟값을 구할 수 없습니다.");
|
|
31
|
+
let 결과 = 목록[0]; for (const 항목 of 목록) if (항목 < 결과) 결과 = 항목;
|
|
32
|
+
return 결과;
|
|
33
|
+
}
|
|
34
|
+
function 최댓값(목록: readonly number[]): number {
|
|
35
|
+
if (!목록.length) throw new Error("빈 목록의 최댓값을 구할 수 없습니다.");
|
|
36
|
+
let 결과 = 목록[0]; for (const 항목 of 목록) if (항목 > 결과) 결과 = 항목;
|
|
37
|
+
return 결과;
|
|
38
|
+
}
|
|
39
|
+
function 정렬(값: readonly number[]): number[] { return [...값].sort((가, 나) => 가 - 나); }
|
|
40
|
+
function 범위(시작: number, 끝?: number, 간격 = 1): number[] {
|
|
41
|
+
if (끝 === undefined) { 끝 = 시작; 시작 = 0; }
|
|
42
|
+
if (!Number.isFinite(시작) || !Number.isFinite(끝) || !Number.isFinite(간격) || 간격 === 0) throw new Error("범위에는 유한한 수와 0이 아닌 간격이 필요합니다.");
|
|
43
|
+
const 개수 = Math.max(0, Math.ceil((끝 - 시작) / 간격));
|
|
44
|
+
if (개수 > 100000) throw new Error("범위는 최대 100000개까지 만들 수 있습니다.");
|
|
45
|
+
return Array.from({ length: 개수 }, (_, 순서) => 시작 + 순서 * 간격);
|
|
46
|
+
}
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
export function compileSentences(source, { includeRuntime = true } = {}) {
|
|
50
|
+
const output = includeRuntime ? [sentenceRuntime] : [];
|
|
51
|
+
const indents = [0];
|
|
52
|
+
let needsIndent = false;
|
|
53
|
+
const lines = source.split(/\r?\n/);
|
|
54
|
+
for (let index = 0; index < lines.length; index++) {
|
|
55
|
+
const raw = lines[index];
|
|
56
|
+
const text = raw.trim();
|
|
57
|
+
if (!text || text === '문장으로 시작' || text.startsWith('#')) continue;
|
|
58
|
+
const fail = message => { throw new SyntaxError(`${index + 1}행: ${message}`); };
|
|
59
|
+
if (/^\s*\t/.test(raw)) fail('들여쓰기는 탭 대신 공백을 사용하세요.');
|
|
60
|
+
const indent = raw.length - raw.trimStart().length;
|
|
61
|
+
if (needsIndent) {
|
|
62
|
+
if (indent <= indents.at(-1)) fail('블록 안의 문장을 들여써 주세요.');
|
|
63
|
+
indents.push(indent);
|
|
64
|
+
} else if (indent > indents.at(-1)) fail('예상하지 못한 들여쓰기입니다.');
|
|
65
|
+
while (indent < indents.at(-1)) { indents.pop(); output.push('}'); }
|
|
66
|
+
if (indent !== indents.at(-1)) fail('이전 블록과 들여쓰기를 맞춰 주세요.');
|
|
67
|
+
needsIndent = false;
|
|
68
|
+
let match;
|
|
69
|
+
if ((match = text.match(/^(.+\))를 정의한다:$/u))) {
|
|
70
|
+
// Unannotated parameters are intentionally dynamic, like Python.
|
|
71
|
+
const signature = match[1].replace(/\((.*)\)$/, (_, params) => '(' + params.split(',').filter(x => x.trim()).map(x => x.includes(':') ? x.trim() : `${x.trim()}: any`).join(', ') + ')');
|
|
72
|
+
output.push(`function ${signature} {`); needsIndent = true;
|
|
73
|
+
} else if ((match = text.match(/^각 (.+?)(?:을|를) (.+)에서:$/u))) {
|
|
74
|
+
if (!/^[\p{L}_$][\p{L}\p{N}_$]*$/u.test(match[1])) fail('반복 변수 이름을 확인하세요.');
|
|
75
|
+
output.push(`for (const ${match[1]} of ${match[2]}) {`); needsIndent = true;
|
|
76
|
+
} else if ((match = text.match(/^만약 (.+) (?:이)?라면:$/u))) {
|
|
77
|
+
output.push(`if (${match[1]}) {`); needsIndent = true;
|
|
78
|
+
} else if ((match = text.match(/^아니고 (.+) (?:이)?라면:$/u))) {
|
|
79
|
+
if (output.at(-1) !== '}') fail('아니고 앞에는 조건 블록이 필요합니다.');
|
|
80
|
+
output.push(`else if (${match[1]}) {`); needsIndent = true;
|
|
81
|
+
} else if (text === '아니면:') {
|
|
82
|
+
if (output.at(-1) !== '}') fail('아니면 앞에는 조건 블록이 필요합니다.');
|
|
83
|
+
output.push('else {'); needsIndent = true;
|
|
84
|
+
} else if ((match = text.match(/^동안 (.+):$/u))) {
|
|
85
|
+
output.push(`while (${match[1]}) {`); needsIndent = true;
|
|
86
|
+
} else if ((match = text.match(/^(.+?)(?:은|는) (.+)이다$/u))) {
|
|
87
|
+
if (!/^[\p{L}_$][\p{L}\p{N}_$]*$/u.test(match[1])) fail('변수 이름을 확인하세요.');
|
|
88
|
+
output.push(`let ${match[1]}${match[2].trim() === '[]' ? ': any[]' : ''} = ${match[2]};`);
|
|
89
|
+
} else if ((match = text.match(/^돌려주기(?: (.+))?$/u))) {
|
|
90
|
+
output.push(`return ${match[1] ?? ''};`);
|
|
91
|
+
} else if (text === '멈추기') output.push('break;');
|
|
92
|
+
else if (text === '계속하기') output.push('continue;');
|
|
93
|
+
else if (text.endsWith(':')) fail('지원하지 않는 블록 문장입니다.');
|
|
94
|
+
else output.push(text + (text.endsWith(';') ? '' : ';'));
|
|
95
|
+
}
|
|
96
|
+
if (needsIndent) throw new SyntaxError('마지막 블록에 실행할 문장이 필요합니다.');
|
|
97
|
+
while (indents.length > 1) { indents.pop(); output.push('}'); }
|
|
98
|
+
return output.join('\n');
|
|
99
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
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
|
+
}
|
|
332
|
+
|
|
333
|
+
export function compileSource(source) {
|
|
334
|
+
const sentenceMode = isSentenceSource(source);
|
|
335
|
+
if (sentenceMode) source = compileSentences(source, { includeRuntime: false });
|
|
336
|
+
const transformed = fixClassMethods(transformKeywords(source));
|
|
337
|
+
return sentenceMode ? sentenceRuntime + '\n' + transformed : transformed;
|
|
338
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
문장으로 시작
|
|
2
|
+
첫째는 24이다
|
|
3
|
+
둘째는 6이다
|
|
4
|
+
연산은 "나누기"이다
|
|
5
|
+
|
|
6
|
+
계산(가, 나, 방식)를 정의한다:
|
|
7
|
+
만약 방식 == "더하기" 이라면:
|
|
8
|
+
돌려주기 가 + 나
|
|
9
|
+
아니고 방식 == "빼기" 이라면:
|
|
10
|
+
돌려주기 가 - 나
|
|
11
|
+
아니고 방식 == "곱하기" 이라면:
|
|
12
|
+
돌려주기 가 * 나
|
|
13
|
+
아니면:
|
|
14
|
+
만약 나 == 0 이라면:
|
|
15
|
+
돌려주기 "0으로 나눌 수 없습니다"
|
|
16
|
+
돌려주기 가 / 나
|
|
17
|
+
|
|
18
|
+
출력("계산 결과:", 계산(첫째, 둘째, 연산))
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
상수 이름 = "철수";
|
|
2
|
+
변수 나이: 숫자 = 12;
|
|
3
|
+
|
|
4
|
+
함수 인사(사람이름: 문자열): 문자열 {
|
|
5
|
+
반환 `안녕 ${사람이름}!`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
클래스 사람 {
|
|
9
|
+
이름: 문자열;
|
|
10
|
+
나이: 숫자;
|
|
11
|
+
생성자(이름: 문자열, 나이: 숫자) {
|
|
12
|
+
이것.이름 = 이름;
|
|
13
|
+
이것.나이 = 나이;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
함수 소개(): 문자열 {
|
|
17
|
+
반환 `${이것.이름}(${이것.나이}) 입니다`;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
콘솔.출력(인사(이름));
|
package/package.json
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uiwwsw/hangulscript",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "한글 문장으로 작성하고 JavaScript와 타입 선언으로 배포하는 실험적 프로그래밍 언어",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./browser": {
|
|
14
|
+
"types": "./dist/transformer.d.ts",
|
|
15
|
+
"import": "./dist/transformer.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"bin": {
|
|
19
|
+
"ksc": "dist/cli.js"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LANGUAGE.md",
|
|
25
|
+
"LICENSE",
|
|
26
|
+
"examples/*.hgs"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "node ./scripts/build.js",
|
|
30
|
+
"prepublishOnly": "npm test && npm run build",
|
|
31
|
+
"test": "node --test",
|
|
32
|
+
"build:site": "node scripts/build-site.js",
|
|
33
|
+
"benchmark": "node scripts/benchmark.js"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"typescript": "^5.4.0"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"hangul",
|
|
40
|
+
"korean",
|
|
41
|
+
"hgs",
|
|
42
|
+
"typescript",
|
|
43
|
+
"transpiler"
|
|
44
|
+
],
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/uiwwsw/hangulscript.git"
|
|
48
|
+
},
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/uiwwsw/hangulscript/issues"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/uiwwsw/hangulscript#readme",
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=20"
|
|
55
|
+
},
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"esbuild": "^0.25.0"
|
|
61
|
+
},
|
|
62
|
+
"types": "dist/index.d.ts"
|
|
63
|
+
}
|