@uiwwsw/hangulscript 0.6.0 → 0.8.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/CHANGELOG.md ADDED
@@ -0,0 +1,36 @@
1
+ # 0.8.0
2
+
3
+ - 23개 페이지의 공식 문서: 검색·코드 복사·실행 링크·함수별 타입과 동작·SEO.
4
+ - 목록·집합·사전·복사·정렬·검증 등을 포함한 61개 한글 함수와 타입 별칭.
5
+ - 정렬·복사·중복 제거를 문장형 값으로 표현하는 문법.
6
+ - ㅎ 파일 아이콘과 표준 기능을 제공하는 VS Code 확장 0.3.0.
7
+ - GitHub Actions NPM_TOKEN 기반 릴리스 게시 워크플로.
8
+
9
+ # 0.7.0
10
+
11
+ - 공식 VS Code 확장 0.2.0: 문법 강조·포맷·진단·타입 정보·정의 이동·스코프 자동 완성·스니펫.
12
+ - 실제 VS Code Extension Host 통합 테스트와 CI 추가.
13
+ - Node 등록 진입점과 CLI에서 상대 경로 .hgs 모듈 직접 실행.
14
+ - 다중 파일 JS·타입 선언 빌드와 .js import 경로 연결.
15
+ - tsconfig compilerOptions·extends 적용과 지원·호환성 계약 문서화.
16
+
17
+ # 0.7.0-dev.0 (개발 버전)
18
+
19
+ - VS Code 확장 0.1.0: `.hgs` 문법 강조, 들여쓰기, 문서 포맷, Problems 진단.
20
+ - Vite·CLI·브라우저의 문법·타입 진단에 원본 HGS 위치 표시.
21
+ - 포맷터 API와 확장용 VSIX 빌드 추가.
22
+
23
+ # 변경 내역
24
+
25
+ ## 0.6.0
26
+
27
+ 문장형이 유일한 기본 문법입니다. 소스는 첫 문장부터 바로 작성합니다.
28
+
29
+ - **호환되지 않는 변경:** 키워드형 소스 모드·시작 표기·`.ks` 입력을 제거했습니다. 기존 파일은 `.hgs`와 문장형으로 옮겨야 합니다.
30
+ - 모드 감지·전역 키워드 치환·클래스 사후 보정을 제거했습니다.
31
+ - 모듈 가져오기·공개, 비동기·기다리기, 생성기, 예외 처리, 클래스·상속·읽기/쓰기 속성, 자료형·열거값·제네릭을 문장형으로 제공합니다.
32
+ - 잘못된 조건 분기 연결과 반복/함수 밖의 제어 문장을 진단합니다. 문자열·정규식·템플릿·속성 이름을 보존합니다.
33
+ - 기본 함수 15개, 화면 함수 5개, CLI JS+.d.ts, React/Vue Vite 연동을 검증했습니다.
34
+ - 기본 예제의 입력창은 즉시 표시하며 코드는 버튼을 눌렀을 때 실행합니다.
35
+
36
+ 지원 문법과 제약은 [LANGUAGE.md](LANGUAGE.md), React·Vue 연동은 [가이드](docs/frameworks.md)를 참고하세요.
package/LANGUAGE.md CHANGED
@@ -135,7 +135,7 @@ React·Vue의 Vite 플러그인은 `.hgs` import를 빌드할 때 처리합니
135
135
 
136
136
  자료형·타입 별칭·열거값은 최상위에 작성합니다. 클래스의 추상·접근 제한자·데코레이터 및 전체 TypeScript 문법은 지원 범위가 아닙니다.
137
137
 
138
- ## 기본 함수 15개
138
+ ## 기본 함수와 표준 기능
139
139
 
140
140
  | 함수 | 의미 |
141
141
  | --- | --- |
@@ -170,3 +170,11 @@ React·Vue의 Vite 플러그인은 `.hgs` import를 빌드할 때 처리합니
170
170
  웹 데모와 Vite 플러그인은 문법 변환·JS 구문 진단을 제공하며 전체 타입 검사는 하지 않습니다.
171
171
  문장 오류는 원본 행 번호를 표시합니다. TypeScript 타입 진단은 생성 코드 위치를 사용하며 원본 소스맵은 아직 제공하지 않습니다.
172
172
  임의의 한국어를 해석하는 AI 실행기·Python 호환 언어·전체 JavaScript/TypeScript 대체 언어가 아닙니다.
