Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Word Export LT-21891: Add guidewords and page numbers #217

Merged
merged 3 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 98 additions & 3 deletions Src/xWorks/LcmWordGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public static void SavePublishedDocx(int[] entryHvos, DictionaryPublicationDecor
{ ContentGenerator = generator, StylesGenerator = generator};
settings.StylesGenerator.AddGlobalStyles(configuration, readOnlyPropertyTable);
string lastHeader = null;
string firstHeadwordStyle = null;
var entryContents = new Tuple<ICmObject, IFragment>[entryCount];
var entryActions = new List<Action>();

Expand Down Expand Up @@ -119,14 +120,21 @@ public static void SavePublishedDocx(int[] entryHvos, DictionaryPublicationDecor

// Append the entry to the word doc
fragment.Append(entry.Item2);

if (string.IsNullOrEmpty(firstHeadwordStyle))
{
firstHeadwordStyle = GetFirstHeadwordStyle((DocFragment)entry.Item2);
}
}
}
col?.Dispose();

// Set the last section of the document to be two columns. (The last section is all the
// entries after the last letter header.) For the last section this information is stored
// Set the last section of the document to be two columns and add the page headers. (The last section
// is all the entries after the last letter header.) For the last section this information is stored
// different than all the other sections. It is stored as the last child element of the body.
var sectProps = new SectionProperties(
new HeaderReference() { Id = WordStylesGenerator.PageHeaderIdEven, Type = HeaderFooterValues.Even },
new HeaderReference() { Id = WordStylesGenerator.PageHeaderIdOdd, Type = HeaderFooterValues.Default },
new Columns() { EqualWidth = true, ColumnCount = 2 },
new SectionType() { Val = SectionMarkValues.Continuous }
);
Expand Down Expand Up @@ -169,6 +177,13 @@ public static void SavePublishedDocx(int[] entryHvos, DictionaryPublicationDecor
stylePart.Styles = ((Styles)styleSheet.CloneNode(true));
}

// Add the page headers.
var headerParts = fragment.mainDocPart.HeaderParts;
if (!headerParts.Any())
{
AddPageHeaderPartsToPackage(fragment.DocFrag, firstHeadwordStyle);
}

// Add document settings
DocumentSettingsPart settingsPart = fragment.mainDocPart.DocumentSettingsPart;
if (settingsPart == null)
Expand All @@ -192,7 +207,9 @@ public static void SavePublishedDocx(int[] entryHvos, DictionaryPublicationDecor
Name = CompatSettingNameValues.OverrideTableStyleFontSizeAndJustification,
Val = new StringValue("0"),
Uri = new StringValue("http://schemas.microsoft.com/office/word")
}
},
new EvenAndOddHeaders() // Use different page headers for the even and odd pages.

