1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
|
using Newtonsoft.Json.Linq;
using System.Collections;
using System.Data;
using System.Net;
using System.Text;
using System.Text.Json.Nodes;
namespace Tango.Portal.Chat.Web.Utils
{
public static class DataHelper
{
/// <summary>
/// Converts a DataTable into a robust Markdown table string.
/// - HTML-encodes (&, <, >) to avoid HTML injection.
/// - Replaces '|' with | so it doesn't break cell boundaries.
/// - Normalizes CR/LF and turns newlines into <br/> so multi-line cells stay in-row.
/// - Replaces tabs with spaces.
/// - Optionally truncates very long cells.
/// </summary>
public static string ToMarkdownTable(DataTable table, int maxCellChars = 100)
{
if (table == null || table.Columns.Count == 0) return string.Empty;
string Esc(string? s)
{
if (string.IsNullOrEmpty(s)) return string.Empty;
// HTML-encode first (safer in Markdown renderers that allow HTML)
string t = WebUtility.HtmlEncode(s);
// Normalize newlines and tabs
t = t.Replace("\r\n", "\n").Replace("\r", "\n");
t = t.Replace("\t", " ");
t = t.Replace("\n", "<br/>");
// Escape Markdown table pipes via HTML entity (more reliable than backslash)
t = t.Replace("|", "|");
if (maxCellChars > 0 && t.Length > maxCellChars)
t = t.Substring(0, maxCellChars) + "…";
return t;
}
var sb = new StringBuilder();
// Header row
for (int c = 0; c < table.Columns.Count; c++)
sb.Append("| ").Append(Esc(table.Columns[c].ColumnName)).Append(' ');
sb.AppendLine("|");
// Separator row
for (int c = 0; c < table.Columns.Count; c++)
sb.Append("|---");
sb.AppendLine("|");
// Data rows
foreach (DataRow row in table.Rows)
{
for (int c = 0; c < table.Columns.Count; c++)
sb.Append("| ").Append(Esc(row[c]?.ToString())).Append(' ');
sb.AppendLine("|");
}
return sb.ToString();
}
public static object ToPreview(DataTable dt, int maxRows)
{
var cols = dt.Columns.Cast<DataColumn>().Select(c => c.ColumnName).ToArray();
var rows = new List<Dictionary<string, object?>>();
int count = 0;
foreach (DataRow r in dt.Rows)
{
if (count++ >= maxRows) break;
var d = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
foreach (var c in cols)
{
var cell = r[c];
d[c] = NormalizeCell(cell);
}
rows.Add(d);
}
// (Optional but helpful) include the full row count
return new { columns = cols, rows, totalRows = dt.Rows.Count };
}
private static object? NormalizeCell(object? v)
{
if (v is null || v is DBNull) return null;
// Newtonsoft JToken -> System.Text.Json node
if (v is JToken jt)
{
// Preserves arrays/objects rather than stringifying
return JsonNode.Parse(jt.ToString(Newtonsoft.Json.Formatting.None));
}
// Parse JSON strings (dynamic often arrives as text)
if (v is string s)
{
s = s.Trim();
if ((s.StartsWith("[") && s.EndsWith("]")) || (s.StartsWith("{") && s.EndsWith("}")))
{
try { return JsonNode.Parse(s); } catch { /* fall through */ }
}
return s; // plain string
}
// Flatten enumerables (but not strings)
if (v is IEnumerable en && v is not string)
{
var list = new List<object?>();
foreach (var item in en) list.Add(NormalizeCell(item));
return list;
}
// Optional: normalize DateTime -> ISO 8601 for consistency
if (v is DateTime dt) return dt.ToUniversalTime().ToString("o");
// Numbers, bools, etc. pass through
return v;
}
}
}
|