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

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,687 @@ 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
+ /**
737
+ * Chiều cao MỘT dòng, lấy từ CSS `line-height` của vùng nội dung.
738
+ *
739
+ * 🔴 KHÔNG đo `getBoundingClientRect()` của `.cm-line`: số đó phụ thuộc thời điểm gọi và cho ra
740
+ * hàng loạt giá trị sai khi editor vừa dựng — đo trên modal 08–09/09/2026 với dòng thật `18.19px`
741
+ * đã gặp `5.82` · `11.61` · `15.88` · `24` · `59`, mỗi lần mở một số khác. `defaultLineHeight` của
742
+ * CodeMirror cũng không dùng được vì là số mặc định (20/24px), không theo font sản phẩm.
743
+ *
744
+ * `line-height` trong CSS thì có ngay khi phần tử gắn vào DOM và không đổi theo thời điểm đọc.
745
+ */
746
+ const readLineHeight = (view) => {
747
+ const style = getComputedStyle(view.contentDOM);
748
+ const lineHeight = parseFloat(style.lineHeight);
749
+ if (Number.isFinite(lineHeight) && lineHeight > 0) {
750
+ return lineHeight;
751
+ }
752
+ // `line-height: normal` → suy ra từ cỡ chữ, hệ số 1.4 là mức CodeMirror dùng cho editor.
753
+ const fontSize = parseFloat(style.fontSize);
754
+ if (Number.isFinite(fontSize) && fontSize > 0) {
755
+ return fontSize * 1.4;
756
+ }
757
+ return view.defaultLineHeight || 20;
758
+ };
759
+ /**
760
+ * Chạy `apply` khi đã CÓ dòng thật để đo.
761
+ *
762
+ * Vì sao không chỉ chờ `document.fonts.ready`: editor có thể dựng SAU lúc font đã nạp (vd ô JSON chỉ
763
+ * hiện khi người dùng bấm chạy thử). Lúc đó `fonts.ready` giải quyết ngay nhưng `.cm-line` chưa
764
+ * render, phép đo rơi vào `defaultLineHeight` — đo 08/09/2026: khung JSON thành 895px (48 dòng)
765
+ * thay vì 281px. Nên: chờ cả font VÀ dòng đầu tiên xuất hiện, thử lại tối đa `SO_LAN_THU` nhịp.
766
+ */
767
+ const SO_LAN_THU_DO = 30;
768
+ const runWhenFontReady = (view, apply) => {
769
+ const fonts = typeof document === 'undefined' ? undefined : document.fonts;
770
+ const doKhiSanSang = (conLai) => {
771
+ // Chỉ cần vùng nội dung đã gắn vào DOM là `line-height` đọc được — không cần chờ vẽ dòng.
772
+ if (view.contentDOM.isConnected) {
773
+ apply();
774
+ return;
775
+ }
776
+ if (conLai <= 0) {
777
+ // Hết nhịp chờ vẫn chưa có dòng thật — đo bằng số dự phòng, thà lệch hơn không chặn chiều cao.
778
+ apply();
779
+ return;
780
+ }
781
+ requestAnimationFrame(() => doKhiSanSang(conLai - 1));
782
+ };
783
+ if (!fonts?.ready) {
784
+ doKhiSanSang(SO_LAN_THU_DO);
785
+ return;
786
+ }
787
+ fonts.ready.then(() => doKhiSanSang(SO_LAN_THU_DO)).catch(() => doKhiSanSang(SO_LAN_THU_DO));
788
+ };
789
+ /** 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. */
790
+ const applyMinLines = (content, minLines) => {
791
+ const value = content ?? '';
792
+ if (!minLines || minLines <= 0) {
793
+ return value;
794
+ }
795
+ const currentLines = value === '' ? 1 : value.split('\n').length;
796
+ if (currentLines >= minLines) {
797
+ return value;
798
+ }
799
+ return value + '\n'.repeat(minLines - currentLines);
800
+ };
801
+ /**
802
+ * Tính min/max-height vùng cuộn theo CSS của container ngoài, snap về bội số dòng.
803
+ * Trừ offset phần header/toolbar (giữa đỉnh container và đỉnh vùng cuộn) để vừa khít container.
804
+ */
805
+ const applyContainerHeight = (view, container) => {
806
+ const style = getComputedStyle(container);
807
+ const minPx = parsePx(style.minHeight);
808
+ const maxPx = parsePx(style.maxHeight);
809
+ if (minPx === undefined && maxPx === undefined) {
810
+ return;
811
+ }
812
+ runWhenFontReady(view, () => view.requestMeasure({
813
+ read: (measureView) => {
814
+ const lineHeight = readLineHeight(measureView);
815
+ const offsetTop = Math.max(measureView.scrollDOM.getBoundingClientRect().top - container.getBoundingClientRect().top, 0);
816
+ const toLines = (px) => Math.max(1, Math.floor((px - offsetTop - 8) / lineHeight));
817
+ return { lineHeight, minLines: minPx === undefined ? undefined : toLines(minPx), maxLines: maxPx === undefined ? undefined : toLines(maxPx) };
818
+ },
819
+ write: ({ lineHeight, minLines, maxLines }, writeView) => {
820
+ if (maxLines !== undefined) {
821
+ writeView.scrollDOM.style.maxHeight = `${maxLines * lineHeight + 8}px`;
822
+ writeView.scrollDOM.style.overflowY = 'auto';
823
+ }
824
+ // min-height: bù dòng trống vào content (hiện đủ line-number 1..N), giống cơ chế minLines.
825
+ if (minLines !== undefined) {
826
+ const currentLines = writeView.state.doc.lines;
827
+ if (currentLines < minLines) {
828
+ queueMicrotask(() => writeView.dispatch({ changes: { from: writeView.state.doc.length, insert: '\n'.repeat(minLines - currentLines) } }));
829
+ }
830
+ }
831
+ },
832
+ }));
833
+ };
834
+ /**
835
+ * Giới hạn chiều cao editor (xử lý 1 lần). 2 chế độ loại trừ nhau:
836
+ * - `maxLines`: cap theo số dòng truyền vào.
837
+ * - `container`: tự tính số dòng theo `min-height`/`max-height` (CSS) của container, snap bội số dòng.
838
+ * Cấu hình đồng thời `container` + `minLines`/`maxLines` → throw.
839
+ */
840
+ const applyMaxHeight = (view, options) => {
841
+ const { minLines, maxLines, container } = options;
842
+ if (container && (minLines || maxLines)) {
843
+ 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.');
844
+ }
845
+ if (!view) {
846
+ return;
847
+ }
848
+ if (container) {
849
+ applyContainerHeight(view, container);
850
+ return;
851
+ }
852
+ if (!maxLines || maxLines <= 0) {
853
+ return;
854
+ }
855
+ runWhenFontReady(view, () => view.requestMeasure({
856
+ read: (measureView) => readLineHeight(measureView),
857
+ write: (lineHeight, writeView) => {
858
+ writeView.scrollDOM.style.maxHeight = `${maxLines * lineHeight + 8}px`;
859
+ writeView.scrollDOM.style.overflowY = 'auto';
860
+ },
861
+ }));
862
+ };
863
+
171
864
  class LibsUiComponentsPreviewTextDataComponent {
172
865
  // ==========================================================================
173
866
  // PRIVATE PROPERTIES
@@ -211,6 +904,23 @@ class LibsUiComponentsPreviewTextDataComponent {
211
904
  hiddenAction = input(false, {
212
905
  transform: (value) => value ?? false,
213
906
  });
907
+ /** 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ù. */
908
+ minLines = input();
909
+ /** 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. */
910
+ maxLines = input();
911
+ /**
912
+ * Element container bên ngoài để editor tự tính chiều cao theo `min-height`/`max-height` (CSS) của nó.
913
+ * Khi truyền, editor đọc min/max-height của container, trừ phần header/toolbar phía trên, rồi snap
914
+ * 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.
915
+ * CẤM cấu hình đồng thời với `minLines`/`maxLines` (sẽ throw lỗi) — container tự quản lý min/max.
916
+ */
917
+ containerElement = input();
918
+ /** z-index cho popover dropdown chọn ngôn ngữ. */
919
+ zIndexPopover = input();
920
+ /** Ẩ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). */
921
+ hiddenSearchLang = input(false, {
922
+ transform: (value) => value ?? false,
923
+ });
214
924
  lintIgnorePatterns = input(['Cannot use import statement outside a module', 'Unexpected token export', 'import ', '@angular/core'], {
215
925
  transform: (value) => value ?? ['Cannot use import statement outside a module', 'Unexpected token export', 'import ', '@angular/core'],
216
926
  });
@@ -273,7 +983,7 @@ class LibsUiComponentsPreviewTextDataComponent {
273
983
  if (!this.isInitialized || !this.editorViewInstance)
274
984
  return;
275
985
  this.editorViewInstance.dispatch({
276
- effects: this.completionsCompartment.reconfigure(items.length ? this.buildCompletionExtension(items) : []),
986
+ effects: this.completionsCompartment.reconfigure(items.length ? buildCompletionExtension(items) : []),
277
987
  });
278
988
  });
279
989
  // Effect để cập nhật forbidden patterns khi input thay đổi
@@ -282,7 +992,7 @@ class LibsUiComponentsPreviewTextDataComponent {
282
992
  if (!this.isInitialized || !this.editorViewInstance)
283
993
  return;
284
994
  this.editorViewInstance.dispatch({
285
- effects: this.forbiddenCompartment.reconfigure(patterns.length ? this.buildForbiddenExtension(patterns) : []),
995
+ effects: this.forbiddenCompartment.reconfigure(patterns.length ? buildForbiddenExtension(patterns, (violations) => this.outViolations.emit(violations)) : []),
286
996
  });
287
997
  });
