@salesforce-ux/eslint-plugin-slds 1.0.6-internal → 1.0.7-internal

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.
Files changed (43) hide show
  1. package/README.md +27 -62
  2. package/build/index.js +128 -258
  3. package/build/index.js.map +4 -4
  4. package/build/rules/v9/lwc-token-to-slds-hook.js.map +2 -2
  5. package/build/rules/v9/no-hardcoded-values/handlers/boxShadowHandler.js +30 -86
  6. package/build/rules/v9/no-hardcoded-values/handlers/boxShadowHandler.js.map +3 -3
  7. package/build/rules/v9/no-hardcoded-values/handlers/colorHandler.js +104 -116
  8. package/build/rules/v9/no-hardcoded-values/handlers/colorHandler.js.map +3 -3
  9. package/build/rules/v9/no-hardcoded-values/handlers/densityHandler.js +30 -83
  10. package/build/rules/v9/no-hardcoded-values/handlers/densityHandler.js.map +3 -3
  11. package/build/rules/v9/no-hardcoded-values/handlers/fontHandler.js +74 -78
  12. package/build/rules/v9/no-hardcoded-values/handlers/fontHandler.js.map +3 -3
  13. package/build/rules/v9/no-hardcoded-values/handlers/index.js +99 -175
  14. package/build/rules/v9/no-hardcoded-values/handlers/index.js.map +3 -3
  15. package/build/rules/v9/no-hardcoded-values/no-hardcoded-values-slds1.js +101 -221
  16. package/build/rules/v9/no-hardcoded-values/no-hardcoded-values-slds1.js.map +3 -3
  17. package/build/rules/v9/no-hardcoded-values/no-hardcoded-values-slds2.js +101 -221
  18. package/build/rules/v9/no-hardcoded-values/no-hardcoded-values-slds2.js.map +3 -3
  19. package/build/rules/v9/no-hardcoded-values/noHardcodedValueRule.js +101 -221
  20. package/build/rules/v9/no-hardcoded-values/noHardcodedValueRule.js.map +3 -3
  21. package/build/rules/v9/no-slds-namespace-for-custom-hooks.js.map +2 -2
  22. package/build/rules/v9/no-slds-var-without-fallback.js.map +2 -2
  23. package/build/src/types/index.d.ts +0 -31
  24. package/build/src/utils/color-lib-utils.d.ts +9 -16
  25. package/build/src/utils/hardcoded-shared-utils.d.ts +0 -1
  26. package/build/src/utils/property-matcher.d.ts +1 -3
  27. package/build/types/index.js.map +1 -1
  28. package/build/utils/boxShadowValueParser.js.map +2 -2
  29. package/build/utils/color-lib-utils.js +50 -26
  30. package/build/utils/color-lib-utils.js.map +2 -2
  31. package/build/utils/css-utils.js.map +2 -2
  32. package/build/utils/hardcoded-shared-utils.js +16 -29
  33. package/build/utils/hardcoded-shared-utils.js.map +2 -2
  34. package/build/utils/property-matcher.js +6 -20
  35. package/build/utils/property-matcher.js.map +2 -2
  36. package/eslint.config.mjs +2 -5
  37. package/package.json +2 -2
  38. package/build/rules/v9/no-hardcoded-values/ruleOptionsSchema.js +0 -63
  39. package/build/rules/v9/no-hardcoded-values/ruleOptionsSchema.js.map +0 -7
  40. package/build/src/rules/v9/no-hardcoded-values/ruleOptionsSchema.d.ts +0 -40
  41. package/build/src/utils/custom-mapping-utils.d.ts +0 -9
  42. package/build/utils/custom-mapping-utils.js +0 -62
  43. package/build/utils/custom-mapping-utils.js.map +0 -7
@@ -70,7 +70,11 @@ function isCssColorFunction(value) {
70
70
  }
71
71
 
72
72
  // src/utils/color-lib-utils.ts
