@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/dist/index.js CHANGED
@@ -1,102 +1,11 @@
1
1
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { createRequire } from 'node:module';
4
3
  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
- }
4
+ import { analyzeSource } from './checker.js';
5
+ import { formatSourceDiagnostics } from './diagnostics.js';
6
+ import { emitSource } from './emitter.js';
7
+ export { checkSource } from './checker.js';
8
+ export { HangulScriptError, formatSourceDiagnostics } from './diagnostics.js';
100
9
 
101
10
  export function compileString(source) {
102
11
  return compileSource(source);
@@ -118,16 +27,13 @@ export async function compileFileToDefaultDestination(inputPath) {
118
27
  }
119
28
 
120
29
  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);
30
+ compileSource(source); // Keep syntax errors explicit for callers of the legacy lint API.
31
+ const result = analyzeSource(source, filePath, { compilerOptions });
127
32
  return {
128
- diagnostics,
129
- formattedDiagnostics: formatDiagnostics(ts, diagnostics),
130
- hasErrors: diagnostics.some((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error)
33
+ diagnostics: result.nativeDiagnostics,
34
+ sourceDiagnostics: result.diagnostics,
35
+ formattedDiagnostics: formatSourceDiagnostics(result.diagnostics),
36
+ hasErrors: result.hasErrors
131
37
  };
132
38
  }
133
39
 
@@ -140,42 +46,30 @@ export { compileSource };
140
46
 
141
47
  /** Compile through TypeScript once, emitting executable JS and declarations in memory. */
142
48
  export function buildSource(source, filePath = 'input.hgs') {
143
- const ts = loadTypeScriptModule();
144
- const virtualPath = path.resolve(filePath).replace(/\.hgs$/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
- };
49
+ const { javascript, declaration } = emitSource(source, filePath);
50
+ return { javascript, declaration };
169
51
  }
170
52
 
171
53
  export async function buildFile(inputPath, outputDirectory = path.dirname(inputPath)) {
172
54
  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 };
55
+ const { javascript, declaration, modules } = emitSource(source, inputPath);
56
+ const rootDirectory = path.dirname(path.resolve(inputPath));
57
+ const destinations = modules.map(module => {
58
+ const relative = path.relative(rootDirectory, module.sourcePath);
59
+ if (relative.startsWith('..' + path.sep) || path.isAbsolute(relative)) {
60
+ throw new Error('빌드 입력 파일의 폴더 밖에 있는 .hgs 모듈은 지원하지 않습니다. 공통 상위 폴더에 진입 파일을 두세요.');
61
+ }
62
+ const base = path.join(outputDirectory, relative.replace(/\.hgs$/, ''));
63
+ return { ...module, javascriptPath: base + '.js', declarationPath: base + '.d.ts' };
64
+ });
65
+ // Validate the complete graph before writing any output.
66
+ for (const module of destinations) {
67
+ await mkdir(path.dirname(module.javascriptPath), { recursive: true });
68
+ await writeFile(module.javascriptPath, module.javascript, 'utf8');
69
+ await writeFile(module.declarationPath, module.declaration, 'utf8');
70
+ }
71
+ const root = destinations.find(module => module.sourcePath === path.resolve(inputPath));
72
+ return { javascript, declaration, javascriptPath: root.javascriptPath, declarationPath: root.declarationPath };
181
73
  }
74
+
75
+ export { formatSource } from './formatter.js';
package/dist/loader.js ADDED
@@ -0,0 +1,16 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { fileURLToPath } from 'node:url';
3
+ import ts from 'typescript';
4
+ import { analyzeSource } from './checker.js';
5
+ import { HangulScriptError } from './diagnostics.js';
6
+ export async function load(url, context, nextLoad) {
7
+ if (url.startsWith('file:') && new URL(url).pathname.endsWith('.hgs')) {
8
+ const filePath=fileURLToPath(url);
9
+ const source=await readFile(filePath,'utf8');
10
+ const result=analyzeSource(source,filePath);
11
+ if(result.hasErrors) throw new HangulScriptError(result.diagnostics);
12
+ const compiled=ts.transpileModule(result.compiled,{compilerOptions:{module:ts.ModuleKind.ESNext,target:ts.ScriptTarget.ES2022}}).outputText;
13
+ return {format:'module',source:compiled,shortCircuit:true};
14
+ }
15
+ return nextLoad(url,context);
16
+ }
@@ -0,0 +1,2 @@
1
+ import { register } from 'node:module';
2
+ register('./loader.js',import.meta.url);
package/dist/runtime.js CHANGED
@@ -1,10 +1,11 @@
1
+ import { collectionsRuntime } from './collections.js';
1
2
  import { uiRuntime } from './ui.js';
