-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathTable.cs
326 lines (290 loc) · 12.7 KB
/
Table.cs
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
using MDSDKBase;
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
namespace MDSDK
{
/// <summary>
/// Represents a state during attempted parsing for a markdown table.
/// </summary>
internal enum TableParseState
{
NothingFound,
HeaderColumnHeadingsRowFound,
HeaderUnderlineRowFound,
BodyFound,
EndFound
}
/// <summary>
/// A class representing a markdown table row.
/// </summary>
internal class TableRow
{
/// <summary>
/// A string collection representing each cell in the row.
/// </summary>
public List<string> RowCells { get; private set; }
public TableRow(List<string> rowCells)
{
var sanitizedRowCells = new List<string>();
foreach (string cell in rowCells)
{
string trimmedCell = cell.Trim();
while (trimmedCell.EndsWith(@"<br/>") || trimmedCell.EndsWith(@"<br>"))
{
if (trimmedCell.EndsWith(@"<br/>"))
{
trimmedCell = trimmedCell.Substring(0, trimmedCell.LastIndexOf(@"<br/>"));
}
if (trimmedCell.EndsWith(@"<br>"))
{
trimmedCell = trimmedCell.Substring(0, trimmedCell.LastIndexOf(@"<br>"));
}
trimmedCell = trimmedCell.Trim();
}
sanitizedRowCells.Add(trimmedCell);
}
this.RowCells = sanitizedRowCells;
}
}
/// <summary>
/// A class representing a markdown table.
/// This version of the class ignores (discards) alignment; I've never seen a need for it.
/// </summary>
internal class Table
{
/// <summary>
/// A string collection representing the column headings.
/// </summary>
public List<string> ColumnHeadings { get; private set; }
/// <summary>
/// A string collection representing each cell in a row.
/// </summary>
public List<TableRow> Rows { get; private set; }
public int RowCount { get { return this.Rows.Count; } }
public int FirstLineNumberOneBased { get; private set; }
public int LastLineNumberOneBased { get; private set; }
private static Regex TableRowRegex = new Regex(@"\|.*\|", RegexOptions.Compiled);
private static Regex TableCellRegex = new Regex(@"\|[^\|]*", RegexOptions.Compiled);
public Table(List<string>? columnHeadings = null, List<TableRow>? rows = null, int firstLineNumberOneBased = -1)
{
this.ColumnHeadings = columnHeadings ?? new List<string>();
this.Rows = rows ?? new List<TableRow>();
this.FirstLineNumberOneBased = firstLineNumberOneBased;
this.LastLineNumberOneBased = -1;
}
public void RemoveRowNumberOneBased(int rowNumberOneBased)
{
this.Rows.RemoveAt(rowNumberOneBased - 1);
}
public void RemoveRedundantColumns(params string[] redundantColumnHeadings)
{
var redundantColumnHeadingList = new List<string>();
var alreadyEncounteredFirstOccurrence = new List<bool>();
foreach (string columnHeading in redundantColumnHeadings)
{
redundantColumnHeadingList.Add(columnHeading);
alreadyEncounteredFirstOccurrence.Add(false);
}
var indicesToDelete = new List<int>();
for (int ix = 0; ix < this.ColumnHeadings.Count; ++ix)
{
int indexOfRedundantColumnHeading = -1;
if (-1 != (indexOfRedundantColumnHeading = redundantColumnHeadingList.IndexOf(this.ColumnHeadings[ix])))
{
if (alreadyEncounteredFirstOccurrence[indexOfRedundantColumnHeading])
{
indicesToDelete.Add(ix);
}
else
{
alreadyEncounteredFirstOccurrence[indexOfRedundantColumnHeading] = true;
}
}
}
for (int ix = indicesToDelete.Count - 1; ix >= 0; --ix)
{
this.ColumnHeadings.RemoveAt(indicesToDelete[ix]);
foreach (TableRow row in this.Rows)
{
row.RowCells.RemoveAt(indicesToDelete[ix]);
}
}
}
public string Render()
{
var rendered = new StringBuilder();
rendered.Append("|");
foreach (var columnHeading in this.ColumnHeadings)
{
rendered.Append($" {columnHeading} |");
}
rendered.Append($"{Environment.NewLine}|");
foreach (var columnHeading in this.ColumnHeadings)
{
rendered.Append($" - |");
}
foreach (TableRow row in this.Rows)
{
rendered.Append($"{Environment.NewLine}|");
foreach (var cell in row.RowCells)
{
if (cell.Length > 0)
{
rendered.Append($" {cell} |");
}
else
{
rendered.Append($" |");
}
}
}
return rendered.ToString();
}
public (List<Table> tablePerRow, List<List<string>> skippedCellsPerRow) SliceHorizontally(List<string> columnHeadings, int firstColumnIndexZeroBased = 0)
{
var tablePerRow = new List<Table>();
var skippedCellsPerRow = new List<List<string>>();
foreach (TableRow row in this.Rows)
{
var skippedCellsThisRow = new List<string>();
var rows = new List<TableRow>();
for (int columnIndex = 0; columnIndex < this.ColumnHeadings.Count; ++columnIndex)
{
if (columnIndex < firstColumnIndexZeroBased)
{
skippedCellsThisRow.Add(row.RowCells[columnIndex]);
}
else
{
var rowCells = new List<string>() { this.ColumnHeadings[columnIndex], row.RowCells[columnIndex] };
rows.Add(new TableRow(rowCells));
}
}
tablePerRow.Add(new Table(columnHeadings, rows));
skippedCellsPerRow.Add(skippedCellsThisRow);
}
return (tablePerRow, skippedCellsPerRow);
}
public static Table? GetNextTable(string filename, List<string> fileLines, ref int lineNumberToStartAtZeroBased)
{
Table? table = null;
TableParseState tableParseState = TableParseState.NothingFound;
int currentLineNumberOneBased = lineNumberToStartAtZeroBased + 1;
string? currentTableRowString;
for (int ix = lineNumberToStartAtZeroBased; ix < fileLines.Count; ++ix)
{
string eachLineTrimmed = fileLines[ix].Trim();
switch (tableParseState)
{
case TableParseState.NothingFound:
currentTableRowString = Table.LineToTableRow(eachLineTrimmed);
if (currentTableRowString is not null)
{
tableParseState = TableParseState.HeaderColumnHeadingsRowFound;
table = new Table(Table.RowToCells(filename, currentTableRowString), null, currentLineNumberOneBased);
}
break;
case TableParseState.HeaderColumnHeadingsRowFound:
currentTableRowString = Table.LineToTableRow(eachLineTrimmed);
if (currentTableRowString is not null)
{
tableParseState = TableParseState.HeaderUnderlineRowFound;
table!.ConfirmCellCountsMatch(filename, currentTableRowString);
}
else
{
tableParseState = TableParseState.EndFound;
table!.LastLineNumberOneBased = currentLineNumberOneBased - 1;
}
break;
case TableParseState.HeaderUnderlineRowFound:
currentTableRowString = Table.LineToTableRow(eachLineTrimmed);
if (currentTableRowString is not null)
{
tableParseState = TableParseState.BodyFound;
table!.AddRowIfCellCountsMatch(filename, currentTableRowString);
}
else
{
tableParseState = TableParseState.EndFound;
table!.LastLineNumberOneBased = currentLineNumberOneBased - 1;
}
break;
case TableParseState.BodyFound:
currentTableRowString = Table.LineToTableRow(eachLineTrimmed);
if (currentTableRowString is not null)
{
table!.AddRowIfCellCountsMatch(filename, currentTableRowString);
if (ix + 1 == fileLines.Count)
{
tableParseState = TableParseState.EndFound;
table!.LastLineNumberOneBased = currentLineNumberOneBased - 1;
}
}
else
{
tableParseState = TableParseState.EndFound;
table!.LastLineNumberOneBased = currentLineNumberOneBased - 1;
}
break;
case TableParseState.EndFound:
break;
}
++currentLineNumberOneBased;
}
if (table is not null) lineNumberToStartAtZeroBased = table.LastLineNumberOneBased - 1; // To make zero-based.
return table;
}
private TableRow ConfirmCellCountsMatch(string filename, string currentTableRowString)
{
List<string> rowCells = Table.RowToCells(filename, currentTableRowString);
if (this.ColumnHeadings.Count == rowCells.Count)
{
return new TableRow(rowCells);
}
else
{
ProgramBase.ConsoleWrite($"{Environment.NewLine}Row found with unexpected number of cells.", ConsoleWriteStyle.Error);
ProgramBase.ConsoleWrite(filename, ConsoleWriteStyle.Error);
ProgramBase.ConsoleWrite(currentTableRowString, ConsoleWriteStyle.Error, 2);
throw new MDSDKException();
}
}
private void AddRowIfCellCountsMatch(string filename, string currentTableRowString)
{
this.Rows.Add(ConfirmCellCountsMatch(filename, currentTableRowString));
}
private static string? LineToTableRow(string line)
{
var rowMatches = Table.TableRowRegex.Matches(line);
if (rowMatches.Count == 1)
return rowMatches[0].Value;
else
return null;
}
private static List<string> RowToCells(string filename, string row)
{
// Replace \| with |.
row = row.Replace(@"\|", "|");
var cells = new List<string>();
var cellMatches = Table.TableCellRegex.Matches(row);
for (int ix = 0; ix < cellMatches.Count - 1; ++ix)
{
// To normalize the cell, remove the leading pipe and then trim.
// If that results in the empty string, then that's the cell contents.
string normalizedCell = cellMatches[ix].Value.Substring(1).Trim();
if (EditorBase.RetrieveMatchesForTwoSpaces(normalizedCell).Count != 0)
{
ProgramBase.ConsoleWrite($"{Environment.NewLine}Two spaces found in table cell.", ConsoleWriteStyle.Warning);
ProgramBase.ConsoleWrite(filename, ConsoleWriteStyle.Warning);
ProgramBase.ConsoleWrite(normalizedCell, ConsoleWriteStyle.Warning, 2);
}
cells.Add(normalizedCell);
}
return cells;
}
}
}