@libs-ui/components-preview-text-data 0.2.357-2 → 0.2.357-20

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,638 @@ 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
+ // Cho phép path nhiều cấp (a.b.c.) — [\w.]+ bắt trọn chuỗi trước con trỏ.
639
+ const dotMatch = context.matchBefore(/[\w.]+\.\w*/);
640
+ if (dotMatch) {
641
+ const segments = dotMatch.text.split('.');
642
+ const partial = segments[segments.length - 1]; // phần đang gõ dở sau dấu '.' cuối
643
+ const pathSegments = segments.slice(0, -1); // các cấp đã hoàn tất
644
+ let current = items.find((item) => item.label === pathSegments[0]);
645
+ for (let level = 1; level < pathSegments.length && current; level++) {
646
+ current = current.properties?.find((property) => property.label === pathSegments[level]);
647
+ }
648
+ if (current?.properties?.length) {
649
+ return {
650
+ from: context.pos - partial.length,
651
+ options: current.properties.map((p) => ({ label: p.label, type: p.type ?? 'property', detail: p.detail ?? '', info: p.info ?? '' })),
652
+ };
653
+ }
654
+ }
655
+ const word = context.matchBefore(/\w*/);
656
+ if (!word || (word.from === word.to && !context.explicit))
657
+ return null;
658
+ return {
659
+ from: word.from,
660
+ options: items.map((item) => ({ label: item.label, type: item.type ?? 'variable', detail: item.detail ?? '', info: item.info ?? '' })),
661
+ };
662
+ };
663
+ return autocompletion({ override: [completionSource] });
664
+ };
665
+
666
+ const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
667
+ const toGlobalRegex = (pattern) => {
668
+ if (pattern instanceof RegExp) {
669
+ const flags = pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`;
670
+ return new RegExp(pattern.source, flags);
671
+ }
672
+ return new RegExp(escapeRegex(pattern), 'gi');
673
+ };
674
+ /**
675
+ * Extension bảo mật gồm 2 layer:
676
+ * 1. Linter: highlight đỏ tại đúng vị trí match (bỏ qua match trong comment) + gọi `emitViolations`.
677
+ * 2. Transaction filter: chặn paste nếu nội dung paste chứa pattern cấm.
678
+ */
679
+ const buildForbiddenExtension = (patterns, emitViolations) => {
680
+ const forbiddenLinter = linter((view) => {
681
+ const diagnostics = [];
682
+ const violations = [];
683
+ const text = view.state.doc.toString();
684
+ const commentRanges = [];
685
+ syntaxTree(view.state).iterate({
686
+ enter: (node) => {
687
+ if (node.type.name.toLowerCase().includes('comment')) {
688
+ commentRanges.push({ from: node.from, to: node.to });
689
+ }
690
+ },
691
+ });
692
+ const isInComment = (from, to) => commentRanges.some((r) => from >= r.from && to <= r.to);
693
+ for (const pattern of patterns) {
694
+ const re = toGlobalRegex(pattern);
695
+ re.lastIndex = 0;
696
+ let match;
697
+ while ((match = re.exec(text)) !== null) {
698
+ const from = match.index;
699
+ const to = from + match[0].length;
700
+ if (isInComment(from, to))
701
+ continue;
702
+ const line = view.state.doc.lineAt(from).number;
703
+ const patternLabel = pattern instanceof RegExp ? pattern.source : pattern;
704
+ diagnostics.push({ from, to, severity: 'error', message: `Mã bị cấm: "${match[0]}" — vi phạm chính sách bảo mật` });
705
+ violations.push({ from, to, line, pattern: patternLabel, matched: match[0] });
706
+ }
707
+ }
708
+ emitViolations(violations);
709
+ return diagnostics;
710
+ });
711
+ const pasteFilter = EditorState.transactionFilter.of((tr) => {
712
+ if (!tr.docChanged)
713
+ return tr;
714
+ if (!tr.annotation(Transaction.userEvent)?.startsWith('input.paste'))
715
+ return tr;
716
+ const newText = tr.newDoc.toString();
717
+ for (const pattern of patterns) {
718
+ const re = toGlobalRegex(pattern);
719
+ re.lastIndex = 0;
720
+ if (re.test(newText))
721
+ return [];
722
+ }
723
+ return tr;
724
+ });
725
+ return [forbiddenLinter, pasteFilter];
726
+ };
727
+
728
+ /** Parse giá trị CSS px → number. Trả về undefined nếu không phải px hợp lệ (vd `none`, `0`). */
729
+ const parsePx = (raw) => {
730
+ if (!raw || !raw.endsWith('px')) {
731
+ return undefined;
732
+ }
733
+ const value = parseFloat(raw);
734
+ return Number.isFinite(value) && value > 0 ? value : undefined;
735
+ };
736
+ /** 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. */
737
+ const applyMinLines = (content, minLines) => {
738
+ const value = content ?? '';
739
+ if (!minLines || minLines <= 0) {
740
+ return value;
741
+ }
742
+ const currentLines = value === '' ? 1 : value.split('\n').length;
743
+ if (currentLines >= minLines) {
744
+ return value;
745
+ }
746
+ return value + '\n'.repeat(minLines - currentLines);
747
+ };
748
+ /**
749
+ * Tính min/max-height vùng cuộn theo CSS của container ngoài, snap về bội số dòng.
750
+ * Trừ offset phần header/toolbar (giữa đỉnh container và đỉnh vùng cuộn) để vừa khít container.
751
+ */
752
+ const applyContainerHeight = (view, container) => {
753
+ const style = getComputedStyle(container);
754
+ const minPx = parsePx(style.minHeight);
755
+ const maxPx = parsePx(style.maxHeight);
756
+ if (minPx === undefined && maxPx === undefined) {
757
+ return;
758
+ }
759
+ view.requestMeasure({
760
+ read: (measureView) => {
761
+ const line = measureView.contentDOM.querySelector('.cm-line');
762
+ const lineHeight = (line ? line.getBoundingClientRect().height : measureView.defaultLineHeight) || 20;
763
+ const offsetTop = Math.max(measureView.scrollDOM.getBoundingClientRect().top - container.getBoundingClientRect().top, 0);
764
+ const toLines = (px) => Math.max(1, Math.floor((px - offsetTop - 8) / lineHeight));
765
+ return { lineHeight, minLines: minPx === undefined ? undefined : toLines(minPx), maxLines: maxPx === undefined ? undefined : toLines(maxPx) };
766
+ },
767
+ write: ({ lineHeight, minLines, maxLines }, writeView) => {
768
+ if (maxLines !== undefined) {
769
+ writeView.scrollDOM.style.maxHeight = `${maxLines * lineHeight + 8}px`;
770
+ writeView.scrollDOM.style.overflowY = 'auto';
771
+ }
772
+ // min-height: bù dòng trống vào content (hiện đủ line-number 1..N), giống cơ chế minLines.
773
+ if (minLines !== undefined) {
774
+ const currentLines = writeView.state.doc.lines;
775
+ if (currentLines < minLines) {
776
+ queueMicrotask(() => writeView.dispatch({ changes: { from: writeView.state.doc.length, insert: '\n'.repeat(minLines - currentLines) } }));
777
+ }
778
+ }
779
+ },
780
+ });
781
+ };
782
+ /**
783
+ * Giới hạn chiều cao editor (xử lý 1 lần). 2 chế độ loại trừ nhau:
784
+ * - `maxLines`: cap theo số dòng truyền vào.
785
+ * - `container`: tự tính số dòng theo `min-height`/`max-height` (CSS) của container, snap bội số dòng.
786
+ * Cấu hình đồng thời `container` + `minLines`/`maxLines` → throw.
787
+ */
788
+ const applyMaxHeight = (view, options) => {
789
+ const { minLines, maxLines, container } = options;
790
+ if (container && (minLines || maxLines)) {
791
+ 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.');
792
+ }
793
+ if (!view) {
794
+ return;
795
+ }
796
+ if (container) {
797
+ applyContainerHeight(view, container);
798
+ return;
799
+ }
800
+ if (!maxLines || maxLines <= 0) {
801
+ return;
802
+ }
803
+ view.requestMeasure({
804
+ read: (measureView) => {
805
+ const line = measureView.contentDOM.querySelector('.cm-line');
806
+ return (line ? line.getBoundingClientRect().height : measureView.defaultLineHeight) || 20;
807
+ },
808
+ write: (lineHeight, writeView) => {
809
+ writeView.scrollDOM.style.maxHeight = `${maxLines * lineHeight + 8}px`;
810
+ writeView.scrollDOM.style.overflowY = 'auto';
811
+ },
812
+ });
813
+ };
814
+
171
815
  class LibsUiComponentsPreviewTextDataComponent {
172
816
  // ==========================================================================
173
817
  // PRIVATE PROPERTIES
@@ -211,6 +855,23 @@ class LibsUiComponentsPreviewTextDataComponent {
211
855
  hiddenAction = input(false, {
212
856
  transform: (value) => value ?? false,
213
857
  });
858
+ /** 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ù. */
859
+ minLines = input();
860
+ /** 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. */
861
+ maxLines = input();
862
+ /**
863
+ * Element container bên ngoài để editor tự tính chiều cao theo `min-height`/`max-height` (CSS) của nó.
864
+ * Khi truyền, editor đọc min/max-height của container, trừ phần header/toolbar phía trên, rồi snap
865
+ * 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.
866
+ * CẤM cấu hình đồng thời với `minLines`/`maxLines` (sẽ throw lỗi) — container tự quản lý min/max.
867
+ */
868
+ containerElement = input();
869
+ /** z-index cho popover dropdown chọn ngôn ngữ. */
870
+ zIndexPopover = input();
871
+ /** Ẩn ô search trong dropdown chọn ngôn ngữ (danh sách ngôn ngữ ngắn thường không cần tìm kiếm). */
872
+ hiddenSearchLang = input(false, {
873
+ transform: (value) => value ?? false,
874
+ });
214
875
  lintIgnorePatterns = input(['Cannot use import statement outside a module', 'Unexpected token export', 'import ', '@angular/core'], {
215
876
  transform: (value) => value ?? ['Cannot use import statement outside a module', 'Unexpected token export', 'import ', '@angular/core'],
216
877
  });
@@ -273,7 +934,7 @@ class LibsUiComponentsPreviewTextDataComponent {
273
934
  if (!this.isInitialized || !this.editorViewInstance)
274
935
  return;
275
936
  this.editorViewInstance.dispatch({
276
- effects: this.completionsCompartment.reconfigure(items.length ? this.buildCompletionExtension(items) : []),
937
+ effects: this.completionsCompartment.reconfigure(items.length ? buildCompletionExtension(items) : []),
277
938
  });
278
939
  });
279
940
  // Effect để cập nhật forbidden patterns khi input thay đổi
@@ -282,7 +943,7 @@ class LibsUiComponentsPreviewTextDataComponent {
282
943
  if (!this.isInitialized || !this.editorViewInstance)
283
944
  return;
284
945
  this.editorViewInstance.dispatch({
285
- effects: this.forbiddenCompartment.reconfigure(patterns.length ? this.buildForbiddenExtension(patterns) : []),
946
+ effects: this.forbiddenCompartment.reconfigure(patterns.length ? buildForbiddenExtension(patterns, (violations) => this.outViolations.emit(violations)) : []),
286
947
  });
287
948
  });
288
949
  // Cleanup khi component bị destroy
@@ -314,10 +975,11 @@ class LibsUiComponentsPreviewTextDataComponent {
314
975
  const languageExtension = await this.loadLanguageExtension(this.langSelected());
315
976
  const linterExtension = await this.loadLinterExtension(this.langSelected());
316
977
  this.editorViewInstance = new EditorView({
317
- doc: this.content(),
978
+ doc: applyMinLines(this.content(), this.minLines()),
318
979
  parent: this.containerPreview().nativeElement,
319
980
  extensions: this.createExtensions(languageExtension, linterExtension),
320
981
  });
982
+ applyMaxHeight(this.editorViewInstance, { minLines: this.minLines(), maxLines: this.maxLines(), container: this.containerElement() });
321
983
  }
322
984
  /**
323
985
  * Tạo danh sách extensions cho editor
@@ -330,8 +992,8 @@ class LibsUiComponentsPreviewTextDataComponent {
330
992
  this.languageCompartment.of(languageExtension),
331
993
  this.linterCompartment.of(linterExtension),
332
994
  // 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) : []),
995
+ Prec.highest(this.completionsCompartment.of(initialCompletions.length ? buildCompletionExtension(initialCompletions) : [])),
996
+ this.forbiddenCompartment.of(initialPatterns.length ? buildForbiddenExtension(initialPatterns, (violations) => this.outViolations.emit(violations)) : []),
335
997
  this.createLightTheme(),
336
998
  EditorView.editable.of(this.editable()),
337
999
  this.wrapCompartment.of(EditorView.lineWrapping),
@@ -411,336 +1073,38 @@ class LibsUiComponentsPreviewTextDataComponent {
411
1073
  // ==========================================================================
412
1074
  // PUBLIC METHODS - Linter factories (được gọi từ registry)
413
1075
  // ==========================================================================
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
- */
1076
+ /** JavaScript/TypeScript linter — pipeline trong `linters/js-linter.ts` (babel + trailing-dot + thiếu `;` + biến chưa khai báo). */
419
1077
  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
- });
1078
+ return linter(async (view) => {
1079
+ const diagnostics = await computeJsDiagnostics(view.state.doc, view.state.doc.toString(), new Set(this.completions().map((item) => item.label)));
467
1080
  this.syntaxErrors.emit(diagnostics);
468
1081
  return diagnostics;
469
1082
  });
470
1083
  }
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
- */
1084
+ /** JSON linter — logic trong `linters/json-linter.ts`. */
496
1085
  createJsonLinter() {
497
1086
  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
- }
1087
+ const diagnostics = computeJsonDiagnostics(view);
511
1088
  this.syntaxErrors.emit(diagnostics);
512
1089
  return diagnostics;
513
1090
  });
514
1091
  }
515
- /**
516
- * Tạo SQL linter
517
- */
1092
+ /** SQL linter — logic trong `linters/sql-linter.ts`. */
518
1093
  createSqlLinter() {
519
1094
  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
- });
1095
+ const diagnostics = computeSqlDiagnostics(view);
535
1096
  this.syntaxErrors.emit(diagnostics);
536
1097
  return diagnostics;
537
1098
  });
538
1099
  }
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
- */
1100
+ /** Python linter — logic trong `linters/python-linter.ts`. */
572
1101
  createPythonLinter() {
573
1102
  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
- });
1103
+ const diagnostics = computePythonDiagnostics(view);
591
1104
  this.syntaxErrors.emit(diagnostics);
592
1105
  return diagnostics;
593
1106
  });
594
1107
  }
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
1108
  // ==========================================================================
745
1109
  // PROTECTED METHODS - Event handlers
746
1110
  // ==========================================================================
@@ -786,16 +1150,16 @@ class LibsUiComponentsPreviewTextDataComponent {
786
1150
  });
787
1151
  }
788
1152
  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 });
1153
+ 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 }, hiddenSearchLang: { classPropertyName: "hiddenSearchLang", publicName: "hiddenSearchLang", 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 [listHiddenInputSearch]=\"hiddenSearchLang()\"\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", "onlyEmitMultiKeyWhenManualClick", "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
1154
  }
791
1155
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LibsUiComponentsPreviewTextDataComponent, decorators: [{
792
1156
  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"] }]
1157
+ 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 [listHiddenInputSearch]=\"hiddenSearchLang()\"\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
1158
  }], ctorParameters: () => [] });
795
1159
 
796
1160
  /**
797
1161
  * Generated bundle index. Do not edit.
798
1162
  */
799
1163
 
800
- export { DEFAULT_SECURITY_PATTERNS, LibsUiComponentsPreviewTextDataComponent, createDefaultLanguage, httpRequestConfigGetOptionsLang, languageRegistry, optionsLangData };
1164
+ export { DEFAULT_SECURITY_PATTERNS, LibsUiComponentsPreviewTextDataComponent, astSkipKeys, astTypeKeys, createDefaultLanguage, httpRequestConfigGetOptionsLang, languageRegistry, optionsLangData, tsValueNodes };
801
1165
  //# sourceMappingURL=libs-ui-components-preview-text-data.mjs.map