2
3
  export const sentenceRuntime = `${uiRuntime}
3
4
 
4
5
  function 출력(...값: unknown[]): void { console.log(...값); }
5
- function 길이(값: string | readonly unknown[]): number { return 값.length; }
6
+ function 길이(값: string | readonly unknown[] | ReadonlySet<unknown> | ReadonlyMap<unknown, unknown>): number { return "size" in Object(값) ? (값 as ReadonlySet<unknown>).size : (값 as string | readonly unknown[]).length; }
6
7
  function 추가<T>(목록: T[], 값: T): void { 목록.push(값); }
7
- function 포함<T>(목록: readonly T[], 값: T): boolean { return 목록.includes(값); }
8
+ function 포함<T>(목록: readonly T[] | ReadonlySet<T>, 값: T): boolean { return 목록 instanceof Set ? 목록.has(값) : (목록 as readonly T[]).includes(값); }
8
9
  function 나누기(글: string, 구분: string): string[] { return 글.split(구분); }
9
10
  function 잇기(목록: readonly unknown[], 구분 = ""): string { return 목록.join(구분); }
10
11
  function 합계(값: readonly number[]): number { return 값.reduce((합, 항목) => 합 + 항목, 0); }
@@ -33,7 +34,17 @@ function 최댓값(목록: readonly number[]): number {
33
34
  let 결과 = 목록[0]; for (const 항목 of 목록) if (항목 > 결과) 결과 = 항목;
34
35
  return 결과;
35
36
  }
36
- function 정렬(값: readonly number[]): number[] { return [...값].sort((가, 나) => 가 - 나); }
37
+ function 정렬비교(가: number | string, 나: number | string): number {
38
+ if (typeof 가 !== typeof 나) throw new Error("같은 종류의 숫자 또는 문자열을 정렬하세요.");
39
+ if (typeof 가 === "number" && typeof 나 === "number") { if (!Number.isFinite(가) || !Number.isFinite(나)) throw new Error("정렬할 숫자는 유한해야 합니다."); return 가 - 나; }
40
+ return (가 as string).localeCompare(나 as string, "ko", { numeric: true });
41
+ }
42
+ function 정렬<T extends number | string>(값: readonly T[]): T[];
43
+ function 정렬<T>(값: readonly T[], 비교: (가: T, 나: T) => number): T[];
44
+ function 정렬<T>(값: readonly T[], 비교?: (가: T, 나: T) => number): T[] {
45
+ if (!비교 && 값.some(항목 => !["number", "string"].includes(typeof 항목) || typeof 항목 === "number" && !Number.isFinite(항목))) throw new Error("정렬할 값은 유한한 숫자 또는 문자열이어야 합니다.");
46
+ return [...값].sort(비교 ?? ((가, 나) => 정렬비교(가 as number | string, 나 as number | string)));
47
+ }
37
48
  function 범위(시작: number, 끝?: number, 간격 = 1): number[] {
38
49
  if (끝 === undefined) { 끝 = 시작; 시작 = 0; }
39
50
  if (!Number.isFinite(시작) || !Number.isFinite(끝) || !Number.isFinite(간격) || 간격 === 0) throw new Error("범위에는 유한한 수와 0이 아닌 간격이 필요합니다.");
@@ -41,5 +52,5 @@ function 범위(시작: number, 끝?: number, 간격 = 1): number[] {
41
52
  if (개수 > 100000) throw new Error("범위는 최대 100000개까지 만들 수 있습니다.");
42
53
  return Array.from({ length: 개수 }, (_, 순서) => 시작 + 순서 * 간격);
43
54
  }
55
+ ${collectionsRuntime}
44
56
  `;
45
-
package/dist/sentences.js CHANGED
@@ -6,8 +6,12 @@ const identifier = /^[\p{L}_$][\p{L}\p{N}_$]*$/u;
6
6
  const functionName = /^[\p{L}_$][\p{L}\p{N}_$]*(?:<.+>)?$/u;
