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));
}
}