Exemplo n.º 1
0
        private void toString(int?maxWidth, Action <string> outputString, Action <ConsoleColoredString> outputColoredString)
        {
            int rows = _cells.Count;

            if (rows == 0)
            {
                return;
            }
            int cols = _cells.Max(row => row.Count);

            // Create a lookup array which, for each column, and for each possible value of colspan, tells you which cells in that column have this colspan and end in this column
            var cellsByColspan = new SortedDictionary <int, List <int> > [cols];

            for (var col = 0; col < cols; col++)
            {
                var cellsInThisColumn = new SortedDictionary <int, List <int> >();
                for (int row = 0; row < rows; row++)
                {
                    if (col >= _cells[row].Count)
                    {
                        continue;
                    }
                    var cel = _cells[row][col];
                    if (cel == null)
                    {
                        continue;
                    }
                    if (cel is surrogateCell && ((surrogateCell)cel).RealRow != row)
                    {
                        continue;
                    }
                    int realCol  = cel is surrogateCell ? ((surrogateCell)cel).RealCol : col;
                    var realCell = (trueCell)_cells[row][realCol];
                    if (realCol + realCell.ColSpan - 1 != col)
                    {
                        continue;
                    }
                    cellsInThisColumn.AddSafe(realCell.ColSpan, row);
                }
                cellsByColspan[col] = cellsInThisColumn;
            }

            // Find out the width that each column would have if the text wasn't wrapped.
            // If this fits into the total width, then we want each column to be at least this wide.
            var columnWidths = generateColumnWidths(cols, cellsByColspan, c => Math.Max(1, c.LongestParagraph()));
            var unwrapped    = true;

            // If the table is now too wide, use the length of the longest word, or longest paragraph if nowrap
            if (maxWidth != null && columnWidths.Sum() > maxWidth - (cols - 1) * ColumnSpacing)
            {
                columnWidths = generateColumnWidths(cols, cellsByColspan, c => Math.Max(1, c.MinWidth()));
                unwrapped    = false;
            }

            // If the table is still too wide, use the length of the longest paragraph if nowrap, otherwise 0
            if (maxWidth != null && columnWidths.Sum() > maxWidth - (cols - 1) * ColumnSpacing)
            {
                columnWidths = generateColumnWidths(cols, cellsByColspan, c => c.NoWrap ? Math.Max(1, c.LongestParagraph()) : 1);
            }

            // If the table is still too wide, we will have to wrap like crazy.
            if (maxWidth != null && columnWidths.Sum() > maxWidth - (cols - 1) * ColumnSpacing)
            {
                columnWidths = new int[cols];
                for (int i = 0; i < cols; i++)
                {
                    columnWidths[i] = 1;
                }
            }

            // If the table is STILL too wide, all bets are off.
            if (maxWidth != null && columnWidths.Sum() > maxWidth - (cols - 1) * ColumnSpacing)
            {
                throw new InvalidOperationException(@"The specified table width is too narrow. It is not possible to fit the {0} columns and the column spacing of {1} per column into a total width of {2} characters.".Fmt(cols, ColumnSpacing, maxWidth));
            }

            // If we have any extra width to spare...
            var missingTotalWidth = maxWidth == null ? 0 : maxWidth - columnWidths.Sum() - (cols - 1) * ColumnSpacing;

            if (missingTotalWidth > 0 && (UseFullWidth || !unwrapped))
            {
                // Use the length of the longest paragraph in each column to calculate a proportion by which to enlarge each column
                var widthProportionByCol = new int[cols];
                for (var col = 0; col < cols; col++)
                {
                    foreach (var kvp in cellsByColspan[col])
                    {
                        distributeEvenly(
                            widthProportionByCol,
                            col,
                            kvp.Key,
                            kvp.Value.Max(row => ((trueCell)_cells[row][col - kvp.Key + 1]).LongestParagraph()) - widthProportionByCol.Skip(col - kvp.Key + 1).Take(kvp.Key).Sum() - (unwrapped ? 0 : columnWidths.Skip(col - kvp.Key + 1).Take(kvp.Key).Sum())
                            );
                    }
                }
                var widthProportionTotal = widthProportionByCol.Sum();

                // Adjust the width of the columns according to the calculated proportions so that they fill the missing width.
                // We do this in two steps. Step one: enlarge the column widths by the integer part of the calculated portion (round down).
                // After this the width remaining will be smaller than the number of columns, so each column is missing at most 1 character.
                var widthRemaining  = missingTotalWidth;
                var fractionalParts = new double[cols];
                for (int col = 0; col < cols; col++)
                {
                    var widthToAdd  = (double)(widthProportionByCol[col] * missingTotalWidth) / widthProportionTotal;
                    var integerPart = (int)widthToAdd;
                    columnWidths[col]   += integerPart;
                    fractionalParts[col] = widthToAdd - integerPart;
                    widthRemaining      -= integerPart;
                }

                // Step two: enlarge a few more columns by 1 character so that we reach the desired width.
                // The columns with the largest fractional parts here are the furthest from what we ideally want, so we favour those.
                foreach (var elem in fractionalParts.Select((frac, col) => new { Value = frac, Col = col }).OrderByDescending(e => e.Value))
                {
                    if (widthRemaining < 1)
                    {
                        break;
                    }
                    columnWidths[elem.Col]++;
                    widthRemaining--;
                }
            }

            // Word-wrap all the contents of all the cells
            trueCell truCel;

            foreach (var row in _cells)
            {
                for (int col = 0; col < row.Count; col++)
                {
                    if ((truCel = row[col] as trueCell) != null)
                    {
                        truCel.Wordwrap(columnWidths.Skip(col).Take(truCel.ColSpan).Sum() + (truCel.ColSpan - 1) * ColumnSpacing);
                    }
                }
            }

            // Calculate the string index for each column
            var strIndexByCol = new int[cols + 1];

            for (var i = 0; i < cols; i++)
            {
                strIndexByCol[i + 1] = strIndexByCol[i] + columnWidths[i] + ColumnSpacing;
            }
            var realWidth = strIndexByCol[cols] - ColumnSpacing;

            // Make sure we don't render rules if we can't
            bool verticalRules   = VerticalRules && ColumnSpacing > 0;
            bool horizontalRules = HorizontalRules && RowSpacing > 0;

            // If we do render vertical rules, where should it be (at which string offset, counted backwards from the end of the column spacing)
            var vertRuleOffset = (ColumnSpacing + 1) / 2;

            // Finally, render the entire output
            List <ConsoleColoredString> currentLine = null;

            for (int row = 0; row < rows; row++)
            {
                var  rowList          = _cells[row];
                var  extraRows        = RowSpacing + 1;
                var  isFirstIteration = true;
                bool anyMoreContentInThisRow;
                do
                {
                    ConsoleColoredString previousLine = currentLine == null ? null : new ConsoleColoredString(currentLine.ToArray());
                    currentLine             = new List <ConsoleColoredString>();
                    anyMoreContentInThisRow = false;
                    for (int col = 0; col < cols; col++)
                    {
                        var cel = col < rowList.Count ? rowList[col] : null;

                        // For cells with colspan, consider only the first cell they're spanning and skip the rest
                        if (cel is surrogateCell && ((surrogateCell)cel).RealCol != col)
                        {
                            continue;
                        }

                        // If the cell has rowspan, what row did this cell start in?
                        var valueRow = cel is surrogateCell ? ((surrogateCell)cel).RealRow : row;

                        // Retrieve the data for the cell
                        var realCell      = col < _cells[valueRow].Count ? (trueCell)_cells[valueRow][col] : null;
                        var colspan       = realCell == null ? 1 : realCell.ColSpan;
                        var rowspan       = realCell == null ? 1 : realCell.RowSpan;
                        var rowBackground = row >= _rowBackgrounds.Count ? null : _rowBackgrounds[row];

                        // Does this cell end in this row?
                        var isLastRow = valueRow + rowspan - 1 == row;

                        // If we are inside the cell, render one line of the contents of the cell
                        if (realCell != null && realCell.WordwrappedValue.Length > realCell.WordwrappedIndex)
                        {
                            var align          = realCell.Alignment ?? DefaultAlignment;
                            var curLineLength  = currentLine.Sum(c => c.Length);
                            var cellBackground = realCell.Background ?? rowBackground;
                            if (strIndexByCol[col] > curLineLength)
                            {
                                currentLine.Add(new string(' ', strIndexByCol[col] - curLineLength).Color(null, cellBackground));
                            }
                            object textRaw            = realCell.WordwrappedValue[realCell.WordwrappedIndex];
                            ConsoleColoredString text = textRaw is ConsoleColoredString ? (ConsoleColoredString)textRaw : (string)textRaw;    // implicit conversion to ConsoleColoredString
                            if (align == HorizontalTextAlignment.Center)
                            {
                                currentLine.Add(new string(' ', (strIndexByCol[col + colspan] - strIndexByCol[col] - ColumnSpacing - text.Length) / 2).Color(null, cellBackground));
                            }
                            else if (align == HorizontalTextAlignment.Right)
                            {
                                currentLine.Add(new string(' ', strIndexByCol[col + colspan] - strIndexByCol[col] - ColumnSpacing - text.Length).Color(null, cellBackground));
                            }
                            if (cellBackground == null)
                            {
                                currentLine.Add(text);
                            }
                            else
                            {
                                currentLine.Add(text.ColorBackgroundWhereNull(cellBackground.Value));
                                if (align == HorizontalTextAlignment.Center)
                                {
                                    currentLine.Add(new string(' ', (strIndexByCol[col + colspan] - strIndexByCol[col] - ColumnSpacing - text.Length + 1) / 2).Color(null, cellBackground));
                                }
                                else if (align == HorizontalTextAlignment.Left)
                                {
                                    currentLine.Add(new string(' ', strIndexByCol[col + colspan] - strIndexByCol[col] - ColumnSpacing - text.Length).Color(null, cellBackground));
                                }
                            }
                            realCell.WordwrappedIndex++;
                        }

                        // If we are at the end of a row, render horizontal rules
                        var horizRuleStart           = col > 0 ? strIndexByCol[col] - vertRuleOffset + 1 : 0;
                        var horizRuleEnd             = (col + colspan < cols) ? strIndexByCol[col + colspan] - vertRuleOffset + (verticalRules ? 0 : 1) : realWidth;
                        var renderingHorizontalRules = horizontalRules && isLastRow && extraRows == 1;
                        if (renderingHorizontalRules)
                        {
                            currentLine.Add(new string(' ', horizRuleStart - currentLine.Sum(c => c.Length)));
                            currentLine.Add(new string((row == HeaderRows - 1) ? '=' : '-', horizRuleEnd - horizRuleStart));
                        }
                        else
                        {
                            var subtract = (col + colspan == cols ? ColumnSpacing : vertRuleOffset) + currentLine.Sum(c => c.Length);
                            currentLine.Add(new string(' ', strIndexByCol[col + colspan] - subtract).Color(null, (realCell == null ? null : realCell.Background) ?? rowBackground));
                        }

                        // If we are at the beginning of a row, render the horizontal rules for the row above by modifying the previous line.
                        // We want to do this because it may have an unwanted vertical rule if this is a cell with colspan and there are
                        // multiple cells with smaller colspans above it.
                        if (isFirstIteration && horizontalRules && row > 0 && cel is trueCell)
                        {
                            previousLine = new ConsoleColoredString(previousLine.Substring(0, horizRuleStart), new string((row == HeaderRows) ? '=' : '-', horizRuleEnd - horizRuleStart), previousLine.Substring(horizRuleEnd));
                        }

                        // Render vertical rules
                        if (verticalRules && (col + colspan < cols))
                        {
                            currentLine.Add((new string(' ', strIndexByCol[col + colspan] - vertRuleOffset - currentLine.Sum(c => c.Length)) + "|").Color(null, renderingHorizontalRules ? null : rowBackground));
                        }

                        // Does this cell still contain any more content that needs to be output before this row can be finished?
                        anyMoreContentInThisRow = anyMoreContentInThisRow || (realCell != null && isLastRow && realCell.WordwrappedValue.Length > realCell.WordwrappedIndex);
                    }

                    if (previousLine != null)
                    {
                        if (LeftMargin > 0)
                        {
                            outputString(new string(' ', LeftMargin));
                        }
                        outputColoredString(previousLine);
                        outputString(Environment.NewLine);
                    }

                    isFirstIteration = false;

                    // If none of the cells in this row contain any more content, start counting down the row spacing
                    if (!anyMoreContentInThisRow)
                    {
                        extraRows--;
                    }
                }while (anyMoreContentInThisRow || (extraRows > 0 && row < rows - 1));
            }

            // Output the last line
            if (LeftMargin > 0)
            {
                outputString(new string(' ', LeftMargin));
            }
            outputColoredString(new ConsoleColoredString(currentLine.ToArray()));
            outputString(Environment.NewLine);
        }
