@uiwwsw/hangulscript 0.5.0 → 0.7.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 +28 -0
- package/LANGUAGE.md +148 -83
- package/README.md +51 -50
- package/dist/checker.js +80 -0
- package/dist/cli.js +7 -11
- package/dist/diagnostics.js +61 -0
- package/dist/editor.d.ts +5 -0
- package/dist/editor.js +50 -0
- package/dist/emitter.js +39 -0
- package/dist/expressions.js +59 -0
- package/dist/formatter.d.ts +2 -0
- package/dist/formatter.js +29 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +33 -139
- package/dist/loader.js +16 -0
- package/dist/register.js +2 -0
- package/dist/runtime.js +44 -0
- package/dist/sentences.js +133 -92
- package/dist/transformer.d.ts +3 -0
- package/dist/transformer.js +9 -334
- package/dist/vite.js +18 -11
- package/docs/frameworks.md +152 -0
- package/docs/support.md +42 -0
- package/examples/calculator-ui.hgs +0 -1
- package/examples/calculator.hgs +0 -1
- package/examples/discount-ui.hgs +0 -1
- package/examples/discount.hgs +0 -1
- package/examples/hello.hgs +4 -19
- package/examples/math-library.hgs +1 -2
- package/examples/scores.hgs +0 -1
- package/examples/statistics-ui.hgs +0 -1
- package/examples/statistics.hgs +0 -1
- package/examples/text-ui.hgs +0 -1
- package/examples/text.hgs +0 -1
- package/package.json +21 -5
package/dist/sentences.js
CHANGED
|
@@ -1,103 +1,144 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
return source.split(/\r?\n/).some(line => /^\s*(?:문장으로 시작\s*$|[\p{L}_$][\p{L}\p{N}_$]*(?:은|는)\s+.+이다\s*$|각 .+에서:\s*$|[\p{L}_$][\p{L}\p{N}_$]*\(.*\)를 정의한다:\s*$)/u.test(line));
|
|
5
|
-
}
|
|
1
|
+
import { sentenceRuntime } from './runtime.js';
|
|
2
|
+
import { expression, typeExpression, signature, statement } from './expressions.js';
|
|
3
|
+
export { sentenceRuntime } from './runtime.js';
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
const identifier = /^[\p{L}_$][\p{L}\p{N}_$]*$/u;
|
|
6
|
+
const functionName = /^[\p{L}_$][\p{L}\p{N}_$]*(?:<.+>)?$/u;
|
|
7
|
+
const obsolete = /^(?:상수|변수|함수|반환|클래스|가져오기|내보내기|반복|비동기함수|스위치|선택|케이스|경우|인터페이스|열거형|시도|잡기|마침내|던지기|선언|추상)(?:\s|[({*])|^콘솔\.출력/u;
|
|
8
8
|
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (typeof 값 === "string" && 값.trim() === "") throw new Error("빈 문자열은 숫자로 바꿀 수 없습니다.");
|
|
18
|
-
const 결과 = Number(값);
|
|
19
|
-
if (!Number.isFinite(결과)) throw new Error("유한한 숫자를 입력하세요.");
|
|
20
|
-
return 결과;
|
|
21
|
-
}
|
|
22
|
-
function 다듬기(글: string): string { return 글.trim(); }
|
|
23
|
-
function 반올림(값: number, 자리 = 0): number {
|
|
24
|
-
if (!Number.isFinite(값) || !Number.isInteger(자리) || 자리 < -10 || 자리 > 10) throw new Error("유한한 수와 -10~10 사이의 정수 자리가 필요합니다.");
|
|
25
|
-
return Math.round(값 * 10 ** 자리) / 10 ** 자리;
|
|
26
|
-
}
|
|
27
|
-
function 평균(목록: readonly number[]): number {
|
|
28
|
-
if (!목록.length) throw new Error("빈 목록의 평균을 구할 수 없습니다.");
|
|
29
|
-
return 합계(목록) / 목록.length;
|
|
30
|
-
}
|
|
31
|
-
function 최솟값(목록: readonly number[]): number {
|
|
32
|
-
if (!목록.length) throw new Error("빈 목록의 최솟값을 구할 수 없습니다.");
|
|
33
|
-
let 결과 = 목록[0]; for (const 항목 of 목록) if (항목 < 결과) 결과 = 항목;
|
|
34
|
-
return 결과;
|
|
35
|
-
}
|
|
36
|
-
function 최댓값(목록: readonly number[]): number {
|
|
37
|
-
if (!목록.length) throw new Error("빈 목록의 최댓값을 구할 수 없습니다.");
|
|
38
|
-
let 결과 = 목록[0]; for (const 항목 of 목록) if (항목 > 결과) 결과 = 항목;
|
|
39
|
-
return 결과;
|
|
40
|
-
}
|
|
41
|
-
function 정렬(값: readonly number[]): number[] { return [...값].sort((가, 나) => 가 - 나); }
|
|
42
|
-
function 범위(시작: number, 끝?: number, 간격 = 1): number[] {
|
|
43
|
-
if (끝 === undefined) { 끝 = 시작; 시작 = 0; }
|
|
44
|
-
if (!Number.isFinite(시작) || !Number.isFinite(끝) || !Number.isFinite(간격) || 간격 === 0) throw new Error("범위에는 유한한 수와 0이 아닌 간격이 필요합니다.");
|
|
45
|
-
const 개수 = Math.max(0, Math.ceil((끝 - 시작) / 간격));
|
|
46
|
-
if (개수 > 100000) throw new Error("범위는 최대 100000개까지 만들 수 있습니다.");
|
|
47
|
-
return Array.from({ length: 개수 }, (_, 순서) => 시작 + 순서 * 간격);
|
|
48
|
-
}
|
|
49
|
-
`;
|
|
50
|
-
|
|
51
|
-
export function compileSentences(source, { includeRuntime = true } = {}) {
|
|
52
|
-
const output = includeRuntime ? [sentenceRuntime] : [];
|
|
53
|
-
const indents = [0];
|
|
54
|
-
let needsIndent = false;
|
|
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);
|
|
15
|
+
const frames = [{ indent: 0, kind: 'root' }];
|
|
16
|
+
let pending = null;
|
|
55
17
|
const lines = source.split(/\r?\n/);
|
|
18
|
+
const inside = kind => {
|
|
19
|
+
for (let i = frames.length - 1; i > 0; i--) {
|
|
20
|
+
if (kind.includes(frames[i].kind)) return true;
|
|
21
|
+
if (['function', 'method', 'generator', 'async'].includes(frames[i].kind)) return false;
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
};
|
|
56
25
|
for (let index = 0; index < lines.length; index++) {
|
|
26
|
+
currentLine = index + 1;
|
|
57
27
|
const raw = lines[index];
|
|
58
28
|
const text = raw.trim();
|
|
59
|
-
if (!text || text
|
|
29
|
+
if (!text || text.startsWith('#')) continue;
|
|
60
30
|
const fail = message => { throw new SyntaxError(`${index + 1}행: ${message}`); };
|
|
31
|
+
if (/^문장으로 시작[.]?$/.test(text)) fail('시작 표기는 사용하지 않습니다. 첫 문장부터 작성하세요.');
|
|
61
32
|
if (/^\s*\t/.test(raw)) fail('들여쓰기는 탭 대신 공백을 사용하세요.');
|
|
62
33
|
const indent = raw.length - raw.trimStart().length;
|
|
63
|
-
if (
|
|
64
|
-
if (indent <=
|
|
65
|
-
|
|
66
|
-
} else if (indent >
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
34
|
+
if (pending) {
|
|
35
|
+
if (indent <= frames.at(-1).indent) fail('블록 안의 문장을 들여써 주세요.');
|
|
36
|
+
frames.push({ ...pending, indent }); pending = null;
|
|
37
|
+
} else if (indent > frames.at(-1).indent) fail('예상하지 못한 들여쓰기입니다.');
|
|
38
|
+
let closed;
|
|
39
|
+
while (indent < frames.at(-1).indent) {
|
|
40
|
+
closed = frames.pop();
|
|
41
|
+
push(closed.kind === 'case' ? 'break; }' : '}');
|
|
42
|
+
}
|
|
43
|
+
if (indent !== frames.at(-1).indent) fail('이전 블록과 들여쓰기를 맞춰 주세요.');
|
|
44
|
+
const parent = frames.at(-1).kind;
|
|
45
|
+
const open = (code, kind) => { push(code + ' {'); pending = { kind }; };
|
|
46
|
+
const emit = code => push(code);
|
|
47
|
+
const exp = value => expression(value);
|
|
48
|
+
const top = () => { if (parent !== 'root') fail('가져오기·공개·타입 선언은 파일의 최상위에 작성하세요.'); };
|
|
49
|
+
let m;
|
|
50
|
+
try {
|
|
51
|
+
if (closed?.kind === 'do' && !/^.+인 동안 반복한다$/.test(text)) fail('먼저 실행한다 블록 뒤에 「조건인 동안 반복한다」가 필요합니다.');
|
|
52
|
+
if (obsolete.test(text)) fail('키워드형 문법은 제거됐습니다. 문장형으로 작성하세요.');
|
|
53
|
+
if ((m = text.match(/^(비동기로 |차례로 )?(.+?)\((.*)\)를 정의한다:$/u))) {
|
|
54
|
+
if (!functionName.test(m[2])) fail('함수 이름을 확인하세요.');
|
|
55
|
+
const method = parent === 'class';
|
|
56
|
+
const kind = m[1] === '비동기로 ' ? 'async' : m[1] === '차례로 ' ? 'generator' : method ? 'method' : 'function';
|
|
57
|
+
const prefix = (kind === 'async' ? 'async ' : '') + (method ? (kind === 'generator' ? '*' : '') : (kind === 'generator' ? 'function* ' : 'function '));
|
|
58
|
+
open(`${prefix}${m[2]}(${signature('(' + m[3] + ')', m[2])})`, kind);
|
|
59
|
+
} else if ((m = text.match(/^생성할 때\((.*)\):$/u))) {
|
|
60
|
+
if (parent !== 'class') fail('생성할 때는 클래스 안에서 사용하세요.');
|
|
61
|
+
open(`constructor(${signature('(' + m[1] + ')')})`, 'method');
|
|
62
|
+
} else if ((m = text.match(/^(.+?)을 읽으면:$/u))) {
|
|
63
|
+
if (parent !== 'class' || !identifier.test(m[1])) fail('읽기 속성은 클래스 안에 작성하세요.');
|
|
64
|
+
open(`get ${m[1]}()`, 'method');
|
|
65
|
+
} else if ((m = text.match(/^(.+?)을 바꾸면\((.*)\):$/u))) {
|
|
66
|
+
if (parent !== 'class' || !identifier.test(m[1])) fail('쓰기 속성은 클래스 안에 작성하세요.');
|
|
67
|
+
open(`set ${m[1]}(${signature('(' + m[2] + ')')})`, 'method');
|
|
68
|
+
} else if ((m = text.match(/^(?:(.+?)을 이어받아 )?(.+?) 클래스를 정의한다:$/u))) {
|
|
69
|
+
if (!functionName.test(m[2])) fail('클래스 이름을 확인하세요.');
|
|
70
|
+
open(`class ${m[2]}${m[1] ? ' extends ' + m[1] : ''}`, 'class');
|
|
71
|
+
} else if ((m = text.match(/^(.+?) 자료형을 정의한다:$/u))) {
|
|
72
|
+
top(); if (!functionName.test(m[1])) fail('자료형 이름을 확인하세요.'); open(`interface ${m[1]}`, 'interface');
|
|
73
|
+
} else if ((m = text.match(/^(.+?) 타입은 (.+)이다$/u))) {
|
|
74
|
+
top(); if (!functionName.test(m[1])) fail('타입 이름을 확인하세요.'); emit(`type ${m[1]} = ${typeExpression(m[2])};`);
|
|
75
|
+
} else if ((m = text.match(/^(.+?) 열거값을 정의한다:$/u))) {
|
|
76
|
+
top(); if (!identifier.test(m[1])) fail('열거값 이름을 확인하세요.'); open(`enum ${m[1]}`, 'enum');
|
|
77
|
+
} else if (parent === 'enum') {
|
|
78
|
+
if (!identifier.test(text)) fail('열거값 항목에는 이름 하나를 작성하세요.'); emit(text + ',');
|
|
79
|
+
} else if ((m = text.match(/^(.+)에서 (.+?)(?:을|를) 가져온다$/u))) {
|
|
80
|
+
top(); if (!/^(?:".*"|'.*')$/.test(m[1])) fail('모듈 경로를 따옴표로 감싸세요.');
|
|
81
|
+
const binding = m[2].replace(/^모두를 (.+)로$/, '* as $1'); emit(`import ${binding} from ${m[1]};`);
|
|
82
|
+
} else if ((m = text.match(/^(.+?)(?:을|를) 기본으로 공개한다$/u))) {
|
|
83
|
+
top(); emit(`export default ${exp(m[1])};`);
|
|
84
|
+
} else if ((m = text.match(/^(.+?)(?:을|를) 공개한다$/u))) {
|
|
85
|
+
top(); emit(`export ${m[1].startsWith('{') ? m[1] : '{ ' + m[1] + ' }'};`);
|
|
86
|
+
} else if ((m = text.match(/^("(?:[^"\\]|\\.)*") 버튼을 누르면:$/u))) {
|
|
87
|
+
open(`if (버튼(${m[1]}))`, 'if');
|
|
88
|
+
} else if ((m = text.match(/^각 (.+?)(?:을|를) (.+)에서:$/u))) {
|
|
89
|
+
if (!identifier.test(m[1])) fail('반복 변수 이름을 확인하세요.'); open(`for (const ${m[1]} of ${exp(m[2])})`, 'loop');
|
|
90
|
+
} else if ((m = text.match(/^만약 (.+) (?:이)?라면:$/u))) {
|
|
91
|
+
open(`if (${exp(m[1])})`, 'if');
|
|
92
|
+
} else if ((m = text.match(/^아니고 (.+) (?:이)?라면:$/u))) {
|
|
93
|
+
if (!['if', 'elseif'].includes(closed?.kind)) fail('아니고 앞에는 조건 블록이 필요합니다.'); open(`else if (${exp(m[1])})`, 'elseif');
|
|
94
|
+
} else if (text === '아니면:') {
|
|
95
|
+
if (!['if', 'elseif'].includes(closed?.kind)) fail('아니면 앞에는 조건 블록이 필요합니다.'); open('else', 'else');
|
|
96
|
+
} else if ((m = text.match(/^동안 (.+):$/u))) {
|
|
97
|
+
open(`while (${exp(m[1])})`, 'loop');
|
|
98
|
+
} else if (text === '먼저 실행한다:') {
|
|
99
|
+
open('do', 'do');
|
|
100
|
+
} else if ((m = text.match(/^(.+)인 동안 반복한다$/u))) {
|
|
101
|
+
if (closed?.kind !== 'do') fail('먼저 실행한다 블록 뒤에 작성하세요.'); emit(`while (${exp(m[1])});`);
|
|
102
|
+
} else if ((m = text.match(/^(.+)에 따라:$/u))) {
|
|
103
|
+
open(`switch (${exp(m[1])})`, 'switch');
|
|
104
|
+
} else if ((m = text.match(/^(.+)인 경우:$/u))) {
|
|
105
|
+
if (parent !== 'switch') fail('경우는 값에 따라 블록 안에 작성하세요.'); open(`case ${exp(m[1])}:`, 'case');
|
|
106
|
+
} else if (text === '그 밖의 경우:') {
|
|
107
|
+
if (parent !== 'switch') fail('그 밖의 경우는 값에 따라 블록 안에 작성하세요.'); open('default:', 'case');
|
|
108
|
+
} else if (text === '시도한다:') {
|
|
109
|
+
open('try', 'try');
|
|
110
|
+
} else if ((m = text.match(/^(.+?)(?:을|를) 잡으면:$/u))) {
|
|
111
|
+
if (closed?.kind !== 'try' || !identifier.test(m[1])) fail('잡으면 앞에는 시도한다 블록이 필요합니다.'); open(`catch (${m[1]})`, 'catch');
|
|
112
|
+
} else if (text === '마지막으로:') {
|
|
113
|
+
if (!['try', 'catch'].includes(closed?.kind)) fail('마지막으로 앞에는 시도한다 또는 잡으면 블록이 필요합니다.'); open('finally', 'finally');
|
|
114
|
+
} else if ((m = text.match(/^오류를 일으킨다 (.+)$/u))) {
|
|
115
|
+
emit(`throw ${exp(m[1])};`);
|
|
116
|
+
} else if ((m = text.match(/^(.+?)(?:은|는) (.+?)(이다|으로 고정한다)$/u))) {
|
|
117
|
+
const binding = m[1].match(/^([\p{L}_$][\p{L}\p{N}_$]*)(?::\s*(.+))?$/u);
|
|
118
|
+
if (!binding) fail('변수 이름과 타입을 확인하세요.');
|
|
119
|
+
const type = binding[2] ? ': ' + typeExpression(binding[2]) : m[2] === '[]' ? ': any[]' : '';
|
|
120
|
+
emit(`${parent === 'class' ? '' : m[3] === '으로 고정한다' ? 'const ' : 'let '}${binding[1]}${type} = ${exp(m[2])};`);
|
|
121
|
+
} else if (['class', 'interface'].includes(parent) && (m = text.match(/^([\p{L}_$][\p{L}\p{N}_$]*\??): (.+)$/u))) {
|
|
122
|
+
emit(`${m[1]}: ${typeExpression(m[2])};`);
|
|
123
|
+
} else if ((m = text.match(/^돌려주기(?: (.+))?$/u))) {
|
|
124
|
+
if (!frames.some(f => ['function', 'method', 'async', 'generator'].includes(f.kind))) fail('돌려주기는 함수 안에서 사용하세요.'); emit(`return ${m[1] ? exp(m[1]) : ''};`);
|
|
125
|
+
} else if ((m = text.match(/^내주기 (.+)$/u))) {
|
|
126
|
+
if (!inside(['generator'])) fail('내주기는 차례로 정의한 함수 안에서 사용하세요.'); emit(`yield ${exp(m[1])};`);
|
|
127
|
+
} else if (text === '멈추기' || text === '계속하기') {
|
|
128
|
+
if (!inside(text === '멈추기' ? ['loop', 'do', 'case'] : ['loop', 'do'])) fail('반복 블록 안에서 사용하세요.'); emit(text === '멈추기' ? 'break;' : 'continue;');
|
|
129
|
+
} else if (text.endsWith(':')) fail('지원하지 않는 블록 문장입니다.');
|
|
130
|
+
else emit(statement(exp(text)));
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (error.message.startsWith(`${index + 1}행:`)) throw error;
|
|
133
|
+
fail(error.message);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (pending) throw new SyntaxError('마지막 블록에 실행할 문장이 필요합니다.');
|
|
137
|
+
while (frames.length > 1) {
|
|
138
|
+
const frame = frames.pop();
|
|
139
|
+
if (frame.kind === 'do') throw new SyntaxError('먼저 실행한다 블록 뒤에 반복 조건이 필요합니다.');
|
|
140
|
+
push(frame.kind === 'case' ? 'break; }' : '}');
|
|
99
141
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return output.join('\n');
|
|
142
|
+
const code = output.join('\n');
|
|
143
|
+
return withMapping ? { code, lineMap } : code;
|
|
103
144
|
}
|
package/dist/transformer.d.ts
CHANGED
|
@@ -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;
|
package/dist/transformer.js
CHANGED
|
@@ -1,338 +1,13 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { compileSentences, isSentenceSource, sentenceRuntime } from './sentences.js';
|
|
1
|
+
import { compileSentences } from './sentences.js';
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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;
|
|
3
|
+
/** HangulScript has one syntax: indentation-based Korean sentences. */
|
|
4
|
+
export function compileSource(source) {
|
|
5
|
+
if (typeof source !== 'string') throw new TypeError('한글스크립트 소스는 문자열이어야 합니다.');
|
|
6
|
+
return compileSentences(source);
|
|
331
7
|
}
|
|
332
8
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (
|
|
336
|
-
|
|
337
|
-
return sentenceMode ? sentenceRuntime + '\n' + transformed : transformed;
|
|
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 });
|
|
338
13
|
}
|
package/dist/vite.js
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
|
-
import {
|
|
2
|
+
import { analyzeSource } from './checker.js';
|
|
3
|
+
import { HangulScriptError } from './diagnostics.js';
|
|
3
4
|
|
|
4
|
-
/**
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
};
|