namespace ZB.MOM.WW.OtOpcUa.AdminUI.ScriptAnalysis; /// /// Presentation rules for the script editor's Problems list — the markers Monaco reports through /// MonacoEditor.MarkersChanged (the same shape the diagnostics endpoint /// returns), rendered by ScriptProblemsList. /// public static class ScriptProblems { /// Monaco's error severity. public const int Error = 8; /// Monaco's warning severity. public const int Warning = 4; /// Errors first, then warnings, then the rest; within a severity, by position. /// The reported markers (null treated as none). /// The markers in display order. public static IReadOnlyList Order(IEnumerable? markers) => (markers ?? Array.Empty()) .OrderByDescending(m => m.Severity) .ThenBy(m => m.StartLineNumber) .ThenBy(m => m.StartColumn) .ToList(); /// The label for a Monaco severity value. /// 8 = error, 4 = warning, 2 = info, otherwise hint. /// The display label. public static string SeverityLabel(int severity) => severity switch { Error => "Error", Warning => "Warning", 2 => "Info", _ => "Hint", }; /// A one-line count, e.g. "2 errors, 1 warning". /// The reported markers. /// The summary, or "No problems". public static string Summary(IReadOnlyCollection markers) { if (markers.Count == 0) return "No problems"; var errors = markers.Count(m => m.Severity == Error); var warnings = markers.Count(m => m.Severity == Warning); var other = markers.Count - errors - warnings; var parts = new List(3); if (errors > 0) parts.Add(Plural(errors, "error")); if (warnings > 0) parts.Add(Plural(warnings, "warning")); if (other > 0) parts.Add($"{other} other"); return string.Join(", ", parts); } /// A one-line count for a (possibly capped) report, e.g. "200 errors (showing 200 of 4321)". /// The report the editor sent. /// The summary of the shown markers, with the shown/total suffix when some were left out. public static string Summary(ScriptProblemReport report) { var summary = Summary(report.Markers.ToList()); return report.Omitted > 0 ? $"{summary} (showing {report.Markers.Count} of {report.Total})" : summary; } /// Cuts to at most characters, ending in an ellipsis. /// The text. /// The maximum length, ellipsis included (at least 1). /// The text, unchanged when it already fits. public static string Truncate(string text, int maxChars) => text.Length <= maxChars ? text : string.Concat(text.AsSpan(0, Math.Max(0, maxChars - 1)), "…"); private static string Plural(int n, string noun) => n == 1 ? $"1 {noun}" : $"{n} {noun}s"; } /// /// What the editor reported for the Problems list: at most /// markers in display order (errors first) plus the the diagnostics pass produced. The JS caps /// the report before it crosses the circuit — one hub message, bounded by the 1 MB receive limit — and /// clamps it again on arrival. /// /// The markers shown. /// How many markers the diagnostics pass produced (≥ Markers.Count). public sealed record ScriptProblemReport(IReadOnlyList Markers, int Total) { /// No problems. public static readonly ScriptProblemReport Empty = new(Array.Empty(), 0); /// How many markers were left out of . public int Omitted => Math.Max(0, Total - Markers.Count); /// Builds a report from what the editor JS sent, re-applying the circuit caps defensively. /// The markers sent (already capped by the JS). /// The total the JS counted. /// The clamped report. public static ScriptProblemReport FromEditor(IReadOnlyList? markers, int total) { if (markers is null || markers.Count == 0) return total <= 0 ? Empty : new(Array.Empty(), total); var shown = ScriptProblems.Order(markers) .Take(ScriptEditorLimits.MaxReportedMarkers) .Select(m => m.Message.Length <= ScriptEditorLimits.MaxReportedMessageChars ? m : m with { Message = ScriptProblems.Truncate(m.Message, ScriptEditorLimits.MaxReportedMessageChars) }) .ToList(); return new ScriptProblemReport(shown, Math.Max(total, markers.Count)); } }