Exemplo n.º 2
0
        private void DiscordNamesToolStripMenuItem_Click(object sender, EventArgs e)
        {
            var regex         = new Regex(@"(?:^|\n*)(?:(?:[^\n:]|\|)*(?:dc|discord)[ \t\v\f\r]*:?[ \t\v\f\r]*)?([^\n]*#\d{4})", RegexOptions.Multiline | RegexOptions.IgnoreCase); // .*#(\d{4})
            var confirmResult = MessageBox.Show(
                $"This will search through all your friends statuses and extract the ones that might be discord names (matching {regex.ToString()}).\n\nYou can select multiple rows and copy them with [CTRL] + [C]"
                , "Search for discord names?", MessageBoxButtons.OKCancel, MessageBoxIcon.Question);

            if (confirmResult != DialogResult.OK)
            {
                return;
            }
            var dict = new SortedDictionary <string, string>();

            foreach (TreeNode node in tree_users.Nodes.GetAllChilds())
            {
                var tag = node.Tag;
                if (tag is null)
                {
                    continue;
                }
                var Tag = tag as TreeNodeTag;
                if (Tag.userBriefResponse is null)
                {
                    continue;
                }
                if (!string.IsNullOrWhiteSpace(Tag.userBriefResponse.statusDescription))
                {
                    foreach (Match m in regex.Matches(input: Tag.userBriefResponse.statusDescription))
                    {
                        dict.AddSafe(Tag.userBriefResponse.displayName.Trim(), m.Value.Trim());
                    }
                }
            }
            if (dict.Count < 1)
            {
                MessageBox.Show("Sorry, we could not find any discord names in your friends statuses :c");
                return;
            }
            dict.OrderBy(kv => kv.Key);
            Form form = new Form();

            form.Text    = $"Discord names of your friends ({dict.Count})";
            form.Height *= 2;
            form.Width  *= 3;
            form.Icon    = ActiveForm.Icon;

            DataTable DiscordNames = new DataTable("DiscordNames");

            DiscordNames.Columns.Clear();
            DataColumn c0 = new DataColumn("Ingame Name"); c0.ReadOnly = true; DiscordNames.Columns.Add(c0);
            DataColumn c1 = new DataColumn("Status"); /* c1.ReadOnly = true; */ DiscordNames.Columns.Add(c1);

            foreach (var user in dict)
            {
                DataRow row = DiscordNames.NewRow();
                row[c0.ColumnName] = user.Key;
                row[c1.ColumnName] = user.Value;
                DiscordNames.Rows.Add(row);
            }

            var table = new DataGridView();

            table.Dock       = DockStyle.Fill;
            table.DataSource = DiscordNames;
            form.Controls.Add(table);
            form.Show();
            table.Columns[c0.ColumnName].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
            table.Columns[c1.ColumnName].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
            table.Sort(table.Columns[c0.ColumnName], ListSortDirection.Ascending);
        }
