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());
+
+ }
}
}
}