/// <summary>
        /// Retrieves information about each of the columns in the table including
        /// max width and title. Looks at the first row of the table data.
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        public List <ColumnInfo> GetColumnInfo(TableData data)
        {
            var headers = new List <ColumnInfo>();

            if (data == null || data.Headers.Count < 1 && data.Rows.Count < 1)
            {
                return(headers);
            }

            var cols = data.Headers;

            if (cols == null || cols.Count < 0)
            {
                return(headers);
            }

            for (int i = 0; i < cols.Count; i++)
            {
                var header  = cols[i]?.Trim() ?? string.Empty;
                var colInfo = new ColumnInfo
                {
                    Title    = header,
                    MaxWidth = header.Length
                };

                if (header.EndsWith(":") && header.StartsWith(":"))
                {
                    colInfo.Justification = ColumnJustifications.Center;
                }
                else if (header.EndsWith(":"))
                {
                    colInfo.Justification = ColumnJustifications.Right;
                }

                // figure out max length in rows
                for (int x = 0; x < data.Rows.Count; x++)
                {
                    var row = data.Rows[x];

                    var colText = row[i];
                    if (colText.IndexOf('\n') < 0)
                    {
                        if (colText.Length > colInfo.MaxWidth)
                        {
                            colInfo.MaxWidth = colText.Length;
                        }
                    }
                    else
                    {
                        // max width for multiple lines
                        var tokens    = colText.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
                        var maxLength = tokens.Max(t => t.Trim().Length);
                        if (maxLength > colInfo.MaxWidth)
                        {
                            colInfo.MaxWidth = maxLength;
                        }
                    }
                }
                headers.Add(colInfo);
            }

            return(headers);
        }
        /// <summary>
        /// Parses and HTML table to a TableData object
        /// </summary>
        /// <param name="html"></param>
        /// <returns></returns>
        public TableData ParseHtmlToData(string html)
        {
            var data = new TableData();

            if (string.IsNullOrEmpty(html))
            {
                return(data);
            }

            var doc = new HtmlDocument();

            doc.LoadHtml(html);

            var headerRow = doc.DocumentNode.SelectSingleNode("//tr");

            if (headerRow == null)
            {
                return(data);
            }

            var headerColumns = new List <string>();
            var headerCols    = headerRow.SelectNodes("th");

            if (headerCols == null)
            {
                headerCols = headerRow.SelectNodes("td");
            }
            if (headerCols == null)
            {
                return(data);
            }

            foreach (var node in headerCols)
            {
                var text      = node.InnerText;
                var attrStyle = node.Attributes["style"];
                if (attrStyle != null)
                {
                    var style = attrStyle.Value;
                    if (style.Contains("text-align: right"))
                    {
                        text = text + ":";
                    }
                    else if (style.Contains("text-align: center"))
                    {
                        text = ":" + text + ":";
                    }
                }
                headerColumns.Add(text);
            }

            data.Headers = headerColumns;

            var nodes = doc.DocumentNode.SelectNodes("//tr");

            foreach (var trNode in nodes.Skip(1))
            {
                var rowColumns = new List <string>();
                var cols       = trNode.SelectNodes("td");
                if (cols == null)
                {
                    continue;
                }

                foreach (var node in cols)
                {
                    string text;
                    var    nodeHtml = node.InnerHtml;

                    // check for common replacements
                    if (!nodeHtml.Contains("<"))
                    {
                        text = node.InnerText;
                    }
                    else
                    {
                        text = nodeHtml
                               .Replace("<b>", "**")
                               .Replace("</b>", "**")
                               .Replace("<i>", "*")
                               .Replace("</i>", "*")
                               .Replace("<code>", "`")
                               .Replace("</code>", "`")
                               .Replace("<br>", "\n");

                        // convert links and images
                        if (text.Contains("<"))
                        {
                            text = ParseLinkAndImage(text);
                        }
                    }

                    rowColumns.Add(text);
                }

                data.Rows.Add(rowColumns);
            }

            BalanceTableColumns(data);

            return(data);
        }
        public TableData ParseCsvStreamToData(Stream stream, string delimiter = ",")
        {
            if (string.IsNullOrEmpty(delimiter))
            {
                delimiter = ",";
            }
            if (delimiter == "\\t")
            {
                delimiter = "\t";
            }

            char charDelimiter = delimiter[0];

            bool firstLine = true;

            using (var reader = new StreamReader(stream))
            {
                using (var csv = new CachedCsvReader(reader, true, charDelimiter))
                {
                    var list = new TableData();

                    var colCount         = csv.Columns.Count;
                    var columnCollection = new List <string>();

                    if (!csv.ReadNextRecord())
                    {
                        return(list);
                    }

                    for (var index = 0; index < csv.Columns.Count; index++)
                    {
                        Column column = csv.Columns[index];
                        columnCollection.Add(column.Name);
                    }

                    if (firstLine)
                    {
                        list.Headers = columnCollection;
                        firstLine    = false;
                    }
                    else
                    {
                        list.Rows.Add(columnCollection);
                    }

                    // Field headers will automatically be used as column names
                    while (true)
                    {
                        columnCollection = new List <string>();
                        for (int index = 0; index < csv.Columns.Count; index++)
                        {
                            var colValue = csv[index];
                            columnCollection.Add(colValue);
                        }
                        list.Rows.Add(columnCollection);

                        if (!csv.ReadNextRecord())
                        {
                            break;
                        }
                    }

                    return(list);
                }
            }
        }
        /// <summary>
        /// Parses a Markdown Grid table to a Data Observable Collection
        /// </summary>
        /// <param name="tableMarkdown"></param>
        /// <returns></returns>
        public TableData ParseMarkdownGridTableToData(string tableMarkdown)
        {
            var data = new TableData();

            if (string.IsNullOrEmpty(tableMarkdown))
            {
                return(data);
            }

            var lines = StringUtils.GetLines(tableMarkdown.Trim());

            // loop through rows
            for (var index = 0; index < lines.Length; index++)
            {
                var rowText = lines[index]?.Trim();
                if (rowText.Length == 0)
                {
                    continue;
                }

                // Skip over grid lines
                if (rowText.StartsWith("+--") || rowText.StartsWith("+=="))
                {
                    var columnData = new List <string>();

                    // goto next 'column line'
                    index++;
                    if (index >= lines.Length)
                    {
                        break;
                    }
                    rowText = lines[index]?.Trim();

                    var      cellText = new List <StringBuilder>();
                    string[] cols     = new string[0];

                    while (true)  // loop through multiple lines
                    {
                        cols = rowText.TrimEnd().Trim('|').Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);

                        if (cellText.Count < 1)
                        {
                            for (var i = 0; i < cols.Length; i++)
                            {
                                cellText.Add(new StringBuilder());
                            }
                        }

                        for (var i = 0; i < cols.Length; i++)
                        {
                            var col = cols[i]?.Trim();
                            if (!string.IsNullOrEmpty(col))
                            {
                                cellText[i].AppendLine(col.Trim());
                            }
                        }

                        // get the next line of this column
                        if (lines[index + 1].Trim().StartsWith("|"))
                        {
                            index++;
                            rowText = lines[index]?.Trim();  // process next line
                        }
                        else
                        {
                            break;
                        }
                    }

                    if (cols.Length == 0)
                    {
                        continue;
                    }

                    // collect multiple lines per column
                    for (var i = 0; i < cols.Length; i++)
                    {
                        cellText[i].Length -= 2; // strip off trailing \r\n
                        var ctext = cellText[i].ToString().Replace("\r", "");
                        columnData.Add(ctext);
                    }

                    if (index < 2)
                    {
                        data.Headers = columnData;
                    }
                    else
                    {
                        data.Rows.Add(columnData);
                    }
                }
            }

            BalanceTableColumns(data);

            return(data);
        }
        /// <summary>
        ///
        /// </summary>
        /// <param name="tableData"></param>
        /// <returns></returns>
        public string ToPipeTableMarkdown(TableData tableData = null)
        {
            if (tableData == null)
            {
                tableData = TableData;
            }

            if (tableData == null || tableData.Headers.Count < 1 && tableData.Rows.Count < 1)
            {
                return(string.Empty);
            }

            var columnInfo = GetColumnInfo(tableData);

            var sb = new StringBuilder();

            sb.Clear();

            string line      = $"| ";
            string separator = "|";

            for (int i = 0; i < columnInfo.Count; i++)
            {
                var    colInfo = columnInfo[i];
                string title   = colInfo.Title;
                title = title.Trim(':');
                line += $"{title.PadRight(colInfo.MaxWidth)} | ";

                if (colInfo.Title.StartsWith(":"))
                {
                    separator += ":" + "-".PadRight(colInfo.MaxWidth, '-');
                }
                else
                {
                    separator += "-" + "-".PadRight(colInfo.MaxWidth, '-');
                }

                if (colInfo.Title.EndsWith(":"))
                {
                    separator += ":|";
                }
                else
                {
                    separator += "-|";
                }
            }

            sb.AppendLine(line.TrimEnd());
            sb.AppendLine(separator);


            foreach (var row in tableData.Rows)
            {
                line = "| ";
                for (int i = 0; i < row.Count; i++)
                {
                    if (i >= columnInfo.Count)
                    {
                        break;
                    }

                    var col = row[i];
                    col = col.Replace("\n", "<br>").Replace("\r", "");

                    var colInfo = columnInfo[i];
                    line += col.PadRight(colInfo.MaxWidth) + " | ";
                }

                sb.AppendLine(line.Trim());
            }

            return(sb.ToString());
        }
        /// <summary>
        /// Parses a Markdown Pipe Table to an Observable Data Collection
        /// </summary>
        /// <param name="tableMarkdown"></param>
        /// <returns></returns>
        TableData ParseMarkdownPipeTableToData(string tableMarkdown)
        {
            var data  = new TableData();
            var lines = StringUtils.GetLines(tableMarkdown.Trim());

            for (var index = 0; index < lines.Length; index++)
            {
                var row = lines[index]?.Trim();
                if (string.IsNullOrEmpty(row))
                {
                    continue;
                }

                // check for aligned headers
                if (data.Headers.Count > 0 &&   // header row must exist
                    row.StartsWith("|--") || row.StartsWith("| --") ||
                    row.StartsWith("|:-") || row.StartsWith("| :-") ||
                    row.StartsWith("--"))
                {
                    if (!row.Contains(":"))
                    {
                        continue;
                    }

                    var headerCols = row.TrimEnd().Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
                    for (int i = 0; i < headerCols.Length; i++)
                    {
                        var sepLine = headerCols[i].Trim();
                        if (sepLine.StartsWith(":"))
                        {
                            data.Headers[i] = ":" + data.Headers[i];
                        }
                        if (sepLine.EndsWith(":"))
                        {
                            data.Headers[i] = data.Headers[i] + ":";
                        }
                    }

                    continue;
                }

                var cols       = row.TrimEnd().Trim('|').Split('|');
                var columnData = new List <string>();
                foreach (var col in cols)
                {
                    var txt = col.Trim()
                              .Replace("<br>", "\n")
                              .Replace("</br>", "\n");
                    columnData.Add(txt);
                }

                if (index == 0)  // assume it's the header
                {
                    data.Headers = columnData;
                }
                else
                {
                    data.Rows.Add(columnData);
                }
            }

            BalanceTableColumns(data);

            return(data);
        }
        /// <summary>
        /// Takes the input collection and parses it into an HTML string. First row is considered to be the
        /// header of the table.
        /// </summary>
        /// <param name="tableData"></param>
        /// <returns></returns>
        public string ToTableHtml(TableData tableData = null)
        {
            if (tableData == null)
            {
                tableData = TableData;
            }

            if (tableData == null || tableData.Rows.Count < 1 && tableData.Headers.Count < 1)
            {
                return(string.Empty);
            }

            var mdParser   = MarkdownParserFactory.GetParser(usePragmaLines: false, forceLoad: true);
            var columnInfo = GetColumnInfo(tableData);

            StringBuilder sb = new StringBuilder();

            sb.Clear();

            sb.AppendLine("\n<table>");
            sb.AppendLine("<thead>");
            sb.AppendLine("\t<tr>");

            for (int i = 0; i < columnInfo.Count; i++)
            {
                var colInfo = columnInfo[i];

                var align = string.Empty;
                if (columnInfo[i].Justification == ColumnJustifications.Right)
                {
                    align = " style=\"text-align:right\"";
                }
                else if (columnInfo[i].Justification == ColumnJustifications.Center)
                {
                    align = " style=\"text-align:center\"";
                }

                sb.AppendLine($"\t\t<th{align}>{WebUtility.HtmlEncode(colInfo.Title.Trim(' ',':','\n','\r'))}</th>");
            }
            sb.AppendLine("\t</tr>");
            sb.AppendLine("</thead>");

            sb.AppendLine("<tbody>");
            foreach (var row in tableData.Rows)
            {
                sb.AppendLine("\t<tr>");
                for (int i = 0; i < columnInfo.Count; i++)
                {
                    var col = row[i];
                    if (string.IsNullOrEmpty(col))
                    {
                        col = string.Empty;
                    }
                    else
                    {
                        col = col.Replace("\n", "<br>")
                              .Replace("\r", "");
                    }


                    var align = string.Empty;
                    if (columnInfo[i].Justification == ColumnJustifications.Right)
                    {
                        align = " style=\"text-align: right\"";
                    }
                    else if (columnInfo[i].Justification == ColumnJustifications.Center)
                    {
                        align = " style=\"text-align: center\"";
                    }

                    var text = mdParser.Parse(col.Trim())
                               .Replace("<p>", "")
                               .Replace("</p>", "")
                               .Trim();
                    sb.AppendLine($"\t\t<td{align}>{text}</td>");
                }

                sb.AppendLine("\t</tr>");
            }

            sb.AppendLine("</tbody>");
            sb.AppendLine($"</table>{mmApp.NewLine}");

            return(sb.ToString());
        }
        /// <summary>
        ///
        /// </summary>
        /// <param name="tableData"></param>
        /// <returns></returns>
        public string ToGridTableMarkdown(TableData tableData = null)
        {
            if (tableData == null)
            {
                tableData = TableData;
            }

            if (tableData == null || tableData.Rows.Count < 1 && tableData.Headers.Count < 1)
            {
                return(string.Empty);
            }


            var columnInfo = GetColumnInfo(tableData);

            var sb = new StringBuilder();

            sb.Clear();
            string separatorLine = "+-";
            string line          = "| ";

            for (int i = 0; i < columnInfo.Count; i++)
            {
                var colInfo = columnInfo[i];
                line          += $"{colInfo.Title.Trim(':').PadRight(colInfo.MaxWidth)} | ";
                separatorLine += "-".PadRight(colInfo.MaxWidth, '-') + "-+-";
            }

            separatorLine = separatorLine.TrimEnd('-');

            sb.AppendLine(separatorLine);
            sb.AppendLine(line.TrimEnd());
            sb.AppendLine(separatorLine.Replace("-", "="));

            for (var rowIndex = 0; rowIndex < tableData.Rows.Count; rowIndex++) // rows
            {
                var row          = tableData.Rows[rowIndex];
                var maxLineCount = row.Max(s => s.Count(s2 => s2 == '\n')) + 1;

                for (int x = 0; x < maxLineCount; x++)    // text lines (line count)
                {
                    line = "| ";
                    for (int i = 0; i < columnInfo.Count; i++) // columns
                    {
                        var col       = row[i];
                        var textLines = StringUtils.GetLines(col);

                        string content;
                        if (textLines.Length <= x)
                        {
                            content = string.Empty;
                        }
                        else
                        {
                            content = textLines[x];
                        }

                        line += content.PadRight(columnInfo[i].MaxWidth) + " | ";
                    }
                    sb.AppendLine(line.Trim());
                }
                sb.AppendLine(separatorLine);
            }


            return(sb + mmApp.NewLine);
        }