From f8043a2623fb612dda2262e5a33e30400101bac1 Mon Sep 17 00:00:00 2001 From: Mark Kidder <83427558+mark-sil@users.noreply.github.com> Date: Thu, 13 Jun 2024 10:11:55 -0400 Subject: [PATCH] LT-21785: Add more style support (#77) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * LT-21785: Add more style support - When a run is created set the style name. (It may get modified with basedOn information as we get other notifications.) - Add styles during run creation. - Allow styles to be based on other styles, which can be based on other styles… - Support different styles on different document fragments. TODO: A remaining task is to change the style names from the config.Style to the config.DisplayLabel. This will improve the style names that appear in Word. At that time we will need to add additional code to generate a unique style name if the properties of a style are different. --- Src/xWorks/ConfiguredLcmGenerator.cs | 4 +- Src/xWorks/ILcmContentGenerator.cs | 2 +- Src/xWorks/LcmJsonGenerator.cs | 2 +- Src/xWorks/LcmWordGenerator.cs | 472 ++++++++++++++++----------- Src/xWorks/LcmXhtmlGenerator.cs | 2 +- Src/xWorks/WordStylesGenerator.cs | 77 ++--- 6 files changed, 309 insertions(+), 250 deletions(-) diff --git a/Src/xWorks/ConfiguredLcmGenerator.cs b/Src/xWorks/ConfiguredLcmGenerator.cs index 6d31f7ee34..a131ff96a7 100644 --- a/Src/xWorks/ConfiguredLcmGenerator.cs +++ b/Src/xWorks/ConfiguredLcmGenerator.cs @@ -2807,7 +2807,7 @@ private static IFragment GenerateAudioWsContent(string wsId, private static void GenerateRunWithPossibleLink(GeneratorSettings settings, string writingSystem, IFragmentWriter writer, string style, string text, Guid linkDestination, bool rightToLeft, ConfigurableDictionaryNode config, string externalLink = null) { - settings.ContentGenerator.StartRun(writer, writingSystem); + settings.ContentGenerator.StartRun(writer, config, settings.PropertyTable, writingSystem); var wsRtl = settings.Cache.WritingSystemFactory.get_Engine(writingSystem).RightToLeftScript; if (rightToLeft != wsRtl) { @@ -3031,7 +3031,7 @@ private static void GenerateTableRow(ITsString rowUSFM, IFragmentWriter writer, private static void GenerateError(string text, IFragmentWriter writer, GeneratorSettings settings) { var writingSystem = settings.Cache.WritingSystemFactory.GetStrFromWs(settings.Cache.WritingSystemFactory.UserWs); - settings.ContentGenerator.StartRun(writer, writingSystem); + settings.ContentGenerator.StartRun(writer, null, settings.PropertyTable, writingSystem); settings.ContentGenerator.SetRunStyle(writer, null, settings.PropertyTable, writingSystem, null, true); if (text.Contains(TxtLineSplit)) { diff --git a/Src/xWorks/ILcmContentGenerator.cs b/Src/xWorks/ILcmContentGenerator.cs index 186294e118..53e556eeeb 100644 --- a/Src/xWorks/ILcmContentGenerator.cs +++ b/Src/xWorks/ILcmContentGenerator.cs @@ -32,7 +32,7 @@ IFragment GenerateGroupingNode(object field, string className, ConfigurableDicti void EndMultiRunString(IFragmentWriter writer); void StartBiDiWrapper(IFragmentWriter writer, bool rightToLeft); void EndBiDiWrapper(IFragmentWriter writer); - void StartRun(IFragmentWriter writer, string writingSystem); + void StartRun(IFragmentWriter writer, ConfigurableDictionaryNode config, ReadOnlyPropertyTable propTable, string writingSystem); void EndRun(IFragmentWriter writer); void SetRunStyle(IFragmentWriter writer, ConfigurableDictionaryNode config, ReadOnlyPropertyTable propertyTable, string writingSystem, string runStyle, bool error); void StartLink(IFragmentWriter writer, ConfigurableDictionaryNode config, Guid destination); diff --git a/Src/xWorks/LcmJsonGenerator.cs b/Src/xWorks/LcmJsonGenerator.cs index 3821c6bfd9..0a85fee66f 100644 --- a/Src/xWorks/LcmJsonGenerator.cs +++ b/Src/xWorks/LcmJsonGenerator.cs @@ -161,7 +161,7 @@ public void EndBiDiWrapper(IFragmentWriter writer) { } - public void StartRun(IFragmentWriter writer, string writingSystem) + public void StartRun(IFragmentWriter writer, ConfigurableDictionaryNode config, ReadOnlyPropertyTable propTable, string writingSystem) { var jsonWriter = (JsonFragmentWriter)writer; jsonWriter.StartObject(); diff --git a/Src/xWorks/LcmWordGenerator.cs b/Src/xWorks/LcmWordGenerator.cs index 2e5367f0bf..f38ec6a70a 100644 --- a/Src/xWorks/LcmWordGenerator.cs +++ b/Src/xWorks/LcmWordGenerator.cs @@ -35,7 +35,6 @@ namespace SIL.FieldWorks.XWorks public class LcmWordGenerator : ILcmContentGenerator, ILcmStylesGenerator { private LcmCache Cache { get; } - private static Styles _styleSheet { get; set; } = new Styles(); private static Dictionary _styleDictionary = new Dictionary(); private ReadOnlyPropertyTable _propertyTable; internal const int maxImageHeightInches = 1; @@ -127,21 +126,22 @@ public static void SavePublishedDocx(int[] entryHvos, DictionaryPublicationDecor { // Initialize word doc's styles xml stylePart = AddStylesPartToPackage(fragment.DocFrag); + Styles styleSheet = new Styles(); - // Add generated styles into the stylesheet from the dictionary - foreach (var style in _styleDictionary.Values) + lock(_styleDictionary) { - _styleSheet.AppendChild(style.CloneNode(true)); + // Add generated styles into the stylesheet from the dictionary + foreach (var style in _styleDictionary.Values) + { + styleSheet.AppendChild(style.CloneNode(true)); + } + + // clear the dictionary + _styleDictionary = new Dictionary(); } // Clone styles from the stylesheet into the word doc's styles xml - stylePart.Styles = ((Styles)_styleSheet.CloneNode(true)); - - // clear the dictionary - _styleDictionary = new Dictionary(); - - // clear the styleSheet - _styleSheet = new WP.Styles(); + stylePart.Styles = ((Styles)styleSheet.CloneNode(true)); } fragment.DocFrag.Dispose(); @@ -214,7 +214,7 @@ public DocFragment(MemoryStream str) /// public DocFragment(string str) : this() { - // Only create paragraph, run, and text objects if the string is nonempty + // Only create run, and text objects if the string is nonempty if (!string.IsNullOrEmpty(str)) { WP.Run run = DocBody.AppendChild(new WP.Run()); @@ -248,115 +248,28 @@ internal static DocFragment GenerateLetterHeaderDocFragment(string str, string s return docFrag; } - public static void LinkStyleOrInheritParentStyle(IFragment content, ConfigurableDictionaryNode config) + public static string GetWsStyleName(LcmCache cache, string styleName, ConfigurableDictionaryNode config, string writingSystem) { - DocFragment frag = ((DocFragment)content); - - // Don't add style for tables. - if (frag.DocBody.Elements().FirstOrDefault() != null) + // If the config does not contain writing system options, then just return the style name.(An example is custom fields.) + if (!(config.DictionaryNodeOptions is DictionaryNodeWritingSystemOptions)) { - return; + return styleName; } - if (!string.IsNullOrEmpty(config.Style)) - { - frag.AddStyleLink(config.Style, config, config.StyleType); - } - else if (!string.IsNullOrEmpty(config.Parent?.Style)) + var wsStr = writingSystem; + var possiblyMagic = WritingSystemServices.GetMagicWsIdFromName(writingSystem); + // If it is magic, then get the associated ws. + if (possiblyMagic != 0) { - frag.AddStyleLink(config.Parent.Style, config.Parent, config.Parent.StyleType); + // Get a list of the writing systems for the magic name, and use the first one. + wsStr = WritingSystemServices.GetWritingSystemList(cache, possiblyMagic, false).First().Id; } - } - public void AddStyleLink(string styleName, ConfigurableDictionaryNode config, ConfigurableDictionaryNode.StyleTypes styleType) - { - styleName = GetWsStyleName(styleName, config); - if (string.IsNullOrEmpty(styleName)) - return; - - if (styleType == ConfigurableDictionaryNode.StyleTypes.Paragraph) - { - if (string.IsNullOrEmpty(ParagraphStyle)) - { - ParagraphStyle = styleName; - } - } - else - LinkCharStyle(styleName); - } - - /// - /// 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. - /// - /// - internal void LinkParaStyle(string styleName) - { - if (string.IsNullOrEmpty(styleName)) - return; - - WP.Paragraph par = GetLastParagraph(); - if (par.ParagraphProperties != null) - { - // if a style is already linked to the paragraph, return without adding another link - if (par.ParagraphProperties.Descendants().Any()) - return; - - par.ParagraphProperties.PrependChild(new ParagraphStyleId() { Val = styleName }); - } - else - { - WP.ParagraphProperties paragraphProps = new WP.ParagraphProperties(new ParagraphStyleId() { Val = styleName }); - par.PrependChild(paragraphProps); - } - } - - private void LinkCharStyle(string styleName) - { - WP.Run run = GetLastRun(); - if (run.RunProperties != null) - { - // if a style is already linked to the run, replace the stylename and return without adding another link - if (run.RunProperties.Descendants().Any()) - { - run.RunProperties.Descendants().Last().Val = styleName; - return; - } - - run.RunProperties.Append(new RunStyle() { Val = styleName }); - } - else - { - WP.RunProperties runProps = - new WP.RunProperties(new RunStyle() { Val = styleName }); - // Prepend runproperties so it appears before any text elements contained in the run - run.PrependChild(runProps); - } - } - - public static string GetWsStyleName(string styleName, ConfigurableDictionaryNode config) - { - if (config.DictionaryNodeOptions is DictionaryNodeWritingSystemOptions) - { - foreach (var opt in ((DictionaryNodeWritingSystemOptions)config.DictionaryNodeOptions).Options) - { - if (opt.IsEnabled) - { - // If it's magic then don't return a language tag specific style. - var possiblyMagic = WritingSystemServices.GetMagicWsIdFromName(opt.Id); - if (possiblyMagic != 0) - { - return styleName; - } - // else, the DictionaryNodeOption Id specifies a particular writing system - // if there is no base style, return just the ws style - if (styleName == null) - return WordStylesGenerator.GetWsString(opt.Id); - // if there is a base style, return the ws-specific version of that style - return styleName + WordStylesGenerator.GetWsString(opt.Id); - } - } - } - return styleName; + // If there is no base style, return just the ws style. + if (styleName == null) + return WordStylesGenerator.GetWsString(wsStr); + // If there is a base style, return the ws-specific version of that style. + return styleName + WordStylesGenerator.GetWsString(wsStr); } /// @@ -704,17 +617,57 @@ public void Insert(IFragment frag) /// /// Add a new run to the WordFragment DocBody. /// - public void CreateRun(string writingSystem) + public void AddRun(LcmCache cache, ConfigurableDictionaryNode config, ReadOnlyPropertyTable propTable, string writingSystem) { + var run = new WP.Run(); + WordFragment.DocBody.AppendChild(run); + if (writingSystem == null) - WordFragment.DocBody.AppendChild(new WP.Run()); + { + return; + } + + string styleName = null; + if (config == null || string.IsNullOrEmpty(config.Style)) + { + styleName = WordStylesGenerator.GetWsString(writingSystem); + } else { - var wsString = WordStylesGenerator.GetWsString(writingSystem); - var run = new WP.Run(); - run.Append(new RunProperties()); - run.RunProperties.Append(new RunStyle() { Val = "span"+wsString }); - WordFragment.DocBody.AppendChild(run); + styleName = DocFragment.GetWsStyleName(cache, config.Style, config, writingSystem); + } + run.Append(new RunProperties(new RunStyle() { Val = styleName })); + + // If the style is not in the dictionary, then add it. + lock (_styleDictionary) + { + if (!_styleDictionary.ContainsKey(styleName)) + { + if (config != null && !string.IsNullOrEmpty(config.Style)) + { + var wsId = cache.LanguageWritingSystemFactoryAccessor.GetWsFromStr(writingSystem); + Style style = WordStylesGenerator.GenerateWordStyleFromLcmStyleSheet(config.Style, wsId, propTable); + if (style == null || style.Type != StyleValues.Character) + { + // If we hit this assert, then we might end up referencing a style that + // does not get created. + Debug.Assert(false); + return; + } + + var wsString = WordStylesGenerator.GetWsString(writingSystem); + style.Append(new BasedOn() { Val = wsString }); + style.StyleId = styleName; + style.StyleName = new StyleName() { Val = style.StyleId }; + _styleDictionary[styleName] = style; + } + else + { + // If we hit this assert, then we might need to create a style for just the ws. + // We are expecting the ws style to be added to the _styleDictionary in GetDefaultWordStyles(). + Debug.Assert(false); + } + } } } } @@ -737,17 +690,21 @@ public IFragment GenerateAudioLinkContent(string classname, string srcAttribute, } public IFragment WriteProcessedObject(bool isBlock, IFragment elementContent, ConfigurableDictionaryNode config, string className) { - return WriteProcessedElementContent(elementContent, config, className); + return WriteProcessedElementContent(elementContent, config); } public IFragment WriteProcessedCollection(bool isBlock, IFragment elementContent, ConfigurableDictionaryNode config, string className) { - return WriteProcessedElementContent(elementContent, config, className); + return WriteProcessedElementContent(elementContent, config); } - private IFragment WriteProcessedElementContent(IFragment elementContent, ConfigurableDictionaryNode config, string className) + private IFragment WriteProcessedElementContent(IFragment elementContent, ConfigurableDictionaryNode config) { - // Use the style name and type of the config node or its parent to link a style to the elementContent fragment where the processed contents are written. - DocFragment.LinkStyleOrInheritParentStyle(elementContent, config); + // Check if the character style for the last run should be modified. + if (string.IsNullOrEmpty(config.Style) && !string.IsNullOrEmpty(config.Parent.Style) && + (config.Parent.StyleType != ConfigurableDictionaryNode.StyleTypes.Paragraph)) + { + AddRunStyle(elementContent, config.Parent.Style, false); + } bool eachOnANewLine = config != null && config.DictionaryNodeOptions is IParaOption && @@ -885,15 +842,14 @@ public IFragment AddSenseData(IFragment senseNumberSpan, Guid ownerGuid, Configu return senseData; } + public IFragment AddCollectionItem(bool isBlock, string collectionItemClass, ConfigurableDictionaryNode config, IFragment content, bool first) { - if (!string.IsNullOrEmpty(config.Style)) + // Add the style to all the runs in the content fragment. + if (!string.IsNullOrEmpty(config.Style) && + (config.StyleType != ConfigurableDictionaryNode.StyleTypes.Paragraph)) { - if (isBlock && (config.StyleType == ConfigurableDictionaryNode.StyleTypes.Paragraph)) - ((DocFragment)content).AddStyleLink(config.Style, config, ConfigurableDictionaryNode.StyleTypes.Paragraph); - - else if (!isBlock) - ((DocFragment)content).AddStyleLink(config.Style, config,ConfigurableDictionaryNode.StyleTypes.Character); + AddRunStyle(content, config.Style, true); } var collData = CreateFragment(); @@ -974,9 +930,9 @@ public void EndBiDiWrapper(IFragmentWriter writer) /// /// /// - public void StartRun(IFragmentWriter writer, string writingSystem) + public void StartRun(IFragmentWriter writer, ConfigurableDictionaryNode config, ReadOnlyPropertyTable propTable, string writingSystem) { - ((WordFragmentWriter)writer).CreateRun(writingSystem); + ((WordFragmentWriter)writer).AddRun(Cache, config, propTable, writingSystem); } public void EndRun(IFragmentWriter writer) { @@ -986,7 +942,7 @@ public void EndRun(IFragmentWriter writer) } /// - /// Set the style for a specific run. + /// Overrides the style for a specific run. /// This is needed to set the specific style for any field that allows the /// default style to be overridden (Table Cell, Custom Field, Note...). /// @@ -994,32 +950,15 @@ public void SetRunStyle(IFragmentWriter writer, ConfigurableDictionaryNode confi { if (!string.IsNullOrEmpty(runStyle)) { - // Add the style link. - ((WordFragmentWriter)writer).WordFragment.AddStyleLink(runStyle, config, ConfigurableDictionaryNode.StyleTypes.Character); - - // Only add the style to the styleSheet if not already there. - if (!_styleSheet.ChildElements.Any(p => ((Style)p).StyleId == runStyle)) - { - int ws = Cache.WritingSystemFactory.GetWsFromStr(writingSystem); - var wpStyle = WordStylesGenerator.GenerateWordStyleFromLcmStyleSheet(runStyle, ws, _propertyTable); - _styleSheet.Append(wpStyle); - } + AddRunStyle(((WordFragmentWriter)writer).WordFragment, runStyle, false); } } public void StartLink(IFragmentWriter writer, ConfigurableDictionaryNode config, Guid destination) { - if (config != null && !string.IsNullOrEmpty(config.Style)) - { - ((WordFragmentWriter)writer).WordFragment.AddStyleLink(config.Style, config, ConfigurableDictionaryNode.StyleTypes.Character); - } return; } public void StartLink(IFragmentWriter writer, ConfigurableDictionaryNode config, string externalDestination) { - if (config != null && !string.IsNullOrEmpty(config.Style)) - { - ((WordFragmentWriter)writer).WordFragment.AddStyleLink(config.Style, config, ConfigurableDictionaryNode.StyleTypes.Character); - } return; } public void EndLink(IFragmentWriter writer) @@ -1284,16 +1223,17 @@ public void AddEntryData(IFragmentWriter writer, List()) { - _styleSheet.Append(style.CloneNode(true)); + lock(_styleDictionary) + { + _styleDictionary[style.StyleId] = (WP.Style)style.CloneNode(true); + } } } @@ -1476,6 +1424,37 @@ public void AddGlobalStyles(DictionaryConfigurationModel model, ReadOnlyProperty // TODO: Generate style for audiows after we add audio to export //WordStylesGenerator.GenerateWordStyleForAudioWs(_styleSheet, cache); } + + /// + /// Gets the style from the dictionary (if it is in the dictionary). If not in the + /// dictionary then create the Word style from the LCM Style Sheet and add it to the dictionary. + /// + /// Returns null if it fails to find or create the character style. + private Style GetOrCreateCharacterStyle(string styleName) + { + Style retStyle = null; + lock (_styleDictionary) + { + if (_styleDictionary.TryGetValue(styleName, out retStyle)) + { + if (retStyle.Type != StyleValues.Character) + { + return null; + } + } + else + { + retStyle = WordStylesGenerator.GenerateWordStyleFromLcmStyleSheet(styleName, 0, _propertyTable); + if (retStyle == null || retStyle.Type != StyleValues.Character) + { + return null; + } + _styleDictionary[styleName] = retStyle; + } + } + return retStyle; + } + public string AddStyles(ConfigurableDictionaryNode node) { return AddStyles(node, false); @@ -1494,23 +1473,24 @@ public string AddStyles(ConfigurableDictionaryNode node, bool addEntryContinuati // Generate all styles that are needed by this class and add them to the dictionary with their stylename as the key. var className = $".{CssGenerator.GetClassAttributeForConfig(node)}"; - lock (_styleDictionary) + Styles styleContent = null; + if (addEntryContinuationStyle) { - Styles styleContent = null; - if (addEntryContinuationStyle) - { - styleContent = WordStylesGenerator.CheckRangeOfStylesForEmpties(WordStylesGenerator.GenerateContinuationWordStyles(node, _propertyTable)); - } - else - { - styleContent = WordStylesGenerator.CheckRangeOfStylesForEmpties(WordStylesGenerator.GenerateWordStylesFromConfigurationNode(node, className, _propertyTable)); - } - if (styleContent == null) - return className; - if (!styleContent.Any()) - return className; + styleContent = WordStylesGenerator.CheckRangeOfStylesForEmpties(WordStylesGenerator.GenerateContinuationWordStyles(node, _propertyTable)); + } + else + { + styleContent = WordStylesGenerator.CheckRangeOfStylesForEmpties(WordStylesGenerator.GenerateWordStylesFromConfigurationNode(node, className, _propertyTable)); + } + + if (styleContent == null) + return className; + if (!styleContent.Any()) + return className; - foreach (Style style in styleContent.Descendants