@libs-ui/components-preview-text-data 0.2.357-3 → 0.2.357-4

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.
@@ -1,16 +1,16 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { signal, inject, DestroyRef, computed, input, model, output, viewChild, effect, Component, ChangeDetectionStrategy } from '@angular/core';
3
- import { autocompletion } from '@codemirror/autocomplete';
4
- import { syntaxTree } from '@codemirror/language';
5
- import { lintGutter, linter } from '@codemirror/lint';
6
- import { Compartment, Prec, EditorState, Transaction } from '@codemirror/state';
3
+ import { linter, lintGutter } from '@codemirror/lint';
4
+ import { EditorState, Transaction, Compartment, Prec } from '@codemirror/state';
7
5
  import { EditorView, lineNumbers } from '@codemirror/view';
8
6
  import { LibsUiComponentsButtonsButtonComponent } from '@libs-ui/components-buttons-button';
9
7
  import { LibsUiComponentsDropdownComponent } from '@libs-ui/components-dropdown';
10
8
  import { LibsUiNotificationService } from '@libs-ui/services-notification';
11
- import { UtilsHttpParamsRequest, get } from '@libs-ui/utils';
12
9
  import { basicSetup } from 'codemirror6';
13
10
  import { returnListObject } from '@libs-ui/services-http-request';
11
+ import { UtilsHttpParamsRequest, get } from '@libs-ui/utils';
12
+ import { syntaxTree } from '@codemirror/language';
13
+ import { autocompletion } from '@codemirror/autocomplete';
14
14
 