7
7
  const obsolete = /^(?:상수|변수|함수|반환|클래스|가져오기|내보내기|반복|비동기함수|스위치|선택|케이스|경우|인터페이스|열거형|시도|잡기|마침내|던지기|선언|추상)(?:\s|[({*])|^콘솔\.출력/u;
8
8
 
9
- export function compileSentences(source, { includeRuntime = true } = {}) {
10
- const output = includeRuntime ? [sentenceRuntime] : [];
9
+ export function compileSentences(source, { includeRuntime = true, withMapping = false } = {}) {
10
+ const output = [];
11
+ const lineMap = [];
12
+ let currentLine = 0;
13
+ const push = code => { output.push(code); lineMap.push(...code.split('\n').map(() => currentLine)); };
14
+ if (includeRuntime) push(sentenceRuntime);
11
15
  const frames = [{ indent: 0, kind: 'root' }];
12
16
  let pending = null;
13
17
  const lines = source.split(/\r?\n/);
@@ -19,6 +23,7 @@ export function compileSentences(source, { includeRuntime = true } = {}) {
19
23
  return false;
20
24
  };
21
25
  for (let index = 0; index < lines.length; index++) {
26
+ currentLine = index + 1;
22
27
  const raw = lines[index];
23
28
  const text = raw.trim();
24
29
  if (!text || text.startsWith('#')) continue;
@@ -33,12 +38,12 @@ export function compileSentences(source, { includeRuntime = true } = {}) {
33
38
  let closed;
34
39
  while (indent < frames.at(-1).indent) {
35
40
  closed = frames.pop();
36
- output.push(closed.kind === 'case' ? 'break; }' : '}');
41
+ push(closed.kind === 'case' ? 'break; }' : '}');
37
42
  }
38
43
  if (indent !== frames.at(-1).indent) fail('이전 블록과 들여쓰기를 맞춰 주세요.');
39
44
  const parent = frames.at(-1).kind;
40
- const open = (code, kind) => { output.push(code + ' {'); pending = { kind }; };
41
- const emit = code => output.push(code);
45
+ const open = (code, kind) => { push(code + ' {'); pending = { kind }; };
46
+ const emit = code => push(code);
42
47
  const exp = value => expression(value);
43
48
  const top = () => { if (parent !== 'root') fail('가져오기·공개·타입 선언은 파일의 최상위에 작성하세요.'); };
44
49
  let m;
@@ -132,7 +137,8 @@ export function compileSentences(source, { includeRuntime = true } = {}) {
132
137
  while (frames.length > 1) {
133
138
  const frame = frames.pop();
134
139
  if (frame.kind === 'do') throw new SyntaxError('먼저 실행한다 블록 뒤에 반복 조건이 필요합니다.');
135
- output.push(frame.kind === 'case' ? 'break; }' : '}');
140
+ push(frame.kind === 'case' ? 'break; }' : '}');
136
141
  }
137
- return output.join('\n');
142
+ const code = output.join('\n');
143
+ return withMapping ? { code, lineMap } : code;
138
144
  }
@@ -1,2 +1,5 @@
1
1
  /** Convert keyword-style or sentence-style .hgs source into TypeScript. */
2
2
  export function compileSource(source: string): string;
3
+
4
+ export interface MappedSource { code: string; lineMap: number[]; }
5
+ export function compileMappedSource(source: string): MappedSource;
@@ -5,3 +5,9 @@ export function compileSource(source) {
5
5
  if (typeof source !== 'string') throw new TypeError('한글스크립트 소스는 문자열이어야 합니다.');
6
6
  return compileSentences(source);
7
7
  }
8
+
9
+ /** Generated TypeScript lines map to 1-based HGS lines; runtime lines map to zero. */
10
+ export function compileMappedSource(source) {
11
+ if (typeof source !== 'string') throw new TypeError('한글스크립트 소스는 문자열이어야 합니다.');
12
+ return compileSentences(source, { withMapping: true });
13
+ }
package/dist/vite.js CHANGED
@@ -1,21 +1,28 @@
1
1
  import ts from 'typescript';
2
- import { compileSource } from './transformer.js';
2
+ import { analyzeSource } from './checker.js';
3
+ import { HangulScriptError } from './diagnostics.js';
3
4
 
4
- /** Build-time .hgs imports for Vite projects, including React and Vue. */
5
+ /** Semantic checking is mandatory in both Vite development and production builds. */
5
6
  export default function hangulscript() {
7
+ const files = new Map();
6
8
  return {
7
9
  name: 'hangulscript',
8
10
  enforce: 'pre',
11
+ buildStart() { files.clear(); },
12
+ watchChange(id, change) { if (change.event === 'delete') files.delete(id.split('?')[0]); },
9
13
  transform(source, id) {
10
- if (!/\.hgs$/.test(id.split('?')[0]) || /[?&](?:raw|url)(?:[=&]|$)/.test(id)) return null;
11
- const compiled = compileSource(source);
12
- const result = ts.transpileModule(compiled, {
13
- fileName: id.replace(/\.hgs(?:\?.*)?$/, '.ts'),
14
- compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext, sourceMap: false },
15
- reportDiagnostics: true
16
- });
17
- const errors = (result.diagnostics ?? []).filter(d => d.category === ts.DiagnosticCategory.Error);
18
- if (errors.length) throw new Error(errors.map(d => ts.flattenDiagnosticMessageText(d.messageText, '\n')).join('\n'));
14
+ const filePath = id.split('?')[0];
15
+ if (!/\.hgs$/.test(filePath) || /[?&](?:raw|url)(?:[=&]|$)/.test(id)) return null;
16
+ files.set(filePath, source);
17
+ const checked = analyzeSource(source, filePath, { files, allFiles: true });
18
+ if (typeof this?.addWatchFile === 'function') for (const doc of checked.documents.values()) this.addWatchFile(doc.filePath);
19
+ if (checked.hasErrors) {
20
+ const error = new HangulScriptError(checked.diagnostics);
21
+ const first = checked.diagnostics.find(d => d.severity === 'error');
22
+ if (typeof this?.error === 'function') return this.error({ message: error.message, id: first.filePath, loc: { file: first.filePath, line: first.line, column: first.column - 1 } });
23
+ throw error;
24
+ }
25
+ const result = ts.transpileModule(checked.compiled, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext } });
19
26
  return { code: result.outputText, map: null };
20
27
  }
21
28
  };
@@ -0,0 +1,45 @@
1
+ # 한글스크립트의 큰 그림
2
+
3
+ 한글 문장으로 프로그램의 의도를 분명히 쓰고, JavaScript 생태계에서 실행하는 언어를 만듭니다. 현재의 중심은 **문장형 소스 → 타입 검사 → JavaScript + 타입 선언**입니다. 이 문서는 완료 보고가 아니라 현재 지원 범위와 이후 개발의 기준입니다. 기능 추가만으로 1.0을 선언하지 않습니다.
4
+
5
+ <div class="architecture" role="group" aria-label="한글스크립트 컴파일과 실행 구조"><div><b>01</b><strong>한글 문장 .hgs</strong><span>이름 · 조건 · 행동 · 결과</span></div><div><b>02</b><strong>문법과 들여쓰기</strong><span>정해진 문장 규칙 해석</span></div><div><b>03</b><strong>타입과 오류 검사</strong><span>TypeScript 검사기 활용</span></div><div><b>04</b><strong>JS + .d.ts</strong><span>실행 모듈과 타입 계약</span></div><div><b>05</b><strong>Node · 웹</strong><span>Vite · React · Vue 연결</span></div><div><b>06</b><strong>VS Code · 문서</strong><span>작성 · 이해 · 수정 지원</span></div></div>
6
+
7
+ ## 지금의 기반
8
+
9
+ | 계층 | 현재 제공 | 부분 지원·한계 | 다음 개발 |
10
+ | --- | --- | --- | --- |
11
+ | 문법 | 문장형 선언·함수·분기·반복·모듈·예외·비동기·클래스·타입 | 정해진 문장만 해석. 전체 JS/TS 소스 호환 아님 | 전용 AST, 문법 오류 복구, 더 정밀한 토큰 위치 |
12
+ | 타입 | TypeScript strict 기본 검사·제네릭·타입 선언 출력 | any·빈 목록의 검사 제한, 변환 토큰 열은 근사할 수 있음 | 더 나은 기본 추론, 문장별 진단, 명시적인 안전성 규칙 |
13
+ | 표준 기능 | 61개 한글 함수: 목록·집합·사전·복사·정렬·숫자·문자열·검증·화면 | 모든 내장 JS API의 한글 별칭은 아님. 일부 원본 변경 함수 존재 | 객체·문자열·날짜 확장, 표준 모듈 분리, 이름 충돌 완화 |
14
+ | 실행과 빌드 | Node 등록 로더·CLI·ESM·JS + .d.ts·Vite | 진입 폴더 밖 HGS 의존성 빌드와 자산 복사 미지원 | 프로젝트 빌드·증분 컴파일·watch·정밀한 소스맵 |
15
+ | 편집기 | 포맷·진단·타입 정보·정의 이동·스코프 자동 완성·스니펫·ㅎ 아이콘 | 전체 속성 완성·자동 import·참조 찾기·이름 변경·디버거 미지원 | LSP로 기능 분리, 증분 분석, 안전한 리팩터링 |
16
+ | 신뢰성 | 문법·실행·타입·문서 예제·프레임워크·실제 편집기 테스트 | 테스트 통과는 모든 입력의 올바름을 뜻하지 않음 | 명세 기반 적합성 테스트·퍼징·회귀·실측 성능 기준 |
17
+ | 문서와 배포 | 버전별 변경 내역·함수 계약·예제·npm·Marketplace·CI | 0.x에서 문법과 API가 바뀔 수 있음 | 마이그레이션 도구·릴리스 규칙·1.0 호환성 약속 |
18
+
19
+ ## 순서대로 만들기
20
+
21
+ ### 1. 지금 있는 언어를 믿고 사용할 수 있게
22
+
23
+ 함수마다 입력·반환·원본 변경·실패 조건을 일관되게 문서화하고 예제를 실행해 검사합니다. 잘못된 입력과 오류 위치를 회귀 테스트로 남깁니다. 문장형의 모든 조합을 지원한다고 가정하지 않고 지원·부분 지원·미구현을 공개합니다. 현재 0.8 작업은 문서·자료구조·아이콘·배포에 집중합니다.
24
+
25
+ ### 2. 언어 내부를 확장하기 좋은 구조로
26
+
27
+ 현재 문장·들여쓰기는 규칙 기반 파서로 처리하고 표현식에는 TypeScript AST를 활용합니다. 문장 전체를 나타내는 전용 AST와 범위 정보를 도입한 뒤 문법 검사·타입 연결·출력·포맷을 분리합니다. 이를 기반으로 오류를 여러 개 복구해서 보여주고, 정확한 소스맵과 프로젝트 단위 증분 분석을 만듭니다.
28
+
29
+ ### 3. 실용적인 개발 환경을 완성하기
30
+
31
+ 편집기의 전체 속성 자동 완성·자동 import·참조와 이름 변경을 LSP에 연결합니다. 프로젝트 설정·증분 빌드·테스트 명령을 제공하고 React/Vue에서 안정적으로 쓰는 예제를 유지합니다. 파일·네트워크·날짜·객체 등은 Node와 브라우저의 권한 차이를 구분한 선택 모듈로 설계합니다.
32
+
33
+ ### 4. 문장이 탄탄할수록 검사할 수 있는 내용 늘리기
34
+
35
+ 이름과 타입은 등장인물과 사물을, 사전·사후 조건은 세계의 규칙을, 함수와 모듈은 사건과 장면을 표현합니다. 현재는 `확인한다`로 실행 중 조건을 검사할 수 있습니다. 다음으로 입력 조건·결과 조건·예제 테스트를 더 읽기 쉬운 문장으로 선언하는 방식을 설계할 수 있습니다. 효과가 있는 문법인지 작은 실험과 실제 예제로 먼저 검증합니다.
36
+
37
+ ### 5. 1.0을 위한 기준 정하기
38
+
39
+ 문법 명세와 표준 API를 고정하고 호환성·폐기 정책을 확정합니다. 핵심 오류 사례, 적합성 테스트, 편집기 기능, 선언 출력, Node·브라우저·프레임워크 연동을 릴리스 기준으로 정합니다. 성능은 환경과 입력 크기를 함께 공개하고 회귀를 추적합니다. 완료되지 않은 항목이 있으면 이름을 붙여 공개합니다.
40
+
41
+ ## AI와 별도 실행 엔진의 위치
42
+
43
+ 핵심 문법·타입·컴파일·조건 검사에는 AI API가 필요하지 않습니다. 자유로운 산문에서 의도를 추출하는 기능은 선택적인 작성 보조로 실험할 수 있고 로컬 모델도 가능합니다. 생성한 결과도 같은 검사·테스트를 통과해야 합니다. 현재 자유 산문 실행, AI 기반 문장 해석, 자동 정형 증명은 제공하지 않습니다.
44
+
45
+ JS + .d.ts 경로는 기존 생태계와 타입을 재사용하는 현재의 실행 계약입니다. 독립 VM·바이트코드·네이티브 출력은 별도의 큰 프로젝트이므로 실제 요구와 측정 결과가 생기면 검토합니다. 정식 문서와 지원 도구를 갖추는 데 독립 실행 엔진이 필수인 것은 아닙니다.
@@ -0,0 +1,152 @@
1
+ # React·Vue에서 한글스크립트 사용하기
2
+
3
+ 이 가이드는 `@uiwwsw/hangulscript@0.8.0` 문법과 공통 Vite 플러그인을 사용합니다.
4
+ 화면과 상태는 React/Vue로 만들고 계산·조건·검증 함수를 `.hgs`에 작성합니다.
5
+ 데모의 `숫자입력`·`버튼`·`보여주기`는 데모 UI 실행 환경의 API이며 React/Vue 컴포넌트로 자동 변환되지 않습니다.
6
+
7
+ ## 완성된 예제를 먼저 실행하기
8
+
9
+ Node.js 24에서 검증했습니다. 예제의 Vite는 Node.js `^20.19.0 || >=22.12.0`을 요구합니다.
10
+
11
+ ```bash
12
+ git clone https://github.com/uiwwsw/hangulscript.git
13
+ cd hangulscript/examples/react-vite
14
+ npm ci
15
+ npm run dev
16
+ ```
17
+
18
+ Vue는 같은 저장소의 `examples/vue-vite`에서 `npm ci`와 `npm run dev`를 실행하세요.
19
+ 터미널에 표시된 localhost 주소를 열고 가격 10000, 할인율 20을 입력한 뒤 **계산하기**를 누르면 결제 금액 8,000원이 표시됩니다.
20
+ 개발 서버는 loopback에서 실행하며, 포트가 사용 중이면 터미널에 표시된 실제 주소를 사용하세요.
21
+ 배포할 정적 파일은 각 예제에서 `npm run build`로 만들고 `npm run preview`로 확인합니다.
22
+
23
+ - [React 전체 소스](../examples/react-vite)
24
+ - [Vue 전체 소스](../examples/vue-vite)
25
+
26
+ ## 기존 Vite 프로젝트에 설치하기
27
+
28
+ ```bash
29
+ npm install --save-dev @uiwwsw/hangulscript@^0.8.0
30
+ ```
31
+
32
+ React/Vue와 해당 Vite 플러그인은 기존 프로젝트에 설치돼 있다고 가정합니다.
33
+ 새 프로젝트는 위의 완성된 예제를 사용할 수 있습니다.
34
+
35
+ React의 `vite.config.js`:
36
+
37
+ ```js
38
+ import { defineConfig } from 'vite';
39
+ import react from '@vitejs/plugin-react';
40
+ import hangulscript from '@uiwwsw/hangulscript/vite';
41
+
42
+ export default defineConfig({ plugins: [hangulscript(), react()] });
43
+ ```
44
+
45
+ Vue의 `vite.config.js`:
46
+
47
+ ```js
48
+ import { defineConfig } from 'vite';
49
+ import vue from '@vitejs/plugin-vue';
50
+ import hangulscript from '@uiwwsw/hangulscript/vite';
51
+
52
+ export default defineConfig({ plugins: [hangulscript(), vue()] });
53
+ ```
54
+
55
+ ## 공통 로직 작성하기
56
+
57
+ 두 예제의 `src/discount.hgs`는 동일합니다. 함수와 조건을 한글 문장으로 작성하고 ESM `export`로 내보냅니다.
58
+
59
+ ```text
60
+ 결제금액(가격: 숫자, 할인율: 숫자)를 정의한다:
61
+ 만약 !Number.isFinite(가격) || !Number.isFinite(할인율) || 가격 < 0 || 할인율 < 0 || 할인율 > 100 이라면:
62
+ 돌려주기 "가격은 0 이상, 할인율은 0~100 사이로 입력하세요"
63
+ 돌려주기 가격 - 반올림(가격 * 할인율 / 100)
64
+
65
+ 결제금액을 공개한다
66
+ ```
67
+
68
+ ## React에서 호출하기
69
+
70
+ `src/main.jsx`에서 함수를 가져와 submit 이벤트에 연결합니다. 입력과 결과 상태는 React가 관리합니다.
71
+
72
+ ```jsx
73
+ import { useState } from 'react';
74
+ import { createRoot } from 'react-dom/client';
75
+ import { 결제금액 } from './discount.hgs';
76
+ import './style.css';
77
+
78
+ function App() {
79
+ const [가격, 가격변경] = useState('10000');
80
+ const [할인율, 할인율변경] = useState('20');
81
+ const [결과, 결과변경] = useState(null);
82
+ function 계산(event) {
83
+ event.preventDefault();
84
+ 결과변경(결제금액(Number(가격), Number(할인율)));
85
+ }
86
+ return <main>
87
+ <h1>React + 한글스크립트</h1>
88
+ <p>화면은 React, 계산과 검증은 discount.hgs에서 처리합니다.</p>
89
+ <form onSubmit={계산}>
90
+ <label>가격 (원)<input type="number" min="0" required value={가격} onChange={e => 가격변경(e.target.value)} /></label>
91
+ <label>할인율 (%)<input type="number" min="0" max="100" step="any" required value={할인율} onChange={e => 할인율변경(e.target.value)} /></label>
92
+ <button type="submit">계산하기</button>
93
+ </form>
94
+ <output aria-live="polite">{결과 === null ? '입력 후 계산하기를 눌러 주세요.' : typeof 결과 === 'number' ? `결제 금액: ${결과.toLocaleString('ko-KR')}원` : 결과}</output>
95
+ </main>;
96
+ }
97
+ createRoot(document.getElementById('app')).render(<App />);
98
+ ```
99
+
100
+ ## Vue에서 호출하기
101
+
102
+ `src/App.vue`에서 같은 함수를 가져와 submit 이벤트에 연결합니다. 반응형 상태는 Vue가 관리합니다.
103
+
104
+ ```vue
105
+ <script setup>
106
+ import { ref } from 'vue';
107
+ import { 결제금액 } from './discount.hgs';
108
+ const 가격 = ref(10000);
109
+ const 할인율 = ref(20);
110
+ const 결과 = ref(null);
111
+ function 계산() {
112
+ 결과.value = 결제금액(Number(가격.value), Number(할인율.value));
113
+ }
114
+ </script>
115
+
116
+ <template>
117
+ <main>
118
+ <h1>Vue + 한글스크립트</h1>
119
+ <p>화면은 Vue, 계산과 검증은 discount.hgs에서 처리합니다.</p>
120
+ <form @submit.prevent="계산">
121
+ <label>가격 (원)<input v-model="가격" type="number" min="0" required /></label>
122
+ <label>할인율 (%)<input v-model="할인율" type="number" min="0" max="100" step="any" required /></label>
123
+ <button type="submit">계산하기</button>
124
+ </form>
125
+ <output aria-live="polite">{{ 결과 === null ? '입력 후 계산하기를 눌러 주세요.' : typeof 결과 === 'number' ? `결제 금액: ${결과.toLocaleString('ko-KR')}원` : 결과 }}</output>
126
+ </main>
127
+ </template>
128
+ ```
129
+
130
+ ## TypeScript 프로젝트와 빌드 범위
131
+
132
+ | 방식 | 제공 범위 | 적합한 용도 |
133
+ | --- | --- | --- |
134
+ | Vite 플러그인으로 `.hgs` import | JavaScript 변환, Vite 개발 서버·빌드 연동 | React/Vue JavaScript 프로젝트 |
135
+ | `ksc build` 후 `.js` import | 타입 검사, JavaScript와 `.d.ts` 생성 | TypeScript 프로젝트·라이브러리 |
136
+ | 온라인 데모의 화면 함수 | 격리 실행, 입력창·버튼·결과 카드 | 설치 없는 도구 만들기·코드 공유 |
137
+
138
+ 0.7.0부터 Vite 플러그인은 구문과 타입을 검사하고 오류가 있으면 빌드를 중단합니다. 선언 생성·디버깅용 원본 `.hgs` 소스맵은 제공하지 않습니다.
139
+ TypeScript에서 `.hgs` 파일을 직접 가져오면 모듈 타입을 찾지 못할 수 있습니다.
140
+ 외부 TypeScript 소비자에게 선언을 제공하려면 다음처럼 빌드한 `.js`를 가져오세요. 인접한 `.d.ts`를 TypeScript가 읽습니다.
141
+
142
+ ```bash
143
+ npx ksc build src/discount.hgs
144
+ ```
145
+
146
+ ```ts
147
+ import { 결제금액 } from './discount.js';
148
+ const 결과 = 결제금액(10000, 20); // number | string
149
+ ```
150
+
151
+ 이 방식은 `.hgs`를 수정할 때 다시 빌드해야 합니다. 명시적인 타입과 추론 가능한 값은 검사하며, `any` 값에 대한 검사는 제한됩니다.
152
+ Next.js·Webpack처럼 Vite를 사용하지 않는 환경에서도 미리 만든 `.js`·`.d.ts`를 가져오는 방식을 사용할 수 있습니다. 전용 Next.js·Webpack 플러그인은 제공하지 않습니다.
@@ -0,0 +1,61 @@
1
+ # 목록·집합·사전 고르기
2
+
3
+ 배열은 순서를 가진 **목록**, Set은 중복 없는 **집합**, Map은 키와 값을 연결하는 **사전**으로 표현합니다. 값과 목적에 맞춰 자료구조를 고르면 코드의 뜻이 더 명확해집니다.
4
+
5
+ ```hgs
6
+ 점수들은 [75, 92, 88, 92]이다
7
+ 통과점수는 골라내기(점수들, 점수 => 점수 >= 80)이다
8
+ 서로다른점수는 집합(통과점수)이다
9
+ 출력(내림차순(목록으로(서로다른점수)))
10
+ ```
11
+
12
+ `배열`을 나타내는 타입은 `목록<숫자>` 또는 `숫자[]`입니다. 집합은 `집합<문자열>`, 사전은 `사전<문자열, 숫자>`로 적습니다. 함수의 매개변수에 타입을 쓰면 잘못된 값이 들어오는 호출을 빌드 단계에서 찾을 수 있습니다.
13
+
14
+ ```hgs
15
+ 재고: 사전<문자열, 숫자>는 사전()이다
16
+ 사전에담기(재고, "사과", 3)
17
+ 사전에담기(재고, "배", 5)
18
+ 출력(키목록(재고), 사전에서읽기(재고, "사과"))
19
+ ```
20
+
21
+ ## 원본과 복사본
22
+
23
+ `정렬`, `오름차순`, `내림차순`, `골라내기`, `각각바꾸기`, `뒤집기`, `잘라내기`는 원본 배열을 바꾸지 않습니다. `추가`, `집합에추가`, `집합에서삭제`, `사전에담기`, `사전에서삭제`는 원본을 바꿉니다. 함수별 참조 문서에서 확인하세요.
24
+
25
+ 얕은 복사는 바깥 그릇만 새로 만듭니다. 안쪽 객체는 공유합니다. 깊은 복사는 복제 가능한 안쪽 값까지 별도로 만듭니다.
26
+
27
+ ```hgs
28
+ 원본은 [{ 점수: 10 }]이다
29
+ 얕은복사본은 얕게복사(원본)이다
30
+ 깊은복사본은 깊게복사(원본)이다
31
+ 얕은복사본[0].점수 = 20
32
+ 출력(원본[0].점수, 깊은복사본[0].점수)
33
+ ```
34
+
35
+ 결과는 `20 10`입니다. 깊은 복사는 함수·DOM 노드 등 복제 불가능한 값에 오류를 냅니다. 객체를 중복 제거하거나 집합에 넣을 때에는 내용이 같아도 참조가 다르면 서로 다른 값입니다.
36
+
37
+ ## 정렬 기준 쓰기
38
+
39
+ ```hgs
40
+ 학생들은 [{ 이름: "하늘", 점수: 88 }, { 이름: "바다", 점수: 92 }]이다
41
+ 점수순은 기준정렬(학생들, 학생 => 학생.점수, "내림차순")이다
42
+ 출력(점수순)
43
+ ```
44
+
45
+ 숫자는 크기로, 문자열은 한국어 locale의 자연 정렬로 비교합니다. 같은 키의 항목은 원래 순서를 유지합니다. 더 복잡한 순서는 `정렬(목록, 비교함수)`로 표현하며 비교 함수는 음수·0·양수를 반환해야 합니다.
46
+
47
+ ## 비용을 알고 쓰기
48
+
49
+ 목록 변환·필터·복사는 항목 수에 비례한 시간과 새 배열 메모리가 필요합니다. 정렬은 일반적으로 O(n log n), 집합·사전 검색은 평균적으로 상수 시간에 가깝습니다. 깊은 복사는 중첩 데이터 전체의 시간·메모리를 사용합니다. `기준정렬`은 기준 함수를 항목마다 한 번만 호출합니다. 큰 반복에서 불필요하게 복사하지 말고, 2초 제한의 웹 데모와 일반 Node 실행을 구분하세요.
50
+
51
+ ## 문장처럼 적기
52
+
53
+ 정렬·복사·중복 제거는 값 자리에서 다음 문장으로도 적을 수 있습니다. 함수 호출과 같은 결과이며, 정해진 표현만 지원합니다.
54
+
55
+ ```hgs
56
+ 점수들은 [88, 92, 88]이다
57
+ 점수순은 점수들을 내림차순으로 정렬한 값이다
58
+ 서로다른값은 점수들에서 중복을 제거한 값이다
59
+ 독립본은 점수들을 깊게 복사한 값이다
60
+ 출력(점수순, 서로다른값, 독립본)
61
+ ```