private Table ConvertTableBlock(MDT.Table tableBlock, int numberingId, int nestLevel) { var styleId = UserSettingStyleMap.GetStyleId(UserSettingStyleMap.StyleMapKeyType.Table, null); var oxmlTable = Manipulator.ElementCreator.CreateTableElement(styleId, numberingId, nestLevel); foreach (MDT.TableRow tableRow in tableBlock) { var oxmlTableRow = Manipulator.ElementCreator.CreateTableRowElement(); foreach (MDT.TableCell tableCell in tableRow) { var oxmlTableCell = Manipulator.ElementCreator.CreateTableCellElement(); foreach (ParagraphBlock paragraphBlock in tableCell) { var oxmlParagraph = ConvertParagraphBlock(paragraphBlock, WordDocumentNumberingManager.OutsideOfListNumberingId, 0); oxmlTableCell.Append(oxmlParagraph); } oxmlTableRow.Append(oxmlTableCell); } oxmlTable.Append(oxmlTableRow); } return(oxmlTable); }
private static string TranslateTableBlock(MDT.Table tableBlock, int nestLevel) { var tableBlockText = new StringBuilder(); foreach (MDT.TableRow tableRow in tableBlock) { var cellTexts = new List <string>(); foreach (MDT.TableCell tableCell in tableRow) { var cellText = new StringBuilder(); foreach (ParagraphBlock paragraphBlock in tableCell) { cellText.Append(TranslateParagraphBlock(paragraphBlock, nestLevel)); } cellTexts.Add(cellText.ToString()); } tableBlockText.AppendLine(GetIndentWhitespaces(nestLevel) + "| " + string.Join(" | ", cellTexts.ToArray()) + " |"); if (tableRow.IsHeader) { var headerSeparator = new List <string>(); for (int i = 0; i < cellTexts.Count; i++) { headerSeparator.Add("--------"); } tableBlockText.AppendLine(GetIndentWhitespaces(nestLevel) + "| " + string.Join(" | ", headerSeparator.ToArray()) + " |"); } } return(tableBlockText.ToString()); }
/// <summary> /// Calculate the width (in pixels) of a column in a table. /// This is the width of the widest cell in the column. /// </summary> /// <param name="table">The table.</param> /// <param name="columnIndex">Index of a column in the table.</param> private int GetColumnWidth(Table table, int columnIndex) { int width = 0; for (int i = 0; i < table.Count; i++) { TableRow row = (TableRow)table[i]; if (columnIndex < row.Count) { if (row[columnIndex] is TableCell cell) { string cellText = GetCellRawText(cell); int cellWidth = MeasureText(cellText); width = Math.Max(width, cellWidth + tableColumnPadding); } else { throw new NotImplementedException($"Unknown cell type {row[columnIndex].GetType().Name}."); } } } return(width); }
/// <summary> /// Display a table. /// </summary> /// <param name="insertPos"></param> /// <param name="table"></param> /// <param name="indent"></param> private void DisplayTable(ref TextIter insertPos, Table table, int indent) { int spaceWidth = MeasureText(" "); // Setup tab stops for the table. TabArray tabs = new TabArray(table.ColumnDefinitions.Count(), true); int cumWidth = 0; Dictionary <int, int> columnWidths = new Dictionary <int, int>(); for (int i = 0; i < table.ColumnDefinitions.Count(); i++) { // The i-th tab stop will be set to the cumulative column width // of the first i columns (including padding). columnWidths[i] = GetColumnWidth(table, i); cumWidth += columnWidths[i]; tabs.SetTab(i, TabAlign.Left, cumWidth); } // Create a TextTag containing the custom tab stops. // This TextTag will be applied to all text in the table. TextTag tableTag = new TextTag(Guid.NewGuid().ToString()); tableTag.Tabs = tabs; tableTag.WrapMode = Gtk.WrapMode.None; textView.Buffer.TagTable.Add(tableTag); for (int i = 0; i < table.Count; i++) { TableRow row = (TableRow)table[i]; for (int j = 0; j < Math.Min(table.ColumnDefinitions.Count(), row.Count); j++) { if (row[j] is TableCell cell) { // Figure out which tags to use for this cell. We always // need to use the tableTag (which includes the tab stops) // but if it's the top row, we also include the bold tag. TextTag[] tags; if (row.IsHeader) { tags = new TextTag[2] { tableTag, textView.Buffer.TagTable.Lookup("Bold") } } ; else { tags = new TextTag[1] { tableTag } }; TableColumnAlign?alignment = table.ColumnDefinitions[j].Alignment; // If the column is center- or right-aligned, we will insert // some whitespace characters to pad out the text. if (alignment == TableColumnAlign.Center || alignment == TableColumnAlign.Right) { // Calculate the width of the cell contents. int cellWidth = MeasureText(GetCellRawText(cell)); // Number of whitespace characters we can fit will be the // number of spare pixels in the cell (width of cell - width // of cell text) divided by the width of a single space char. int spareSpace = (columnWidths[j] - cellWidth) / spaceWidth; if (spareSpace >= 2) { // The number of spaces to insert will be either the total // amount we can fit if right-aligning, or half that amount // if center-aligning. int numSpaces = alignment == TableColumnAlign.Center ? spareSpace / 2 : spareSpace; string whitespace = new string(' ', spareSpace / 2); textView.Buffer.Insert(ref insertPos, whitespace); } } // Recursively process all markdown blocks inside this cell. In // theory, this supports both blocks and inline content. In practice // I wouldn't recommend using blocks inside a table cell. ProcessMarkdownBlocks(cell, ref insertPos, textView, indent, false, tags); if (j != row.Count - 1) { textView.Buffer.InsertWithTags(ref insertPos, "\t", tableTag); } } else { // This shouldn't happen. throw new Exception($"Unknown cell type {row[(int)j].GetType()}."); } } // Insert a newline after each row - except the last row. if (i + 1 < table.Count) { textView.Buffer.Insert(ref insertPos, "\n"); } } }
public bool PostProcess(InlineProcessor state, Inline?root, Inline?lastChild, int postInlineProcessorIndex, bool isFinalProcessing) { var container = root as ContainerInline; var tableState = state.ParserStates[Index] as TableState; // If the delimiters are being processed by an image link, we need to transform them back to literals if (!isFinalProcessing) { if (container is null || tableState is null) { return(true); } var child = container.LastChild; List <PipeTableDelimiterInline>?delimitersToRemove = null; while (child != null) { if (child is PipeTableDelimiterInline pipeDelimiter) { delimitersToRemove ??= new List <PipeTableDelimiterInline>(); delimitersToRemove.Add(pipeDelimiter); } if (child == lastChild) { break; } var subContainer = child as ContainerInline; child = subContainer?.LastChild; } // If we have found any delimiters, transform them to literals if (delimitersToRemove != null) { bool leftIsDelimiter = false; bool rightIsDelimiter = false; for (int i = 0; i < delimitersToRemove.Count; i++) { var pipeDelimiter = delimitersToRemove[i]; pipeDelimiter.ReplaceByLiteral(); // Check that the pipe that is being removed is not going to make a line without pipe delimiters var tableDelimiters = tableState.ColumnAndLineDelimiters; var delimiterIndex = tableDelimiters.IndexOf(pipeDelimiter); if (i == 0) { leftIsDelimiter = delimiterIndex > 0 && tableDelimiters[delimiterIndex - 1] is PipeTableDelimiterInline; } else if (i + 1 == delimitersToRemove.Count) { rightIsDelimiter = delimiterIndex + 1 < tableDelimiters.Count && tableDelimiters[delimiterIndex + 1] is PipeTableDelimiterInline; } // Remove this delimiter from the table processor tableState.ColumnAndLineDelimiters.Remove(pipeDelimiter); } // If we didn't have any delimiter before and after the delimiters we just removed, we mark the processor of the current line as no pipe if (!leftIsDelimiter && !rightIsDelimiter) { tableState.LineHasPipe = false; } } return(true); } // Remove previous state state.ParserStates[Index] = null !; // Continue if (tableState is null || container is null || tableState.IsInvalidTable || !tableState.LineHasPipe) //|| tableState.LineIndex != state.LocalLineIndex) { return(true); } // Detect the header row var delimiters = tableState.ColumnAndLineDelimiters; // TODO: we could optimize this by merging FindHeaderRow and the cell loop var aligns = FindHeaderRow(delimiters); if (Options.RequireHeaderSeparator && aligns is null) { return(true); } var table = new Table(); // If the current paragraph block has any attributes attached, we can copy them var attributes = state.Block !.TryGetAttributes(); if (attributes != null) { attributes.CopyTo(table.GetAttributes()); } state.BlockNew = table; var cells = tableState.Cells; cells.Clear(); //delimiters[0].DumpTo(state.DebugLog); // delimiters contain a list of `|` and `\n` delimiters // The `|` delimiters are created as child containers. // So the following: // | a | b \n // | d | e \n // // Will generate a tree of the following node: // | // a // | // b // \n // | // d // | // e // \n // When parsing delimiters, we need to recover whether a row is of the following form: // 0) | a | b | \n // 1) | a | b \n // 2) a | b \n // 3) a | b | \n // If the last element is not a line break, add a line break to homogenize parsing in the next loop var lastElement = delimiters[delimiters.Count - 1]; if (!(lastElement is LineBreakInline)) { while (true) { if (lastElement is ContainerInline lastElementContainer) { var nextElement = lastElementContainer.LastChild; if (nextElement != null) { lastElement = nextElement; continue; } } break; } var endOfTable = new LineBreakInline(); // If the last element is a container, we have to add the EOL to its child // otherwise only next sibling if (lastElement is ContainerInline) { ((ContainerInline)lastElement).AppendChild(endOfTable); } else { lastElement.InsertAfter(endOfTable); } delimiters.Add(endOfTable); tableState.EndOfLines.Add(endOfTable); } // Cell loop // Reconstruct the table from the delimiters TableRow?row = null; TableRow?firstRow = null; for (int i = 0; i < delimiters.Count; i++) { var delimiter = delimiters[i]; var pipeSeparator = delimiter as PipeTableDelimiterInline; var isLine = delimiter is LineBreakInline; if (row is null) { row = new TableRow(); firstRow ??= row; // If the first delimiter is a pipe and doesn't have any parent or previous sibling, for cases like: // 0) | a | b | \n // 1) | a | b \n if (pipeSeparator != null && (delimiter.PreviousSibling is null || delimiter.PreviousSibling is LineBreakInline)) { delimiter.Remove(); continue; } } // We need to find the beginning/ending of a cell from a right delimiter. From the delimiter 'x', we need to find a (without the delimiter start `|`) // So we iterate back to the first pipe or line break // x // 1) | a | b \n // 2) a | b \n Inline?endOfCell = null; Inline?beginOfCell = null; var cellContentIt = delimiter; while (true) { cellContentIt = cellContentIt.PreviousSibling ?? cellContentIt.Parent; if (cellContentIt is null || cellContentIt is LineBreakInline) { break; } // The cell begins at the first effective child after a | or the top ContainerInline (which is not necessary to bring into the tree + it contains an invalid span calculation) if (cellContentIt is PipeTableDelimiterInline || (cellContentIt.GetType() == typeof(ContainerInline) && cellContentIt.Parent is null)) { beginOfCell = ((ContainerInline)cellContentIt).FirstChild; if (endOfCell is null) { endOfCell = beginOfCell; } break; } beginOfCell = cellContentIt; if (endOfCell is null) { endOfCell = beginOfCell; } } // If the current deilimiter is a pipe `|` OR // the beginOfCell/endOfCell are not null and // either they are : // - different // - they contain a single element, but it is not a line break (\n) or an empty/whitespace Literal. // Then we can add a cell to the current row if (!isLine || (beginOfCell != null && endOfCell != null && (beginOfCell != endOfCell || !(beginOfCell is LineBreakInline || (beginOfCell is LiteralInline beingOfCellLiteral && beingOfCellLiteral.Content.IsEmptyOrWhitespace()))))) { if (!isLine) { // If the delimiter is a pipe, we need to remove it from the tree // so that previous loop looking for a parent will not go further on subsequent cells delimiter.Remove(); } // We trim whitespace at the beginning and ending of the cell TrimStart(beginOfCell); TrimEnd(endOfCell); var cellContainer = new ContainerInline(); // Copy elements from beginOfCell on the first level var cellIt = beginOfCell; while (cellIt != null && !IsLine(cellIt) && !(cellIt is PipeTableDelimiterInline)) { var nextSibling = cellIt.NextSibling; cellIt.Remove(); if (cellContainer.Span.IsEmpty) { cellContainer.Line = cellIt.Line; cellContainer.Column = cellIt.Column; cellContainer.Span = cellIt.Span; } cellContainer.AppendChild(cellIt); cellContainer.Span.End = cellIt.Span.End; cellIt = nextSibling; } // Create the cell and add it to the pending row var tableParagraph = new ParagraphBlock() { Span = cellContainer.Span, Line = cellContainer.Line, Column = cellContainer.Column, Inline = cellContainer }; var tableCell = new TableCell() { Span = cellContainer.Span, Line = cellContainer.Line, Column = cellContainer.Column, }; tableCell.Add(tableParagraph); if (row.Span.IsEmpty) { row.Span = cellContainer.Span; row.Line = cellContainer.Line; row.Column = cellContainer.Column; } row.Add(tableCell); cells.Add(tableCell); } // If we have a new line, we can add the row if (isLine) { Debug.Assert(row != null); if (table.Span.IsEmpty) { table.Span = row !.Span; table.Line = row.Line; table.Column = row.Column; } table.Add(row !); row = null; } } // Once we are done with the cells, we can remove all end of lines in the table tree foreach (var endOfLine in tableState.EndOfLines) { endOfLine.Remove(); } // If we have a header row, we can remove it // TODO: we could optimize this by merging FindHeaderRow and the previous loop var tableRow = (TableRow)table[0]; tableRow.IsHeader = Options.RequireHeaderSeparator; if (aligns != null) { tableRow.IsHeader = true; table.RemoveAt(1); table.ColumnDefinitions.AddRange(aligns); } // Perform delimiter processor that are coming after this processor foreach (var cell in cells) { var paragraph = (ParagraphBlock)cell[0]; state.PostProcessInlines(postInlineProcessorIndex + 1, paragraph.Inline, null, true); } // Clear cells when we are done cells.Clear(); // Normalize the table if (Options.UseHeaderForColumnCount) { table.NormalizeUsingHeaderRow(); } else { table.NormalizeUsingMaxWidth(); } // We don't want to continue procesing delimiters, as we are already processing them here return(false); }