@lithent/lithent-template-parser 0.1.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/LICENSE +21 -0
- package/README.md +159 -0
- package/dist/codegen/createElement.d.ts +10 -0
- package/dist/codegen/index.d.ts +1 -0
- package/dist/compiler.d.ts +26 -0
- package/dist/docPipe/index.d.ts +40 -0
- package/dist/docPipe/scan.d.ts +6 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.mjs +2302 -0
- package/dist/index.mjs.map +1 -0
- package/dist/index.umd.cjs +13 -0
- package/dist/index.umd.cjs.map +1 -0
- package/dist/parser/ast.d.ts +207 -0
- package/dist/parser/lexer.d.ts +109 -0
- package/dist/parser/parser.d.ts +79 -0
- package/dist/parser/tokens.d.ts +51 -0
- package/dist/transform/directives.d.ts +30 -0
- package/dist/transform/index.d.ts +14 -0
- package/dist/transform/optimize.d.ts +21 -0
- package/package.json +53 -0
- package/src/codegen/createElement.ts +661 -0
- package/src/codegen/index.ts +1 -0
- package/src/compiler.ts +70 -0
- package/src/docPipe/index.ts +104 -0
- package/src/docPipe/scan.ts +318 -0
- package/src/index.ts +16 -0
- package/src/parser/ast.ts +478 -0
- package/src/parser/lexer.ts +561 -0
- package/src/parser/parser.ts +514 -0
- package/src/parser/tokens.ts +84 -0
- package/src/playground/main.ts +206 -0
- package/src/playground/style.css +135 -0
- package/src/shims/babel-env.ts +17 -0
- package/src/tests/compiler.test.ts +434 -0
- package/src/tests/docPipe.test.ts +41 -0
- package/src/tests/helpers.ts +96 -0
- package/src/tests/lexer.test.ts +282 -0
- package/src/tests/parser.test.ts +691 -0
- package/src/transform/directives.ts +160 -0
- package/src/transform/index.ts +29 -0
- package/src/transform/optimize.ts +203 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import './style.css';
|
|
2
|
+
|
|
3
|
+
import { compile, parse, tokenize } from '..';
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
interface Window {
|
|
7
|
+
compile: typeof compile;
|
|
8
|
+
parse: typeof parse;
|
|
9
|
+
tokenize: typeof tokenize;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const defaultTemplate = `<div class="todo-list">
|
|
14
|
+
<h2>Todos ({todos.length})</h2>
|
|
15
|
+
<div l-for={(todo, index) in todos} class="todo-item">
|
|
16
|
+
<span l-if={todo.done}>✓</span>
|
|
17
|
+
<span>{index + 1}. {todo.text}</span>
|
|
18
|
+
</div>
|
|
19
|
+
<>
|
|
20
|
+
<p l-else>No todos yet!</p>
|
|
21
|
+
</>
|
|
22
|
+
</div>`;
|
|
23
|
+
|
|
24
|
+
const app = document.getElementById('app');
|
|
25
|
+
|
|
26
|
+
if (!app) {
|
|
27
|
+
throw new Error('Playground root element not found');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
app.innerHTML = `
|
|
31
|
+
<h1>Lithent Template Parser Playground</h1>
|
|
32
|
+
<p class="info">
|
|
33
|
+
템플릿을 수정하면서 토큰, AST, 생성된 코드를 실시간으로 확인하세요. 브라우저 개발자 도구에서 <code>compile</code>, <code>parse</code>, <code>tokenize</code>를 활용해 추가 디버깅도 가능합니다.
|
|
34
|
+
</p>
|
|
35
|
+
<div class="playground">
|
|
36
|
+
<div class="panel">
|
|
37
|
+
<div class="panel-header">
|
|
38
|
+
<span>Template 입력</span>
|
|
39
|
+
<span class="badge">입력</span>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="panel-body">
|
|
42
|
+
<textarea id="template-input" spellcheck="false"></textarea>
|
|
43
|
+
<div class="error" id="template-error"></div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="output-group">
|
|
47
|
+
<div class="panel">
|
|
48
|
+
<div class="panel-header">
|
|
49
|
+
<span>생성된 코드</span>
|
|
50
|
+
<div>
|
|
51
|
+
<span class="badge">결과</span>
|
|
52
|
+
<button class="copy" data-target="code-output">Copy</button>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="panel-body">
|
|
56
|
+
<pre id="code-output">// 결과가 여기에 표시됩니다.</pre>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="panel">
|
|
60
|
+
<div class="panel-header">
|
|
61
|
+
<span>토큰</span>
|
|
62
|
+
<div>
|
|
63
|
+
<span class="badge" id="token-count">0 tokens</span>
|
|
64
|
+
<button class="copy" data-target="tokens-output">Copy</button>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="panel-body">
|
|
68
|
+
<pre id="tokens-output">[]</pre>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="panel">
|
|
72
|
+
<div class="panel-header">
|
|
73
|
+
<span>AST</span>
|
|
74
|
+
<div>
|
|
75
|
+
<span class="badge">Transformed</span>
|
|
76
|
+
<button class="copy" data-target="ast-output">Copy</button>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="panel-body">
|
|
80
|
+
<pre id="ast-output">{}</pre>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
const templateInput = document.getElementById(
|
|
88
|
+
'template-input'
|
|
89
|
+
) as HTMLTextAreaElement | null;
|
|
90
|
+
const tokenBadge = document.getElementById('token-count');
|
|
91
|
+
const tokensOutput = document.getElementById('tokens-output');
|
|
92
|
+
const astOutput = document.getElementById('ast-output');
|
|
93
|
+
const codeOutput = document.getElementById('code-output');
|
|
94
|
+
const errorOutput = document.getElementById('template-error');
|
|
95
|
+
|
|
96
|
+
if (
|
|
97
|
+
!templateInput ||
|
|
98
|
+
!tokenBadge ||
|
|
99
|
+
!tokensOutput ||
|
|
100
|
+
!astOutput ||
|
|
101
|
+
!codeOutput ||
|
|
102
|
+
!errorOutput
|
|
103
|
+
) {
|
|
104
|
+
throw new Error('Playground UI 초기화에 실패했습니다.');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
templateInput.value = defaultTemplate.trim();
|
|
108
|
+
|
|
109
|
+
const circularReplacer = () => {
|
|
110
|
+
const seen = new WeakSet();
|
|
111
|
+
return (key: string, value: unknown) => {
|
|
112
|
+
if (key === '__conditionalGroup') {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
if (typeof value === 'object' && value !== null) {
|
|
116
|
+
if (seen.has(value as object)) {
|
|
117
|
+
return '[Circular]';
|
|
118
|
+
}
|
|
119
|
+
seen.add(value as object);
|
|
120
|
+
}
|
|
121
|
+
return value;
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const formatJSON = (value: unknown) =>
|
|
126
|
+
JSON.stringify(value, circularReplacer(), 2);
|
|
127
|
+
|
|
128
|
+
const copyButtons = Array.from(
|
|
129
|
+
document.querySelectorAll<HTMLButtonElement>('button.copy')
|
|
130
|
+
);
|
|
131
|
+
copyButtons.forEach(button => {
|
|
132
|
+
button.addEventListener('click', async () => {
|
|
133
|
+
const targetId = button.dataset.target;
|
|
134
|
+
if (!targetId) return;
|
|
135
|
+
|
|
136
|
+
const target = document.getElementById(targetId);
|
|
137
|
+
if (!target) return;
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
await navigator.clipboard.writeText(target.textContent ?? '');
|
|
141
|
+
const original = button.textContent;
|
|
142
|
+
button.textContent = 'Copied!';
|
|
143
|
+
setTimeout(() => {
|
|
144
|
+
button.textContent = original ?? 'Copy';
|
|
145
|
+
}, 1200);
|
|
146
|
+
} catch {
|
|
147
|
+
button.textContent = 'Failed';
|
|
148
|
+
setTimeout(() => {
|
|
149
|
+
button.textContent = 'Copy';
|
|
150
|
+
}, 1200);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
let debounceTimer: number | undefined;
|
|
156
|
+
|
|
157
|
+
const runCompilation = () => {
|
|
158
|
+
const template = templateInput.value;
|
|
159
|
+
errorOutput.textContent = '';
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const tokens = tokenize(template);
|
|
163
|
+
tokenBadge.textContent = `${tokens.length} tokens`;
|
|
164
|
+
tokensOutput.textContent = formatJSON(tokens);
|
|
165
|
+
|
|
166
|
+
let ast;
|
|
167
|
+
try {
|
|
168
|
+
ast = parse(tokens);
|
|
169
|
+
console.log('AST', ast);
|
|
170
|
+
} catch (parseError) {
|
|
171
|
+
astOutput.textContent = '{}';
|
|
172
|
+
codeOutput.textContent = '// 파싱에 실패했습니다.';
|
|
173
|
+
throw parseError;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const result = compile(template);
|
|
177
|
+
|
|
178
|
+
console.log(' - RESULT - ', result);
|
|
179
|
+
|
|
180
|
+
if (result.errors.length > 0) {
|
|
181
|
+
const [firstError] = result.errors;
|
|
182
|
+
errorOutput.textContent = `⚠️ ${firstError.message}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
codeOutput.textContent = result.code || '// 생성된 코드가 없습니다.';
|
|
186
|
+
astOutput.textContent = formatJSON(result.ast ?? ast);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
189
|
+
errorOutput.textContent = `⚠️ ${message}`;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const scheduleCompilation = () => {
|
|
194
|
+
if (debounceTimer !== undefined) {
|
|
195
|
+
window.clearTimeout(debounceTimer);
|
|
196
|
+
}
|
|
197
|
+
debounceTimer = window.setTimeout(runCompilation, 160);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
templateInput.addEventListener('input', scheduleCompilation);
|
|
201
|
+
|
|
202
|
+
// 초기 실행
|
|
203
|
+
runCompilation();
|
|
204
|
+
|
|
205
|
+
// 전역 디버깅을 위해 노출
|
|
206
|
+
Object.assign(window, { compile, parse, tokenize });
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
* {
|
|
2
|
+
box-sizing: border-box;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
:root {
|
|
6
|
+
color-scheme: dark light;
|
|
7
|
+
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
8
|
+
background: #111;
|
|
9
|
+
color: #f1f5f9;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
body {
|
|
13
|
+
margin: 0;
|
|
14
|
+
min-height: 100vh;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#app {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
gap: 1.5rem;
|
|
21
|
+
padding: 2rem 3rem;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
h1 {
|
|
25
|
+
font-size: 1.75rem;
|
|
26
|
+
margin: 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.playground {
|
|
30
|
+
display: grid;
|
|
31
|
+
gap: 1.5rem;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@media (min-width: 1080px) {
|
|
35
|
+
.playground {
|
|
36
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.panel {
|
|
41
|
+
background: rgba(15, 23, 42, 0.9);
|
|
42
|
+
border: 1px solid rgba(148, 163, 184, 0.2);
|
|
43
|
+
border-radius: 0.75rem;
|
|
44
|
+
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.4);
|
|
45
|
+
overflow: hidden;
|
|
46
|
+
display: flex;
|
|
47
|
+
flex-direction: column;
|
|
48
|
+
min-height: 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.panel-header {
|
|
52
|
+
padding: 0.75rem 1rem;
|
|
53
|
+
background: rgba(30, 41, 59, 0.9);
|
|
54
|
+
border-bottom: 1px solid rgba(148, 163, 184, 0.2);
|
|
55
|
+
font-size: 0.95rem;
|
|
56
|
+
font-weight: 600;
|
|
57
|
+
display: flex;
|
|
58
|
+
justify-content: space-between;
|
|
59
|
+
align-items: center;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.panel-body {
|
|
63
|
+
padding: 1rem;
|
|
64
|
+
flex: 1;
|
|
65
|
+
overflow: auto;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
textarea {
|
|
69
|
+
width: 100%;
|
|
70
|
+
min-height: 360px;
|
|
71
|
+
font: 0.95rem/1.4 'JetBrains Mono', 'Fira Code', 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
|
|
72
|
+
color: inherit;
|
|
73
|
+
background: rgba(15, 23, 42, 0.7);
|
|
74
|
+
border: 1px solid rgba(148, 163, 184, 0.25);
|
|
75
|
+
border-radius: 0.5rem;
|
|
76
|
+
padding: 0.9rem 1rem;
|
|
77
|
+
resize: vertical;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
textarea:focus {
|
|
81
|
+
outline: 2px solid rgba(59, 130, 246, 0.5);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
pre {
|
|
85
|
+
margin: 0;
|
|
86
|
+
white-space: pre-wrap;
|
|
87
|
+
word-break: break-word;
|
|
88
|
+
font: 0.9rem/1.45 'JetBrains Mono', 'Fira Code', 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.error {
|
|
92
|
+
color: #f87171;
|
|
93
|
+
font-size: 0.9rem;
|
|
94
|
+
margin-top: 0.75rem;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.badge {
|
|
98
|
+
font-size: 0.75rem;
|
|
99
|
+
color: rgba(226, 232, 240, 0.8);
|
|
100
|
+
background: rgba(51, 65, 85, 0.8);
|
|
101
|
+
padding: 0.2rem 0.5rem;
|
|
102
|
+
border-radius: 999px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.output-group {
|
|
106
|
+
display: flex;
|
|
107
|
+
flex-direction: column;
|
|
108
|
+
gap: 1rem;
|
|
109
|
+
min-height: 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.output-group .panel {
|
|
113
|
+
min-height: 280px;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.info {
|
|
117
|
+
font-size: 0.85rem;
|
|
118
|
+
color: rgba(148, 163, 184, 0.8);
|
|
119
|
+
margin-bottom: 0.5rem;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
button.copy {
|
|
123
|
+
font-size: 0.75rem;
|
|
124
|
+
padding: 0.35rem 0.7rem;
|
|
125
|
+
border-radius: 999px;
|
|
126
|
+
border: none;
|
|
127
|
+
background: rgba(59, 130, 246, 0.2);
|
|
128
|
+
color: rgba(191, 219, 254, 0.95);
|
|
129
|
+
cursor: pointer;
|
|
130
|
+
transition: background 0.2s ease;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
button.copy:hover {
|
|
134
|
+
background: rgba(59, 130, 246, 0.35);
|
|
135
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure minimal Node-like globals for Babel packages in browser environments.
|
|
3
|
+
* Babel checks process.env for feature flags, so we provide a stub when absent.
|
|
4
|
+
*/
|
|
5
|
+
const globalObject: any = globalThis as any;
|
|
6
|
+
|
|
7
|
+
if (typeof globalObject.process === 'undefined') {
|
|
8
|
+
globalObject.process = { env: {} };
|
|
9
|
+
} else if (typeof globalObject.process.env === 'undefined') {
|
|
10
|
+
globalObject.process.env = {};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (typeof globalObject.process.env.NODE_ENV === 'undefined') {
|
|
14
|
+
globalObject.process.env.NODE_ENV = 'development';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export {};
|