From 8095b5e1582c49626ef728db7f7aa4ab56aa6395 Mon Sep 17 00:00:00 2001 From: Mark Kidder <83427558+mark-sil@users.noreply.github.com> Date: Fri, 22 Mar 2024 14:52:39 -0400 Subject: [PATCH] LT-21672: Add Tables to Word Export (#14) This adds the basic table data, without styling. Styling will be added in a separate change. Tables cannot be added to a paragraph, they need to be added directly to a Body. So, the majority of this change was to not create paragraphs until the end of the process; when we are in AddEntryData(). What previously was a single paragraph for the Entry, could now be multiple paragraphs if there are one or more tables not at the end of the Entry. Change-Id: I4923a87c014c2c4f51159d37d22404fceb64e5f9 --- Src/xWorks/LcmWordGenerator.cs | 180 +++++++++++++++++++-------------- 1 file changed, 102 insertions(+), 78 deletions(-) diff --git a/Src/xWorks/LcmWordGenerator.cs b/Src/xWorks/LcmWordGenerator.cs index 615b7477b4..448c55f70b 100644 --- a/Src/xWorks/LcmWordGenerator.cs +++ b/Src/xWorks/LcmWordGenerator.cs @@ -157,9 +157,8 @@ internal static IFragment GenerateLetterHeaderIfNeeded(ICmObject entry, ref stri StringBuilder headerTextBuilder = ConfiguredLcmGenerator.GenerateLetterHeaderIfNeeded(entry, ref lastHeader, headwordWsCollator, settings, clerk); - // Create LetterHeader doc fragment and link it with the letterheadingstyle - return new DocFragment(headerTextBuilder.ToString(), WordStylesGenerator.LetterHeadingStyleName); - + // Create LetterHeader doc fragment and link it with the letter heading style. + return DocFragment.GenerateLetterHeaderDocFragment(headerTextBuilder.ToString(), WordStylesGenerator.LetterHeadingStyleName); } /* @@ -172,6 +171,7 @@ public class DocFragment : IFragment internal WordprocessingDocument DocFrag { get; } internal MainDocumentPart mainDocPart { get; } internal WP.Body DocBody { get; } + internal string ParagraphStyle { get; private set; } /// /// Constructs a new memory stream and creates an empty doc fragment @@ -212,8 +212,8 @@ public DocFragment(string str) : this() // Only create paragraph, run, and text objects if the string is nonempty if (!string.IsNullOrEmpty(str)) { - WP.Paragraph para = DocBody.AppendChild(new WP.Paragraph()); - WP.Run run = para.AppendChild(new WP.Run()); + WP.Run run = DocBody.AppendChild(new WP.Run()); + // For spaces to show correctly, set preserve spaces on the text element WP.Text txt = new WP.Text(str); txt.Space = SpaceProcessingModeValues.Preserve; @@ -221,25 +221,40 @@ public DocFragment(string str) : this() } } - public DocFragment(string str, string styleName) : this() + /// + /// Generate the document fragment for a letter header. + /// + /// Letter header string. + /// Letter header style. + internal static DocFragment GenerateLetterHeaderDocFragment(string str, string styleName) { + var docFrag = new DocFragment(); // Only create paragraph, run, and text objects if string is nonempty if (!string.IsNullOrEmpty(str)) { WP.ParagraphProperties paragraphProps = new WP.ParagraphProperties(new ParagraphStyleId() { Val = styleName }); - WP.Paragraph para = DocBody.AppendChild(new WP.Paragraph(paragraphProps)); + WP.Paragraph para = docFrag.DocBody.AppendChild(new WP.Paragraph(paragraphProps)); WP.Run run = para.AppendChild(new WP.Run()); // For spaces to show correctly, set preserve spaces on the text element WP.Text txt = new WP.Text(str); txt.Space = SpaceProcessingModeValues.Preserve; run.AppendChild(txt); } + return docFrag; } public static void LinkStyleOrInheritParentStyle(IFragment content, ConfigurableDictionaryNode config) { DocFragment frag = ((DocFragment)content); - if (!string.IsNullOrEmpty(config.Style)) + + // Check if this is a Table + bool bTable = frag.DocBody.Elements().FirstOrDefault() != null; + + if (bTable) + { + // TODO - Add Table Style info. + } + else if (!string.IsNullOrEmpty(config.Style)) { frag.AddStyleLink(config.Style, config.StyleType); } @@ -255,7 +270,12 @@ public void AddStyleLink(string styleName, ConfigurableDictionaryNode.StyleTypes return; if (styleType == ConfigurableDictionaryNode.StyleTypes.Paragraph) - LinkParaStyle(styleName); + { + if (string.IsNullOrEmpty(ParagraphStyle)) + { + ParagraphStyle = styleName; + } + } else LinkCharStyle(styleName); } @@ -264,8 +284,11 @@ public void AddStyleLink(string styleName, ConfigurableDictionaryNode.StyleTypes /// Appends the given styleName as a style ID for the last paragraph in the doc, or creates a new paragraph with the given styleID if no paragraph exists. /// /// - private void LinkParaStyle(string styleName) + internal void LinkParaStyle(string styleName) { + if (string.IsNullOrEmpty(styleName)) + return; + WP.Paragraph par = GetLastParagraph(); if (par.ParagraphProperties != null) { @@ -293,7 +316,6 @@ private void LinkCharStyle(string styleName) run.RunProperties.Append(new RunStyle() { Val = styleName }); } - //run.RunProperties.Append(new StyleId() {Val = styleName}) ; else { WP.RunProperties runProps = @@ -364,43 +386,42 @@ public int Length() /// /// Appends one doc fragment to another. - /// Use this if styles have already been applied - /// and if not attempting to append within the same paragraph. + /// Use this if styles have already been applied. /// public void Append(IFragment frag) { - - foreach (WP.Paragraph para in ((DocFragment)frag).DocBody.OfType().ToList()) + foreach (OpenXmlElement elem in ((DocFragment)frag).DocBody.Elements().ToList()) { - // Append each paragraph. It is necessary to deep clone the node to maintain its tree of document properties + // Append each element. It is necessary to deep clone the node to maintain its tree of document properties // and to ensure its styles will be maintained in the copy. - this.DocBody.AppendChild(para.CloneNode(true)); + this.DocBody.AppendChild(elem.CloneNode(true)); } } /// - /// Appends a new paragraph to the doc fragment. - /// The run will be added to the end of the paragraph. + /// Append a table to the doc fragment. /// - public void Append(WP.Paragraph par) + public void Append(WP.Table table) { // Deep clone the run b/c of its tree of properties and to maintain styles. - this.DocBody.AppendChild(par.CloneNode(true)); + this.DocBody.AppendChild(table.CloneNode(true)); } /// /// Appends a new run inside the last paragraph of the doc fragment--creates a new paragraph if none exists. /// The run will be added to the end of the paragraph. /// - public void Append(WP.Run run) + /// The run to append. + /// Even if a paragraph exists, force the creation of a new paragraph. + public void AppendToParagraph(WP.Run run, bool forceNewParagraph) { // Deep clone the run b/c of its tree of properties and to maintain styles. - WP.Paragraph lastPar = GetLastParagraph(); + WP.Paragraph lastPar = forceNewParagraph ? GetNewParagraph() : GetLastParagraph(); lastPar.AppendChild(run.CloneNode(true)); } /// - /// Appends text to the last run inside the last paragraph of the doc fragment. + /// Appends text to the last run inside the doc fragment. /// If no run exists, a new one will be created. /// public void Append(string text) @@ -466,14 +487,13 @@ public WP.Paragraph GetNewParagraph() /// Returns last run in the document if it contains any, /// else creates and returns a new run. /// - private WP.Run GetLastRun() + internal WP.Run GetLastRun() { - WP.Paragraph lastPara = GetLastParagraph(); - List runList = lastPara.OfType().ToList(); + List runList = DocBody.OfType().ToList(); if (runList.Any()) return runList.Last(); - return lastPara.AppendChild(new WP.Run()); + return DocBody.AppendChild(new WP.Run()); } } #endregion DocFragment class @@ -487,6 +507,7 @@ public class WordFragmentWriter : IFragmentWriter public DocFragment WordFragment { get; } private bool isDisposed; internal Dictionary collatorCache = new Dictionary(); + public bool ForceNewParagraph { get; set; } = false; public WordFragmentWriter(DocFragment frag) { @@ -524,39 +545,15 @@ public void Insert(IFragment frag) WordFragment.Append(frag); } - public void Insert(WP.Paragraph par) - { - WordFragment.Append(par); - } - - public void Insert(WP.Run run) - { - WordFragment.Append(run); - } + internal WP.Table CurrentTable { get; set; } + internal WP.TableRow CurrentTableRow { get; set; } /// - /// Gets and returns the last run in the document, if one exists. - /// Otherwise, creates and returns a new run. - /// - public WP.Run GetCurrentRun() - { - List runList = WordFragment.DocBody.Descendants().ToList(); - if (runList.Any()) - return runList.Last(); - - // If there is no run, create one - WP.Run lastRun = WordFragment.DocBody.AppendChild(new WP.Run()); - return lastRun; - } - - /// - /// Get the last paragraph in the doc if it contains any, and add a new run to it. - /// Else, create and add the run to a new paragraph. + /// Add a new run to the WordFragment DocBody. /// public void CreateRun() { - WP.Paragraph curPar = WordFragment.GetLastParagraph(); - curPar.AppendChild(new WP.Run()); + WordFragment.DocBody.AppendChild(new WP.Run()); } } #endregion WordFragmentWriter class @@ -666,8 +663,7 @@ public void EndBiDiWrapper(IFragmentWriter writer) return; } /// - /// Creates a new run that is appended to the doc's last paragraph, - /// if one exists, or to a new paragraph otherwise. + /// Add a new run to the writers WordFragment DocBody. /// /// /// @@ -719,45 +715,57 @@ public void AddToRunContent(IFragmentWriter writer, string txtContent) // For spaces to show correctly, set preserve spaces on the new text element WP.Text txt = new WP.Text(txtContent); txt.Space = SpaceProcessingModeValues.Preserve; - ((WordFragmentWriter)writer).GetCurrentRun() + ((WordFragmentWriter)writer).WordFragment.GetLastRun() .AppendChild(txt); } public void AddLineBreakInRunContent(IFragmentWriter writer) { - ((WordFragmentWriter)writer).GetCurrentRun() + ((WordFragmentWriter)writer).WordFragment.GetLastRun() .AppendChild(new WP.Break()); } public void StartTable(IFragmentWriter writer) { - return; + Debug.Assert(((WordFragmentWriter)writer).CurrentTable == null, + "Not expecting nested tables. Treating it as a new table."); + + ((WordFragmentWriter)writer).CurrentTable = new WP.Table(); + ((WordFragmentWriter)writer).WordFragment.DocBody.Append(((WordFragmentWriter)writer).CurrentTable); } public void AddTableTitle(IFragmentWriter writer, IFragment content) { - return; + WP.TableRow tblTitleRow = new WP.TableRow(); + tblTitleRow.Append(new WP.TableCell(new WP.Paragraph(new WP.Run(new WP.Text(content.ToString()))))); + ((WordFragmentWriter)writer).CurrentTable.Append(tblTitleRow); } public void StartTableBody(IFragmentWriter writer) { - return; + // Nothing to do for Word export. } public void StartTableRow(IFragmentWriter writer) { - return; + Debug.Assert(((WordFragmentWriter)writer).CurrentTableRow == null, + "Not expecting nested tables rows. Treating it as a new table row."); + + ((WordFragmentWriter)writer).CurrentTableRow = new WP.TableRow(); + ((WordFragmentWriter)writer).CurrentTable.Append(((WordFragmentWriter)writer).CurrentTableRow); } public void AddTableCell(IFragmentWriter writer, bool isHead, int colSpan, HorizontalAlign alignment, IFragment content) { - return; + WP.TableCell tableCell = new WP.TableCell(); + tableCell.Append(new WP.Paragraph(new WP.Run(new WP.Text(content.ToString())))); + ((WordFragmentWriter)writer).CurrentTableRow.Append(tableCell); } public void EndTableRow(IFragmentWriter writer) { - return; + ((WordFragmentWriter)writer).CurrentTableRow = null; } public void EndTableBody(IFragmentWriter writer) { - return; + // Nothing to do for Word export. } public void EndTable(IFragmentWriter writer) { - return; + ((WordFragmentWriter)writer).CurrentTable = null; } public void StartEntry(IFragmentWriter writer, ConfigurableDictionaryNode config, string className, Guid entryGuid, int index, RecordClerk clerk) { @@ -783,17 +791,33 @@ public void AddEntryData(IFragmentWriter writer, List().ToList()); - foreach (WP.Run run in pieceRuns) + var elements = frag.DocBody.Elements().ToList(); + foreach (OpenXmlElement elem in elements) { - // For spaces to show correctly, set preserve spaces on the text element - WP.Text txt = new WP.Text(" "); - txt.Space = SpaceProcessingModeValues.Preserve; - run.AppendChild(txt); - wordWriter.Insert(run); + switch (elem) + { + case WP.Run run: + // For spaces to show correctly, set preserve spaces on the text element + WP.Text txt = new WP.Text(" "); + txt.Space = SpaceProcessingModeValues.Preserve; + run.AppendChild(txt); + wordWriter.WordFragment.AppendToParagraph(run, wordWriter.ForceNewParagraph); + wordWriter.ForceNewParagraph = false; + + // Add the paragraph style. + wordWriter.WordFragment.LinkParaStyle(frag.ParagraphStyle); + + break; + case WP.Table table: + wordWriter.WordFragment.Append(table); + + // Start a new paragraph with the next run to maintain the correct position of the table. + wordWriter.ForceNewParagraph = true; + break; + default: + throw new Exception("Unexpected element type on DocBody: " + elem.GetType().ToString()); + + } } } }