288
998
  // Cleanup khi component bị destroy
@@ -314,10 +1024,11 @@ class LibsUiComponentsPreviewTextDataComponent {
314
1024
  const languageExtension = await this.loadLanguageExtension(this.langSelected());
315
1025
  const linterExtension = await this.loadLinterExtension(this.langSelected());
316
1026
  this.editorViewInstance = new EditorView({
317
- doc: this.content(),
1027
+ doc: applyMinLines(this.content(), this.minLines()),
318
1028
  parent: this.containerPreview().nativeElement,
319
1029
  extensions: this.createExtensions(languageExtension, linterExtension),
320
1030
  });
1031
+ applyMaxHeight(this.editorViewInstance, { minLines: this.minLines(), maxLines: this.maxLines(), container: this.containerElement() });
321
1032
  }
322
1033
  /**
323
1034
  * Tạo danh sách extensions cho editor
@@ -330,8 +1041,8 @@ class LibsUiComponentsPreviewTextDataComponent {
330
1041
  this.languageCompartment.of(languageExtension),
331
1042
  this.linterCompartment.of(linterExtension),
332
1043
  // 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) : []),
1044
+ Prec.highest(this.completionsCompartment.of(initialCompletions.length ? buildCompletionExtension(initialCompletions) : [])),
1045
+ this.forbiddenCompartment.of(initialPatterns.length ? buildForbiddenExtension(initialPatterns, (violations) => this.outViolations.emit(violations)) : []),
335
1046
  this.createLightTheme(),
336
1047
  EditorView.editable.of(this.editable()),
337
1048
  this.wrapCompartment.of(EditorView.lineWrapping),
@@ -411,336 +1122,38 @@ class LibsUiComponentsPreviewTextDataComponent {
411
1122
  // ==========================================================================
412
1123
  // PUBLIC METHODS - Linter factories (được gọi từ registry)
413
1124
  // ==========================================================================
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
- */
1125
+ /** JavaScript/TypeScript linter — pipeline trong `linters/js-linter.ts` (babel + trailing-dot + thiếu `;` + biến chưa khai báo). */
419
1126
  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