Exemplo n.º 3
0
        private void toString(int? maxWidth, Action<string> outputString, Action<ConsoleColoredString> outputColoredString)
        {
            int rows = _cells.Count;
            if (rows == 0)
                return;
            int cols = _cells.Max(row => row.Count);

            // Create a lookup array which, for each column, and for each possible value of colspan, tells you which cells in that column have this colspan and end in this column
            var cellsByColspan = new SortedDictionary<int, List<int>>[cols];
            for (var col = 0; col < cols; col++)
            {
                var cellsInThisColumn = new SortedDictionary<int, List<int>>();
                for (int row = 0; row < rows; row++)
                {
                    if (col >= _cells[row].Count)
                        continue;
                    var cel = _cells[row][col];
                    if (cel == null)
                        continue;
                    if (cel is surrogateCell && ((surrogateCell) cel).RealRow != row)
                        continue;
                    int realCol = cel is surrogateCell ? ((surrogateCell) cel).RealCol : col;
                    var realCell = (trueCell) _cells[row][realCol];
                    if (realCol + realCell.ColSpan - 1 != col)
                        continue;
                    cellsInThisColumn.AddSafe(realCell.ColSpan, row);
                }
                cellsByColspan[col] = cellsInThisColumn;
            }

            // Find out the width that each column would have if the text wasn't wrapped.
            // If this fits into the total width, then we want each column to be at least this wide.
            var columnWidths = generateColumnWidths(cols, cellsByColspan, c => Math.Max(1, c.LongestParagraph()));
            var unwrapped = true;

            // If the table is now too wide, use the length of the longest word, or longest paragraph if nowrap
            if (maxWidth != null && columnWidths.Sum() > maxWidth - (cols - 1) * ColumnSpacing)
            {
                columnWidths = generateColumnWidths(cols, cellsByColspan, c => Math.Max(1, c.MinWidth()));
                unwrapped = false;
            }

            // If the table is still too wide, use the length of the longest paragraph if nowrap, otherwise 0
            if (maxWidth != null && columnWidths.Sum() > maxWidth - (cols - 1) * ColumnSpacing)
                columnWidths = generateColumnWidths(cols, cellsByColspan, c => c.NoWrap ? Math.Max(1, c.LongestParagraph()) : 1);

            // If the table is still too wide, we will have to wrap like crazy.
            if (maxWidth != null && columnWidths.Sum() > maxWidth - (cols - 1) * ColumnSpacing)
            {
                columnWidths = new int[cols];
                for (int i = 0; i < cols; i++) columnWidths[i] = 1;
            }

            // If the table is STILL too wide, all bets are off.
            if (maxWidth != null && columnWidths.Sum() > maxWidth - (cols - 1) * ColumnSpacing)
                throw new InvalidOperationException(@"The specified table width is too narrow. It is not possible to fit the {0} columns and the column spacing of {1} per column into a total width of {2} characters.".Fmt(cols, ColumnSpacing, maxWidth));

            // If we have any extra width to spare...
            var missingTotalWidth = maxWidth == null ? 0 : maxWidth - columnWidths.Sum() - (cols - 1) * ColumnSpacing;
            if (missingTotalWidth > 0 && (UseFullWidth || !unwrapped))
            {
                // Use the length of the longest paragraph in each column to calculate a proportion by which to enlarge each column
                var widthProportionByCol = new int[cols];
                for (var col = 0; col < cols; col++)
                    foreach (var kvp in cellsByColspan[col])
                        distributeEvenly(
                            widthProportionByCol,
                            col,
                            kvp.Key,
                            kvp.Value.Max(row => ((trueCell) _cells[row][col - kvp.Key + 1]).LongestParagraph()) - widthProportionByCol.Skip(col - kvp.Key + 1).Take(kvp.Key).Sum() - (unwrapped ? 0 : columnWidths.Skip(col - kvp.Key + 1).Take(kvp.Key).Sum())
                        );
                var widthProportionTotal = widthProportionByCol.Sum();

                // Adjust the width of the columns according to the calculated proportions so that they fill the missing width.
                // We do this in two steps. Step one: enlarge the column widths by the integer part of the calculated portion (round down).
                // After this the width remaining will be smaller than the number of columns, so each column is missing at most 1 character.
                var widthRemaining = missingTotalWidth;
                var fractionalParts = new double[cols];
                for (int col = 0; col < cols; col++)
                {
                    var widthToAdd = (double) (widthProportionByCol[col] * missingTotalWidth) / widthProportionTotal;
                    var integerPart = (int) widthToAdd;
                    columnWidths[col] += integerPart;
                    fractionalParts[col] = widthToAdd - integerPart;
                    widthRemaining -= integerPart;
                }

                // Step two: enlarge a few more columns by 1 character so that we reach the desired width.
                // The columns with the largest fractional parts here are the furthest from what we ideally want, so we favour those.
                foreach (var elem in fractionalParts.Select((frac, col) => new { Value = frac, Col = col }).OrderByDescending(e => e.Value))
                {
                    if (widthRemaining < 1) break;
                    columnWidths[elem.Col]++;
                    widthRemaining--;
                }
            }

            // Word-wrap all the contents of all the cells
            trueCell truCel;
            foreach (var row in _cells)
                for (int col = 0; col < row.Count; col++)
                    if ((truCel = row[col] as trueCell) != null)
                        truCel.Wordwrap(columnWidths.Skip(col).Take(truCel.ColSpan).Sum() + (truCel.ColSpan - 1) * ColumnSpacing);

            // Calculate the string index for each column
            var strIndexByCol = new int[cols + 1];
            for (var i = 0; i < cols; i++)
                strIndexByCol[i + 1] = strIndexByCol[i] + columnWidths[i] + ColumnSpacing;
            var realWidth = strIndexByCol[cols] - ColumnSpacing;

            // Make sure we don't render rules if we can't
            bool verticalRules = VerticalRules && ColumnSpacing > 0;
            bool horizontalRules = HorizontalRules && RowSpacing > 0;

            // If we do render vertical rules, where should it be (at which string offset, counted backwards from the end of the column spacing)
            var vertRuleOffset = (ColumnSpacing + 1) / 2;

            // Finally, render the entire output
            List<ConsoleColoredString> currentLine = null;
            for (int row = 0; row < rows; row++)
            {
                var rowList = _cells[row];
                var extraRows = RowSpacing + 1;
                var isFirstIteration = true;
                bool anyMoreContentInThisRow;
                do
                {
                    ConsoleColoredString previousLine = currentLine == null ? null : new ConsoleColoredString(currentLine.ToArray());
                    currentLine = new List<ConsoleColoredString>();
                    anyMoreContentInThisRow = false;
                    for (int col = 0; col < cols; col++)
                    {
                        var cel = col < rowList.Count ? rowList[col] : null;

                        // For cells with colspan, consider only the first cell they're spanning and skip the rest
                        if (cel is surrogateCell && ((surrogateCell) cel).RealCol != col)
                            continue;

                        // If the cell has rowspan, what row did this cell start in?
                        var valueRow = cel is surrogateCell ? ((surrogateCell) cel).RealRow : row;

                        // Retrieve the data for the cell
                        var realCell = col < _cells[valueRow].Count ? (trueCell) _cells[valueRow][col] : null;
                        var colspan = realCell == null ? 1 : realCell.ColSpan;
                        var rowspan = realCell == null ? 1 : realCell.RowSpan;
                        var rowBackground = row >= _rowBackgrounds.Count ? null : _rowBackgrounds[row];

                        // Does this cell end in this row?
                        var isLastRow = valueRow + rowspan - 1 == row;

                        // If we are inside the cell, render one line of the contents of the cell
                        if (realCell != null && realCell.WordwrappedValue.Length > realCell.WordwrappedIndex)
                        {
                            var align = realCell.Alignment ?? DefaultAlignment;
                            var curLineLength = currentLine.Sum(c => c.Length);
                            var cellBackground = realCell.Background ?? rowBackground;
                            if (strIndexByCol[col] > curLineLength)
                                currentLine.Add(new string(' ', strIndexByCol[col] - curLineLength).Color(null, cellBackground));
                            object textRaw = realCell.WordwrappedValue[realCell.WordwrappedIndex];
                            ConsoleColoredString text = textRaw is ConsoleColoredString ? (ConsoleColoredString) textRaw : (string) textRaw;  // implicit conversion to ConsoleColoredString
                            if (align == HorizontalTextAlignment.Center)
                                currentLine.Add(new string(' ', (strIndexByCol[col + colspan] - strIndexByCol[col] - ColumnSpacing - text.Length) / 2).Color(null, cellBackground));
                            else if (align == HorizontalTextAlignment.Right)
                                currentLine.Add(new string(' ', strIndexByCol[col + colspan] - strIndexByCol[col] - ColumnSpacing - text.Length).Color(null, cellBackground));
                            if (cellBackground == null)
                                currentLine.Add(text);
                            else
                            {
                                currentLine.Add(text.ColorBackgroundWhereNull(cellBackground.Value));
                                if (align == HorizontalTextAlignment.Center)
                                    currentLine.Add(new string(' ', (strIndexByCol[col + colspan] - strIndexByCol[col] - ColumnSpacing - text.Length + 1) / 2).Color(null, cellBackground));
                                else if (align == HorizontalTextAlignment.Left)
                                    currentLine.Add(new string(' ', strIndexByCol[col + colspan] - strIndexByCol[col] - ColumnSpacing - text.Length).Color(null, cellBackground));
                            }
                            realCell.WordwrappedIndex++;
                        }

                        // If we are at the end of a row, render horizontal rules
                        var horizRuleStart = col > 0 ? strIndexByCol[col] - vertRuleOffset + 1 : 0;
                        var horizRuleEnd = (col + colspan < cols) ? strIndexByCol[col + colspan] - vertRuleOffset + (verticalRules ? 0 : 1) : realWidth;
                        var renderingHorizontalRules = horizontalRules && isLastRow && extraRows == 1;
                        if (renderingHorizontalRules)
                        {
                            currentLine.Add(new string(' ', horizRuleStart - currentLine.Sum(c => c.Length)));
                            currentLine.Add(new string((row == HeaderRows - 1) ? '=' : '-', horizRuleEnd - horizRuleStart));
                        }
                        else
                        {
                            var subtract = (col + colspan == cols ? ColumnSpacing : vertRuleOffset) + currentLine.Sum(c => c.Length);
                            currentLine.Add(new string(' ', strIndexByCol[col + colspan] - subtract).Color(null, (realCell == null ? null : realCell.Background) ?? rowBackground));
                        }

                        // If we are at the beginning of a row, render the horizontal rules for the row above by modifying the previous line.
                        // We want to do this because it may have an unwanted vertical rule if this is a cell with colspan and there are
                        // multiple cells with smaller colspans above it.
                        if (isFirstIteration && horizontalRules && row > 0 && cel is trueCell)
                            previousLine = new ConsoleColoredString(previousLine.Substring(0, horizRuleStart), new string((row == HeaderRows) ? '=' : '-', horizRuleEnd - horizRuleStart), previousLine.Substring(horizRuleEnd));

                        // Render vertical rules
                        if (verticalRules && (col + colspan < cols))
                            currentLine.Add((new string(' ', strIndexByCol[col + colspan] - vertRuleOffset - currentLine.Sum(c => c.Length)) + "|").Color(null, renderingHorizontalRules ? null : rowBackground));

                        // Does this cell still contain any more content that needs to be output before this row can be finished?
                        anyMoreContentInThisRow = anyMoreContentInThisRow || (realCell != null && isLastRow && realCell.WordwrappedValue.Length > realCell.WordwrappedIndex);
                    }

                    if (previousLine != null)
                    {
                        if (LeftMargin > 0)
                            outputString(new string(' ', LeftMargin));
                        outputColoredString(previousLine);
                        outputString(Environment.NewLine);
                    }

                    isFirstIteration = false;

                    // If none of the cells in this row contain any more content, start counting down the row spacing
                    if (!anyMoreContentInThisRow)
                        extraRows--;
                }
                while (anyMoreContentInThisRow || (extraRows > 0 && row < rows - 1));
            }

            // Output the last line
            if (LeftMargin > 0)
                outputString(new string(' ', LeftMargin));
            outputColoredString(new ConsoleColoredString(currentLine.ToArray()));
            outputString(Environment.NewLine);
        }