// If in the future, if we find that certain style items are different in different versions of word,
// it may help to specify more compatibility settings.
// A full list of all possible compatibility settings may be found here:
Expand Down Expand Up @@ -301,6 +318,8 @@ internal static DocFragment GenerateLetterHeaderDocFragment(string str, string s
// paragraph with two columns for the last paragraph in the section that uses 2
// columns. (The section is all the entries after the previous letter header.)
var sectProps2 = new SectionProperties(
new HeaderReference() { Id = WordStylesGenerator.PageHeaderIdEven, Type = HeaderFooterValues.Even },
new HeaderReference() { Id = WordStylesGenerator.PageHeaderIdOdd, Type = HeaderFooterValues.Default },
new Columns() { EqualWidth = true, ColumnCount = 2 },
new SectionType() { Val = SectionMarkValues.Continuous }
);
Expand All @@ -318,6 +337,8 @@ internal static DocFragment GenerateLetterHeaderDocFragment(string str, string s
// Only the Letter Header should be 1 column. Create a empty paragraph with one
// column so the previous letter header paragraph uses 1 column.
var sectProps1 = new SectionProperties(
new HeaderReference() { Id = WordStylesGenerator.PageHeaderIdEven, Type = HeaderFooterValues.Even },
new HeaderReference() { Id = WordStylesGenerator.PageHeaderIdOdd, Type = HeaderFooterValues.Default },
new Columns() { EqualWidth = true, ColumnCount = 1 },
new SectionType() { Val = SectionMarkValues.Continuous }
);
Expand Down Expand Up @@ -1774,6 +1795,10 @@ public void AddGlobalStyles(DictionaryConfigurationModel model, ReadOnlyProperty
s_styleCollection.AddParagraphStyle(normStyle, WordStylesGenerator.NormalParagraphStyleName, normStyle.StyleId, bulletInfo);
}

var pageHeaderStyle = WordStylesGenerator.GeneratePageHeaderStyle(normStyle);
// Intentionally re-using the bulletInfo from Normal.
s_styleCollection.AddParagraphStyle(pageHeaderStyle, WordStylesGenerator.PageHeaderStyleName, pageHeaderStyle.StyleId, bulletInfo);

var mainStyle = WordStylesGenerator.GenerateMainEntryParagraphStyle(propertyTable, model, out ConfigurableDictionaryNode node, out bulletInfo);
if (mainStyle != null)
{
Expand Down Expand Up @@ -1982,6 +2007,58 @@ public static NumberingDefinitionsPart AddNumberingPartToPackage(WordprocessingD
return part;
}

// Add the page HeaderParts to the document.
public static void AddPageHeaderPartsToPackage(WordprocessingDocument doc, string headwordStyle)
{
// Generate header for even pages.
HeaderPart even = doc.MainDocumentPart.AddNewPart<HeaderPart>(WordStylesGenerator.PageHeaderIdEven);
GenerateHeaderPartContent(even, true, headwordStyle);

// Generate header for odd pages.
HeaderPart odd = doc.MainDocumentPart.AddNewPart<HeaderPart>(WordStylesGenerator.PageHeaderIdOdd);
GenerateHeaderPartContent(odd, false, headwordStyle);
}

/// <summary>
/// Adds the page number and the first or last headword to the HeaderPart.
/// </summary>
/// <param name="part">HeaderPart to modify.</param>
/// <param name="even">True = generate content for even pages.
/// False = generate content for odd pages.</param>
/// <param name="headwordStyle">The style that will be used to find the first or last headword on the page.</param>
private static void GenerateHeaderPartContent(HeaderPart part, bool even, string headwordStyle)
{
ParagraphStyleId paraStyleId = new ParagraphStyleId() { Val = WordStylesGenerator.PageHeaderStyleName };
Paragraph para = new Paragraph(new ParagraphProperties(paraStyleId));

if (even)
{
if (!string.IsNullOrEmpty(headwordStyle))
{
// Add the first headword on the page to the header.
para.Append(new Run(new SimpleField() { Instruction = "STYLEREF " + headwordStyle + " \\* MERGEFORMAT" }));
}
para.Append(new WP.Run(new WP.TabChar()));
// Add the page number to the header.
para.Append(new WP.Run(new SimpleField() { Instruction = "PAGE" }));
}
else
{
// Add the page number to the header.
para.Append(new WP.Run(new SimpleField() { Instruction = "PAGE" }));
para.Append(new WP.Run(new WP.TabChar()));
if (!string.IsNullOrEmpty(headwordStyle))
{
// Add the last headword on the page to the header.
para.Append(new WP.Run(new SimpleField() { Instruction = "STYLEREF " + headwordStyle + " \\l \\* MERGEFORMAT" }));
}
}

Header header = new Header(para);
part.Header = header;
part.Header.Save();
}

// Add an ImagePart to the document. Returns the part ID.
public static string AddImagePartToPackage(WordprocessingDocument doc, string imagePath, ImagePartType imageType = ImagePartType.Jpeg)
{
Expand Down Expand Up @@ -2853,6 +2930,24 @@ internal static bool IsWritingSystemRightToLeft(LcmCache cache, int wsId)
return lgWritingSystem.RightToLeftScript;
}

/// <summary>
/// Get the full style name for the first RunStyle that begins with "Headword".
/// </summary>
/// <returns>The full style name that begins with "Headword".
/// Null if none are found.</returns>
public static string GetFirstHeadwordStyle(DocFragment frag)
{
// Find the first run style with a value that begins with "Headword".
foreach (RunStyle runStyle in frag.DocBody.Descendants<RunStyle>())
{
if (runStyle.Val.Value.StartsWith(WordStylesGenerator.HeadwordDisplayName))
{
return runStyle.Val.Value;
}
}
return null;
}

/// <summary>
/// Added to support tests.
/// </summary>
Expand Down
26 changes: 25 additions & 1 deletion Src/xWorks/WordStylesGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
using System.Linq;
using XCore;


namespace SIL.FieldWorks.XWorks
{
public class WordStylesGenerator
Expand All @@ -32,18 +31,23 @@ public class WordStylesGenerator
internal const string SenseNumberDisplayName = "Sense Number";
internal const string WritingSystemStyleName = "Writing System Abbreviation";
internal const string WritingSystemDisplayName = "Writing System Abbreviation";
internal const string HeadwordDisplayName = "Headword";
internal const string StyleSeparator = " : ";
internal const string LangTagPre = "[lang=\'";
internal const string LangTagPost = "\']";

// Globals and default paragraph styles.
internal const string NormalParagraphStyleName = "Normal";
internal const string PageHeaderStyleName = "Header";
internal const string MainEntryParagraphDisplayName = "Main Entry";
internal const string LetterHeadingStyleName = "Dictionary-LetterHeading";
internal const string LetterHeadingDisplayName = "Letter Heading";
internal const string PictureAndCaptionTextframeStyle = "Image-Textframe-Style";
internal const string EntryStyleContinue = "-Continue";

internal const string PageHeaderIdEven = "EvenPages";
internal const string PageHeaderIdOdd = "OddPages";

public static Style GenerateLetterHeaderParagraphStyle(ReadOnlyPropertyTable propertyTable, out BulletInfo? bulletInfo)
{
var style = GenerateParagraphStyleFromLcmStyleSheet(LetterHeadingStyleName, DefaultStyle, propertyTable, out bulletInfo);
Expand Down Expand Up @@ -85,6 +89,26 @@ public static Style GenerateMainEntryParagraphStyle(ReadOnlyPropertyTable proper
return style;
}

/// <summary>
/// Generate the style that will be used for the header that goes on the top of
/// every page. The header style will be similar to the provided style, with the
/// addition of the tab stop.
/// </summary>
/// <param name="style">The style to based the header style on.</param>
/// <returns>The header style.</returns>
internal static Style GeneratePageHeaderStyle(Style style)
{
Style pageHeaderStyle = (Style)style.CloneNode(true);
pageHeaderStyle.StyleId = PageHeaderStyleName;
pageHeaderStyle.StyleName.Val = pageHeaderStyle.StyleId;

// Add the tab stop.
var tabs = new Tabs();
tabs.Append(new TabStop() { Val = TabStopValues.End, Position = (int)(1440 * 6.5/*inches*/) });
pageHeaderStyle.StyleParagraphProperties.Append(tabs);
return pageHeaderStyle;
}

/// <summary>
/// Generates a Word Paragraph Style for the requested FieldWorks style.
/// </summary>
Expand Down
37 changes: 37 additions & 0 deletions Src/xWorks/xWorksTests/LcmWordGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -816,5 +816,42 @@ public void GenerateContinueParagraph()
// Assert that the continuation paragraph uses the continuation style.
Assert.True(result.mainDocPart.RootElement.OuterXml.Contains(MainEntryParagraphDisplayName + WordStylesGenerator.EntryStyleContinue));
}

[Test]
public void GetFirstHeadwordStyle()
{
LcmWordGenerator.ClearStyleCollection();
DefaultSettings.StylesGenerator.AddGlobalStyles(null, new ReadOnlyPropertyTable(m_propertyTable));
var wsOpts = ConfiguredXHTMLGeneratorTests.GetWsOptionsForLanguages(new[] { "en" });
var glossNode = new ConfigurableDictionaryNode
{
FieldDescription = "Gloss",
DictionaryNodeOptions = wsOpts,
Style = "Dictionary-Headword",
Label = WordStylesGenerator.HeadwordDisplayName
};
var sensesNode = new ConfigurableDictionaryNode
{
FieldDescription = "Senses",
DictionaryNodeOptions = ConfiguredXHTMLGeneratorTests.GetSenseNodeOptions(),
Children = new List<ConfigurableDictionaryNode> { glossNode },
Style = DictionaryNormal
};
var mainEntryNode = new ConfigurableDictionaryNode
{
FieldDescription = "LexEntry",
Children = new List<ConfigurableDictionaryNode> { sensesNode },
Style = MainEntryParagraphStyleName,
Label = MainEntryParagraphDisplayName
};
CssGeneratorTests.PopulateFieldsForTesting(mainEntryNode);
var entry = ConfiguredXHTMLGeneratorTests.CreateInterestingLexEntry(Cache);
var result = ConfiguredLcmGenerator.GenerateContentForEntry(entry, mainEntryNode, null, DefaultSettings, 0) as DocFragment;

//SUT
string firstHeadwordStyle = LcmWordGenerator.GetFirstHeadwordStyle(result);

Assert.True(firstHeadwordStyle == "Headword[lang='en']");
}
}
}
Loading