- });
1127
+ return linter(async (view) => {
1128
+ const diagnostics = await computeJsDiagnostics(view.state.doc, view.state.doc.toString(), new Set(this.completions().map((item) => item.label)));
467
1129
  this.syntaxErrors.emit(diagnostics);
468
1130
  return diagnostics;
469
1131
  });
470
1132
  }
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
- */
1133
+ /** JSON linter — logic trong `linters/json-linter.ts`. */
496
1134
  createJsonLinter() {
497
1135
  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
- }
1136
+ const diagnostics = computeJsonDiagnostics(view);
511
1137
  this.syntaxErrors.emit(diagnostics);
512
1138
  return diagnostics;
513
1139
  });
514
1140
  }
515
- /**
516
- * Tạo SQL linter
517
- */
1141
+ /** SQL linter — logic trong `linters/sql-linter.ts`. */
518
1142
  createSqlLinter() {
519
1143
  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
- });
1144
+ const diagnostics = computeSqlDiagnostics(view);
535
1145
  this.syntaxErrors.emit(diagnostics);
536
1146
  return diagnostics;
537
1147
  });
538
1148
  }
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
- */
1149
+ /** Python linter — logic trong `linters/python-linter.ts`. */
572
1150
  createPythonLinter() {
573
1151
  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
- });
1152
+ const diagnostics = computePythonDiagnostics(view);
591
1153
  this.syntaxErrors.emit(diagnostics);
592
1154
  return diagnostics;
593
1155
  });
594
1156
  }
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
1157
  // ==========================================================================
745
1158
  // PROTECTED METHODS - Event handlers
746
1159
  // ==========================================================================
@@ -786,16 +1199,16 @@ class LibsUiComponentsPreviewTextDataComponent {
786
1199
  });
787
1200
  }
788
1201
  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 });
1202
+ 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
1203
  }
791
1204
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LibsUiComponentsPreviewTextDataComponent, decorators: [{
792
1205
  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"] }]
1206
+ 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
1207
  }], ctorParameters: () => [] });
795
1208
 
796
1209
  /**
797
1210
  * Generated bundle index. Do not edit.
798
1211
  */
799
1212
 
800
- export { DEFAULT_SECURITY_PATTERNS, LibsUiComponentsPreviewTextDataComponent, createDefaultLanguage, httpRequestConfigGetOptionsLang, languageRegistry, optionsLangData };
1213
+ export { DEFAULT_SECURITY_PATTERNS, LibsUiComponentsPreviewTextDataComponent, astSkipKeys, astTypeKeys, createDefaultLanguage, httpRequestConfigGetOptionsLang, languageRegistry, optionsLangData, tsValueNodes };
801
1214
  //# sourceMappingURL=libs-ui-components-preview-text-data.mjs.map