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