173
+
174
+ ## 목록·집합·사전과 문장형 값
175
+
176
+ 0.8.0은 목록·집합·사전·복사·정렬을 포함해 61개 한글 함수를 제공합니다. 타입 자리에서 `목록<T>`는 `Array<T>`, `집합<T>`는 `Set<T>`, `사전<K, V>`는 `Map<K, V>`입니다. `목록으로(값)`으로 순회 가능한 값을 배열로 바꿉니다.
177
+
178
+ 값 자리에서 「대상을 오름차순으로 정렬한 값」「대상을 내림차순으로 정렬한 값」「대상을 얕게 복사한 값」「대상을 깊게 복사한 값」「대상에서 중복을 제거한 값」을 사용할 수 있습니다. 정해진 문법이며 임의의 산문을 추측하지 않습니다.
179
+
180
+ [자료구조 안내](https://hangulscript.vercel.app/docs/collections/) · [전체 표준 기능](https://hangulscript.vercel.app/docs/standard-library/)
package/README.md CHANGED
@@ -3,12 +3,12 @@
3
3
  [![CI on main](https://github.com/uiwwsw/hangulscript/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/uiwwsw/hangulscript/actions/workflows/ci.yml)
4
4
  [![MIT license](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](https://github.com/uiwwsw/hangulscript/blob/main/LICENSE)
5
5
 
6
- **한글 문장으로 작성하고, JavaScript와 타입 선언으로 배포하는 실험적 프로그래밍 언어.**
6
+ **한글 문장으로 작성하고, JavaScript와 타입 선언으로 배포하는 프로그래밍 언어.**
7
7
 
8
8
  Python의 들여쓰기와 익숙한 기본 함수, JavaScript의 표현식과 웹 생태계,
9
9
  TypeScript의 타입 검사를 결합합니다. 소스 확장자는 **`.hgs`**입니다.
10
10
 
11
- [온라인에서 실행하기](https://hangulscript.vercel.app/) · [React·Vue 사용법](docs/frameworks.md) · [문법 명세](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) · [변경 내역](CHANGELOG.md) · [릴리스](https://github.com/uiwwsw/hangulscript/releases) · [의견 보내기](https://github.com/uiwwsw/hangulscript/issues)
12
12
 
13
13
  ```text
14
14
  점수들은 [75, 92, 88]이다
@@ -18,7 +18,7 @@ TypeScript의 타입 검사를 결합합니다. 소스 확장자는 **`.hgs`**
18
18
  출력("평균:", 평균(점수들))
19
19
  ```
20
20
 
21
- 문장형은 실험판입니다. 정해진 한국어 문법을 파싱하며, 임의의 자연어를 추측해 실행하지 않습니다.
21
+ 정해진 한국어 문법을 파싱하며, 임의의 자연어를 추측해 실행하지 않습니다.
22
22
  Python 소스 호환이나 전체 TypeScript 문법 지원을 뜻하지 않습니다.
23
23
 
24
24
  ## 지금 만들 수 있는 것
@@ -39,7 +39,7 @@ Python 소스 호환이나 전체 TypeScript 문법 지원을 뜻하지 않습
39
39
 
40
40
  ## 설치
41
41
 
42
- Node.js **20 이상**이 필요합니다. 아래의 JS + .d.ts 빌드와 `run` 명령은 **0.3.0부터** 제공합니다.
42
+ Node.js **20.19 이상**이 필요합니다. 아래의 JS + .d.ts 빌드와 `run` 명령은 **0.3.0부터** 제공합니다.
43
43
  공개된 버전은 [npm](https://www.npmjs.com/package/@uiwwsw/hangulscript) 또는 `npm view @uiwwsw/hangulscript version`으로 확인하세요.
44
44
 
45
45
  ```bash
@@ -235,7 +235,7 @@ Vite 연동을 제공합니다. 독립 AST·원본 `.hgs` 소스맵·LSP·모듈
235
235
 
236
236
  [MIT](LICENSE)
237
237
 
238
- ## 이름과 확장자 이전 (0.4)
238
+ ## 이름과 확장자
239
239
 
240
240
  제품명은 한글스크립트(HangulScript), 패키지는 `@uiwwsw/hangulscript`입니다.
241
241
  0.6부터 소스 확장자는 `.hgs`만 지원하며 키워드형과 시작 표기를 제거했습니다. 기존 코드는 [문법 명세](LANGUAGE.md)의 문장형으로 옮겨야 합니다.
@@ -270,7 +270,7 @@ HTML·이벤트 핸들러 문자열을 삽입하지 않습니다.
270
270
  **0.6.0 이상**의 공통 Vite 플러그인을 사용합니다. React/Vue의 화면과 상태 관리에 한글스크립트 계산·검증 함수를 연결할 수 있습니다.
271
271
 
272
272
  ```bash
273
- npm install --save-dev @uiwwsw/hangulscript@^0.6.0
273
+ npm install --save-dev @uiwwsw/hangulscript@^0.8.0
274
274
  ```
275
275
 
276
276
  ```js
@@ -303,5 +303,31 @@ console.log(결제금액(10000, 20)); // 8000
303
303
  - [온라인 연동 안내](https://hangulscript.vercel.app/#frameworks)
304
304
 
305
305
  예제는 Node.js `^20.19.0 || >=22.12.0`이 필요하며 Node.js 24에서 검증했습니다.
306
- Vite 플러그인은 타입 검사·선언 생성·소스맵을 제공하지 않습니다. TypeScript 프로젝트는 `ksc build`로 생성한 `.js`와 `.d.ts`를 가져오는 방식이 적합합니다.
306
+ **0.7.0부터** Vite 플러그인은 구문·타입 오류가 있으면 개발 서버와 빌드를 실패시키고 원본 `.hgs` 위치를 표시합니다. 선언 생성·디버깅 소스맵은 제공하지 않습니다. TypeScript 프로젝트는 `ksc build`로 생성한 `.js`와 `.d.ts`를 가져오는 방식이 적합합니다.
307
307
  데모의 UI 문장을 React/Vue 컴포넌트로 자동 변환하는 기능은 포함하지 않습니다.
308
+
309
+ ## VS Code 확장
310
+
311
+ 문법 강조·자동 들여쓰기·문서 포맷·Problems 오류 표시·타입 정보·정의 이동·스코프 자동 완성을 제공하는 확장을 같은 저장소의 [`extensions/vscode`](extensions/vscode)에서 개발합니다. [설치·저장 시 포맷·Marketplace 게시 안내](extensions/vscode/README.md)를 참고하세요. VSIX 패키지와 Marketplace 공개는 별도 단계입니다.
312
+
313
+ ## 공식 도구와 지원 범위
314
+
315
+ Node에서 직접 실행하려면 `node --import @uiwwsw/hangulscript/register src/main.hgs`를 사용합니다. `ksc run`도 같은 로더를 사용하며 상대 경로 `.hgs` import를 처리합니다. `ksc build`는 진입 폴더 아래 HGS 모듈을 함께 빌드하고 import를 `.js`로 연결합니다.
316
+
317
+ [환경별 지원 계약·타입 설정·호환성 정책](docs/support.md)을 확인하세요. 언어 패키지와 VS Code 확장은 같은 저장소에서 검증하며 별도 버전으로 배포합니다.
318
+
319
+ ## 문서와 자료구조
320
+
321
+ [공식 문서](https://hangulscript.vercel.app/docs/)는 시작하기·문법·자료구조·모듈·프레임워크·편집기·오류 해결을 페이지별로 제공합니다. [61개 표준 함수](https://hangulscript.vercel.app/docs/standard-library/)에는 타입·실행 예제·원본 변경 여부가 있습니다.
322
+
323
+ ```text
324
+ 점수들은 [88, 92, 88]이다
325
+ 점수순은 점수들을 내림차순으로 정렬한 값이다
326
+ 고유값은 점수들에서 중복을 제거한 값이다
327
+ 독립본은 점수들을 깊게 복사한 값이다
328
+ 출력(점수순, 고유값, 독립본)
329
+ ```
330
+
331
+ 목록의 변환·검색·누적·펼치기, 집합 연산, 사전(Map), 얕은·깊은 복사, 안정적인 기준 정렬을 제공합니다. 전체 JS API를 한글화한 상태는 아니며 [지원 범위](docs/support.md)에 제약을 명시합니다. [자동 게시 설정](docs/publishing.md)은 GitHub Actions의 `NPM_TOKEN` 시크릿을 사용합니다.
332
+
333
+ [한글스크립트의 큰 그림과 로드맵](https://hangulscript.vercel.app/docs/roadmap/)에서 현재 지원·부분 지원·미구현과 다음 개발 순서를 확인하세요.
@@ -0,0 +1,80 @@
1
+ import ts from 'typescript';
2
+ import path from 'node:path';
3
+ import { compileMappedSource } from './transformer.js';
4
+ import { mapDiagnostic, syntaxDiagnostic } from './diagnostics.js';
5
+
6
+ const libraryCache = new Map();
7
+ export function analyzeSource(source, filePath = 'input.hgs', settings = {}) {
8
+ const configPath = ts.findConfigFile(path.dirname(path.resolve(filePath)), ts.sys.fileExists);
9
+ const configRead = configPath ? ts.readConfigFile(configPath, ts.sys.readFile) : undefined;
10
+ const converted = configRead?.config ? ts.parseJsonConfigFileContent(configRead.config, { ...ts.sys, readDirectory: () => [] }, path.dirname(configPath)) : { options: {}, errors: [] };
11
+ const options = {
12
+ strict: true, skipLibCheck: true, types: [],
13
+ ...converted.options, ...settings.compilerOptions,
14
+ target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext, moduleResolution: ts.ModuleResolutionKind.Bundler,
15
+ moduleDetection: ts.ModuleDetectionKind.Force, declaration: true, noEmitOnError: true,
16
+ noEmit: false, emitDeclarationOnly: false, outDir: undefined, outFile: undefined, rootDir: undefined, incremental: false, composite: false, sourceMap: false, declarationMap: false, inlineSourceMap: false
17
+ };
18
+ const originals = new Map([...settings.files ?? []].map(([name, text]) => [path.resolve(name), text]));
19
+ const root = path.resolve(filePath);
20
+ originals.set(root, source);
21
+ const virtual = name => name + '.ts';
22
+ const documents = new Map();
23
+ const syntaxErrors = [];
24
+ const base = ts.createCompilerHost(options, true);
25
+ const libDirectory = settings.libDirectory ?? path.dirname(ts.getDefaultLibFilePath(options));
26
+ function document(name) {
27
+ if (documents.has(virtual(name))) return documents.get(virtual(name));
28
+ const text = originals.get(name) ?? ts.sys.readFile(name);
29
+ if (text === undefined) return null;
30
+ let mapped;
31
+ try { mapped = compileMappedSource(text); }
32
+ catch (error) { syntaxErrors.push(syntaxDiagnostic(error, text, name)); mapped = { code: 'export {};', lineMap: [1] }; }
33
+ const result = { ...mapped, source: text, filePath: name };
34
+ documents.set(virtual(name), result);
35
+ return result;
36
+ }
37
+ const isVirtual = name => name.endsWith('.hgs.ts') || originals.has(name.slice(0, -3));
38
+ const host = {
39
+ ...base,
40
+ getCurrentDirectory: () => configPath ? path.dirname(configPath) : path.dirname(root),
41
+ getDefaultLibFileName: () => path.join(libDirectory, ts.getDefaultLibFileName(options)),
42
+ getDefaultLibLocation: () => libDirectory,
43
+ fileExists(name) { return isVirtual(name) ? originals.has(name.slice(0, -3)) || ts.sys.fileExists(name.slice(0, -3)) : base.fileExists(name); },
44
+ readFile(name) { return isVirtual(name) ? document(name.slice(0, -3))?.code : base.readFile(name); },
45
+ getSourceFile(name, languageVersion) {
46
+ const text = isVirtual(name) ? document(name.slice(0, -3))?.code : base.readFile(name);
47
+ if (text === undefined) return undefined;
48
+ if (name.startsWith(libDirectory) && name.endsWith('.d.ts')) {
49
+ const cached = libraryCache.get(name);
50
+ if (cached?.text === text) return cached;
51
+ const file = ts.createSourceFile(name, text, languageVersion, true); libraryCache.set(name, file); return file;
52
+ }
53
+ return ts.createSourceFile(name, text, languageVersion, true);
54
+ },
55
+ resolveModuleNames(names, containingFile) {
56
+ return names.map(name => {
57
+ if (name.endsWith('.hgs') && name.startsWith('.')) {
58
+ const original = path.resolve(path.dirname(containingFile), name);
59
+ if (originals.has(original) || ts.sys.fileExists(original)) return { resolvedFileName: virtual(original), extension: ts.Extension.Ts };
60
+ }
61
+ return ts.resolveModuleName(name, containingFile, options, host).resolvedModule;
62
+ });
63
+ }
64
+ };
65
+ const roots = settings.allFiles ? [...originals.keys()] : [root];
66
+ const program = ts.createProgram(roots.map(virtual), options, host);
67
+ const nativeDiagnostics = ts.getPreEmitDiagnostics(program).filter(d => {
68
+ if (![6133, 6196].includes(d.code) || !d.file || d.start === undefined) return true;
69
+ const doc = documents.get(d.file.fileName);
70
+ return !doc || Boolean(doc.lineMap[d.file.getLineAndCharacterOfPosition(d.start).line]);
71
+ });
72
+ const configErrors = [...converted.errors.filter(d => d.code !== 18003), ...(configRead?.error ? [configRead.error] : [])].map(d => ({ ...mapDiagnostic(d, documents), filePath: configPath ?? root }));
73
+ const diagnostics = [...configErrors, ...syntaxErrors, ...nativeDiagnostics.map(d => mapDiagnostic(d, documents))];
74
+ return { program, documents, diagnostics, nativeDiagnostics, virtualRoot: virtual(root), compiled: documents.get(virtual(root))?.code ?? '', hasErrors: diagnostics.some(d => d.severity === 'error') };
75
+ }
76
+
77
+ export function checkSource(source, filePath = 'input.hgs', settings = {}) {
78
+ const { diagnostics, hasErrors } = analyzeSource(source, filePath, settings);
79
+ return { diagnostics, hasErrors };
80
+ }
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { compileFileToDefaultDestination, compileFile, lintFile, buildFile, buildSource } from './index.js';
3
- import { readFile } from 'node:fs/promises';
2
+ import { compileFileToDefaultDestination, compileFile, lintFile, buildFile } from './index.js';
3
+ import { fileURLToPath } from 'node:url';
4
4
  import { spawn } from 'node:child_process';
5
5
  import path from 'node:path';
6
6
 
@@ -15,7 +15,7 @@ function usage() {
15
15
  --typescript 변환된 TypeScript 확인용 출력
16
16
  -h, --help 도움말
17
17
 
18
- 문장형은 실험판입니다. run은 신뢰하는 로컬 코드만 실행하세요.`);
18
+ run은 신뢰하는 로컬 코드만 실행하세요.`);
19
19
  }
20
20
 
21
21
  async function main() {
@@ -42,13 +42,9 @@ async function main() {
42
42
  if (mode === 'run') {
43
43
  if (inputs.length !== 1 || outDir || typescript || check) throw new Error('run에는 .hgs 파일 하나와 -- 뒤의 실행 인자만 지정하세요.');
44
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']
45
+ const child = spawn(process.execPath, ['--import', fileURLToPath(new URL('./register.js', import.meta.url)), input, ...programArgs], {
46
+ cwd: path.dirname(input), stdio: 'inherit'
48
47
  });
49
- // Preserve imports relative to the source directory. Runtime needs Node.js 20+.
50
- child.stdin.on('error', () => {});
51
- child.stdin.end(javascript);
52
48
  const exitCode = await new Promise((resolve, reject) => {
53
49
  child.once('error', reject);
54
50
  child.once('exit', (code, signal) => resolve(code ?? (signal ? 1 : 0)));
@@ -0,0 +1,57 @@
1
+ export const collectionsRuntime = `
2
+ function 목록으로<T>(값: Iterable<T>): T[] { return Array.from(값); }
3
+ function 첫번째<T>(값: readonly T[]): T | undefined { return 값[0]; }
4
+ function 마지막<T>(값: readonly T[]): T | undefined { return 값.at(-1); }
5
+ function 꺼내기<T>(값: readonly T[], 순서: number): T | undefined { return 값.at(순서); }
6
+ function 각각바꾸기<T, U>(값: readonly T[], 바꿀: (항목: T, 순서: number) => U): U[] { return 값.map(바꿀); }
7
+ function 골라내기<T>(값: readonly T[], 조건: (항목: T, 순서: number) => boolean): T[] { return 값.filter(조건); }
8
+ function 찾기<T>(값: readonly T[], 조건: (항목: T, 순서: number) => boolean): T | undefined { return 값.find(조건); }
9
+ function 순서찾기<T>(값: readonly T[], 조건: (항목: T, 순서: number) => boolean): number { return 값.findIndex(조건); }
10
+ function 모두만족<T>(값: readonly T[], 조건: (항목: T) => boolean): boolean { return 값.every(조건); }
11
+ function 하나라도만족<T>(값: readonly T[], 조건: (항목: T) => boolean): boolean { return 값.some(조건); }
12
+ function 누적하기<T, U>(값: readonly T[], 시작: U, 합칠: (누적: U, 항목: T, 순서: number) => U): U { return 값.reduce(합칠, 시작); }
13
+ function 잘라내기<T>(값: readonly T[], 시작: number, 끝?: number): T[] { return 값.slice(시작, 끝); }
14
+ function 이어붙이기<T>(...목록들: readonly (readonly T[])[]): T[] { return 목록들.flatMap(값 => [...값]); }
15
+ function 뒤집기<T>(값: readonly T[]): T[] { return [...값].reverse(); }
16
+ function 펼치기<T>(값: readonly (readonly T[])[]): T[] { return 값.flatMap(항목 => [...항목]); }
17
+ function 나눠묶기<T>(값: readonly T[], 크기: number): T[][] {
18
+ if (!Number.isSafeInteger(크기) || 크기 < 1) throw new Error("묶음 크기는 1 이상의 안전한 정수여야 합니다.");
19
+ const 결과: T[][] = []; for (let 순서 = 0; 순서 < 값.length; 순서 += 크기) 결과.push(값.slice(순서, 순서 + 크기)); return 결과;
20
+ }
21
+ function 짝짓기<T, U>(가: readonly T[], 나: readonly U[]): [T, U][] { return 가.slice(0, 나.length).map((값, 순서) => [값, 나[순서]]); }
22
+ function 중복제거<T>(값: Iterable<T>): T[] { return [...new Set(값)]; }
23
+ function 집합<T>(값: Iterable<T> = []): Set<T> { return new Set(값); }
24
+ function 집합에추가<T>(값: Set<T>, 항목: T): Set<T> { return 값.add(항목); }
25
+ function 집합에서삭제<T>(값: Set<T>, 항목: T): boolean { return 값.delete(항목); }
26
+ function 합집합<T>(가: ReadonlySet<T>, 나: ReadonlySet<T>): Set<T> { const 결과 = new Set(가); for (const 값 of 나) 결과.add(값); return 결과; }
27
+ function 교집합<T>(가: ReadonlySet<T>, 나: ReadonlySet<T>): Set<T> { const 결과 = new Set<T>(); for (const 값 of 가) if (나.has(값)) 결과.add(값); return 결과; }
28
+ function 차집합<T>(가: ReadonlySet<T>, 나: ReadonlySet<T>): Set<T> { const 결과 = new Set<T>(); for (const 값 of 가) if (!나.has(값)) 결과.add(값); return 결과; }
29
+ function 대칭차집합<T>(가: ReadonlySet<T>, 나: ReadonlySet<T>): Set<T> { const 결과 = new Set(가); for (const 값 of 나) { if (가.has(값)) 결과.delete(값); else 결과.add(값); } return 결과; }
30
+ function 부분집합인가<T>(가: ReadonlySet<T>, 나: ReadonlySet<T>): boolean { for (const 값 of 가) if (!나.has(값)) return false; return true; }
31
+ function 사전<K, V>(항목: Iterable<readonly [K, V]> = []): Map<K, V> { return new Map(항목); }
32
+ function 사전에담기<K, V>(대상: Map<K, V>, 키: K, 값: V): Map<K, V> { 대상.set(키, 값); return 대상; }
33
+ function 사전에서읽기<K, V>(대상: ReadonlyMap<K, V>, 키: K): V | undefined { return 대상.get(키); }
34
+ function 사전에있는가<K, V>(대상: ReadonlyMap<K, V>, 키: K): boolean { return 대상.has(키); }
35
+ function 사전에서삭제<K, V>(대상: Map<K, V>, 키: K): boolean { return 대상.delete(키); }
36
+ function 키목록<K, V>(대상: ReadonlyMap<K, V>): K[] { return [...대상.keys()]; }
37
+ function 값목록<K, V>(대상: ReadonlyMap<K, V>): V[] { return [...대상.values()]; }
38
+ function 항목목록<K, V>(대상: ReadonlyMap<K, V>): [K, V][] { return [...대상.entries()]; }
39
+ function 기준으로묶기<T, K>(값: readonly T[], 기준: (항목: T) => K): Map<K, T[]> {
40
+ const 결과 = new Map<K, T[]>(); for (const 항목 of 값) { const 키 = 기준(항목); const 묶음 = 결과.get(키); if (묶음) 묶음.push(항목); else 결과.set(키, [항목]); } return 결과;
41
+ }
42
+ function 얕게복사<T extends object>(값: T): T {
43
+ if (Array.isArray(값)) return [...값] as T;
44
+ if (값 instanceof Map) return new Map(값) as T;
45
+ if (값 instanceof Set) return new Set(값) as T;
46
+ if (Object.getPrototypeOf(값) === Object.prototype || Object.getPrototypeOf(값) === null) return Object.assign(Object.create(Object.getPrototypeOf(값)), 값) as T;
47
+ throw new Error("얕게복사는 목록·집합·사전·일반 객체에 사용하세요.");
48
+ }
49
+ function 깊게복사<T>(값: T): T { return structuredClone(값); }
50
+ function 오름차순<T extends number | string>(값: readonly T[]): T[] { return 정렬(값); }
51
+ function 내림차순<T extends number | string>(값: readonly T[]): T[] { for (const 항목 of 값) 정렬비교(항목, 항목); return 정렬(값, (가, 나) => -정렬비교(가, 나)); }
52
+ function 기준정렬<T>(값: readonly T[], 기준: (항목: T) => number | string, 방향: "오름차순" | "내림차순" = "오름차순"): T[] {
53
+ if (방향 !== "오름차순" && 방향 !== "내림차순") throw new Error("정렬 방향은 오름차순 또는 내림차순이어야 합니다.");
54
+ return 값.map(항목 => (() => { const 키 = 기준(항목); 정렬비교(키, 키); return { 항목, 키 }; })()).sort((가, 나) => 정렬비교(가.키, 나.키) * (방향 === "오름차순" ? 1 : -1)).map(항목 => 항목.항목);
55
+ }
56
+ function 확인한다(조건: unknown, 설명 = "확인 조건을 만족하지 못했습니다."): asserts 조건 { if (!조건) throw new Error(설명); }
57
+ `;
@@ -0,0 +1,61 @@
1
+ import ts from 'typescript';
2
+
3
+ const hints = {
4
+ 2304: '이름을 먼저 선언했는지 확인하세요.',
5
+ 2307: '모듈 경로와 파일이 있는지 확인하세요.',
6
+ 2322: '선언한 타입과 대입한 값의 타입을 확인하세요.',
7
+ 2339: '이 값에 해당 속성이나 메서드가 있는지 확인하세요.',
8
+ 2345: '함수에 전달한 값의 타입을 확인하세요.',
9
+ 2554: '함수에 전달하는 인자의 개수를 확인하세요.',
10
+ 7006: '공개 함수의 매개변수 타입을 명시하세요.'
11
+ };
12
+
13
+ export class HangulScriptError extends Error {
14
+ constructor(diagnostics) {
15
+ super(formatSourceDiagnostics(diagnostics));
16
+ this.name = 'HangulScriptError';
17
+ this.diagnostics = diagnostics;
18
+ }
19
+ }
20
+
21
+ export function syntaxDiagnostic(error, source, filePath) {
22
+ const lines = source.split(/\r?\n/);
23
+ const match = error.message.match(/^(\d+)행: /);
24
+ const line = match ? Number(match[1]) : Math.max(1, lines.findLastIndex(line => line.trim()) + 1);
25
+ const text = lines[line - 1] ?? '';
26
+ return { filePath, line, column: text.length - text.trimStart().length + 1, length: Math.max(1, text.trim().length), code: 'HGS1001', message: error.message.replace(/^\d+행: /, ''), severity: 'error' };
27
+ }
28
+
29
+ export function mapDiagnostic(diagnostic, documents) {
30
+ const file = diagnostic.file;
31
+ const position = file && diagnostic.start !== undefined ? file.getLineAndCharacterOfPosition(diagnostic.start) : { line: 0, character: 0 };
32
+ const document = file && documents.get(file.fileName);
33
+ let line = position.line + 1;
34
+ let column = position.character + 1;
35
+ let length = diagnostic.length ?? 1;
36
+ if (document) {
37
+ line = document.lineMap[position.line] || 1;
38
+ const original = document.source.split(/\r?\n/)[line - 1] ?? '';
39
+ const token = file.text.slice(diagnostic.start, diagnostic.start + length);
40
+ const generatedLine = file.text.split('\n')[position.line] ?? '';
41
+ const occurrence = token ? generatedLine.slice(0, position.character).split(token).length - 1 : 0;
42
+ let found = -1;
43
+ for (let i = 0; i <= occurrence; i++) {
44
+ const next = original.indexOf(token, found + 1);
45
+ if (next < 0) break;
46
+ found = next;
47
+ }
48
+ column = found >= 0 ? found + 1 : original.length - original.trimStart().length + 1;
49
+ length = Math.max(1, Math.min(length, original.length - column + 1));
50
+ }
51
+ const detail = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
52
+ return {
53
+ filePath: document?.filePath ?? file?.fileName ?? 'input.hgs', line, column, length,
54
+ code: diagnostic.code, message: hints[diagnostic.code] ? `${hints[diagnostic.code]}\n${detail}` : detail,
55
+ severity: diagnostic.category === ts.DiagnosticCategory.Error ? 'error' : 'warning'
56
+ };
57
+ }
58
+
59
+ export function formatSourceDiagnostics(diagnostics) {
60
+ return diagnostics.map(d => `${d.filePath}:${d.line}:${d.column} [${typeof d.code === 'number' ? 'TS' : ''}${d.code}] ${d.message}`).join('\n');
61
+ }
@@ -0,0 +1,5 @@
1
+ import type { CheckSettings } from './index.js';
2
+ export interface EditorLocation { filePath: string; line: number; column: number; length: number; }
3
+ export interface EditorInspection { hover?: string; definitions: EditorLocation[]; completions: { name: string; detail: string }[]; }
4
+ /** One-based UTF-16 positions. Does not execute source. */
5
+ export function inspectSource(source: string, filePath: string, line: number, column: number, settings?: CheckSettings): EditorInspection;
package/dist/editor.js ADDED
@@ -0,0 +1,50 @@
1
+ import ts from 'typescript';
2
+ import path from 'node:path';
3
+ import { analyzeSource } from './checker.js';
4
+ import { mapDiagnostic } from './diagnostics.js';
5
+
6
+ /** One-based source positions; analysis only, never executes user code. */
7
+ export function inspectSource(source, filePath, line, column, settings = {}) {
8
+ const analysis = analyzeSource(source, filePath, settings);
9
+ const file = analysis.program.getSourceFile(analysis.virtualRoot);
10
+ const document = analysis.documents.get(analysis.virtualRoot);
11
+ if (!file || !document) return { completions: [], definitions: [] };
12
+ let target = file;
13
+ let matched;
14
+ const original = source.split(/\r?\n/)[line - 1] ?? '';
15
+ const visit = node => {
16
+ if (ts.isIdentifier(node)) {
17
+ const pos = file.getLineAndCharacterOfPosition(node.getStart(file));
18
+ if (document.lineMap[pos.line] === line) {
19
+ target = node;
20
+ const generatedLine = file.text.split('\n')[pos.line];
21
+ const occurrence = generatedLine.slice(0, pos.character).split(node.text).length - 1;
22
+ let found = -1;
23
+ for (let i=0; i<=occurrence; i++) { const next=original.indexOf(node.text,found+1); if(next<0) break; found=next; }
24
+ if (found >= 0 && column - 1 >= found && column - 1 <= found + node.text.length) matched = node;
25
+ }
26
+ }
27
+ ts.forEachChild(node,visit);
28
+ };
29
+ visit(file);
30
+ const checker = analysis.program.getTypeChecker();
31
+ target = matched ?? target;
32
+ const completions = checker.getSymbolsInScope(target, ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Alias)
33
+ .filter(symbol => symbol.declarations?.some(d => analysis.documents.has(d.getSourceFile().fileName) && !['화면연결', '정렬비교'].includes(symbol.name)))
34
+ .map(symbol => ({ name: symbol.name, detail: checker.typeToString(checker.getTypeOfSymbolAtLocation(symbol,target)) }));
35
+ if (!matched) return { completions, definitions: [] };
36
+ let symbol = checker.getSymbolAtLocation(matched);
37
+ if (!symbol) return { completions, definitions: [] };
38
+ if (symbol.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
39
+ const type = checker.typeToString(checker.getTypeOfSymbolAtLocation(symbol,matched),matched,ts.TypeFormatFlags.NoTruncation);
40
+ const definitions = (symbol.declarations ?? []).flatMap(declaration => {
41
+ const name = declaration.name ?? declaration;
42
+ const sourceFile=name.getSourceFile();
43
+ const doc=analysis.documents.get(sourceFile.fileName);
44
+ const position=sourceFile.getLineAndCharacterOfPosition(name.getStart(sourceFile));
45
+ if (!doc || !doc.lineMap[position.line]) return [];
46
+ const location=mapDiagnostic({file:sourceFile,start:name.getStart(sourceFile),length:name.getWidth(sourceFile),code:0,messageText:'',category:ts.DiagnosticCategory.Message},analysis.documents);
47
+ return [{filePath:path.resolve(location.filePath),line:location.line,column:location.column,length:location.length}];
48
+ });
49
+ return { hover: `${symbol.name}: ${type}`, completions, definitions };
50
+ }
@@ -0,0 +1,39 @@
1
+ import ts from 'typescript';
2
+ import path from 'node:path';
3
+ import { analyzeSource } from './checker.js';
4
+ import { HangulScriptError, mapDiagnostic } from './diagnostics.js';
5
+
6
+ // Keep JavaScript and declaration imports consistent with emitted filenames.
7
+ function rewriteImports(context) {
8
+ const visit = node => {
9
+ if (ts.isStringLiteral(node) && node.parent && node.text.endsWith('.hgs') &&
10
+ ((ts.isImportDeclaration(node.parent) || ts.isExportDeclaration(node.parent)) && node.parent.moduleSpecifier === node ||
11
+ ts.isCallExpression(node.parent) && node.parent.expression.kind === ts.SyntaxKind.ImportKeyword && node.parent.arguments[0] === node)) {
12
+ return context.factory.createStringLiteral(node.text.replace(/\.hgs$/, '.js'));
13
+ }
14
+ // afterDeclarations nodes do not always carry parent links.
15
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.endsWith('.hgs')) {
16
+ return context.factory.updateImportDeclaration(node, node.modifiers, node.importClause, context.factory.createStringLiteral(node.moduleSpecifier.text.replace(/\.hgs$/, '.js')), node.attributes);
17
+ }
18
+ if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.endsWith('.hgs')) {
19
+ return context.factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, context.factory.createStringLiteral(node.moduleSpecifier.text.replace(/\.hgs$/, '.js')), node.attributes);
20
+ }
21
+ return ts.visitEachChild(node, visit, context);
22
+ };
23
+ return node => ts.visitNode(node, visit);
24
+ }
25
+
26
+ export function emitSource(source, filePath) {
27
+ const result = analyzeSource(source, filePath);
28
+ if (result.hasErrors) throw new HangulScriptError(result.diagnostics);
29
+ const files = new Map();
30
+ const emitted = result.program.emit(undefined, (name, content) => files.set(name, content), undefined, undefined, { before: [rewriteImports], afterDeclarations: [rewriteImports] });
31
+ if (emitted.emitSkipped) throw new HangulScriptError(emitted.diagnostics.map(d => mapDiagnostic(d, result.documents)));
32
+ const modules = [...result.documents].map(([virtual, doc]) => ({
33
+ sourcePath: doc.filePath,
34
+ javascript: files.get(virtual.replace(/\.ts$/, '.js')) ?? '',
35
+ declaration: files.get(virtual.replace(/\.ts$/, '.d.ts')) ?? ''
36
+ }));
37
+ const root = modules.find(module => module.sourcePath === path.resolve(filePath));
38
+ return { javascript: root.javascript, declaration: root.declaration, modules };
39
+ }
@@ -1,7 +1,7 @@
1
1
  import ts from 'typescript';
2
2
 
3
3
  const literals = new Map([['참', 'true'], ['거짓', 'false'], ['비어있음', 'null'], ['정의안됨', 'undefined'], ['자신', 'this'], ['부모', 'super']]);
4
- const types = new Map([['숫자', 'number'], ['문자열', 'string'], ['논리', 'boolean'], ['불리언', 'boolean'], ['아무거나', 'any'], ['모름', 'unknown'], ['무효', 'void']]);
4
+ const types = new Map([['숫자', 'number'], ['문자열', 'string'], ['논리', 'boolean'], ['불리언', 'boolean'], ['아무거나', 'any'], ['모름', 'unknown'], ['무효', 'void'], ['목록', 'Array'], ['집합', 'Set'], ['사전', 'Map']]);
5
5
 
6
6
  function identifiers(source, replacements) {
7
7
  const file = ts.createSourceFile('expression.ts', source, ts.ScriptTarget.Latest, true);
@@ -21,6 +21,10 @@ function identifiers(source, replacements) {
21
21
  }
22
22
 
23
23
  export function expression(source) {
24
+ let phrase;
25
+ if ((phrase = source.trim().match(/^(.+?)(?:을|를) (오름차순|내림차순)으로 정렬한 값$/u))) return phrase[2] + '(' + expression(phrase[1]) + ')';
26
+ if ((phrase = source.trim().match(/^(.+?)(?:을|를) (얕게|깊게) 복사한 값$/u))) return (phrase[2] === '얕게' ? '얕게복사' : '깊게복사') + '(' + expression(phrase[1]) + ')';
27
+ if ((phrase = source.trim().match(/^(.+?)에서 중복을 제거한 값$/u))) return '중복제거(' + expression(phrase[1]) + ')';
24
28
  source = source.replace(/^새\s+/, 'new ').replace(/^기다리기\s+/, 'await ');
25
29
  if (/[참거비정자부]/.test(source)) {
26
30
  const prefix = 'const __expression = (';
@@ -0,0 +1,2 @@
1
+ /** Preserve expression/literal contents while normalizing indentation and whitespace. */
2
+ export function formatSource(source: string, options?: { tabSize?: number }): string;
@@ -0,0 +1,29 @@
1
+ import { compileSource } from './transformer.js';
2
+
3
+ /** Normalize indentation and whitespace without rewriting expressions or literals. */
4
+ export function formatSource(source, { tabSize = 4 } = {}) {
5
+ if (!Number.isInteger(tabSize) || tabSize < 1 || tabSize > 8) throw new RangeError('들여쓰기 폭은 1~8 사이의 정수여야 합니다.');
6
+ const eol = source.includes('\r\n') ? '\r\n' : '\n';
7
+ const expanded = source.replace(/^[ \t]*\t[ \t]*/gm, prefix => {
8
+ let width = 0;
9
+ for (const c of prefix) width += c === '\t' ? tabSize - width % tabSize : 1;
10
+ return ' '.repeat(width);
11
+ });
12
+ compileSource(expanded); // Refuse to guess the intended structure of invalid code.
13
+ const levels = [0];
14
+ const output = [];
15
+ for (const raw of expanded.split(/\r?\n/)) {
16
+ const text = raw.trim();
17
+ if (!text) { if (output.length && output.at(-1) !== '') output.push(''); continue; }
18
+ const indent = raw.length - raw.trimStart().length;
19
+ if (text.startsWith('#')) {
20
+ const depth = levels.filter(level => level < indent).length;
21
+ output.push(' '.repeat(depth * tabSize) + text); continue;
22
+ }
23
+ while (levels.at(-1) > indent) levels.pop();
24
+ if (indent > levels.at(-1)) levels.push(indent);
25
+ output.push(' '.repeat((levels.length - 1) * tabSize) + raw.trimStart().trimEnd());
26
+ }
27
+ while (output.at(-1) === '') output.pop();
28
+ return output.length ? output.join(eol) + eol : '';
29
+ }
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export function compileFile(inputPath: string, outputPath: string): Promise<stri
5
5
  export function compileFileToDefaultDestination(inputPath: string): Promise<string>;
6
6
  export interface LintResult {
7
7
  diagnostics: readonly ts.Diagnostic[];
8
+ sourceDiagnostics: readonly SourceDiagnostic[];
8
9
  formattedDiagnostics: string;
9
10
  hasErrors: boolean;
10
11
  }
@@ -13,3 +14,11 @@ export function lintFile(inputPath: string, compilerOptions?: ts.CompilerOptions
13
14
  export interface BuildResult { javascript: string; declaration: string; }
14
15
  export function buildSource(source: string, filePath?: string): BuildResult;
15
16
  export function buildFile(inputPath: string, outputDirectory?: string): Promise<BuildResult & { javascriptPath: string; declarationPath: string }>;
17
+
18
+ export interface SourceDiagnostic { filePath: string; line: number; column: number; length: number; code: number | string; message: string; severity: 'error' | 'warning'; }
19
+ export interface CheckSettings { files?: ReadonlyMap<string, string>; allFiles?: boolean; libDirectory?: string; compilerOptions?: ts.CompilerOptions; }
20
+ export function checkSource(source: string, filePath?: string, settings?: CheckSettings): { diagnostics: SourceDiagnostic[]; hasErrors: boolean };
21
+ export function formatSourceDiagnostics(diagnostics: readonly SourceDiagnostic[]): string;
22
+ export class HangulScriptError extends Error { diagnostics: SourceDiagnostic[]; constructor(diagnostics: SourceDiagnostic[]); }
23
+
24
+ export { formatSource } from './formatter.js';