73
- var DELTAE_THRESHOLD = 10;
73
+ var LAB_THRESHOLD = 25;
74
+ var isHexCode = (color) => {
75
+ const hexPattern = /^#(?:[0-9a-fA-F]{3}){1,2}$/;
76
+ return hexPattern.test(color);
77
+ };
74
78
  var convertToHex = (color) => {
75
79
  try {
76
80
  return (0, import_chroma_js.default)(color).hex();
@@ -78,40 +82,52 @@ var convertToHex = (color) => {
78
82
  return null;
79
83
  }
80
84
  };
81
- var isHookPropertyMatch = (hook, cssProperty) => {
82
- return hook.properties.includes(cssProperty) || hook.properties.includes("*");
83
- };
84
- function getOrderByCssProp(cssProperty) {
85
- if (cssProperty === "color" || cssProperty === "fill") {
86
- return ["surface", "theme", "feedback", "reference"];
87
- } else if (cssProperty.match(/background/)) {
88
- return ["surface", "surface-inverse", "theme", "feedback", "reference"];
89
- } else if (cssProperty.match(/border/) || cssProperty.match(/outline/) || cssProperty.match(/stroke/)) {
90
- return ["borders", "borders-inverse", "feedback", "theme", "reference"];
91
- }
92
- return ["surface", "surface-inverse", "borders", "borders-inverse", "theme", "feedback", "reference"];
93
- }
94
85
  var findClosestColorHook = (color, supportedColors, cssProperty) => {
95
- const closestHooks = [];
86
+ const returnStylingHooks = [];
87
+ const closestHooksWithSameProperty = [];
88
+ const closestHooksWithoutSameProperty = [];
89
+ const closestHooksWithAllProperty = [];
90
+ const labColor = (0, import_chroma_js.default)(color).lab();
96
91
  Object.entries(supportedColors).forEach(([sldsValue, data]) => {
97
- if (sldsValue && isValidColor(sldsValue)) {
92
+ if (sldsValue && isHexCode(sldsValue)) {
98
93
  const hooks = data;
99
94
  hooks.forEach((hook) => {
100
- const distance = sldsValue.toLowerCase() === color.toLowerCase() ? 0 : import_chroma_js.default.deltaE(sldsValue, color);
101
- if (isHookPropertyMatch(hook, cssProperty) && distance <= DELTAE_THRESHOLD) {
102
- closestHooks.push({ distance, group: hook.group, name: hook.name });
95
+ const labSupportedColor = (0, import_chroma_js.default)(sldsValue).lab();
96
+ const distance = JSON.stringify(labColor) === JSON.stringify(labSupportedColor) ? 0 : import_chroma_js.default.distance(import_chroma_js.default.lab(...labColor), import_chroma_js.default.lab(...labSupportedColor), "lab");
97
+ if (hook.properties.includes(cssProperty)) {
98
+ if (distance <= LAB_THRESHOLD) {
99
+ closestHooksWithSameProperty.push({ name: hook.name, distance });
100
+ }
101
+ } else if (hook.properties.includes("*")) {
102
+ if (distance <= LAB_THRESHOLD) {
103
+ closestHooksWithAllProperty.push({ name: hook.name, distance });
104
+ }
105
+ } else {
106
+ if (distance <= LAB_THRESHOLD) {
107
+ closestHooksWithoutSameProperty.push({ name: hook.name, distance });
108
+ }
103
109
  }
104
110
  });
105
111
  }
106
112
  });
107
- const hooksByGroupMap = closestHooks.sort((a, b) => a.distance - b.distance).reduce((acc, hook) => {
108
- if (!acc[hook.group]) {
109
- acc[hook.group] = [];
113
+ const closesthookGroups = [
114
+ { hooks: closestHooksWithSameProperty, distance: 0 },
115
+ { hooks: closestHooksWithAllProperty, distance: 0 },
116
+ { hooks: closestHooksWithSameProperty, distance: Infinity },
117
+ // For hooks with distance > 0
118
+ { hooks: closestHooksWithAllProperty, distance: Infinity },
119
+ { hooks: closestHooksWithoutSameProperty, distance: Infinity }
120
+ ];
121
+ for (const group of closesthookGroups) {
122
+ const filteredHooks = group.hooks.filter(
123
+ (h) => group.distance === 0 ? h.distance === 0 : h.distance > 0
124
+ );
125
+ if (returnStylingHooks.length < 1 && filteredHooks.length > 0) {
126
+ filteredHooks.sort((a, b) => a.distance - b.distance);
127
+ returnStylingHooks.push(...filteredHooks.slice(0, 5).map((h) => h.name));
110
128
  }
111
- acc[hook.group].push(hook.name);
112
- return acc;
113
- }, {});
114
- return getOrderByCssProp(cssProperty).map((group) => hooksByGroupMap[group] || []).flat().slice(0, 5);
129
+ }
130
+ return Array.from(new Set(returnStylingHooks));
115
131
  };
116
132
  var isValidColor = (val) => import_chroma_js.default.valid(val);
117
133
  var extractColorValue = (node) => {
@@ -163,9 +179,9 @@ function isDimensionProperty(cssProperty) {
163
179
  function isInsetProperty(cssProperty) {
164
180
  return INSET_REGEX.test(cssProperty);
165
181
  }
166
- function resolveDensityPropertyToMatch(cssProperty) {
182
+ function resolvePropertyToMatch(cssProperty) {
167
183
  const propertyToMatch = cssProperty.toLowerCase();
168
- if (isOutlineWidthProperty(propertyToMatch) || isBorderWidthProperty(propertyToMatch)) {
184
+ if (propertyToMatch === "outline" || propertyToMatch === "outline-width" || isBorderWidthProperty(propertyToMatch)) {
169
185
  return "border-width";
170
186
  } else if (isMarginProperty(propertyToMatch)) {
171
187
  return "margin";
@@ -177,23 +193,13 @@ function resolveDensityPropertyToMatch(cssProperty) {
177
193
  return "width";
178
194
  } else if (isInsetProperty(propertyToMatch)) {
179
195
  return "top";
180
- }
181
- return propertyToMatch;
182
- }
183
- function resolveColorPropertyToMatch(cssProperty) {
184
- const propertyToMatch = cssProperty.toLowerCase();
185
- if (propertyToMatch === "outline" || propertyToMatch === "outline-color") {
186
- return "border-color";
187
- } else if (propertyToMatch === "background" || propertyToMatch === "background-color") {
196
+ } else if (cssProperty === "background" || cssProperty === "background-color") {
188
197
  return "background-color";
189
- } else if (isBorderColorProperty(propertyToMatch)) {
198
+ } else if (cssProperty === "outline" || cssProperty === "outline-color" || isBorderColorProperty(cssProperty)) {
190
199
  return "border-color";
191
200
  }
192
201
  return propertyToMatch;
193
202
  }
194
- function isOutlineWidthProperty(propertyToMatch) {
195
- return propertyToMatch === "outline" || propertyToMatch === "outline-width";
196
- }
197
203
 
198
204
  // src/utils/hardcoded-shared-utils.ts
199
205
  var import_css_tree2 = require("@eslint/css-tree");
@@ -253,33 +259,13 @@ function isKnownFontWeight(value) {
253
259
  return FONT_WEIGHTS.includes(stringValue.toLowerCase());
254
260
  }
255
261
  function handleShorthandAutoFix(declarationNode, context, valueText, replacements) {
256
- if (!replacements || replacements.length === 0) {
257
- return;
258
- }
259
- const sortedReplacements = replacements.sort((a, b) => b.start - a.start);
260
- const reportNumericValue = context.options?.reportNumericValue || "always";
261
- const fixCallback = (start, originalValue, replacement) => {
262
- let newValue = valueText;
263
- newValue = newValue.substring(0, start) + replacement + newValue.substring(start + originalValue.length);
264
- if (newValue !== valueText) {
265
- return (fixer) => {
266
- return fixer.replaceText(declarationNode.value, newValue);
267
- };
268
- }
269
- };
270
- sortedReplacements.forEach(({ start, end, replacement, displayValue, hasHook, isNumeric }) => {
262
+ const sortedReplacements = replacements.sort((a, b) => a.start - b.start);
263
+ const hasAnyHooks = sortedReplacements.some((r) => r.hasHook);
264
+ const canAutoFix = hasAnyHooks;
265
+ sortedReplacements.forEach(({ start, end, replacement, displayValue, hasHook }) => {
271
266
  const originalValue = valueText.substring(start, end);
272
- if (isNumeric) {
273
- if (reportNumericValue === "never") {
274
- return;
275
- }
276
- if (reportNumericValue === "hasReplacement" && !hasHook) {
277
- return;
278
- }
279
- }
280
- const valueColumnStart = declarationNode.value.loc.start.column + start;
281
- const valueColumnEnd = valueColumnStart + originalValue.length;
282
- const canAutoFix = originalValue !== replacement;
267
+ const valueStartColumn = declarationNode.value.loc.start.column;
268
+ const valueColumn = valueStartColumn + start;
283
269
  const { loc: { start: locStart, end: locEnd } } = declarationNode.value;
284
270
  const reportNode = {
285
271
  ...declarationNode.value,
@@ -287,16 +273,23 @@ function handleShorthandAutoFix(declarationNode, context, valueText, replacement
287
273
  ...declarationNode.value.loc,
288
274
  start: {
289
275
  ...locStart,
290
- column: valueColumnStart
276
+ column: valueColumn
291
277
  },
292
278
  end: {
293
279
  ...locEnd,
294
- column: valueColumnEnd
280
+ column: valueColumn + originalValue.length
295
281
  }
296
282
  }
297
283
  };
298
284
  if (hasHook) {
299
- const fix = canAutoFix ? fixCallback(start, originalValue, replacement) : void 0;
285
+ const fix = canAutoFix ? (fixer) => {
286
+ let newValue = valueText;
287
+ for (let i = sortedReplacements.length - 1; i >= 0; i--) {
288
+ const { start: rStart, end: rEnd, replacement: rReplacement } = sortedReplacements[i];
289
+ newValue = newValue.substring(0, rStart) + rReplacement + newValue.substring(rEnd);
290
+ }
291
+ return fixer.replaceText(declarationNode.value, newValue);
292
+ } : void 0;
300
293
  context.context.report({
301
294
  node: reportNode,
302
295
  messageId: "hardcodedValue",
@@ -450,41 +443,6 @@ function formatSuggestionHooks(hooks) {
450
443
  return "\n" + hooks.map((hook, index) => `${index + 1}. ${hook}`).join("\n");
451
444
  }
452
445
 
453
- // src/utils/custom-mapping-utils.ts
454
- function matchesPropertyPattern(cssProperty, pattern) {
455
- const normalizedProperty = cssProperty.toLowerCase();
456
- const normalizedPattern = pattern.toLowerCase();
457
- if (normalizedProperty === normalizedPattern) {
458
- return true;
459
- }
460
- if (normalizedPattern.endsWith("*")) {
461
- const prefix = normalizedPattern.slice(0, -1);
462
- return normalizedProperty.startsWith(prefix);
463
- }
464
- return false;
465
- }
466
- function getCustomMapping(cssProperty, value, customMapping) {
467
- if (!customMapping) {
468
- return null;
469
- }
470
- const normalizedValue = value.toLowerCase().trim();
471
- for (const [hookName, config] of Object.entries(customMapping)) {
472
- const propertyMatches = config.properties.some(
473
- (pattern) => matchesPropertyPattern(cssProperty, pattern)
474
- );
475
- if (!propertyMatches) {
476
- continue;
477
- }
478
- const valueMatches = config.values.some(
479
- (configValue) => configValue.toLowerCase().trim() === normalizedValue
480
- );
481
- if (valueMatches) {
482
- return hookName;
483
- }
484
- }
485
- return null;
486
- }
487
-
488
446
  // src/rules/v9/no-hardcoded-values/handlers/colorHandler.ts
489
447
  var handleColorDeclaration = (node, context) => {
490
448
  const cssProperty = node.property.toLowerCase();
@@ -508,32 +466,24 @@ function createColorReplacement(colorValue, cssProperty, context, positionInfo,
508
466
  if (!hexValue) {
509
467
  return null;
510
468
  }
469
+ const propToMatch = resolvePropertyToMatch(cssProperty);
470
+ const closestHooks = findClosestColorHook(hexValue, context.valueToStylinghook, propToMatch);
511
471
  const start = positionInfo.start.offset;
512
472
  const end = positionInfo.end.offset;
513
473
  const originalValue = originalValueText ? originalValueText.substring(start, end) : colorValue;
514
- const customHook = getCustomMapping(cssProperty, colorValue, context.options?.customMapping);
515
- let closestHooks = [];
516
- if (customHook) {
517
- closestHooks = [customHook];
518
- } else {
519
- const propToMatch = resolveColorPropertyToMatch(cssProperty);
520
- closestHooks = findClosestColorHook(hexValue, context.valueToStylinghook, propToMatch);
521
- }
522
- let replacement = originalValue;
523
- let paletteHook = null;
524
- if (context.options?.preferPaletteHook && closestHooks.length > 1) {
525
- paletteHook = closestHooks.filter((hook) => hook.includes("-palette-"))[0];
526
- }
527
- if (paletteHook) {
528
- replacement = `var(${paletteHook}, ${colorValue})`;
529
- } else if (closestHooks.length === 1) {
530
- replacement = `var(${closestHooks[0]}, ${colorValue})`;
531
- }
532
- if (closestHooks.length > 0) {
474
+ if (closestHooks.length === 1) {
533
475
  return {
534
476
  start,
535
477
  end,
536
- replacement,
478
+ replacement: `var(${closestHooks[0]}, ${colorValue})`,
479
+ displayValue: closestHooks[0],
480
+ hasHook: true
481
+ };
482
+ } else if (closestHooks.length > 1) {
483
+ return {
484
+ start,
485
+ end,
486
+ replacement: originalValue,
537
487
  // Use original value to preserve spacing
538
488
  displayValue: formatSuggestionHooks(closestHooks),
539
489
  hasHook: true
@@ -542,7 +492,7 @@ function createColorReplacement(colorValue, cssProperty, context, positionInfo,
542
492
  return {
543
493
  start,
544
494
  end,
545
- replacement,
495
+ replacement: originalValue,
546
496
  // Use original value to preserve spacing
547
497
  displayValue: originalValue,
548
498
  hasHook: false
@@ -590,14 +540,8 @@ function createDimensionReplacement(parsedDimension, cssProperty, context, posit
590
540
  return null;
591
541
  }
592
542
  const rawValue = parsedDimension.unit ? `${parsedDimension.number}${parsedDimension.unit}` : parsedDimension.number.toString();
593
- const customHook = getCustomMapping(cssProperty, rawValue, context.options?.customMapping);
594
- let closestHooks = [];
595
- if (customHook) {
596
- closestHooks = [customHook];
597
- } else {
598
- const propToMatch = resolveDensityPropertyToMatch(cssProperty);
599
- closestHooks = getStylingHooksForDensityValue(parsedDimension, context.valueToStylinghook, propToMatch);
600
- }
543
+ const propToMatch = resolvePropertyToMatch(cssProperty);
544
+ const closestHooks = getStylingHooksForDensityValue(parsedDimension, context.valueToStylinghook, propToMatch);
601
545
  const start = positionInfo.start.offset;
602
546
  const end = positionInfo.end.offset;
603
547
  if (closestHooks.length === 1) {
@@ -606,8 +550,7 @@ function createDimensionReplacement(parsedDimension, cssProperty, context, posit
606
550
  end,
607
551
  replacement: `var(${closestHooks[0]}, ${rawValue})`,
608
552
  displayValue: closestHooks[0],
609
- hasHook: true,
610
- isNumeric: true
553
+ hasHook: true
611
554
  };
612
555
  } else if (closestHooks.length > 1) {
613
556
  return {
@@ -615,8 +558,7 @@ function createDimensionReplacement(parsedDimension, cssProperty, context, posit
615
558
  end,
616
559
  replacement: rawValue,
617
560
  displayValue: formatSuggestionHooks(closestHooks),
618
- hasHook: true,
619
- isNumeric: true
561
+ hasHook: true
620
562
  };
621
563
  } else {
622
564
  return {
@@ -624,8 +566,7 @@ function createDimensionReplacement(parsedDimension, cssProperty, context, posit
624
566
  end,
625
567
  replacement: rawValue,
626
568
  displayValue: rawValue,
627
- hasHook: false,
628
- isNumeric: true
569
+ hasHook: false
629
570
  };
630
571
  }
631
572
  }
@@ -666,14 +607,8 @@ function createFontReplacement(fontValue, cssProperty, context, positionInfo) {
666
607
  return null;
667
608
  }
668
609
  const rawValue = fontValue.unit ? `${fontValue.number}${fontValue.unit}` : fontValue.number.toString();
669
- const propToMatch = !fontValue.unit && isKnownFontWeight(fontValue.number) ? "font-weight" : "font-size";
670
- const customHook = getCustomMapping(propToMatch, rawValue, context.options?.customMapping);
671
- let closestHooks = [];
672
- if (customHook) {
673
- closestHooks = [customHook];
674
- } else {
675
- closestHooks = getStylingHooksForDensityValue(fontValue, context.valueToStylinghook, propToMatch);
676
- }
610
+ const propToMatch = !fontValue.unit && isKnownFontWeight(fontValue.number) ? resolvePropertyToMatch("font-weight") : resolvePropertyToMatch("font-size");
611
+ const closestHooks = getStylingHooksForDensityValue(fontValue, context.valueToStylinghook, propToMatch);
677
612
  const start = positionInfo.start.offset;
678
613
  const end = positionInfo.end.offset;
679
614
  if (closestHooks.length === 1) {
@@ -682,8 +617,7 @@ function createFontReplacement(fontValue, cssProperty, context, positionInfo) {
682
617
  end,
683
618
  replacement: `var(${closestHooks[0]}, ${rawValue})`,
684
619
  displayValue: closestHooks[0],
685
- hasHook: true,
686
- isNumeric: true
620
+ hasHook: true
687
621
  };
688
622
  } else if (closestHooks.length > 1) {
689
623
  return {
@@ -691,8 +625,7 @@ function createFontReplacement(fontValue, cssProperty, context, positionInfo) {
691
625
  end,
692
626
  replacement: rawValue,
693
627
  displayValue: formatSuggestionHooks(closestHooks),
694
- hasHook: true,
695
- isNumeric: true
628
+ hasHook: true
696
629
  };
697
630
  } else {
698
631
  return {
@@ -700,8 +633,7 @@ function createFontReplacement(fontValue, cssProperty, context, positionInfo) {
700
633
  end,
701
634
  replacement: rawValue,
702
635
  displayValue: rawValue,
703
- hasHook: false,
704
- isNumeric: true
636
+ hasHook: false
705
637
  };
706
638
  }
707
639
  }
@@ -833,30 +765,9 @@ function shadowValueToHookEntries(supportedStylinghooks) {
833
765
  return [key, value.map((hook) => hook.name)];
834
766
  });
835
767
  }
836
- function reportBoxShadowViolation(node, context, valueText, hooks) {
837
- const positionInfo = {
838
- start: { offset: 0, line: 1, column: 1 },
839
- end: { offset: valueText.length, line: 1, column: valueText.length + 1 }
840
- };
841
- const replacement = createBoxShadowReplacement(
842
- valueText,
843
- hooks,
844
- context,
845
- positionInfo
846
- );
847
- if (replacement) {
848
- const replacements = [replacement];
849
- handleShorthandAutoFix(node, context, valueText, replacements);
850
- }
851
- }
852
768
  var handleBoxShadowDeclaration = (node, context) => {
853
769
  const cssProperty = node.property.toLowerCase();
854
770
  const valueText = context.sourceCode.getText(node.value);
855
- const customHook = getCustomMapping(cssProperty, valueText, context.options?.customMapping);
856
- if (customHook) {
857
- reportBoxShadowViolation(node, context, valueText, [customHook]);
858
- return;
859
- }
860
771
  const shadowHooks = shadowValueToHookEntries(context.valueToStylinghook);
861
772
  const parsedCssValue = toBoxShadowValue(valueText);
862
773
  if (!parsedCssValue) {
@@ -866,7 +777,20 @@ var handleBoxShadowDeclaration = (node, context) => {
866
777
  const parsedValueHook = toBoxShadowValue(shadow);
867
778
  if (parsedValueHook && isBoxShadowMatch(parsedCssValue, parsedValueHook)) {
868
779
  if (closestHooks.length > 0) {
869
- reportBoxShadowViolation(node, context, valueText, closestHooks);
780
+ const positionInfo = {
781
+ start: { offset: 0, line: 1, column: 1 },
782
+ end: { offset: valueText.length, line: 1, column: valueText.length + 1 }
783
+ };
784
+ const replacement = createBoxShadowReplacement(
785
+ valueText,
786
+ closestHooks,
787
+ context,
788
+ positionInfo
789
+ );
790
+ if (replacement) {
791
+ const replacements = [replacement];
792
+ handleShorthandAutoFix(node, context, valueText, replacements);
793
+ }
870
794
  }
871
795
  return;
872
796
  }