using System.Text.Json; using Beam.Abstractions; using HtmlAgilityPack; namespace Beam.Dynamic; public class TableDataProvider : IComposableDataProvider, IComposableDataProvider { public IBinding? Content { get; set; } /// /// One provider per column. Each provider is executed per row. /// Missing columns are filled with defaults that return the td/th text at that column index. /// public IDataProvider[]? ColumnProviders { get; set; } public string[][] Get(HtmlDocument document) { if (Content is null) return []; var node = Select(document); if (node is null) return []; return Get(node); } string IDataProvider.Get(HtmlDocument document) { var node = Select(document); return node is null ? "" : (this as IComposableDataProvider).Get(node); } public string[][] Get(HtmlNode node) { var rows = node.Descendants("tr").ToList(); if (rows.Count == 0) return []; // Determine how many columns we should output: // max of provided providers length and max cell count across rows. var maxCellsInAnyRow = rows .Select(r => r.ChildNodes.Count(n => n.Name == "td" || n.Name == "th")) .DefaultIfEmpty(0) .Max(); var providedCount = ColumnProviders?.Length ?? 0; var columnCount = Math.Max(providedCount, maxCellsInAnyRow); if (columnCount == 0) return []; var effectiveProviders = BuildEffectiveProviders(columnCount); var result = new string[rows.Count][]; for (int r = 0; r < rows.Count; r++) { var rowNode = rows[r]; var rowOut = new string[columnCount]; for (int c = 0; c < columnCount; c++) { var provider = effectiveProviders[c]; if (provider is IComposableDataProvider composable) { // Execute with row context. rowOut[c] = composable.Get(rowNode); } else { // Fallback to document context. rowOut[c] = provider.Get(rowNode.OwnerDocument); } rowOut[c] ??= ""; } result[r] = rowOut; } return result; } string IComposableDataProvider.Get(HtmlNode node) { return JsonSerializer.Serialize(Get(node)); } public HtmlNode? Select(HtmlDocument doc) => Content?.Select(doc); HtmlNode? IComposableDataProvider.Select(HtmlNode node) => node; HtmlNode? IComposableDataProvider.Select(HtmlNode node) => node; private IDataProvider[] BuildEffectiveProviders(int columnCount) { var effective = new IDataProvider[columnCount]; if (ColumnProviders is null || ColumnProviders.Length == 0) { for (int i = 0; i < columnCount; i++) effective[i] = new ColumnCellContentsProvider(i); return effective; } var maxCopy = Math.Min(ColumnProviders.Length, columnCount); for (int i = 0; i < maxCopy; i++) effective[i] = ColumnProviders[i] ?? new ColumnCellContentsProvider(i); for (int i = maxCopy; i < columnCount; i++) effective[i] = new ColumnCellContentsProvider(i); return effective; } /// /// Default column provider: for a given row, returns text of td/th at ColumnIndex. /// private sealed class ColumnCellContentsProvider : IComposableDataProvider { public int ColumnIndex { get; } public ColumnCellContentsProvider(int columnIndex) { ColumnIndex = columnIndex; } public string Get(HtmlDocument document) { var node = Select(document); return node is null ? "" : Get(node); } public string Get(HtmlNode rowNode) { var cells = rowNode .ChildNodes .Where(n => n.Name == "td" || n.Name == "th") .ToList(); if (ColumnIndex < 0 || ColumnIndex >= cells.Count) return ""; return cells[ColumnIndex].InnerText; } public HtmlNode? Select(HtmlDocument doc) => doc.DocumentNode; public HtmlNode? Select(HtmlNode node) => node; } }