timonel 2.14.0-beta.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,34 @@
1
- import { Document, Scalar, isMap, isScalar, visit } from 'yaml';
1
+ import { Document, Scalar, isMap, isScalar, visit, Pair } from 'yaml';
2
+ import { SecurityUtils } from '../security.js';
2
3
  import { isHelmConstruct, isHelmExpression, createHelmExpression, } from './helmControlStructures.js';
4
+ import { isHelmValue, isHelmFieldConditional, isHelmRange, isHelmWith, } from './valuesRef.js';
5
+ const FIELD_CONDITIONAL = '__FIELD_CONDITIONAL__:';
6
+ const FIELD_WITH = '__FIELD_WITH__:';
7
+ const FIELD_WITH_MARKER = '__FIELD_WITH_MARKER__:';
8
+ function simpleHelmYaml(obj) {
9
+ function convertToPlain(value) {
10
+ if (isHelmValue(value)) {
11
+ return `{{ ${value.__path} }}`;
12
+ }
13
+ if (isHelmExpression(value)) {
14
+ return value.value;
15
+ }
16
+ if (Array.isArray(value)) {
17
+ return value.map(convertToPlain);
18
+ }
19
+ if (value !== null && typeof value === 'object') {
20
+ const result = {};
21
+ for (const [k, v] of Object.entries(value)) {
22
+ result[k] = convertToPlain(v);
23
+ }
24
+ return result;
25
+ }
26
+ return value;
27
+ }
28
+ const plain = convertToPlain(obj);
29
+ const doc = new Document(plain);
30
+ return doc.toString({ lineWidth: 0 });
31
+ }
3
32
  function serializeElseIfChain(elseContent, openTag, closeTag) {
4
33
  let result = '';
5
34
  let currentElse = elseContent;
@@ -14,6 +43,9 @@ function serializeElseIfChain(elseContent, openTag, closeTag) {
14
43
  return { result, remainingElse: currentElse };
15
44
  }
16
45
  function serializeHelmConstruct(construct) {
46
+ if (construct.type === 'fieldConditional') {
47
+ throw new Error('fieldConditional should not be serialized directly. It must be handled in preprocessHelmConstructs.');
48
+ }
17
49
  const trimLeft = construct.options?.trimLeft ?? true;
18
50
  const trimRight = construct.options?.trimRight ?? true;
19
51
  const openTag = `{{${trimLeft ? '-' : ''} `;
@@ -21,22 +53,117 @@ function serializeHelmConstruct(construct) {
21
53
  switch (construct.type) {
22
54
  case 'if': {
23
55
  const data = construct.data;
24
- let result = `${openTag}if ${data.condition}${closeTag}\n`;
56
+ const isInline = construct.options?.inline ?? false;
57
+ const newline = isInline ? '' : '\n';
58
+ const space = isInline ? ' ' : '';
59
+ let result = `${openTag}if ${data.condition}${closeTag}${newline}`;
25
60
  result += serializeHelmContent(data.then);
26
61
  if (data.else !== undefined) {
27
- const { result: elseIfResult, remainingElse } = serializeElseIfChain(data.else, openTag, closeTag);
28
- result += elseIfResult;
29
- if (remainingElse !== undefined) {
30
- result += `\n${openTag}else${closeTag}\n`;
31
- result += serializeHelmContent(remainingElse);
62
+ if (isInline) {
63
+ result += `${space}${openTag}else${closeTag}${space}`;
64
+ result += serializeHelmContent(data.else);
65
+ }
66
+ else {
67
+ const { result: elseIfResult, remainingElse } = serializeElseIfChain(data.else, openTag, closeTag);
68
+ result += elseIfResult;
69
+ if (remainingElse !== undefined) {
70
+ result += `\n${openTag}else${closeTag}\n`;
71
+ result += serializeHelmContent(remainingElse);
72
+ }
32
73
  }
33
74
  }
34
- result += `\n${openTag}end${closeTag}`;
75
+ result += `${newline}${openTag}end${closeTag}`;
35
76
  return result;
36
77
  }
37
78
  case 'fragment': {
38
79
  const data = construct.data;
39
- return data.map((item) => serializeHelmContent(item)).join('\n');
80
+ const strings = [];
81
+ const objects = [];
82
+ for (const item of data) {
83
+ if (typeof item === 'object' &&
84
+ item !== null &&
85
+ !Array.isArray(item) &&
86
+ !isHelmConstruct(item) &&
87
+ !isHelmExpression(item)) {
88
+ objects.push(item);
89
+ }
90
+ else {
91
+ const serialized = serializeHelmContent(item);
92
+ if (serialized.trim().length > 0) {
93
+ strings.push(serialized);
94
+ }
95
+ }
96
+ }
97
+ if (strings.length > 0 && objects.length > 0) {
98
+ const combinedObject = objects.reduce((acc, obj) => {
99
+ if (typeof obj === 'object' && obj !== null) {
100
+ return { ...acc, ...obj };
101
+ }
102
+ return acc;
103
+ }, {});
104
+ const preprocessed = preprocessHelmConstructs(combinedObject);
105
+ const doc = new Document(preprocessed);
106
+ visit(doc, (_key, node) => {
107
+ if (isMap(node)) {
108
+ const isHelmExpr = node.items.some((pair) => isScalar(pair.key) &&
109
+ pair.key.value === '__helmExpression' &&
110
+ isScalar(pair.value) &&
111
+ pair.value.value === true);
112
+ if (isHelmExpr) {
113
+ const valuePair = node.items.find((pair) => isScalar(pair.key) && pair.key.value === 'value');
114
+ if (valuePair && isScalar(valuePair.value)) {
115
+ const value = String(valuePair.value.value);
116
+ const scalar = new Scalar(value);
117
+ if (value.trim().startsWith('{{') && value.includes('\n')) {
118
+ scalar.type = 'BLOCK_LITERAL';
119
+ }
120
+ else {
121
+ scalar.type = 'QUOTE_DOUBLE';
122
+ }
123
+ return scalar;
124
+ }
125
+ }
126
+ }
127
+ return undefined;
128
+ });
129
+ let objectYaml = doc.toString({ lineWidth: 0 }).trim();
130
+ return [...strings, objectYaml].filter((s) => s.trim().length > 0).join('\n');
131
+ }
132
+ if (objects.length > 0) {
133
+ const combinedObject = objects.reduce((acc, obj) => {
134
+ if (typeof obj === 'object' && obj !== null) {
135
+ return { ...acc, ...obj };
136
+ }
137
+ return acc;
138
+ }, {});
139
+ const preprocessed = preprocessHelmConstructs(combinedObject);
140
+ const doc = new Document(preprocessed);
141
+ visit(doc, (_key, node) => {
142
+ if (isMap(node)) {
143
+ const isHelmExpr = node.items.some((pair) => isScalar(pair.key) &&
144
+ pair.key.value === '__helmExpression' &&
145
+ isScalar(pair.value) &&
146
+ pair.value.value === true);
147
+ if (isHelmExpr) {
148
+ const valuePair = node.items.find((pair) => isScalar(pair.key) && pair.key.value === 'value');
149
+ if (valuePair && isScalar(valuePair.value)) {
150
+ const value = String(valuePair.value.value);
151
+ const scalar = new Scalar(value);
152
+ if (value.trim().startsWith('{{') && value.includes('\n')) {
153
+ scalar.type = 'BLOCK_LITERAL';
154
+ }
155
+ else {
156
+ scalar.type = 'QUOTE_DOUBLE';
157
+ }
158
+ return scalar;
159
+ }
160
+ }
161
+ }
162
+ return undefined;
163
+ });
164
+ return doc.toString({ lineWidth: 0 }).trim();
165
+ }
166
+ return strings.filter((s) => s.trim().length > 0).join('\n');
40
167
  }
41
168
  case 'range': {
42
169
  const data = construct.data;
@@ -58,7 +185,7 @@ function serializeHelmConstruct(construct) {
58
185
  if (data.pipe) {
59
186
  result += ` | ${data.pipe}`;
60
187
  }
61
- result += `${closeTag}`;
188
+ result += closeTag;
62
189
  return result;
63
190
  }
64
191
  case 'define': {
@@ -104,15 +231,47 @@ function serializeHelmContent(content) {
104
231
  return String(content);
105
232
  }
106
233
  if (typeof content === 'object') {
107
- const doc = new Document(content);
234
+ const preprocessed = preprocessHelmConstructs(content);
235
+ const doc = new Document(preprocessed);
108
236
  let yaml = doc.toString({ lineWidth: 0 });
109
237
  yaml = yaml.trim();
110
238
  return yaml;
111
239
  }
112
240
  return '';
113
241
  }
114
- function preprocessHelmConstructs(obj) {
242
+ export function preprocessHelmConstructs(obj) {
243
+ if (isHelmValue(obj)) {
244
+ const value = obj;
245
+ return createHelmExpression(`{{ ${value.__path} }}`);
246
+ }
247
+ if (isHelmFieldConditional(obj)) {
248
+ return obj;
249
+ }
250
+ if (isHelmRange(obj)) {
251
+ const range = obj;
252
+ const sourcePath = range.source.__path;
253
+ const itemProxy = { __path: '$item' };
254
+ const indexProxy = { __path: '$index' };
255
+ const content = range.callback(itemProxy, indexProxy);
256
+ const processedContent = preprocessHelmConstructs(content);
257
+ const contentDoc = new Document(processedContent);
258
+ const contentStr = contentDoc.toString({ lineWidth: 0 }).trim();
259
+ return createHelmExpression(`{{- range ${sourcePath} }}\n${contentStr}\n{{- end }}`);
260
+ }
261
+ if (isHelmWith(obj)) {
262
+ return obj;
263
+ }
115
264
  if (isHelmConstruct(obj)) {
265
+ if (obj.type === 'fieldConditional') {
266
+ return obj;
267
+ }
268
+ if (obj.type === 'if') {
269
+ const ifData = obj.data;
270
+ const isInline = obj.options?.inline ?? false;
271
+ if (!isInline && (ifData.else === undefined || !('else' in ifData))) {
272
+ return obj;
273
+ }
274
+ }
116
275
  const helmTemplate = serializeHelmConstruct(obj);
117
276
  return createHelmExpression(helmTemplate);
118
277
  }
@@ -125,6 +284,111 @@ function preprocessHelmConstructs(obj) {
125
284
  }
126
285
  const result = {};
127
286
  for (const [key, value] of Object.entries(obj)) {
287
+ if (isHelmWith(value)) {
288
+ const withBlock = value;
289
+ const source = withBlock.source;
290
+ const sourcePath = source?.__path;
291
+ if (!sourcePath) {
292
+ throw new Error('HelmWith source must have a valid __path');
293
+ }
294
+ const ctxProxy = { __path: '.' };
295
+ const content = withBlock.callback(ctxProxy);
296
+ let contentStr;
297
+ if (isHelmExpression(content)) {
298
+ contentStr = content.value;
299
+ }
300
+ else {
301
+ const processedContent = preprocessHelmConstructs(content);
302
+ const contentDoc = new Document(processedContent);
303
+ contentStr = contentDoc.toString({ lineWidth: 0 }).trim();
304
+ }
305
+ const template = `{{- with ${sourcePath} }}\n${key}:\n ${contentStr}\n{{- end }}`;
306
+ const marker = `__FIELD_WITH__:${key}:${template}`;
307
+ result[`__fieldWithTemplate_${key}`] = createHelmExpression(marker);
308
+ continue;
309
+ }
310
+ if (isHelmConstruct(value) && value.type === 'if') {
311
+ const ifData = value.data;
312
+ const isInline = value.options?.inline ?? false;
313
+ if (!isInline && (ifData.else === undefined || !('else' in ifData))) {
314
+ const preprocessedThen = preprocessHelmConstructs(ifData.then);
315
+ let thenValueStr = '';
316
+ let isMultilineObject = false;
317
+ if (isHelmExpression(preprocessedThen)) {
318
+ thenValueStr = preprocessedThen.value.trim();
319
+ isMultilineObject = thenValueStr.includes('\n');
320
+ }
321
+ else if (typeof preprocessedThen === 'string') {
322
+ thenValueStr = preprocessedThen.trim();
323
+ isMultilineObject = thenValueStr.includes('\n');
324
+ }
325
+ else if (Array.isArray(preprocessedThen)) {
326
+ const tempDoc = new Document(preprocessedThen);
327
+ thenValueStr = tempDoc.toString({ lineWidth: 0 }).trim();
328
+ isMultilineObject = true;
329
+ }
330
+ else {
331
+ const tempDoc = new Document(preprocessedThen);
332
+ thenValueStr = tempDoc.toString({ lineWidth: 0 }).trim();
333
+ isMultilineObject = thenValueStr.includes('\n');
334
+ }
335
+ const constructOptions = isHelmConstruct(value) ? value.options : undefined;
336
+ const trimLeft = constructOptions?.trimLeft ?? true;
337
+ const openTag = `{{${trimLeft ? '-' : ''} `;
338
+ const closeTag = ` ${(constructOptions?.trimRight ?? true) ? '-' : ''}}}`;
339
+ let formattedContent;
340
+ if (isMultilineObject) {
341
+ const indentedValue = thenValueStr
342
+ .split('\n')
343
+ .map((line) => ' ' + line)
344
+ .join('\n');
345
+ formattedContent = `${key}:\n${indentedValue}`;
346
+ }
347
+ else {
348
+ formattedContent = `${key}: ${thenValueStr}`;
349
+ }
350
+ const conditionalTemplate = `${openTag}if ${ifData.condition}${closeTag}\n ${formattedContent}\n${openTag}end${closeTag}`;
351
+ result[key] = createHelmExpression(`__FIELD_CONDITIONAL__:${key}:${conditionalTemplate}`);
352
+ continue;
353
+ }
354
+ }
355
+ if (isHelmFieldConditional(value)) {
356
+ const fieldCond = value;
357
+ const condition = fieldCond.condition.__condition;
358
+ const thenValueStr = simpleHelmYaml(fieldCond.thenValue).trim();
359
+ const isMultilineObject = thenValueStr.includes('\n');
360
+ const startsWithDash = thenValueStr.trimStart().startsWith('-');
361
+ let formattedContent;
362
+ if (isMultilineObject || startsWithDash) {
363
+ const lines = thenValueStr.split('\n');
364
+ const indentedValue = lines.map((line) => ' ' + line).join('\n');
365
+ formattedContent = `${key}:\n${indentedValue}`;
366
+ }
367
+ else {
368
+ formattedContent = `${key}: ${thenValueStr}`;
369
+ }
370
+ const conditionalTemplate = `{{- if ${condition} }}\n${formattedContent}\n{{- end }}`;
371
+ result[key] = createHelmExpression(`__FIELD_CONDITIONAL__:${key}:${conditionalTemplate}`);
372
+ continue;
373
+ }
374
+ if (isHelmConstruct(value) && value.type === 'fieldConditional') {
375
+ const fieldData = value.data;
376
+ const preprocessedThen = preprocessHelmConstructs(fieldData.then);
377
+ let thenValueStr = '';
378
+ if (isHelmExpression(preprocessedThen)) {
379
+ thenValueStr = preprocessedThen.value;
380
+ }
381
+ else if (typeof preprocessedThen === 'string') {
382
+ thenValueStr = preprocessedThen;
383
+ }
384
+ else {
385
+ const tempDoc = new Document(preprocessedThen);
386
+ thenValueStr = tempDoc.toString({ lineWidth: 0 }).trim();
387
+ }
388
+ const conditionalTemplate = `{{- if ${fieldData.condition} }}\n ${fieldData.fieldKey}: ${thenValueStr}\n{{- end }}`;
389
+ result[`__fieldConditionalTemplate_${fieldData.fieldKey}`] = conditionalTemplate;
390
+ continue;
391
+ }
128
392
  result[key] = preprocessHelmConstructs(value);
129
393
  }
130
394
  return result;
@@ -144,10 +408,22 @@ export function dumpHelmAwareYaml(obj, options = {}) {
144
408
  const valuePair = node.items.find((pair) => isScalar(pair.key) && pair.key.value === 'value');
145
409
  if (valuePair && isScalar(valuePair.value)) {
146
410
  const value = String(valuePair.value.value);
411
+ if (value.startsWith(FIELD_CONDITIONAL) ||
412
+ value.startsWith(FIELD_WITH) ||
413
+ value.startsWith(FIELD_WITH_MARKER)) {
414
+ const scalar = new Scalar(value);
415
+ scalar.type = 'BLOCK_LITERAL';
416
+ return scalar;
417
+ }
147
418
  const scalar = new Scalar(value);
148
- if (value.trim().startsWith('{{') && value.includes('\n')) {
419
+ const isMultilineTemplate = value.includes('\n') && value.trim().startsWith('{{');
420
+ const isWithOrRange = value.trim().startsWith('{{- with ') || value.trim().startsWith('{{- range ');
421
+ if (isMultilineTemplate && !isWithOrRange) {
149
422
  scalar.type = 'BLOCK_LITERAL';
150
423
  }
424
+ else if (isWithOrRange) {
425
+ scalar.type = 'PLAIN';
426
+ }
151
427
  else {
152
428
  scalar.type = 'QUOTE_DOUBLE';
153
429
  }
@@ -157,14 +433,216 @@ export function dumpHelmAwareYaml(obj, options = {}) {
157
433
  }
158
434
  return undefined;
159
435
  });
160
- const toStringOptions = {};
436
+ const toStringOptions = { lineWidth: 0 };
161
437
  if (options.lineWidth !== undefined) {
162
438
  toStringOptions.lineWidth = options.lineWidth;
163
439
  }
440
+ const templatePairs = [];
441
+ visit(doc, (_key, node) => {
442
+ if (isMap(node)) {
443
+ for (let i = 0; i < node.items.length; i++) {
444
+ const pair = node.items[i];
445
+ if (pair && isScalar(pair.key)) {
446
+ const keyStr = String(pair.key.value);
447
+ if (keyStr.startsWith('__fieldConditionalTemplate_') && isScalar(pair.value)) {
448
+ const template = String(pair.value.value);
449
+ const fieldKey = keyStr.replace('__fieldConditionalTemplate_', '');
450
+ templatePairs.push({ parent: node, index: i, template, fieldKey });
451
+ }
452
+ }
453
+ }
454
+ }
455
+ return undefined;
456
+ });
457
+ for (let i = templatePairs.length - 1; i >= 0; i--) {
458
+ const templatePair = templatePairs[i];
459
+ if (!templatePair)
460
+ continue;
461
+ const { parent, index, template } = templatePair;
462
+ parent.items.splice(index, 1);
463
+ const tempDoc = new Document({ __temp: template });
464
+ const _tempYaml = tempDoc.toString({ lineWidth: 0 });
465
+ const templateScalar = new Scalar(template);
466
+ templateScalar.type = 'BLOCK_LITERAL';
467
+ const specialKeyScalar = new Scalar('__fieldConditionalContent');
468
+ const templatePairObj = new Pair(specialKeyScalar, templateScalar);
469
+ parent.items.splice(index, 0, templatePairObj);
470
+ }
164
471
  let result = doc.toString(toStringOptions);
472
+ while (result.includes('__fieldConditionalContent:')) {
473
+ const markerIndex = result.indexOf('__fieldConditionalContent:');
474
+ if (markerIndex === -1)
475
+ break;
476
+ const lineStart = result.lastIndexOf('\n', markerIndex);
477
+ const lineBeforeMarker = result.substring(lineStart + 1, markerIndex);
478
+ const indentMatch = lineBeforeMarker.match(/^(\s*)/);
479
+ const baseIndent = indentMatch && indentMatch[1] ? indentMatch[1] : '';
480
+ const afterMarker = result.substring(markerIndex);
481
+ let pipeLineMatch = afterMarker.match(/^__fieldConditionalContent:\s*(\|-?)\s*\n(\s*)/);
482
+ let pipeOnSameLine = true;
483
+ if (!pipeLineMatch) {
484
+ pipeLineMatch = afterMarker.match(/^__fieldConditionalContent:\s*\n(\s*)(\|-?)\s*\n/);
485
+ pipeOnSameLine = false;
486
+ }
487
+ if (!pipeLineMatch)
488
+ break;
489
+ const pipeChar = pipeOnSameLine ? pipeLineMatch[1] : pipeLineMatch[2];
490
+ const pipeIndent = pipeOnSameLine ? pipeLineMatch[2] || '' : pipeLineMatch[1] || '';
491
+ if (!pipeChar)
492
+ break;
493
+ const pipeIndex = markerIndex + pipeLineMatch[0].length;
494
+ const afterPipe = result.substring(pipeIndex);
495
+ const templateStartMatch = afterPipe.match(/^\n(\s*)(\{\{-)/);
496
+ if (!templateStartMatch) {
497
+ const templateStartMatchNoNewline = afterPipe.match(/^(\s*)(\{\{-)/);
498
+ if (!templateStartMatchNoNewline)
499
+ break;
500
+ const _templateIndent = templateStartMatchNoNewline[1] || '';
501
+ const templateStart = templateStartMatchNoNewline[2];
502
+ if (!templateStart)
503
+ break;
504
+ const templateStartIndex = pipeIndex + templateStartMatchNoNewline[0].length;
505
+ const templateSection = result.substring(templateStartIndex);
506
+ const endMatch = templateSection.match(/(\{\{-?\s*end\s*-?\}\})/);
507
+ if (!endMatch)
508
+ break;
509
+ const endIndex = endMatch.index;
510
+ if (endIndex === undefined)
511
+ break;
512
+ const templateEndIndex = templateStartIndex + endIndex + endMatch[0].length;
513
+ const templateContent = result.substring(templateStartIndex, templateEndIndex);
514
+ const beforeMarker = result.substring(0, lineStart + 1);
515
+ const afterTemplate = result.substring(templateEndIndex);
516
+ const pipeIndentEscaped = pipeIndent.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
517
+ const adjustedTemplate = templateContent.replace(new RegExp(`^${pipeIndentEscaped}`, 'gm'), baseIndent);
518
+ result = beforeMarker + adjustedTemplate + afterTemplate;
519
+ continue;
520
+ }
521
+ const _templateIndent = templateStartMatch[1] || '';
522
+ const templateStart = templateStartMatch[2];
523
+ if (!templateStart)
524
+ break;
525
+ const templateStartIndex = pipeIndex + templateStartMatch[0].length;
526
+ const templateSection = result.substring(templateStartIndex);
527
+ const endMatch = templateSection.match(/(\{\{-?\s*end\s*-?\}\})/);
528
+ if (!endMatch)
529
+ break;
530
+ const endIndex = endMatch.index;
531
+ if (endIndex === undefined)
532
+ break;
533
+ const templateEndIndex = templateStartIndex + endIndex + endMatch[0].length;
534
+ const templateContent = result.substring(templateStartIndex, templateEndIndex);
535
+ const beforeMarker = result.substring(0, lineStart + 1);
536
+ const afterTemplate = result.substring(templateEndIndex);
537
+ const pipeIndentEscaped = pipeIndent.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
538
+ const adjustedTemplate = templateContent.replace(new RegExp(`^${pipeIndentEscaped}`, 'gm'), baseIndent);
539
+ result = beforeMarker + adjustedTemplate + afterTemplate;
540
+ }
165
541
  result = result.replace(/\\"/g, '"');
166
- result = result.replace(/\\\\/g, '');
167
- result = result.replace(/:\s*\|[-+]?\s*\n(\s*\{\{)/g, ':\n$1');
542
+ result = result.replace(/^(\s+)(\w+):\s+(\{\{-?\s*if\s+[^}]+\}\})\s*\n(\s+)(.+?)\n(\s+)(\{\{-?\s*end\s*-?\}\})/gm, (match, baseIndent, fieldKey, ifTag, contentIndent, content, endIndent, endTag) => {
543
+ const contentLines = content.split('\n');
544
+ const firstContentLine = contentLines[0]?.trim() || '';
545
+ if (firstContentLine.startsWith(`${fieldKey}:`)) {
546
+ const escapedFieldKey = fieldKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
547
+ contentLines[0] = contentLines[0].replace(new RegExp(`^\\s*${escapedFieldKey}:\\s*`), '');
548
+ content = contentLines.join('\n');
549
+ }
550
+ const contentFormatted = content
551
+ ? '\n' +
552
+ content
553
+ .split('\n')
554
+ .map((line) => baseIndent + ' ' + line.trimStart())
555
+ .join('\n')
556
+ : '';
557
+ return `${baseIndent}${ifTag}\n${baseIndent} ${fieldKey}:${contentFormatted}\n${baseIndent}${endTag}`;
558
+ });
559
+ result = postProcessFieldConditionals(result);
560
+ return result;
561
+ }
562
+ export function postProcessFieldConditionals(yaml) {
563
+ let result = yaml;
564
+ while (result.includes('__FIELD_WITH_MARKER__:')) {
565
+ const markerIndex = result.indexOf('__FIELD_WITH_MARKER__:');
566
+ if (markerIndex === -1)
567
+ break;
568
+ const beforeMarker = result.substring(0, markerIndex);
569
+ const lastNewline = beforeMarker.lastIndexOf('\n');
570
+ const secondLastNewline = beforeMarker.lastIndexOf('\n', lastNewline - 1);
571
+ const fieldKeyLine = result.substring(secondLastNewline + 1, lastNewline);
572
+ const fieldKeyMatch = fieldKeyLine.match(/^(\s*)(\w+):\s*|[-]?\s*$/);
573
+ if (!fieldKeyMatch || !fieldKeyMatch[2])
574
+ break;
575
+ const baseIndent = fieldKeyMatch[1] || '';
576
+ const fieldKey = fieldKeyMatch[2];
577
+ const afterMarker = result.substring(markerIndex);
578
+ const markerMatch = afterMarker.match(/^__FIELD_WITH_MARKER__:([^:]+):(.+?)$/m);
579
+ if (!markerMatch || !markerMatch[1] || !markerMatch[2])
580
+ break;
581
+ const path = markerMatch[1];
582
+ const content = markerMatch[2].trim();
583
+ const markerEnd = markerIndex + markerMatch[0].length;
584
+ const template = `${baseIndent}{{- with ${path} }}\n${baseIndent}${fieldKey}:\n${baseIndent} ${content}\n${baseIndent}{{- end }}`;
585
+ result = result.substring(0, secondLastNewline + 1) + template + result.substring(markerEnd);
586
+ }
587
+ while (result.includes('__FIELD_WITH__:')) {
588
+ const markerIndex = result.indexOf('__FIELD_WITH__:');
589
+ if (markerIndex === -1)
590
+ break;
591
+ const beforeMarker = result.substring(0, markerIndex);
592
+ const lastNewline = beforeMarker.lastIndexOf('\n');
593
+ const secondLastNewline = beforeMarker.lastIndexOf('\n', lastNewline - 1);
594
+ const fieldKeyLine = result.substring(secondLastNewline + 1, lastNewline);
595
+ const fieldKeyMatch = fieldKeyLine.match(/^(\s*)__fieldWithTemplate_\w+:\s*|[-]?\s*$/);
596
+ if (!fieldKeyMatch)
597
+ break;
598
+ const baseIndent = fieldKeyMatch[1];
599
+ const afterMarker = result.substring(markerIndex);
600
+ const markerMatch = afterMarker.match(/^__FIELD_WITH__:\w+:(.+?\{\{-?\s*end\s*-?\}\})/s);
601
+ if (!markerMatch || !markerMatch[1])
602
+ break;
603
+ const template = markerMatch[1];
604
+ const markerEnd = markerIndex + markerMatch[0].length;
605
+ const lines = template.split('\n');
606
+ const processed = lines
607
+ .map((line) => {
608
+ if (!line.trim())
609
+ return '';
610
+ const trimmed = line.trimStart();
611
+ return baseIndent + trimmed;
612
+ })
613
+ .join('\n');
614
+ result = result.substring(0, secondLastNewline + 1) + processed + result.substring(markerEnd);
615
+ }
616
+ while (result.includes('__FIELD_CONDITIONAL__:')) {
617
+ const markerIndex = result.indexOf('__FIELD_CONDITIONAL__:');
618
+ if (markerIndex === -1)
619
+ break;
620
+ const beforeMarker = result.substring(0, markerIndex);
621
+ const lastNewline = beforeMarker.lastIndexOf('\n');
622
+ const secondLastNewline = beforeMarker.lastIndexOf('\n', lastNewline - 1);
623
+ const fieldKeyLine = result.substring(secondLastNewline + 1, lastNewline);
624
+ const fieldKeyMatch = fieldKeyLine.match(/^(\s*)(\w+):\s*|[-]?\s*$/);
625
+ if (!fieldKeyMatch)
626
+ break;
627
+ const baseIndent = fieldKeyMatch[1];
628
+ const afterMarker = result.substring(markerIndex);
629
+ const markerMatch = afterMarker.match(/^__FIELD_CONDITIONAL__:\w+:(.+?\{\{-?\s*end\s*-?\}\})/s);
630
+ if (!markerMatch || !markerMatch[1])
631
+ break;
632
+ const template = markerMatch[1];
633
+ const markerEnd = markerIndex + markerMatch[0].length;
634
+ const lines = template.split('\n');
635
+ const processed = lines
636
+ .map((line) => {
637
+ if (!line.trim())
638
+ return '';
639
+ const trimmed = line.trimStart();
640
+ return baseIndent + trimmed;
641
+ })
642
+ .join('\n');
643
+ result = result.substring(0, secondLastNewline + 1) + processed + result.substring(markerEnd);
644
+ }
645
+ result = result.replace(/:\s*\|[-+]?\s*\n([ \t]*\{\{)/g, ':\n$1');
168
646
  result = result.replace(/"(\{\{[\s\S]*?\}\})"/g, '$1');
169
647
  return result;
170
648
  }
@@ -242,13 +720,14 @@ export function parseHelmExpressions(content) {
242
720
  let match;
243
721
  regex.lastIndex = 0;
244
722
  while ((match = regex.exec(line)) !== null) {
723
+ const sanitizedExpression = SecurityUtils.sanitizeLogMessage(match[0] || '');
245
724
  expressions.push({
246
725
  type,
247
- expression: match[0],
726
+ expression: sanitizedExpression,
248
727
  startLine: i + 1,
249
728
  startCol: match.index + 1,
250
729
  endLine: i + 1,
251
- endCol: match.index + match[0].length + 1,
730
+ endCol: match.index + (match[0]?.length || 0) + 1,
252
731
  });
253
732
  if (match.index === regex.lastIndex)
254
733
  regex.lastIndex++;
@@ -304,14 +783,19 @@ function validateFunctionCalls(_content) {
304
783
  function checkCommonIssues(yaml, warnings) {
305
784
  const deprecatedFunctions = ['template'];
306
785
  for (const func of deprecatedFunctions) {
307
- const pattern = new RegExp(`\\{\\{[^}]*\\b${func}\\b[^}]*\\}\\}`, 'g');
786
+ if (!/^[a-zA-Z0-9_]+$/.test(func)) {
787
+ continue;
788
+ }
789
+ const escapedFunc = func.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
790
+ const pattern = new RegExp(`\\{\\{[^}]*\\b${escapedFunc}\\b[^}]*\\}\\}`, 'g');
308
791
  let match;
309
792
  while ((match = pattern.exec(yaml)) !== null) {
793
+ const sanitizedFunc = SecurityUtils.sanitizeLogMessage(func);
310
794
  warnings.push({
311
795
  type: 'semantic',
312
- message: `Function '${func}' is deprecated`,
796
+ message: `Function '${sanitizedFunc}' is deprecated`,
313
797
  expression: match[0],
314
- suggestion: `Consider avoiding deprecated function '${func}'`,
798
+ suggestion: `Consider avoiding deprecated function '${sanitizedFunc}'`,
315
799
  });
316
800
  }
317
801
  }
@@ -322,11 +806,13 @@ function checkQuotedExpressions(yaml, warnings) {
322
806
  let match;
323
807
  pattern.lastIndex = 0;
324
808
  while ((match = pattern.exec(yaml)) !== null) {
809
+ const sanitizedMatch = SecurityUtils.sanitizeLogMessage(match[1] || '');
810
+ const sanitizedExpression = SecurityUtils.sanitizeLogMessage(match[0] || '');
325
811
  warnings.push({
326
812
  type: 'semantic',
327
813
  message: 'Helm expression should not be quoted',
328
- expression: match[0],
329
- suggestion: `Remove quotes around: ${match[1]}`,
814
+ expression: sanitizedExpression,
815
+ suggestion: `Remove quotes around: ${sanitizedMatch}`,
330
816
  });
331
817
  }
332
818
  }