diff --git a/Src/xWorks/ConfiguredLcmGenerator.cs b/Src/xWorks/ConfiguredLcmGenerator.cs index 7769c093bc..cf0d35dd87 100644 --- a/Src/xWorks/ConfiguredLcmGenerator.cs +++ b/Src/xWorks/ConfiguredLcmGenerator.cs @@ -2676,7 +2676,7 @@ private static IFragment GenerateContentForSimpleString(List nodeList); + string AddStyles(List nodeList, bool addSpanBeforeAfter = false); void Init(ReadOnlyPropertyTable propertyTable); } diff --git a/Src/xWorks/CssGenerator.cs b/Src/xWorks/CssGenerator.cs index 73f07d2837..05fcccfb07 100644 --- a/Src/xWorks/CssGenerator.cs +++ b/Src/xWorks/CssGenerator.cs @@ -87,8 +87,13 @@ public void AddGlobalStyles(DictionaryConfigurationModel model, ReadOnlyProperty /// node.Parent's. Where it will differ is when a node has a reference node. Instead /// of containing 'node.Parent' it follows the path from where the reference is made. /// + /// This bool defaults to false but is set to true when the + /// content of a given node is a simple string. In this case, a writing system span will be + /// added to the style in the xhtml, and the before and after styles need "span" added in order to inherit + /// the properties of that writing system span. + /// /// The unique node name for the last node in the list. - public string AddStyles(List nodeList) + public string AddStyles(List nodeList, bool addSpanBeforeAfter = false) { lock (_styleDictionary) { @@ -104,7 +109,7 @@ public string AddStyles(List nodeList) if (!_styleDictionary.ContainsKey(uniqueNodeName)) { - var styleRules = GenerateCssFromConfigurationNode(workingNode, uniqueNodeName, _propertyTable).NonEmpty(); + var styleRules = GenerateCssFromConfigurationNode(workingNode, uniqueNodeName, _propertyTable, addSpanBeforeAfter).NonEmpty(); styleRules = styleRules.Distinct().ToList(); // Remove duplicate rules. AddUniquePathToStyleRules(styleRules, uniqueNodePath); _styleDictionary[uniqueNodeName] = styleRules; @@ -341,7 +346,7 @@ private static void GenerateCssForAudioWs(StyleSheet styleSheet, LcmCache cache) /// /// Generates css rules for a configuration node and adds them to the given stylesheet (recursive). /// - private static List GenerateCssFromConfigurationNode(ConfigurableDictionaryNode configNode, string baseSelection, ReadOnlyPropertyTable propertyTable) + private static List GenerateCssFromConfigurationNode(ConfigurableDictionaryNode configNode, string baseSelection, ReadOnlyPropertyTable propertyTable, bool addSpanBeforeAfter = false) { var cache = propertyTable.GetValue("cache"); switch (configNode.DictionaryNodeOptions) @@ -375,7 +380,7 @@ private static List GenerateCssFromConfigurationNode(ConfigurableDict var rule = new StyleRule(); var selectors = GenerateSelectorsFromNode(configNode, ref baseSelection, - cache, propertyTable); + cache, propertyTable, addSpanBeforeAfter); var wsOptions = configNode.DictionaryNodeOptions as DictionaryNodeWritingSystemOptions; if (wsOptions != null) @@ -828,7 +833,7 @@ private static List GenerateCssFromPictureOptions(ConfigurableDiction /// This method will generate before and after rules if the configuration node requires them. It also generates the selector for the node /// private static List GenerateSelectorsFromNode(ConfigurableDictionaryNode configNode, - ref string baseSelection, LcmCache cache, ReadOnlyPropertyTable propertyTable) + ref string baseSelection, LcmCache cache, ReadOnlyPropertyTable propertyTable, bool addSpanBeforeAfter = false) { var rules = new List(); var fwStyles = FontHeightAdjuster.StyleSheetFromPropertyTable(propertyTable); @@ -937,25 +942,44 @@ private static List GenerateSelectorsFromNode(ConfigurableDictionaryN if (!string.IsNullOrEmpty(configNode.Before)) { var dec = new StyleDeclaration(); + StyleRule beforeRule; dec.Add(new Property("content") { Term = new PrimitiveTerm(UnitType.String, SpecialCharacterHandling.MakeSafeCss(configNode.Before)) }); if (fwStyles != null && fwStyles.Styles.Contains(BeforeAfterBetweenStyleName)) dec.Properties.AddRange(GenerateCssStyleFromLcmStyleSheet(BeforeAfterBetweenStyleName, cache.DefaultAnalWs, propertyTable)); var selectorBase = collectionSelector; if (configNode.FieldDescription == "PicturesOfSenses") selectorBase += "> div:first-child"; - var beforeRule = new StyleRule(dec) { Value = GetBaseSelectionWithSelectors(selectorBase, ":before") }; + + // The addSpanBeforeAfter argument indicates whether we need to add a span to the before/after and skip the usual selector formatting. + // This is only needed in the case that we have a writing system unaware property that has had a writing system added via "GenerateContentForSimpleString". + if (addSpanBeforeAfter) + { + beforeRule = new StyleRule(dec) { Value = selectorBase + " span:before" }; + } + else + beforeRule = new StyleRule(dec) { Value = GetBaseSelectionWithSelectors(selectorBase, ":before") }; rules.Add(beforeRule); } if(!string.IsNullOrEmpty(configNode.After)) { var dec = new StyleDeclaration(); + StyleRule afterRule; dec.Add(new Property("content") { Term = new PrimitiveTerm(UnitType.String, SpecialCharacterHandling.MakeSafeCss(configNode.After)) }); if (fwStyles != null && fwStyles.Styles.Contains(BeforeAfterBetweenStyleName)) dec.Properties.AddRange(GenerateCssStyleFromLcmStyleSheet(BeforeAfterBetweenStyleName, cache.DefaultAnalWs, propertyTable)); var selectorBase = collectionSelector; if (configNode.FieldDescription == "PicturesOfSenses") selectorBase += "> div:last-child"; - var afterRule = new StyleRule(dec) { Value = GetBaseSelectionWithSelectors(selectorBase, ":after") }; + + // The addSpanBeforeAfter argument indicates whether we need to add a span to the before/after and skip the usual selector formatting. + // This is only needed in the case that we have a writing system unaware property that has had a writing system added via "GenerateContentForSimpleString". + if (addSpanBeforeAfter) + { + afterRule = new StyleRule(dec) { Value = selectorBase + " span:after" }; + } + else + afterRule = new StyleRule(dec) { Value = GetBaseSelectionWithSelectors(selectorBase, ":after") }; + rules.Add(afterRule); } return rules; diff --git a/Src/xWorks/LcmWordGenerator.cs b/Src/xWorks/LcmWordGenerator.cs index a621c1814c..e7dcd30d5e 100644 --- a/Src/xWorks/LcmWordGenerator.cs +++ b/Src/xWorks/LcmWordGenerator.cs @@ -1998,7 +1998,7 @@ public void AddGlobalStyles(DictionaryConfigurationModel model, ReadOnlyProperty /// the situations where a unique style name is generated, because the reference needs to use the /// unique name. /// - public string AddStyles(List nodeList) + public string AddStyles(List nodeList, bool addSpanBeforeAfter = false) { var node = nodeList.Last(); if (WordStylesGenerator.IsParagraphStyle(node.Style, _propertyTable)) diff --git a/Src/xWorks/LcmXhtmlGenerator.cs b/Src/xWorks/LcmXhtmlGenerator.cs index 1fd9e954cc..f037115879 100644 --- a/Src/xWorks/LcmXhtmlGenerator.cs +++ b/Src/xWorks/LcmXhtmlGenerator.cs @@ -1101,7 +1101,16 @@ public IFragment AddProperty(List nodeList, xw.WriteStartElement(isBlockProperty ? "div" : "span"); xw.WriteAttributeString("class", className); WriteNodeId(xw, nodeList.Last(), settings); + if (writingSystem != null) + { + xw.WriteStartElement("span"); + xw.WriteAttributeString("lang", writingSystem); + } xw.WriteString(content); + if (writingSystem != null) + { + xw.WriteEndElement(); + } xw.WriteEndElement(); xw.Flush(); return fragment; diff --git a/Src/xWorks/xWorksTests/ConfiguredXHTMLGeneratorTests.cs b/Src/xWorks/xWorksTests/ConfiguredXHTMLGeneratorTests.cs index 381bfee2a2..d42cff67e5 100644 --- a/Src/xWorks/xWorksTests/ConfiguredXHTMLGeneratorTests.cs +++ b/Src/xWorks/xWorksTests/ConfiguredXHTMLGeneratorTests.cs @@ -483,9 +483,15 @@ public void GenerateContentForEntry_HomographNumbersGeneratesCorrectResult() XHTMLStringBuilder.Append(result); XHTMLStringBuilder.AppendLine(""); - var entryWithHomograph = "/TESTWRAPPER/div[@class='lexentry']/span[@class='homographnumber' and text()='1']"; + // Normally the propertyvalue for a headword with homograph number is IMultiStringAccessor. + // However, in the test setup the propertyvalue for homograph number is an int + // and therefore hits the int case of GenerateContentForValue in ConfiguredLcmGenerator, + // and is directed to "GenerateContentForSimpleString", which applies the first analysis WS. + // This creates an extra "/span[@lang='en' and text()=...]" at the end of the lexentry. + // We don't care if a WS is assigned, so we ignore this possible extra span and check only for the correct homograph number. + var entryWithHomograph = "/TESTWRAPPER/div[@class='lexentry']/span[@class='homographnumber' and text()=1] | /TESTWRAPPER/div[@class='lexentry']/span[@class='homographnumber']/*[text()=1]"; AssertThatXmlIn.String(XHTMLStringBuilder.ToString()).HasSpecifiedNumberOfMatchesForXpath(entryWithHomograph, 1); - entryWithHomograph = "/TESTWRAPPER/div[@class='lexentry']/span[@class='homographnumber' and text()='2']"; + entryWithHomograph = "/TESTWRAPPER/div[@class='lexentry']/span[@class='homographnumber' and text()=2] | /TESTWRAPPER/div[@class='lexentry']/span[@class='homographnumber']/*[text()=2]"; AssertThatXmlIn.String(XHTMLStringBuilder.ToString()).HasSpecifiedNumberOfMatchesForXpath(entryWithHomograph, 1); } @@ -5504,7 +5510,7 @@ public void GenerateContentForEntry_PictureWithCreator() //SUT var result = ConfiguredLcmGenerator.GenerateContentForEntry(testEntry, mainEntryNode, null, settings).ToString(); const string oneSenseWithPicture = "/div[@class='lexentry']/span[@class='pictures']/div[@class='picture']/img[@class='photo' and @id]"; - const string oneSenseWithPictureCaption = "/div[@class='lexentry']/span[@class='pictures']/div[@class='picture']/div[@class='captionContent']/span[@class='creator' and text()='Jason Naylor']"; + const string oneSenseWithPictureCaption = "/div[@class='lexentry']/span[@class='pictures']/div[@class='picture']/div[@class='captionContent']/span[@class='creator']/span[@lang='en' and text()='Jason Naylor']"; //This assert is dependent on the specific entry data created in CreateInterestingLexEntry AssertThatXmlIn.String(result).HasSpecifiedNumberOfMatchesForXpath(oneSenseWithPicture, 1); AssertThatXmlIn.String(result).HasSpecifiedNumberOfMatchesForXpath(oneSenseWithPictureCaption, 1); @@ -6488,7 +6494,7 @@ public void GenerateContentForEntry_DateCustomFieldGeneratesContent() var settings = new ConfiguredLcmGenerator.GeneratorSettings(Cache, m_propertyTable, false, false, null); //SUT var result = ConfiguredLcmGenerator.GenerateContentForEntry(testEntry, mainEntryNode, null, settings).ToString(); - var customDataPath = string.Format("/div[@class='lexentry']/span[@class='customdate' and text()='{0}']", customData.ToLongDateString()); + var customDataPath = string.Format("/div[@class='lexentry']/span[@class='customdate']/span[@lang='en' and text()='{0}']", customData.ToLongDateString()); AssertThatXmlIn.String(result).HasSpecifiedNumberOfMatchesForXpath(customDataPath, 1); } } @@ -6518,7 +6524,7 @@ public void GenerateContentForEntry_IntegerCustomFieldGeneratesContent() var settings = new ConfiguredLcmGenerator.GeneratorSettings(Cache, m_propertyTable, false, false, null); //SUT var result = ConfiguredLcmGenerator.GenerateContentForEntry(testEntry, mainEntryNode, null, settings).ToString(); - var customDataPath = string.Format("/div[@class='lexentry']/span[@class='custominteger' and text()='{0}']", customData); + var customDataPath = string.Format("/div[@class='lexentry']/span[@class='custominteger']/span[@lang='en' and text()='{0}']", customData); AssertThatXmlIn.String(result).HasSpecifiedNumberOfMatchesForXpath(customDataPath, 1); } } diff --git a/Src/xWorks/xWorksTests/LcmJsonGeneratorTests.cs b/Src/xWorks/xWorksTests/LcmJsonGeneratorTests.cs index 1d322baf56..0d7f0f4589 100644 --- a/Src/xWorks/xWorksTests/LcmJsonGeneratorTests.cs +++ b/Src/xWorks/xWorksTests/LcmJsonGeneratorTests.cs @@ -1103,13 +1103,31 @@ public void SavePublishedJsonWithStyles_DisplayXhtmlPopulated() DefaultDecorator, 1, new DictionaryConfigurationModel { Parts = new List { mainEntryNode } }, m_propertyTable, "test.json", null, out int[] _); - var expectedResults = @"{""xhtmlTemplate"":""lexentry"",""guid"":""g" + testEntry.Guid + @""",""letterHead"": ""c"",""sortIndex"": 0, + + // An explicitly stated writing system is not necessary for the homograph number to be correct. + // Normally the propertyvalue for a headword with homograph number is IMultiStringAccessor. + // However, in the test setup the propertyvalue for homograph number is an int + // and therefore hits the int case of GenerateContentForValue in ConfiguredLcmGenerator, + // and is directed to "GenerateContentForSimpleString", which applies the first analysis WS. + // The homograph portion of this test is only concerned with checking value of the homograph number; we don't care if a WS is assigned. + var expectedResultsWithoutWs = @"{""xhtmlTemplate"":""lexentry"",""guid"":""g" + testEntry.Guid + @""",""letterHead"": ""c"",""sortIndex"": 0, ""homographnumber"":""0"",""citationform"":[{""lang"":""fr"",""value"":""Citation""}], ""displayXhtml"":""
0Citation
""}"; - var expected = (JObject)JsonConvert.DeserializeObject(expectedResults, new JsonSerializerSettings { Formatting = Formatting.None }); - VerifyJson(results[0][0].ToString(Formatting.None), expected); + var expectedResultsWithWs = @"{""xhtmlTemplate"":""lexentry"",""guid"":""g" + testEntry.Guid + @""",""letterHead"": ""c"",""sortIndex"": 0, + ""homographnumber"":""0"",""citationform"":[{""lang"":""fr"",""value"":""Citation""}], + ""displayXhtml"":""
0Citation
""}"; + + var expectedWithoutWs = (JObject)JsonConvert.DeserializeObject(expectedResultsWithoutWs, new JsonSerializerSettings { Formatting = Formatting.None }); + var expectedWithWs = (JObject)JsonConvert.DeserializeObject(expectedResultsWithWs, new JsonSerializerSettings { Formatting = Formatting.None }); + + dynamic jsonResult = JsonConvert.DeserializeObject(results[0][0].ToString(Formatting.None), new JsonSerializerSettings { Formatting = Formatting.None }); + string actualReformatted = JsonConvert.SerializeObject(jsonResult, Formatting.Indented); + Assert.That(actualReformatted, Is.AnyOf(JsonConvert.SerializeObject(expectedWithoutWs, Formatting.Indented), + JsonConvert.SerializeObject(expectedWithWs, Formatting.Indented))); } [Test]