15
15
  /**
16
16
  * Danh sách các ngôn ngữ được hỗ trợ
@@ -154,6 +154,18 @@ const DEFAULT_SECURITY_PATTERNS = [
154
154
  /document\.write\s*\(/i,
155
155
  /window\.location\s*=/i,
156
156
  ];
157
+ /**
158
+ * Các key của AST node KHÔNG phải node con (metadata vị trí, comment...) — bỏ qua khi duyệt.
159
+ */
160
+ const astSkipKeys = new Set(['loc', 'start', 'end', 'range', 'leadingComments', 'trailingComments', 'innerComments', 'extra', 'comments', 'tokens']);
161
+ /**
162
+ * Các key chứa annotation kiểu TypeScript → con của nó nằm trong "type context" (không phải biến giá trị).
163
+ */
164
+ const astTypeKeys = new Set(['typeAnnotation', 'returnType', 'typeParameters', 'typeArguments', 'superTypeParameters']);
165
+ /**
166
+ * Các node `TS*` mang GIÁ TRỊ (không phải type thuần) → con của chúng vẫn là biến giá trị bình thường.
167
+ */
168
+ const tsValueNodes = new Set(['TSEnumDeclaration', 'TSEnumMember', 'TSModuleDeclaration', 'TSModuleBlock', 'TSAsExpression', 'TSNonNullExpression', 'TSParameterProperty', 'TSExportAssignment', 'TSImportEqualsDeclaration', 'TSExternalModuleReference', 'TSSatisfiesExpression']);
157
169
  /**
158
170
  * Tạo default language extension cho plain text
159
171
  * Sử dụng khi ngôn ngữ không được hỗ trợ
@@ -168,6 +180,633 @@ const createDefaultLanguage = async () => {
168
180
  });
169
181
  };
170
182
 
183
+ /** Lấy token (chuỗi không khoảng trắng) liền trước vị trí `pos` trong document. Dùng chung cho JS/SQL/Python. */
184
+ const getTokenBefore = (doc, pos) => {
185
+ const start = Math.max(0, pos - 30);
186
+ const text = doc.sliceString(start, pos);
187
+ const match = /(\S+)\s*$/.exec(text);
188
+ return match?.[1] ?? '';
189
+ };
190
+
191
+ // Cache module-level (chia sẻ giữa các instance editor) — đều immutable, lazy-load 1 lần.
192
+ let babelParserModule;
193
+ let knownGlobalsSet;
194
+ /** Lazy-load + cache module @babel/parser (chỉ tải lần đầu khi lint JS/TS). */
195
+ const getBabelParser = async () => {
196
+ if (!babelParserModule) {
197
+ babelParserModule = await import('@babel/parser');
198
+ }
199
+ return babelParserModule;
200
+ };
201
+ /** Lazy-load + cache tập biến toàn cục JS/DOM hợp lệ từ package `globals` (JSON thuần, browser-safe). */
202
+ const getKnownGlobals = async () => {
203
+ if (!knownGlobalsSet) {
204
+ const globalsModule = (await import('globals'));
205
+ const globalsData = (globalsModule.default ?? globalsModule);
206
+ knownGlobalsSet = new Set([...Object.keys(globalsData['builtin'] ?? {}), ...Object.keys(globalsData['browser'] ?? {}), ...Object.keys(globalsData['es2021'] ?? {})]);
207
+ }
208
+ return knownGlobalsSet;
209
+ };
210
+ /**
211
+ * Parse code, trả về vừa danh sách lỗi cú pháp vừa AST (nếu parse sạch).
212
+ * `errorRecovery` gom nhiều lỗi vào `result.errors`; lỗi nặng vẫn `throw` → bắt thêm ở catch.
213
+ */
214
+ const parseJs = (parser, code) => {
215
+ try {
216
+ const result = parser.parse(code, {
217
+ sourceType: 'module',
218
+ errorRecovery: true,
219
+ plugins: ['typescript', 'jsx'],
220
+ allowReturnOutsideFunction: true,
221
+ allowAwaitOutsideFunction: true,
222
+ allowImportExportEverywhere: true,
223
+ allowSuperOutsideMethod: true,
224
+ allowNewTargetOutsideFunction: true,
225
+ allowUndeclaredExports: true,
226
+ });
227
+ return { errors: result.errors, ast: result };
228
+ }
229
+ catch (error) {
230
+ const loc = get(error, 'loc');
231
+ const message = get(error, 'message') || 'JavaScript syntax error';
232
+ return { errors: [{ loc, message }] };
233
+ }
234
+ };
235
+ /** Giữ cho test/back-compat: chỉ lấy danh sách lỗi cú pháp. */
236
+ const collectBabelErrors = (parser, code) => parseJs(parser, code).errors;
237
+ /**
238
+ * Quét text từng dòng, flag dấu "." cuối dòng (incomplete property access) tại ĐÚNG vị trí dấu chấm.
239
+ * Thu thập `taintedLines` (dòng có trailing dot) + `dotPositions` (offset dấu chấm để vá khi parse).
240
+ */
241
+ const collectTrailingDotDiagnostics = (doc, diagnostics, taintedLines, dotPositions) => {
242
+ for (let lineNumber = 1; lineNumber <= doc.lines; lineNumber++) {
243
+ const line = doc.line(lineNumber);
244
+ const trimmed = line.text.trimEnd();
245
+ const match = /(\w+)\.\s*$/.exec(trimmed);
246
+ if (!match) {
247
+ continue;
248
+ }
249
+ taintedLines.add(lineNumber);
250
+ const dotPosition = line.from + trimmed.lastIndexOf('.');
251
+ dotPositions.push(dotPosition);
252
+ diagnostics.push({ from: dotPosition, to: dotPosition + 1, severity: 'error', message: `Incomplete property access on "${match[1]}"` });
253
+ }
254
+ };
255
+ /** Thay các ký tự tại `positions` bằng space, giữ nguyên độ dài chuỗi (để offset không đổi). */
256
+ const replaceCharsWithSpace = (code, positions) => {
257
+ if (!positions.length) {
258
+ return code;
259
+ }
260
+ const chars = code.split('');
261
+ for (const position of positions) {
262
+ if (position >= 0 && position < chars.length) {
263
+ chars[position] = ' ';
264
+ }
265
+ }
266
+ return chars.join('');
267
+ };
268
+ /** VariableDeclaration ở `for(let i=0;...)` / `for..in` / `for..of` KHÔNG cần `;` cuối. */
269
+ const requiresSemicolon = (nodeType, parentType, keyInParent) => {
270
+ if (nodeType === 'VariableDeclaration') {
271
+ if (parentType === 'ForStatement' && keyInParent === 'init')
272
+ return false;
273
+ if ((parentType === 'ForInStatement' || parentType === 'ForOfStatement') && keyInParent === 'left')
274
+ return false;
275
+ }
276
+ return true;
277
+ };
278
+ /**
279
+ * Rule custom "require semicolon": câu lệnh (const/let/return/expression...) không kết thúc bằng `;` → báo lỗi.
280
+ * Dùng vị trí `node.end` của AST babel; bỏ qua dòng tainted (trailing dot) để không chồng lỗi.
281
+ */
282
+ const collectMissingSemicolonDiagnostics = (ast, code, diagnostics, taintedLines) => {
283
+ const semiNodes = new Set(['ExpressionStatement', 'VariableDeclaration', 'ReturnStatement', 'ThrowStatement', 'BreakStatement', 'ContinueStatement', 'DoWhileStatement', 'DebuggerStatement']);
284
+ const seen = new Set();
285
+ const lineOf = (offset) => code.slice(0, offset).split('\n').length;
286
+ const walk = (node, parentType, keyInParent) => {
287
+ if (Array.isArray(node)) {
288
+ for (const item of node)
289
+ walk(item, parentType, keyInParent);
290
+ return;
291
+ }
292
+ if (!node || typeof node !== 'object')
293
+ return;
294
+ const current = node;
295
+ if (typeof current.type !== 'string')
296
+ return;
297
+ if (semiNodes.has(current.type) && requiresSemicolon(current.type, parentType, keyInParent) && typeof current.end === 'number') {
298
+ const end = Math.min(current.end, code.length);
299
+ if (end > 0 && code.charAt(end - 1) !== ';' && !seen.has(end) && !taintedLines.has(lineOf(end - 1))) {
300
+ seen.add(end);
301
+ diagnostics.push({ from: end - 1, to: end, severity: 'error', message: 'Missing semicolon' });
302
+ }
303
+ }
304
+ for (const childKey of Object.keys(current)) {
305
+ if (astSkipKeys.has(childKey))
306
+ continue;
307
+ walk(current[childKey], current.type, childKey);
308
+ }
309
+ };
310
+ walk(ast.program ?? ast, '', '');
311
+ };
312
+ /** Chuẩn hóa message: thân thiện cho trailing dot + heuristic kiểu cũ cho "Unexpected token"; bỏ hậu tố "(line:col)". */
313
+ const buildJsErrorMessage = (doc, from, rawMessage) => {
314
+ const charBefore = from > 0 ? doc.sliceString(from - 1, from) : '';
315
+ if (charBefore === '.') {
316
+ const owner = getTokenBefore(doc, from).replace(/\.$/, '');
317
+ return owner ? `Incomplete property access on "${owner}"` : 'Incomplete property access';
318
+ }
319
+ const cleaned = rawMessage.replace(/\s*\(\d+:\d+\)\s*$/, '').trim();
320
+ if (/missing semicolon/i.test(cleaned)) {
321
+ return 'Unexpected token — missing operator or separator between expressions';
322
+ }
323
+ if (/^unexpected token$/i.test(cleaned)) {
324
+ const prevToken = getTokenBefore(doc, from);
325
+ if (prevToken.endsWith('('))
326
+ return 'Missing closing ")"';
327
+ if (prevToken.endsWith('['))
328
+ return 'Missing closing "]"';
329
+ if (/^function$/i.test(prevToken))
330
+ return 'Expected function name or "("';
331
+ if (/^return$/i.test(prevToken))
332
+ return 'Invalid expression after "return"';
333
+ if (/^=>$/.test(prevToken))
334
+ return 'Expected function body after "=>"';
335
+ if (/^const$|^let$|^var$/i.test(prevToken))
336
+ return 'Expected variable name';
337
+ }
338
+ return `JavaScript syntax error: ${cleaned}`;
339
+ };
340
+ /** Lấy tên binding từ pattern (Identifier, destructuring object/array, default, rest). */
341
+ const collectPatternNames = (node, declared) => {
342
+ if (!node || typeof node.type !== 'string')
343
+ return;
344
+ switch (node.type) {
345
+ case 'Identifier':
346
+ if (node.name)
347
+ declared.add(node.name);
348
+ return;
349
+ case 'ObjectPattern':
350
+ if (node.properties)
351
+ for (const property of node.properties)
352
+ collectPatternNames(property.type === 'RestElement' ? property.argument : property.value, declared);
353
+ return;
354
+ case 'ArrayPattern':
355
+ if (node.elements)
356
+ for (const element of node.elements)
357
+ collectPatternNames(element, declared);
358
+ return;
359
+ case 'AssignmentPattern':
360
+ collectPatternNames(node.left, declared);
361
+ return;
362
+ case 'RestElement':
363
+ collectPatternNames(node.argument, declared);
364
+ return;
365
+ case 'TSParameterProperty':
366
+ collectPatternNames(node.parameter, declared);
367
+ return;
368
+ default:
369
+ return;
370
+ }
371
+ };
372
+ /** Thu thập tên binding (biến/hàm/class/param/import/enum...) từ 1 node vào tập `declared`. */
373
+ const collectBindingNames = (node, declared) => {
374
+ switch (node.type) {
375
+ case 'VariableDeclarator':
376
+ collectPatternNames(node.id, declared);
377
+ return;
378
+ case 'FunctionDeclaration':
379
+ case 'FunctionExpression':
380
+ case 'ClassDeclaration':
381
+ case 'ClassExpression':
382
+ if (node.id?.name)
383
+ declared.add(node.id.name);
384
+ if (node.params)
385
+ for (const param of node.params)
386
+ collectPatternNames(param, declared);
387
+ return;
388
+ case 'ArrowFunctionExpression':
389
+ if (node.params)
390
+ for (const param of node.params)
391
+ collectPatternNames(param, declared);
392
+ return;
393
+ case 'CatchClause':
394
+ collectPatternNames(node.param, declared);
395
+ return;
396
+ case 'ImportDefaultSpecifier':
397
+ case 'ImportNamespaceSpecifier':
398
+ case 'ImportSpecifier':
399
+ if (node.local?.name)
400
+ declared.add(node.local.name);
401
+ return;
402
+ case 'TSEnumDeclaration':
403
+ case 'TSModuleDeclaration':
404
+ if (node.id?.name)
405
+ declared.add(node.id.name);
406
+ return;
407
+ default:
408
+ return;
409
+ }
410
+ };
411
+ /** Xác định 1 Identifier có phải tham chiếu giá trị không (loại property access, key object, declaration id, label...). */
412
+ const isReferenceIdentifier = (parent, keyInParent) => {
413
+ if (!parent)
414
+ return true;
415
+ const parentType = parent.type;
416
+ if ((parentType === 'MemberExpression' || parentType === 'OptionalMemberExpression') && keyInParent === 'property' && !parent.computed)
417
+ return false;
418
+ if ((parentType === 'ObjectProperty' || parentType === 'ObjectMethod' || parentType === 'ClassMethod' || parentType === 'ClassProperty' || parentType === 'ClassPrivateProperty' || parentType === 'PropertyDefinition') && keyInParent === 'key' && !parent.computed)
419
+ return false;
420
+ if (keyInParent === 'label')
421
+ return false;
422
+ if (parentType === 'TSQualifiedName' && keyInParent === 'right')
423
+ return false;
424
+ if (parentType === 'VariableDeclarator' && keyInParent === 'id')
425
+ return false;
426
+ if ((parentType === 'FunctionDeclaration' || parentType === 'FunctionExpression' || parentType === 'ClassDeclaration' || parentType === 'ClassExpression') && keyInParent === 'id')
427
+ return false;
428
+ if (parentType === 'ImportSpecifier' || parentType === 'ImportDefaultSpecifier' || parentType === 'ImportNamespaceSpecifier' || parentType === 'ExportSpecifier')
429
+ return false;
430
+ if (parentType === 'MetaProperty')
431
+ return false;
432
+ if (parentType === 'TSEnumMember' && keyInParent === 'id')
433
+ return false;
434
+ return true;
435
+ };
436
+ /** Duyệt đệ quy AST: gom binding name vào `declared`, gom identifier tham chiếu vào `references`. */
437
+ const walkAstNode = (node, parent, keyInParent, inType, declared, references) => {
438
+ if (Array.isArray(node)) {
439
+ for (const item of node)
440
+ walkAstNode(item, parent, keyInParent, inType, declared, references);
441
+ return;
442
+ }
443
+ if (!node || typeof node !== 'object')
444
+ return;
445
+ const current = node;
446
+ if (typeof current.type !== 'string')
447
+ return;
448
+ collectBindingNames(current, declared);
449
+ if (current.type === 'Identifier' && !inType && isReferenceIdentifier(parent, keyInParent)) {
450
+ const start = current.loc?.start;
451
+ if (current.name && start)
452
+ references.push({ name: current.name, line: start.line, column: start.column });
453
+ }
454
+ const opensType = inType || (current.type.startsWith('TS') && !tsValueNodes.has(current.type));
455
+ for (const childKey of Object.keys(current)) {
456
+ if (astSkipKeys.has(childKey))
457
+ continue;
458
+ const child = current[childKey];
459
+ const childOpensType = typeof child === 'object' && child !== null && typeof child.type === 'string' && child.type.startsWith('TS') && !tsValueNodes.has(child.type);
460
+ const childInType = opensType || astTypeKeys.has(childKey) || childOpensType;
461
+ walkAstNode(child, current, childKey, childInType, declared, references);
462
+ }
463
+ };
464
+ /**
465
+ * Pass ngữ nghĩa: báo biến CHƯA KHAI BÁO. Tự duyệt AST (object thuần) — KHÔNG dùng `@babel/traverse`
466
+ * (cần `process` của Node → crash browser). Hợp lệ = binding trong code + global JS/DOM + `completionLabels`.
467
+ */
468
+ const collectUndeclaredIdentifiers = async (ast, doc, completionLabels, taintedLines = new Set()) => {
469
+ const globalsSet = await getKnownGlobals();
470
+ const declared = new Set();
471
+ const references = [];
472
+ walkAstNode(ast, null, '', false, declared, references);
473
+ const diagnostics = [];
474
+ const seen = new Set();
475
+ for (const reference of references) {
476
+ const { name, line: startLine, column: startColumn } = reference;
477
+ if (declared.has(name) || globalsSet.has(name) || completionLabels.has(name))
478
+ continue;
479
+ if (taintedLines.has(startLine))
480
+ continue;
481
+ const dedupeKey = `${startLine}:${startColumn}:${name}`;
482
+ if (seen.has(dedupeKey))
483
+ continue;
484
+ seen.add(dedupeKey);
485
+ const line = doc.line(Math.min(Math.max(startLine, 1), doc.lines));
486
+ const from = Math.min(line.from + Math.max(startColumn, 0), doc.length);
487
+ const to = Math.min(from + name.length, doc.length);
488
+ diagnostics.push({ from, to: Math.max(from + 1, to), severity: 'error', message: `"${name}" is not defined` });
489
+ }
490
+ return diagnostics;
491
+ };
492
+ /**
493
+ * Toàn bộ pipeline validate JS/TS:
494
+ * - Pass 1: trailing dot (text) — flag đúng vị trí dấu `.` cuối dòng + đánh dấu dòng tainted.
495
+ * - Pass 2: lỗi cú pháp babel — bỏ lỗi trên dòng tainted (cascading).
496
+ * - Pass 3: rule cần AST (thiếu `;`, biến chưa khai báo). Trailing dot được "vá" (`.`→space) để babel
497
+ * vẫn parse được phần còn lại → rule vẫn chạy cho các dòng KHÁC.
498
+ */
499
+ const computeJsDiagnostics = async (doc, code, completionLabels) => {
500
+ const diagnostics = [];
501
+ if (code.trim() === '') {
502
+ return diagnostics;
503
+ }
504
+ const taintedLines = new Set();
505
+ const dotPositions = [];
506
+ collectTrailingDotDiagnostics(doc, diagnostics, taintedLines, dotPositions);
507
+ const parseCode = replaceCharsWithSpace(code, dotPositions);
508
+ const parser = await getBabelParser();
509
+ const { errors, ast } = parseJs(parser, parseCode);
510
+ const seen = new Set();
511
+ for (const error of errors) {
512
+ const location = error.loc;
513
+ if (!location)
514
+ continue;
515
+ const lineNumber = Math.min(Math.max(location.line, 1), doc.lines);
516
+ if (taintedLines.has(lineNumber))
517
+ continue;
518
+ const line = doc.line(lineNumber);
519
+ const errorPos = Math.min(line.from + Math.max(location.column, 0), doc.length);
520
+ const to = Math.min(errorPos + 1, doc.length);
521
+ const from = errorPos >= to && errorPos > 0 ? errorPos - 1 : errorPos;
522
+ const message = buildJsErrorMessage(doc, errorPos, error.message);
523
+ const dedupeKey = `${from}:${message}`;
524
+ if (seen.has(dedupeKey))
525
+ continue;
526
+ seen.add(dedupeKey);
527
+ diagnostics.push({ from, to: Math.max(from + 1, to), severity: 'error', message });
528
+ }
529
+ if (errors.length === 0 && ast) {
530
+ collectMissingSemicolonDiagnostics(ast, code, diagnostics, taintedLines);
531
+ diagnostics.push(...(await collectUndeclaredIdentifiers(ast, doc, completionLabels, taintedLines)));
532
+ }
533
+ return diagnostics;
534
+ };
535
+
536
+ /** Validate JSON bằng `JSON.parse`. Lỗi → 1 diagnostic phủ toàn bộ nội dung. */
537
+ const computeJsonDiagnostics = (view) => {
538
+ const text = view.state.doc.toString();
539
+ const diagnostics = [];
540
+ try {
541
+ JSON.parse(text);
542
+ }
543
+ catch (error) {
544
+ diagnostics.push({ from: 0, to: text.length, severity: 'error', message: get(error, 'message', '') });
545
+ }
546
+ return diagnostics;
547
+ };
548
+
549
+ const buildSqlErrorMessage = (doc, node) => {
550
+ if (node.from !== node.to) {
551
+ const text = doc.sliceString(node.from, node.to);
552
+ return `SQL syntax error near "${text}"`;
553
+ }
554
+ const prevToken = getTokenBefore(doc, node.from);
555
+ if (!prevToken)
556
+ return 'SQL syntax error at beginning of statement';
557
+ if (/FROM$/i.test(prevToken))
558
+ return 'Expected table name after FROM';
559
+ if (/SELECT$/i.test(prevToken))
560
+ return 'Expected column list after SELECT';
561
+ if (/WHERE$/i.test(prevToken))
562
+ return 'Expected condition after WHERE';
563
+ if (prevToken.endsWith('('))
564
+ return 'Missing closing ")"';
565
+ return `Unexpected end of SQL after "${prevToken}"`;
566
+ };
567
+ /** Phát hiện lỗi cú pháp SQL từ error node của syntaxTree (Lezer). */
568
+ const computeSqlDiagnostics = (view) => {
569
+ const diagnostics = [];
570
+ const doc = view.state.doc;
571
+ syntaxTree(view.state).iterate({
572
+ enter: (node) => {
573
+ if (!node.type.isError)
574
+ return;
575
+ diagnostics.push({ from: Math.max(0, node.from - 1), to: node.from, severity: 'error', message: buildSqlErrorMessage(doc, node) });
576
+ },
577
+ });
578
+ return diagnostics;
579
+ };
580
+
581
+ const buildPythonErrorMessage = (doc, node) => {
582
+ if (node.from !== node.to) {
583
+ const text = doc.sliceString(node.from, node.to).trim();
584
+ return `Python syntax error${text ? ` near "${text}"` : ''}`;
585
+ }
586
+ const prevToken = getTokenBefore(doc, node.from);
587
+ if (!prevToken)
588
+ return 'Python syntax error';
589
+ if (/^def$/i.test(prevToken))
590
+ return 'Expected function name after "def"';
591
+ if (/^class$/i.test(prevToken))
592
+ return 'Expected class name after "class"';
593
+ if (/^return$/i.test(prevToken))
594
+ return 'Invalid expression after "return"';
595
+ if (/^import$/i.test(prevToken))
596
+ return 'Expected module name after "import"';
597
+ if (/^from$/i.test(prevToken))
598
+ return 'Expected module name after "from"';
599
+ if (/^if$/i.test(prevToken))
600
+ return 'Expected condition after "if"';
601
+ if (/^elif$/i.test(prevToken))
602
+ return 'Expected condition after "elif"';
603
+ if (/^while$/i.test(prevToken))
604
+ return 'Expected condition after "while"';
605
+ if (/^for$/i.test(prevToken))
606
+ return 'Expected variable after "for"';
607
+ if (prevToken.endsWith('.'))
608
+ return `Incomplete attribute access on "${prevToken.slice(0, -1)}"`;
609
+ if (prevToken.endsWith('('))
610
+ return 'Missing closing ")"';
611
+ if (prevToken.endsWith('['))
612
+ return 'Missing closing "]"';
613
+ return `Python syntax error near "${prevToken}"`;
614
+ };
615
+ /** Phát hiện lỗi cú pháp Python từ error node của syntaxTree (Lezer) — cùng approach SQL. */
616
+ const computePythonDiagnostics = (view) => {
617
+ const diagnostics = [];
618
+ const doc = view.state.doc;
619
+ syntaxTree(view.state).iterate({
620
+ enter: (node) => {
621
+ if (!node.type.isError)
622
+ return;
623
+ const from = Math.max(0, node.from - 1);
624
+ const to = node.from === node.to ? node.from + 1 : node.to;
625
+ diagnostics.push({ from, to: Math.min(to, doc.length), severity: 'error', message: buildPythonErrorMessage(doc, node) });
626
+ },
627
+ });
628
+ return diagnostics;
629
+ };
630
+
631
+ /**
632
+ * Tạo extension autocomplete từ danh sách `items`:
633
+ * 1. Gõ `obj.` → gợi ý properties của obj (dot-notation).
634
+ * 2. Gõ ký tự → gợi ý tên biến/object top-level.
635
+ */
636
+ const buildCompletionExtension = (items) => {
637
+ const completionSource = (context) => {
638
+ const dotMatch = context.matchBefore(/\w+\.\w*/);
639
+ if (dotMatch) {
640
+ const dotPos = dotMatch.text.indexOf('.');
641
+ const objectName = dotMatch.text.slice(0, dotPos);
642
+ const parent = items.find((i) => i.label === objectName);
643
+ if (parent?.properties?.length) {
644
+ return {
645
+ from: dotMatch.from + dotPos + 1,
646
+ options: parent.properties.map((p) => ({ label: p.label, type: p.type ?? 'property', detail: p.detail ?? '', info: p.info ?? '' })),
647
+ };
648
+ }
649
+ }
650
+ const word = context.matchBefore(/\w*/);
651
+ if (!word || (word.from === word.to && !context.explicit))
652
+ return null;
653
+ return {
654
+ from: word.from,
655
+ options: items.map((item) => ({ label: item.label, type: item.type ?? 'variable', detail: item.detail ?? '', info: item.info ?? '' })),
656
+ };
657
+ };
658
+ return autocompletion({ override: [completionSource] });
659
+ };
660
+
661
+ const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
662
+ const toGlobalRegex = (pattern) => {
663
+ if (pattern instanceof RegExp) {
664
+ const flags = pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`;
665
+ return new RegExp(pattern.source, flags);
666
+ }
667
+ return new RegExp(escapeRegex(pattern), 'gi');
668
+ };
669
+ /**
670
+ * Extension bảo mật gồm 2 layer:
671
+ * 1. Linter: highlight đỏ tại đúng vị trí match (bỏ qua match trong comment) + gọi `emitViolations`.
672
+ * 2. Transaction filter: chặn paste nếu nội dung paste chứa pattern cấm.
673
+ */
674
+ const buildForbiddenExtension = (patterns, emitViolations) => {
675
+ const forbiddenLinter = linter((view) => {
676
+ const diagnostics = [];
677
+ const violations = [];
678
+ const text = view.state.doc.toString();
679
+ const commentRanges = [];
680
+ syntaxTree(view.state).iterate({
681
+ enter: (node) => {
682
+ if (node.type.name.toLowerCase().includes('comment')) {
683
+ commentRanges.push({ from: node.from, to: node.to });
684
+ }
685
+ },
686
+ });
687
+ const isInComment = (from, to) => commentRanges.some((r) => from >= r.from && to <= r.to);
688
+ for (const pattern of patterns) {
689
+ const re = toGlobalRegex(pattern);
690
+ re.lastIndex = 0;
691
+ let match;
692
+ while ((match = re.exec(text)) !== null) {
693
+ const from = match.index;
694
+ const to = from + match[0].length;
695
+ if (isInComment(from, to))
696
+ continue;
697
+ const line = view.state.doc.lineAt(from).number;
698
+ const patternLabel = pattern instanceof RegExp ? pattern.source : pattern;
699
+ diagnostics.push({ from, to, severity: 'error', message: `Mã bị cấm: "${match[0]}" — vi phạm chính sách bảo mật` });
700
+ violations.push({ from, to, line, pattern: patternLabel, matched: match[0] });
701
+ }
702
+ }
703
+ emitViolations(violations);
704
+ return diagnostics;
705
+ });
706
+ const pasteFilter = EditorState.transactionFilter.of((tr) => {
707
+ if (!tr.docChanged)
708
+ return tr;
709
+ if (!tr.annotation(Transaction.userEvent)?.startsWith('input.paste'))
710
+ return tr;
711
+ const newText = tr.newDoc.toString();
712
+ for (const pattern of patterns) {
713
+ const re = toGlobalRegex(pattern);
714
+ re.lastIndex = 0;
715
+ if (re.test(newText))
716
+ return [];
717
+ }
718
+ return tr;
719
+ });
720
+ return [forbiddenLinter, pasteFilter];
721
+ };
722
+
723
+ /** Parse giá trị CSS px → number. Trả về undefined nếu không phải px hợp lệ (vd `none`, `0`). */
724
+ const parsePx = (raw) => {
725
+ if (!raw || !raw.endsWith('px')) {
726
+ return undefined;
727
+ }
728
+ const value = parseFloat(raw);
729
+ return Number.isFinite(value) && value > 0 ? value : undefined;
730
+ };
731
+ /** 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. */
732
+ const applyMinLines = (content, minLines) => {
733
+ const value = content ?? '';
734
+ if (!minLines || minLines <= 0) {
735
+ return value;
736
+ }
737
+ const currentLines = value === '' ? 1 : value.split('\n').length;
738
+ if (currentLines >= minLines) {
739
+ return value;
740
+ }
741
+ return value + '\n'.repeat(minLines - currentLines);
742
+ };
743
+ /**
744
+ * Tính min/max-height vùng cuộn theo CSS của container ngoài, snap về bội số dòng.
745
+ * Trừ offset phần header/toolbar (giữa đỉnh container và đỉnh vùng cuộn) để vừa khít container.
746
+ */
747
+ const applyContainerHeight = (view, container) => {
748
+ const style = getComputedStyle(container);
749
+ const minPx = parsePx(style.minHeight);
750
+ const maxPx = parsePx(style.maxHeight);
751
+ if (minPx === undefined && maxPx === undefined) {
752
+ return;
753
+ }
754
+ view.requestMeasure({
755
+ read: (measureView) => {
756
+ const line = measureView.contentDOM.querySelector('.cm-line');
757
+ const lineHeight = (line ? line.getBoundingClientRect().height : measureView.defaultLineHeight) || 20;
758
+ const offsetTop = Math.max(measureView.scrollDOM.getBoundingClientRect().top - container.getBoundingClientRect().top, 0);
759
+ const toLines = (px) => Math.max(1, Math.floor((px - offsetTop - 8) / lineHeight));
760
+ return { lineHeight, minLines: minPx === undefined ? undefined : toLines(minPx), maxLines: maxPx === undefined ? undefined : toLines(maxPx) };
761
+ },
762
+ write: ({ lineHeight, minLines, maxLines }, writeView) => {
763
+ if (maxLines !== undefined) {
764
+ writeView.scrollDOM.style.maxHeight = `${maxLines * lineHeight + 8}px`;
765
+ writeView.scrollDOM.style.overflowY = 'auto';
766
+ }
767
+ // min-height: bù dòng trống vào content (hiện đủ line-number 1..N), giống cơ chế minLines.
768
+ if (minLines !== undefined) {
769
+ const currentLines = writeView.state.doc.lines;
770
+ if (currentLines < minLines) {
771
+ queueMicrotask(() => writeView.dispatch({ changes: { from: writeView.state.doc.length, insert: '\n'.repeat(minLines - currentLines) } }));
772
+ }
773
+ }
774
+ },
775
+ });
776
+ };
777
+ /**
778
+ * Giới hạn chiều cao editor (xử lý 1 lần). 2 chế độ loại trừ nhau:
779
+ * - `maxLines`: cap theo số dòng truyền vào.
780
+ * - `container`: tự tính số dòng theo `min-height`/`max-height` (CSS) của container, snap bội số dòng.
781
+ * Cấu hình đồng thời `container` + `minLines`/`maxLines` → throw.
782
+ */
783
+ const applyMaxHeight = (view, options) => {
784
+ const { minLines, maxLines, container } = options;
785
+ if (container && (minLines || maxLines)) {
786
+ throw new Error('[preview-text-data] CẤM cấu hình [containerElement] cùng [minLines]/[maxLines]. Container tự quản lý min/max height.');
787
+ }
788
+ if (!view) {
789
+ return;
790
+ }
791
+ if (container) {
792
+ applyContainerHeight(view, container);
793
+ return;
794
+ }
795
+ if (!maxLines || maxLines <= 0) {
796
+ return;
797
+ }
798
+ view.requestMeasure({
799
+ read: (measureView) => {
800
+ const line = measureView.contentDOM.querySelector('.cm-line');
801
+ return (line ? line.getBoundingClientRect().height : measureView.defaultLineHeight) || 20;
802
+ },
803
+ write: (lineHeight, writeView) => {
804
+ writeView.scrollDOM.style.maxHeight = `${maxLines * lineHeight + 8}px`;
805
+ writeView.scrollDOM.style.overflowY = 'auto';
806
+ },
807
+ });
808
+ };
809
+
171
810
  class LibsUiComponentsPreviewTextDataComponent {
172
811
  // ==========================================================================
173
812
  // PRIVATE PROPERTIES
@@ -211,6 +850,19 @@ class LibsUiComponentsPreviewTextDataComponent {
211
850
  hiddenAction = input(false, {
212
851
  transform: (value) => value ?? false,
213
852
  });
853
+ /** 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). Bỏ trống = không bù. */
854
+ minLines = input();
855
+ /** Số dòng tối đa: vượt quá editor sẽ giới hạn chiều cao và xuất hiện vùng cuộn (xử lý 1 lần khi khởi tạo editor). Bỏ trống = không giới hạn. */
856
+ maxLines = input();
857
+ /**
858
+ * Element container bên ngoài để editor tự tính chiều cao theo `min-height`/`max-height` (CSS) của nó.
859
+ * Khi truyền, editor đọc min/max-height của container, trừ phần header/toolbar phía trên, rồi snap
860
+ * chiều cao vùng cuộn về đúng bội số dòng (không cắt nửa dòng) + bật scroll khi vượt max.
861
+ * CẤM cấu hình đồng thời với `minLines`/`maxLines` (sẽ throw lỗi) — container tự quản lý min/max.
862
+ */
863
+ containerElement = input();
864
+ /** z-index cho popover dropdown chọn ngôn ngữ. */
865
+ zIndexPopover = input();
214
866
  lintIgnorePatterns = input(['Cannot use import statement outside a module', 'Unexpected token export', 'import ', '@angular/core'], {
215
867
  transform: (value) => value ?? ['Cannot use import statement outside a module', 'Unexpected token export', 'import ', '@angular/core'],
216
868
  });
@@ -273,7 +925,7 @@ class LibsUiComponentsPreviewTextDataComponent {
273
925
  if (!this.isInitialized || !this.editorViewInstance)
274
926
  return;
275
927
  this.editorViewInstance.dispatch({
276
- effects: this.completionsCompartment.reconfigure(items.length ? this.buildCompletionExtension(items) : []),
928
+ effects: this.completionsCompartment.reconfigure(items.length ? buildCompletionExtension(items) : []),
277
929
  });
278
930
  });
279
931
  // Effect để cập nhật forbidden patterns khi input thay đổi
@@ -282,7 +934,7 @@ class LibsUiComponentsPreviewTextDataComponent {
282
934
  if (!this.isInitialized || !this.editorViewInstance)
283
935
  return;
284
936
  this.editorViewInstance.dispatch({
285
- effects: this.forbiddenCompartment.reconfigure(patterns.length ? this.buildForbiddenExtension(patterns) : []),
937
+ effects: this.forbiddenCompartment.reconfigure(patterns.length ? buildForbiddenExtension(patterns, (violations) => this.outViolations.emit(violations)) : []),
286
938
  });
287
939
  });
288
940
  // Cleanup khi component bị destroy
@@ -314,10 +966,11 @@ class LibsUiComponentsPreviewTextDataComponent {
314
966
  const languageExtension = await this.loadLanguageExtension(this.langSelected());
315
967
  const linterExtension = await this.loadLinterExtension(this.langSelected());
316
968
  this.editorViewInstance = new EditorView({
317
- doc: this.content(),
969
+ doc: applyMinLines(this.content(), this.minLines()),
318
970
  parent: this.containerPreview().nativeElement,
319
971
  extensions: this.createExtensions(languageExtension, linterExtension),
320
972
  });
973
+ applyMaxHeight(this.editorViewInstance, { minLines: this.minLines(), maxLines: this.maxLines(), container: this.containerElement() });
321
974
  }
322
975
  /**
323
976
  * Tạo danh sách extensions cho editor
@@ -330,8 +983,8 @@ class LibsUiComponentsPreviewTextDataComponent {
330
983
  this.languageCompartment.of(languageExtension),
331
984
  this.linterCompartment.of(linterExtension),
332
985
  // Compartment cho custom completions — Prec.highest để override basicSetup's autocompletion
333
- Prec.highest(this.completionsCompartment.of(initialCompletions.length ? this.buildCompletionExtension(initialCompletions) : [])),
334
- this.forbiddenCompartment.of(initialPatterns.length ? this.buildForbiddenExtension(initialPatterns) : []),
986
+ Prec.highest(this.completionsCompartment.of(initialCompletions.length ? buildCompletionExtension(initialCompletions) : [])),
987
+ this.forbiddenCompartment.of(initialPatterns.length ? buildForbiddenExtension(initialPatterns, (violations) => this.outViolations.emit(violations)) : []),
335
988
  this.createLightTheme(),
336
989
  EditorView.editable.of(this.editable()),
337
990
  this.wrapCompartment.of(EditorView.lineWrapping),
@@ -411,336 +1064,38 @@ class LibsUiComponentsPreviewTextDataComponent {
411
1064
  // ==========================================================================
412
1065
  // PUBLIC METHODS - Linter factories (được gọi từ registry)
413
1066
  // ==========================================================================
414
- /**
415
- * Tạo JavaScript linter với 2-pass detection:
416
- * 1. Text-based scan cho trailing dot (parser thường đặt error node sai vị trí với pattern này)
417
- * 2. syntaxTree cho các lỗi thực sự khác, bỏ qua vùng bị ảnh hưởng bởi trailing dot
418
- */
1067
+ /** JavaScript/TypeScript linter — pipeline trong `linters/js-linter.ts` (babel + trailing-dot + thiếu `;` + biến chưa khai báo). */
419
1068
  createJsLinter() {
420
- return linter((view) => {
421
- const diagnostics = [];
422
- const doc = view.state.doc;
423
- // Pass 1: Text-based trailing dot detection — đặt diagnostic đúng tại vị trí dấu chấm
424
- const trailingDotLines = new Set();
425
- for (let lineNum = 1; lineNum <= doc.lines; lineNum++) {
426
- const line = doc.line(lineNum);
427
- const trimmed = line.text.trimEnd();
428
- const match = /(\w+)\.\s*$/.exec(trimmed);
429
- if (!match)
430
- continue;
431
- trailingDotLines.add(lineNum);
432
- const dotPos = line.from + trimmed.lastIndexOf('.');
433
- diagnostics.push({
434
- from: dotPos,
435
- to: dotPos + 1,
436
- severity: 'error',
437
- message: `Incomplete property access on "${match[1]}"`,
438
- });
439
- }
440
- // Đánh dấu các dòng bị ảnh hưởng bởi trailing dot (trailing-dot line + 4 dòng tiếp theo)
441
- // để lọc cascading errors từ syntaxTree
442
- const taintedLines = new Set();
443
- for (const lineNum of trailingDotLines) {
444
- for (let i = lineNum; i <= Math.min(lineNum + 4, doc.lines); i++) {
445
- taintedLines.add(i);
446
- }
447
- }
448
- // Pass 2: syntaxTree cho các lỗi khác (chỉ non-empty range, bỏ qua vùng tainted)
449
- const tree = syntaxTree(view.state);
450
- tree.iterate({
451
- enter: (node) => {
452
- if (!node.type.isError)
453
- return;
454
- if (node.from === node.to)
455
- return; // Bỏ qua empty-range node (cascading error)
456
- const errorLineNum = doc.lineAt(node.from).number;
457
- if (taintedLines.has(errorLineNum))
458
- return;
459
- diagnostics.push({
460
- from: node.from,
461
- to: Math.min(node.to, doc.length),
462
- severity: 'error',
463
- message: this.buildJsErrorMessage(doc, node),
464
- });
465
- },
466
- });
1069
+ return linter(async (view) => {
1070
+ const diagnostics = await computeJsDiagnostics(view.state.doc, view.state.doc.toString(), new Set(this.completions().map((item) => item.label)));
467
1071
  this.syntaxErrors.emit(diagnostics);
468
1072
  return diagnostics;
469
1073
  });
470
1074
  }
471
- buildJsErrorMessage(doc, node) {
472
- const text = doc.sliceString(node.from, node.to).trim();
473
- if (text)
474
- return `JavaScript syntax error near "${text}"`;
475
- const prevToken = this.getTokenBefore(doc, node.from);
476
- if (!prevToken)
477
- return 'JavaScript syntax error';
478
- if (prevToken.endsWith('('))
479
- return 'Missing closing ")"';
480
- if (prevToken.endsWith('['))
481
- return 'Missing closing "]"';
482
- if (/^function$/i.test(prevToken))
483
- return 'Expected function name or "("';
484
- if (/^return$/i.test(prevToken))
485
- return 'Invalid expression after "return"';
486
- if (/^=>$/.test(prevToken))
487
- return 'Expected function body after "=>"';
488
- if (/^const$|^let$|^var$/i.test(prevToken))
489
- return 'Expected variable name';
490
- return `JavaScript syntax error near "${prevToken}"`;
491
- }
492
- /**
493
- * Tạo JSON linter
494
- * Sử dụng JSON.parse để validate
495
- */
1075
+ /** JSON linter — logic trong `linters/json-linter.ts`. */
496
1076
  createJsonLinter() {
497
1077
  return linter((view) => {
498
- const text = view.state.doc.toString();
499
- const diagnostics = [];
500
- try {
501
- JSON.parse(text);
502
- }
503
- catch (err) {
504
- diagnostics.push({
505
- from: 0,
506
- to: text.length,
507
- severity: 'error',
508
- message: get(err, 'message', ''),
509
- });
510
- }
1078
+ const diagnostics = computeJsonDiagnostics(view);
511
1079
  this.syntaxErrors.emit(diagnostics);
512
1080
  return diagnostics;
513
1081
  });
514
1082
  }
515
- /**
516
- * Tạo SQL linter
517
- */
1083
+ /** SQL linter — logic trong `linters/sql-linter.ts`. */
518
1084
  createSqlLinter() {
519
1085
  return linter((view) => {
520
- const diagnostics = [];
521
- const doc = view.state.doc;
522
- const tree = syntaxTree(view.state);
523
- tree.iterate({
524
- enter: (node) => {
525
- if (!node.type.isError)
526
- return;
527
- diagnostics.push({
528
- from: Math.max(0, node.from - 1),
529
- to: node.from,
530
- severity: 'error',
531
- message: this.buildSqlErrorMessage(doc, node),
532
- });
533
- },
534
- });
1086
+ const diagnostics = computeSqlDiagnostics(view);
535
1087
  this.syntaxErrors.emit(diagnostics);
536
1088
  return diagnostics;
537
1089
  });
538
1090
  }
539
- buildSqlErrorMessage(doc, node) {
540
- if (node.from !== node.to) {
541
- const text = doc.sliceString(node.from, node.to);
542
- return `SQL syntax error near "${text}"`;
543
- }
544
- const prevToken = this.getTokenBefore(doc, node.from);
545
- if (!prevToken) {
546
- return 'SQL syntax error at beginning of statement';
547
- }
548
- if (/FROM$/i.test(prevToken)) {
549
- return 'Expected table name after FROM';
550
- }
551
- if (/SELECT$/i.test(prevToken)) {
552
- return 'Expected column list after SELECT';
553
- }
554
- if (/WHERE$/i.test(prevToken)) {
555
- return 'Expected condition after WHERE';
556
- }
557
- if (prevToken.endsWith('(')) {
558
- return 'Missing closing ")"';
559
- }
560
- return `Unexpected end of SQL after "${prevToken}"`;
561
- }
562
- getTokenBefore(doc, pos) {
563
- const start = Math.max(0, pos - 30);
564
- const text = doc.sliceString(start, pos);
565
- const match = /(\S+)\s*$/.exec(text);
566
- return match?.[1] ?? '';
567
- }
568
- /**
569
- * Tạo Python linter dựa trên syntaxTree parse error — cùng approach với SQL linter.
570
- * Phát hiện lỗi cú pháp mà CodeMirror Python parser đánh dấu là error node.
571
- */
1091
+ /** Python linter — logic trong `linters/python-linter.ts`. */
572
1092
  createPythonLinter() {
573
1093
  return linter((view) => {
574
- const diagnostics = [];
575
- const doc = view.state.doc;
576
- const tree = syntaxTree(view.state);
577
- tree.iterate({
578
- enter: (node) => {
579
- if (!node.type.isError)
580
- return;
581
- const from = Math.max(0, node.from - 1);
582
- const to = node.from === node.to ? node.from + 1 : node.to;
583
- diagnostics.push({
584
- from,
585
- to: Math.min(to, doc.length),
586
- severity: 'error',
587
- message: this.buildPythonErrorMessage(doc, node),
588
- });
589
- },
590
- });
1094
+ const diagnostics = computePythonDiagnostics(view);
591
1095
  this.syntaxErrors.emit(diagnostics);
592
1096
  return diagnostics;
593
1097
  });
594
1098
  }
595
- buildPythonErrorMessage(doc, node) {
596
- if (node.from !== node.to) {
597
- const text = doc.sliceString(node.from, node.to).trim();
598
- const textSuffix = text ? ` near "${text}"` : '';
599
- return `Python syntax error${textSuffix}`;
600
- }
601
- const prevToken = this.getTokenBefore(doc, node.from);
602
- if (!prevToken) {
603
- return 'Python syntax error';
604
- }
605
- if (/^def$/i.test(prevToken))
606
- return 'Expected function name after "def"';
607
- if (/^class$/i.test(prevToken))
608
- return 'Expected class name after "class"';
609
- if (/^return$/i.test(prevToken))
610
- return 'Invalid expression after "return"';
611
- if (/^import$/i.test(prevToken))
612
- return 'Expected module name after "import"';
613
- if (/^from$/i.test(prevToken))
614
- return 'Expected module name after "from"';
615
- if (/^if$/i.test(prevToken))
616
- return 'Expected condition after "if"';
617
- if (/^elif$/i.test(prevToken))
618
- return 'Expected condition after "elif"';
619
- if (/^while$/i.test(prevToken))
620
- return 'Expected condition after "while"';
621
- if (/^for$/i.test(prevToken))
622
- return 'Expected variable after "for"';
623
- if (prevToken.endsWith('.'))
624
- return `Incomplete attribute access on "${prevToken.slice(0, -1)}"`;
625
- if (prevToken.endsWith('('))
626
- return 'Missing closing ")"';
627
- if (prevToken.endsWith('['))
628
- return 'Missing closing "]"';
629
- return `Python syntax error near "${prevToken}"`;
630
- }
631
- /**
632
- * Tạo CodeMirror extension cho custom completions từ danh sách input.
633
- * Hỗ trợ 2 chế độ:
634
- * 1. Gõ tên biến → gợi ý top-level items
635
- * 2. Gõ `obj.` → gợi ý properties của obj
636
- */
637
- buildCompletionExtension(items) {
638
- const completionSource = (context) => {
639
- // Chế độ dot-notation: gõ "obj.prop" → gợi ý properties của obj
640
- const dotMatch = context.matchBefore(/\w+\.\w*/);
641
- if (dotMatch) {
642
- const dotPos = dotMatch.text.indexOf('.');
643
- const objectName = dotMatch.text.slice(0, dotPos);
644
- const parent = items.find((i) => i.label === objectName);
645
- if (parent?.properties?.length) {
646
- return {
647
- from: dotMatch.from + dotPos + 1,
648
- options: parent.properties.map((p) => ({
649
- label: p.label,
650
- type: p.type ?? 'property',
651
- detail: p.detail ?? '',
652
- info: p.info ?? '',
653
- })),
654
- };
655
- }
656
- }
657
- // Chế độ top-level: gợi ý tên biến/object khi gõ ký tự
658
- const word = context.matchBefore(/\w*/);
659
- if (!word || (word.from === word.to && !context.explicit))
660
- return null;
661
- return {
662
- from: word.from,
663
- options: items.map((item) => ({
664
- label: item.label,
665
- type: item.type ?? 'variable',
666
- detail: item.detail ?? '',
667
- info: item.info ?? '',
668
- })),
669
- };
670
- };
671
- return autocompletion({ override: [completionSource] });
672
- }
673
- /**
674
- * Tạo extension bảo mật gồm 2 layer:
675
- * 1. Linter: highlight đỏ tại đúng vị trí match + emit outViolations
676
- * 2. Transaction filter: chặn paste nếu nội dung paste chứa pattern cấm
677
- */
678
- buildForbiddenExtension(patterns) {
679
- const toGlobalRegex = (p) => {
680
- if (p instanceof RegExp) {
681
- const flags = p.flags.includes('g') ? p.flags : `${p.flags}g`;
682
- return new RegExp(p.source, flags);
683
- }
684
- return new RegExp(this.escapeRegex(p), 'gi');
685
- };
686
- const forbiddenLinter = linter((view) => {
687
- const diagnostics = [];
688
- const violations = [];
689
- const text = view.state.doc.toString();
690
- const tree = syntaxTree(view.state);
691
- // Build a Set of comment ranges từ AST để skip match nằm trong comment
692
- const commentRanges = [];
693
- tree.iterate({
694
- enter: (node) => {
695
- if (node.type.name.toLowerCase().includes('comment')) {
696
- commentRanges.push({ from: node.from, to: node.to });
697
- }
698
- },
699
- });
700
- const isInComment = (from, to) => commentRanges.some((r) => from >= r.from && to <= r.to);
701
- for (const pattern of patterns) {
702
- const re = toGlobalRegex(pattern);
703
- re.lastIndex = 0;
704
- let match;
705
- while ((match = re.exec(text)) !== null) {
706
- const from = match.index;
707
- const to = from + match[0].length;
708
- if (isInComment(from, to))
709
- continue;
710
- const line = view.state.doc.lineAt(from).number;
711
- const patternLabel = pattern instanceof RegExp ? pattern.source : pattern;
712
- diagnostics.push({
713
- from,
714
- to,
715
- severity: 'error',
716
- message: `Mã bị cấm: "${match[0]}" — vi phạm chính sách bảo mật`,
717
- });
718
- violations.push({ from, to, line, pattern: patternLabel, matched: match[0] });
719
- }
720
- }
721
- this.outViolations.emit(violations);
722
- return diagnostics;
723
- });
724
- // Chặn paste nếu nội dung paste chứa bất kỳ pattern cấm nào
725
- const pasteFilter = EditorState.transactionFilter.of((tr) => {
726
- if (!tr.docChanged)
727
- return tr;
728
- if (!tr.annotation(Transaction.userEvent)?.startsWith('input.paste'))
729
- return tr;
730
- const newText = tr.newDoc.toString();
731
- for (const pattern of patterns) {
732
- const re = toGlobalRegex(pattern);
733
- re.lastIndex = 0;
734
- if (re.test(newText))
735
- return []; // [] = block transaction
736
- }
737
- return tr;
738
- });
739
- return [forbiddenLinter, pasteFilter];
740
- }
741
- escapeRegex(str) {
742
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
743
- }
744
1099
  // ==========================================================================
745
1100
  // PROTECTED METHODS - Event handlers
746
1101
  // ==========================================================================
@@ -786,16 +1141,16 @@ class LibsUiComponentsPreviewTextDataComponent {
786
1141
  });
787
1142
  }
788
1143
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LibsUiComponentsPreviewTextDataComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
789
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: LibsUiComponentsPreviewTextDataComponent, isStandalone: true, selector: "libs_ui-components-preview_text_data", inputs: { content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null }, langSelected: { classPropertyName: "langSelected", publicName: "langSelected", isSignal: true, isRequired: true, transformFunction: null }, langsAccept: { classPropertyName: "langsAccept", publicName: "langsAccept", isSignal: true, isRequired: false, transformFunction: null }, langsChangeLabel: { classPropertyName: "langsChangeLabel", publicName: "langsChangeLabel", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, hiddenAction: { classPropertyName: "hiddenAction", publicName: "hiddenAction", isSignal: true, isRequired: false, transformFunction: null }, lintIgnorePatterns: { classPropertyName: "lintIgnorePatterns", publicName: "lintIgnorePatterns", isSignal: true, isRequired: false, transformFunction: null }, completions: { classPropertyName: "completions", publicName: "completions", isSignal: true, isRequired: false, transformFunction: null }, forbiddenPatterns: { classPropertyName: "forbiddenPatterns", publicName: "forbiddenPatterns", isSignal: true, isRequired: false, transformFunction: null }, background: { classPropertyName: "background", publicName: "background", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { langSelected: "langSelectedChange", outChange: "outChange", syntaxErrors: "syntaxErrors", outViolations: "outViolations" }, viewQueries: [{ propertyName: "containerPreview", first: true, predicate: ["containerPreview"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"libs-ui-preview-data-container flex flex-col w-full h-auto rounded-[8px] libs-ui-border-general px-[8px]\"\n [style.--background-color]=\"background()\"\n [class.pt-[8px]]=\"!hiddenAction()\">\n @if (!hiddenAction()) {\n <div class=\"flex items-center content-between color-[#6a7383]\">\n <libs_ui-components-dropdown\n classInclude=\"w-[200px]\"\n [listConfig]=\"configLoadDataIsHttpConfig()\"\n [listMaxItemShow]=\"5\"\n [isNgContent]=\"true\"\n [readonly]=\"!editable() || !acceptChangeLang()\"\n [listHasButtonUnSelectOption]=\"false\"\n (outSelectKey)=\"handlerSelectKey($event)\">\n <libs_ui-components-buttons-button\n [type]=\"'button-link-third'\"\n [label]=\"labelLang() || ''\"\n [sizeButton]=\"'small'\"\n [classIconRight]=\"editable() && acceptChangeLang() ? 'libs-ui-icon-move-right rotate-90' : ''\"\n [classInclude]=\"'!p-[0px]' + (editable() && acceptChangeLang() ? '' : '!pointer-events-none !cursor-default hover:!text-[#6A7383]')\" />\n </libs_ui-components-dropdown>\n <div class=\"flex items-center\">\n <libs_ui-components-buttons-button\n [type]=\"'button-link-third'\"\n [label]=\"isWrap() ? 'i18n_remove_line_wrap' : 'i18n_line_wrap'\"\n [sizeButton]=\"'small'\"\n [classIconLeft]=\"isWrap() ? 'libs-ui-icon-unwrap' : 'libs-ui-icon-wrap'\"\n [classInclude]=\"'mo-lib-p-0px mo-lib-mr-16px'\"\n (outClick)=\"handlerLineWrap()\" />\n <libs_ui-components-buttons-button\n [type]=\"'button-link-third'\"\n [label]=\"'i18n_copy'\"\n [sizeButton]=\"'small'\"\n [classIconLeft]=\"'libs-ui-icon-copy'\"\n [classInclude]=\"'mo-lib-p-0px'\"\n (outClick)=\"handlerCopy()\" />\n </div>\n </div>\n }\n <div #containerPreview></div>\n</div>\n", styles: [":host ::ng-deep .libs-ui-preview-data-container{background-color:var(--background-color)!important}:host ::ng-deep .libs-ui-preview-data-container .cm-line{white-space:pre-wrap}:host ::ng-deep .libs-ui-preview-data-container .cm-lintRange-error{background-color:#ff323233;border-bottom:2px solid red}:host ::ng-deep .libs-ui-preview-data-container .cm-lintRange-warning{background-color:#ffc80026}:host ::ng-deep .libs-ui-preview-data-container .cm-tooltip-lint{background:#fff8f8;color:#d32f2f;border:1px solid #f44336;padding:8px 10px;font-size:13px;font-family:Inter,sans-serif;border-radius:6px;box-shadow:0 2px 8px #ff000026}:host ::ng-deep .libs-ui-preview-data-container .cm-focused{outline:none!important}:host ::ng-deep .libs-ui-preview-data-container .cm-gutters{background-color:var(--background-color)!important}\n"], dependencies: [{ kind: "component", type: LibsUiComponentsDropdownComponent, selector: "libs_ui-components-dropdown", inputs: ["useXssFilter", "popoverElementRefCustom", "classInclude", "ignoreStopPropagationEvent", "flagMouse", "flagMouseContent", "popoverCustomConfig", "isNgContent", "zIndex", "convertItemSelected", "getPopoverItemSelected", "httpRequestDetailItemById", "lengthKeys", "textDisplayWhenNoSelect", "textDisplayWhenMultiSelect", "classIncludeTextDisplayWhenNoSelect", "fieldLabel", "fieldGetLabel", "labelPopoverConfig", "labelPopoverFullWidth", "hasContentUnitRight", "listSearchNoDataTemplateRef", "dropdownTemplateRefNotSearchNoData", "fieldGetImage", "imageSize", "typeShape", "fieldGetIcon", "fieldGetTextAvatar", "fieldGetColorAvatar", "classAvatarInclude", "getLastTextAfterSpace", "linkImageError", "showError", "showBorderError", "disable", "readonly", "labelConfig", "disableLabel", "listSearchConfig", "isSearchOnline", "listHiddenInputSearch", "listSearchPadding", "listKeySearch", "listDividerClassInclude", "listConfig", "listButtonsOther", "listHasButtonUnSelectOption", "listClickExactly", "listBackgroundCustom", "listMaxItemShow", "listKeySelected", "listMultiKeySelected", "listKeysDisable", "listKeysHidden", "validRequired", "validMaxItemSelected", "changeValidUndefinedResetError", "allowSelectItemMultiple", "focusInputSearch", "onlyEmitDataWhenReset", "resetKeyWhenSelectAllKey", "listConfigHasDivider", "classIncludeIcon", "classIncludeContent", "listIgnoreClassDisableDefaultWhenUseKeysDisableItem", "tabKeyActive", "tabsConfig", "ignoreBorderBottom"], outputs: ["flagMouseChange", "flagMouseContentChange", "lengthKeysChange", "showBorderErrorChange", "listKeySelectedChange", "listMultiKeySelectedChange", "tabKeyActiveChange", "outSelectKey", "outSelectMultiKey", "outFunctionsControl", "outValidEvent", "outChangStageFlagMouse", "outDataChange", "outClickButtonOther", "outShowList", "outChangeTabKeyActive"] }, { kind: "component", type: LibsUiComponentsButtonsButtonComponent, selector: "libs_ui-components-buttons-button", inputs: ["flagMouse", "type", "buttonCustom", "sizeButton", "label", "disable", "isPending", "imageLeft", "classInclude", "classIconLeft", "classIconRight", "classLabel", "iconOnlyType", "popover", "ignoreStopPropagationEvent", "zIndex", "widthLabelPopover", "styleIconLeft", "styleButton", "ignoreFocusWhenInputTab", "ignoreSetClickWhenShowPopover", "ignorePointerEvent", "isActive", "isHandlerEnterDocumentClickButton"], outputs: ["outClick", "outPopoverEvent", "outFunctionsControl"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1144
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: LibsUiComponentsPreviewTextDataComponent, isStandalone: true, selector: "libs_ui-components-preview_text_data", inputs: { content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null }, langSelected: { classPropertyName: "langSelected", publicName: "langSelected", isSignal: true, isRequired: true, transformFunction: null }, langsAccept: { classPropertyName: "langsAccept", publicName: "langsAccept", isSignal: true, isRequired: false, transformFunction: null }, langsChangeLabel: { classPropertyName: "langsChangeLabel", publicName: "langsChangeLabel", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, hiddenAction: { classPropertyName: "hiddenAction", publicName: "hiddenAction", isSignal: true, isRequired: false, transformFunction: null }, minLines: { classPropertyName: "minLines", publicName: "minLines", isSignal: true, isRequired: false, transformFunction: null }, maxLines: { classPropertyName: "maxLines", publicName: "maxLines", isSignal: true, isRequired: false, transformFunction: null }, containerElement: { classPropertyName: "containerElement", publicName: "containerElement", isSignal: true, isRequired: false, transformFunction: null }, zIndexPopover: { classPropertyName: "zIndexPopover", publicName: "zIndexPopover", isSignal: true, isRequired: false, transformFunction: null }, lintIgnorePatterns: { classPropertyName: "lintIgnorePatterns", publicName: "lintIgnorePatterns", isSignal: true, isRequired: false, transformFunction: null }, completions: { classPropertyName: "completions", publicName: "completions", isSignal: true, isRequired: false, transformFunction: null }, forbiddenPatterns: { classPropertyName: "forbiddenPatterns", publicName: "forbiddenPatterns", isSignal: true, isRequired: false, transformFunction: null }, background: { classPropertyName: "background", publicName: "background", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { langSelected: "langSelectedChange", outChange: "outChange", syntaxErrors: "syntaxErrors", outViolations: "outViolations" }, viewQueries: [{ propertyName: "containerPreview", first: true, predicate: ["containerPreview"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"libs-ui-preview-data-container flex flex-col w-full h-auto rounded-[8px] libs-ui-border-general px-[8px]\"\n [style.--background-color]=\"background()\"\n [class.pt-[8px]]=\"!hiddenAction()\">\n @if (!hiddenAction()) {\n <div class=\"flex items-center content-between color-[#6a7383]\">\n <libs_ui-components-dropdown\n classInclude=\"w-[200px]\"\n [listConfig]=\"configLoadDataIsHttpConfig()\"\n [listMaxItemShow]=\"5\"\n [zIndex]=\"zIndexPopover()\"\n [isNgContent]=\"true\"\n [readonly]=\"!editable() || !acceptChangeLang()\"\n [listHasButtonUnSelectOption]=\"false\"\n (outSelectKey)=\"handlerSelectKey($event)\">\n <libs_ui-components-buttons-button\n [type]=\"'button-link-third'\"\n [label]=\"labelLang() || ''\"\n [sizeButton]=\"'small'\"\n [classIconRight]=\"editable() && acceptChangeLang() ? 'libs-ui-icon-move-right rotate-90' : ''\"\n [classInclude]=\"'!p-[0px]' + (editable() && acceptChangeLang() ? '' : '!pointer-events-none !cursor-default hover:!text-[#6A7383]')\" />\n </libs_ui-components-dropdown>\n <div class=\"flex items-center\">\n <libs_ui-components-buttons-button\n [type]=\"'button-link-third'\"\n [label]=\"isWrap() ? 'i18n_remove_line_wrap' : 'i18n_line_wrap'\"\n [sizeButton]=\"'small'\"\n [classIconLeft]=\"isWrap() ? 'libs-ui-icon-unwrap' : 'libs-ui-icon-wrap'\"\n [classInclude]=\"'mo-lib-p-0px mo-lib-mr-16px'\"\n (outClick)=\"handlerLineWrap()\" />\n <libs_ui-components-buttons-button\n [type]=\"'button-link-third'\"\n [label]=\"'i18n_copy'\"\n [sizeButton]=\"'small'\"\n [classIconLeft]=\"'libs-ui-icon-copy'\"\n [classInclude]=\"'mo-lib-p-0px'\"\n (outClick)=\"handlerCopy()\" />\n </div>\n </div>\n }\n <div #containerPreview></div>\n</div>\n", styles: [":host ::ng-deep .libs-ui-preview-data-container{background-color:var(--background-color)!important}:host ::ng-deep .libs-ui-preview-data-container .cm-line{white-space:pre-wrap}:host ::ng-deep .libs-ui-preview-data-container .cm-lintRange-error{background-color:#ff323233;border-bottom:2px solid red}:host ::ng-deep .libs-ui-preview-data-container .cm-lintRange-warning{background-color:#ffc80026}:host ::ng-deep .libs-ui-preview-data-container .cm-tooltip-lint{background:#fff8f8;color:#d32f2f;border:1px solid #f44336;padding:8px 10px;font-size:13px;font-family:Inter,sans-serif;border-radius:6px;box-shadow:0 2px 8px #ff000026}:host ::ng-deep .libs-ui-preview-data-container .cm-focused{outline:none!important}:host ::ng-deep .libs-ui-preview-data-container .cm-gutters{background-color:var(--background-color)!important}\n"], dependencies: [{ kind: "component", type: LibsUiComponentsDropdownComponent, selector: "libs_ui-components-dropdown", inputs: ["useXssFilter", "popoverElementRefCustom", "classInclude", "ignoreStopPropagationEvent", "flagMouse", "flagMouseContent", "popoverCustomConfig", "isNgContent", "zIndex", "convertItemSelected", "getPopoverItemSelected", "httpRequestDetailItemById", "lengthKeys", "textDisplayWhenNoSelect", "textDisplayWhenMultiSelect", "classIncludeTextDisplayWhenNoSelect", "fieldLabel", "fieldGetLabel", "labelPopoverConfig", "labelPopoverFullWidth", "hasContentUnitRight", "listSearchNoDataTemplateRef", "dropdownTemplateRefNotSearchNoData", "fieldGetImage", "imageSize", "typeShape", "fieldGetIcon", "fieldGetTextAvatar", "fieldGetColorAvatar", "classAvatarInclude", "getLastTextAfterSpace", "linkImageError", "showError", "showBorderError", "disable", "readonly", "labelConfig", "disableLabel", "listSearchConfig", "isSearchOnline", "listHiddenInputSearch", "listSearchPadding", "listKeySearch", "listDividerClassInclude", "listConfig", "listButtonsOther", "listHasButtonUnSelectOption", "listClickExactly", "listBackgroundCustom", "listMaxItemShow", "listKeySelected", "listMultiKeySelected", "listKeysDisable", "listKeysHidden", "validRequired", "validMaxItemSelected", "changeValidUndefinedResetError", "allowSelectItemMultiple", "focusInputSearch", "onlyEmitDataWhenReset", "resetKeyWhenSelectAllKey", "listConfigHasDivider", "classIncludeIcon", "classIncludeContent", "listIgnoreClassDisableDefaultWhenUseKeysDisableItem", "tabKeyActive", "tabsConfig", "ignoreBorderBottom"], outputs: ["flagMouseChange", "flagMouseContentChange", "lengthKeysChange", "showBorderErrorChange", "listKeySelectedChange", "listMultiKeySelectedChange", "tabKeyActiveChange", "outSelectKey", "outSelectMultiKey", "outFunctionsControl", "outValidEvent", "outChangStageFlagMouse", "outDataChange", "outClickButtonOther", "outShowList", "outChangeTabKeyActive"] }, { kind: "component", type: LibsUiComponentsButtonsButtonComponent, selector: "libs_ui-components-buttons-button", inputs: ["flagMouse", "type", "buttonCustom", "sizeButton", "label", "disable", "isPending", "imageLeft", "classInclude", "classIconLeft", "classIconRight", "classLabel", "iconOnlyType", "popover", "ignoreStopPropagationEvent", "zIndex", "widthLabelPopover", "styleIconLeft", "styleButton", "ignoreFocusWhenInputTab", "ignoreSetClickWhenShowPopover", "ignorePointerEvent", "isActive", "isHandlerEnterDocumentClickButton"], outputs: ["outClick", "outPopoverEvent", "outFunctionsControl"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
790
1145
  }
791
1146
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LibsUiComponentsPreviewTextDataComponent, decorators: [{
792
1147
  type: Component,
793
- args: [{ selector: 'libs_ui-components-preview_text_data', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [LibsUiComponentsDropdownComponent, LibsUiComponentsButtonsButtonComponent], template: "<div\n class=\"libs-ui-preview-data-container flex flex-col w-full h-auto rounded-[8px] libs-ui-border-general px-[8px]\"\n [style.--background-color]=\"background()\"\n [class.pt-[8px]]=\"!hiddenAction()\">\n @if (!hiddenAction()) {\n <div class=\"flex items-center content-between color-[#6a7383]\">\n <libs_ui-components-dropdown\n classInclude=\"w-[200px]\"\n [listConfig]=\"configLoadDataIsHttpConfig()\"\n [listMaxItemShow]=\"5\"\n [isNgContent]=\"true\"\n [readonly]=\"!editable() || !acceptChangeLang()\"\n [listHasButtonUnSelectOption]=\"false\"\n (outSelectKey)=\"handlerSelectKey($event)\">\n <libs_ui-components-buttons-button\n [type]=\"'button-link-third'\"\n [label]=\"labelLang() || ''\"\n [sizeButton]=\"'small'\"\n [classIconRight]=\"editable() && acceptChangeLang() ? 'libs-ui-icon-move-right rotate-90' : ''\"\n [classInclude]=\"'!p-[0px]' + (editable() && acceptChangeLang() ? '' : '!pointer-events-none !cursor-default hover:!text-[#6A7383]')\" />\n </libs_ui-components-dropdown>\n <div class=\"flex items-center\">\n <libs_ui-components-buttons-button\n [type]=\"'button-link-third'\"\n [label]=\"isWrap() ? 'i18n_remove_line_wrap' : 'i18n_line_wrap'\"\n [sizeButton]=\"'small'\"\n [classIconLeft]=\"isWrap() ? 'libs-ui-icon-unwrap' : 'libs-ui-icon-wrap'\"\n [classInclude]=\"'mo-lib-p-0px mo-lib-mr-16px'\"\n (outClick)=\"handlerLineWrap()\" />\n <libs_ui-components-buttons-button\n [type]=\"'button-link-third'\"\n [label]=\"'i18n_copy'\"\n [sizeButton]=\"'small'\"\n [classIconLeft]=\"'libs-ui-icon-copy'\"\n [classInclude]=\"'mo-lib-p-0px'\"\n (outClick)=\"handlerCopy()\" />\n </div>\n </div>\n }\n <div #containerPreview></div>\n</div>\n", styles: [":host ::ng-deep .libs-ui-preview-data-container{background-color:var(--background-color)!important}:host ::ng-deep .libs-ui-preview-data-container .cm-line{white-space:pre-wrap}:host ::ng-deep .libs-ui-preview-data-container .cm-lintRange-error{background-color:#ff323233;border-bottom:2px solid red}:host ::ng-deep .libs-ui-preview-data-container .cm-lintRange-warning{background-color:#ffc80026}:host ::ng-deep .libs-ui-preview-data-container .cm-tooltip-lint{background:#fff8f8;color:#d32f2f;border:1px solid #f44336;padding:8px 10px;font-size:13px;font-family:Inter,sans-serif;border-radius:6px;box-shadow:0 2px 8px #ff000026}:host ::ng-deep .libs-ui-preview-data-container .cm-focused{outline:none!important}:host ::ng-deep .libs-ui-preview-data-container .cm-gutters{background-color:var(--background-color)!important}\n"] }]
1148
+ args: [{ selector: 'libs_ui-components-preview_text_data', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [LibsUiComponentsDropdownComponent, LibsUiComponentsButtonsButtonComponent], template: "<div\n class=\"libs-ui-preview-data-container flex flex-col w-full h-auto rounded-[8px] libs-ui-border-general px-[8px]\"\n [style.--background-color]=\"background()\"\n [class.pt-[8px]]=\"!hiddenAction()\">\n @if (!hiddenAction()) {\n <div class=\"flex items-center content-between color-[#6a7383]\">\n <libs_ui-components-dropdown\n classInclude=\"w-[200px]\"\n [listConfig]=\"configLoadDataIsHttpConfig()\"\n [listMaxItemShow]=\"5\"\n [zIndex]=\"zIndexPopover()\"\n [isNgContent]=\"true\"\n [readonly]=\"!editable() || !acceptChangeLang()\"\n [listHasButtonUnSelectOption]=\"false\"\n (outSelectKey)=\"handlerSelectKey($event)\">\n <libs_ui-components-buttons-button\n [type]=\"'button-link-third'\"\n [label]=\"labelLang() || ''\"\n [sizeButton]=\"'small'\"\n [classIconRight]=\"editable() && acceptChangeLang() ? 'libs-ui-icon-move-right rotate-90' : ''\"\n [classInclude]=\"'!p-[0px]' + (editable() && acceptChangeLang() ? '' : '!pointer-events-none !cursor-default hover:!text-[#6A7383]')\" />\n </libs_ui-components-dropdown>\n <div class=\"flex items-center\">\n <libs_ui-components-buttons-button\n [type]=\"'button-link-third'\"\n [label]=\"isWrap() ? 'i18n_remove_line_wrap' : 'i18n_line_wrap'\"\n [sizeButton]=\"'small'\"\n [classIconLeft]=\"isWrap() ? 'libs-ui-icon-unwrap' : 'libs-ui-icon-wrap'\"\n [classInclude]=\"'mo-lib-p-0px mo-lib-mr-16px'\"\n (outClick)=\"handlerLineWrap()\" />\n <libs_ui-components-buttons-button\n [type]=\"'button-link-third'\"\n [label]=\"'i18n_copy'\"\n [sizeButton]=\"'small'\"\n [classIconLeft]=\"'libs-ui-icon-copy'\"\n [classInclude]=\"'mo-lib-p-0px'\"\n (outClick)=\"handlerCopy()\" />\n </div>\n </div>\n }\n <div #containerPreview></div>\n</div>\n", styles: [":host ::ng-deep .libs-ui-preview-data-container{background-color:var(--background-color)!important}:host ::ng-deep .libs-ui-preview-data-container .cm-line{white-space:pre-wrap}:host ::ng-deep .libs-ui-preview-data-container .cm-lintRange-error{background-color:#ff323233;border-bottom:2px solid red}:host ::ng-deep .libs-ui-preview-data-container .cm-lintRange-warning{background-color:#ffc80026}:host ::ng-deep .libs-ui-preview-data-container .cm-tooltip-lint{background:#fff8f8;color:#d32f2f;border:1px solid #f44336;padding:8px 10px;font-size:13px;font-family:Inter,sans-serif;border-radius:6px;box-shadow:0 2px 8px #ff000026}:host ::ng-deep .libs-ui-preview-data-container .cm-focused{outline:none!important}:host ::ng-deep .libs-ui-preview-data-container .cm-gutters{background-color:var(--background-color)!important}\n"] }]
794
1149
  }], ctorParameters: () => [] });
795
1150
 
796
1151
  /**
797
1152
  * Generated bundle index. Do not edit.
798
1153
  */
799
1154
 
800
- export { DEFAULT_SECURITY_PATTERNS, LibsUiComponentsPreviewTextDataComponent, createDefaultLanguage, httpRequestConfigGetOptionsLang, languageRegistry, optionsLangData };
1155
+ export { DEFAULT_SECURITY_PATTERNS, LibsUiComponentsPreviewTextDataComponent, astSkipKeys, astTypeKeys, createDefaultLanguage, httpRequestConfigGetOptionsLang, languageRegistry, optionsLangData, tsValueNodes };
801
1156
  //# sourceMappingURL=libs-ui-components-preview-text-data.mjs.map