diff --git a/Src/LexText/LexTextControls/LexTextControlsTests/LiftExportTests.cs b/Src/LexText/LexTextControls/LexTextControlsTests/LiftExportTests.cs index abe90cc5a7..d64bc01a0f 100644 --- a/Src/LexText/LexTextControls/LexTextControlsTests/LiftExportTests.cs +++ b/Src/LexText/LexTextControls/LexTextControlsTests/LiftExportTests.cs @@ -1983,6 +1983,86 @@ public void LiftExportRanges_PartOfSpeechCatalogIdIsExported() AssertThatXmlIn.Dom(xdocRangeFile).HasAtLeastOneMatchForXpath("//range[@id='grammatical-info']/range-element/trait[@name='catalog-source-id']"); } + ///-------------------------------------------------------------------------------------- + /// + /// LT-22697: FLEx holds names decomposed and normalizes them on export. A + /// range-element id must be normalized like the parent attribute naming it and like + /// its own label, or a consumer comparing raw strings cannot resolve the reference. + /// + ///-------------------------------------------------------------------------------------- + [Test] + public void LiftExportRanges_PartOfSpeechIdIsNormalizedLikeItsParentAndLabel() + { + const string ksDecomposed = "Comple\u0301ments"; // e + U+0301 + const string ksComposed = "Compl\u00e9ments"; // U+00E9 + IPartOfSpeech parentPos = null; + IPartOfSpeech childPos = null; + NonUndoableUnitOfWorkHelper.Do(m_cache.ActionHandlerAccessor, () => + { + var posFactory = m_cache.ServiceLocator.GetInstance(); + parentPos = posFactory.Create(); + m_cache.LangProject.PartsOfSpeechOA.PossibilitiesOS.Add(parentPos); + parentPos.Name.set_String(m_cache.DefaultAnalWs, ksDecomposed); + childPos = posFactory.Create(); + parentPos.SubPossibilitiesOS.Add(childPos); + childPos.Name.set_String(m_cache.DefaultAnalWs, "Comple\u0301ment du lieu"); + }); + var xdocRangeFile = new XmlDocument(); + using (var w = new StringWriter()) + { + // SUT + new LiftExporter(m_cache).ExportLiftRanges(w); + xdocRangeFile.LoadXml(w.ToString()); + } + + var parentElement = xdocRangeFile.SelectSingleNode(string.Format( + "//range[@id='grammatical-info']/range-element[@guid='{0}']", parentPos.Guid)); + var childElement = xdocRangeFile.SelectSingleNode(string.Format( + "//range[@id='grammatical-info']/range-element[@guid='{0}']", childPos.Guid)); + Assert.That(parentElement, Is.Not.Null); + Assert.That(childElement, Is.Not.Null); + var sId = parentElement.Attributes["id"].Value; + Assert.That(sId, Is.EqualTo(ksComposed), + "the id must be normalized, not the decomposed form held in memory"); + Assert.That(childElement.Attributes["parent"].Value, Is.EqualTo(sId), + "the parent attribute must match the id it names"); + Assert.That(parentElement.SelectSingleNode("label/form/text").InnerText, Is.EqualTo(sId), + "the label must match the id of its own element"); + } + + ///-------------------------------------------------------------------------------------- + /// + /// LT-22697: the morph-type id was written raw, so a name holding a markup character + /// left the whole ranges document unparseable. + /// + ///-------------------------------------------------------------------------------------- + [Test] + public void LiftExportRanges_MorphTypeIdWithMarkupCharacterIsEscaped() + { + const string ksName = "prefix & suffix"; + IMoMorphType morphType = null; + NonUndoableUnitOfWorkHelper.Do(m_cache.ActionHandlerAccessor, () => + { + morphType = m_cache.ServiceLocator.GetInstance().Create(); + m_cache.LangProject.LexDbOA.MorphTypesOA.PossibilitiesOS.Add(morphType); + morphType.Name.set_String(m_cache.DefaultAnalWs, ksName); + }); + var xdocRangeFile = new XmlDocument(); + using (var w = new StringWriter()) + { + // SUT + new LiftExporter(m_cache).ExportLiftRanges(w); + Assert.That(() => xdocRangeFile.LoadXml(w.ToString()), Throws.Nothing, + "an unescaped id leaves the whole ranges document unparseable"); + } + + var element = xdocRangeFile.SelectSingleNode(string.Format( + "//range[@id='morph-type']/range-element[@guid='{0}']", morphType.Guid)); + Assert.That(element, Is.Not.Null); + Assert.That(element.Attributes["id"].Value, Is.EqualTo(ksName), + "the parsed id must be the name as stored, escaping undone"); + } + private int m_flidLongText; private void AddStTextCustomFieldAndData() diff --git a/Src/LexText/LexTextControls/LiftExporter.cs b/Src/LexText/LexTextControls/LiftExporter.cs index dfb2807aa5..9fc44d9a0e 100644 --- a/Src/LexText/LexTextControls/LiftExporter.cs +++ b/Src/LexText/LexTextControls/LiftExporter.cs @@ -1735,13 +1735,13 @@ private void WritePartOfSpeechRangeElement(TextWriter w, IPartOfSpeech pos) { var liftIdOwner = ((IPartOfSpeech)(pos.Owner)).Name.BestAnalysisVernacularAlternative.Text; w.WriteLine("", - XmlUtils.MakeSafeXmlAttribute(liftId), pos.Guid, + MakeSafeAndNormalizedAttribute(liftId), pos.Guid, MakeSafeAndNormalizedAttribute(liftIdOwner)); } else { w.WriteLine("", - XmlUtils.MakeSafeXmlAttribute(liftId), pos.Guid); + MakeSafeAndNormalizedAttribute(liftId), pos.Guid); } WriteAllForms(w, "label", null, "form", pos.Name); WriteAllForms(w, "abbrev", null, "form", pos.Abbreviation); @@ -1772,13 +1772,13 @@ private void WriteLexRefType(TextWriter w, ILexRefType refer) { var liftIdOwner = ((ILexRefType)refer.Owner).Name.BestAnalysisVernacularAlternative.Text; w.WriteLine("", - XmlUtils.MakeSafeXmlAttribute(liftId), refer.Guid, + MakeSafeAndNormalizedAttribute(liftId), refer.Guid, MakeSafeAndNormalizedAttribute(liftIdOwner)); } else { w.WriteLine("", - XmlUtils.MakeSafeXmlAttribute(liftId), refer.Guid); + MakeSafeAndNormalizedAttribute(liftId), refer.Guid); } WriteAllForms(w, "label", null, "form", refer.Name); WriteAllForms(w, "abbrev", null, "form", refer.Abbreviation); @@ -2170,19 +2170,19 @@ void WriteMorphTypeRange(TextWriter w) var liftId = type.Name.get_String(m_wsEn).Text; if (String.IsNullOrEmpty(liftId)) liftId = type.Name.BestAnalysisVernacularAlternative.Text; - w.WriteLine("", liftId, type.Guid); + w.WriteLine("", MakeSafeAndNormalizedAttribute(liftId), type.Guid); WriteAllForms(w, "label", null, "form", type.Name); WriteAllForms(w, "abbrev", null, "form", type.Abbreviation); WriteAllForms(w, "description", null, "form", type.Description); if (type.Prefix != null) { w.WriteLine("", - XmlUtils.MakeSafeXmlAttribute(type.Prefix)); + MakeSafeAndNormalizedAttribute(type.Prefix)); } if (type.Postfix != null) { w.WriteLine("", - XmlUtils.MakeSafeXmlAttribute(type.Postfix)); + MakeSafeAndNormalizedAttribute(type.Postfix)); } w.WriteLine(""); } @@ -2291,7 +2291,7 @@ void WriteStemNameRanges(TextWriter w) foreach (var region in stem.RegionsOC) { w.WriteLine("", - XmlUtils.MakeSafeXmlAttribute(region.LiftName)); + MakeSafeAndNormalizedAttribute(region.LiftName)); } w.WriteLine(""); }