diff --git a/Src/LexText/LexTextControls/LexTextControlsTests/LiftExportTests.cs b/Src/LexText/LexTextControls/LexTextControlsTests/LiftExportTests.cs
index abe90cc5a7..9737775708 100644
--- a/Src/LexText/LexTextControls/LexTextControlsTests/LiftExportTests.cs
+++ b/Src/LexText/LexTextControls/LexTextControlsTests/LiftExportTests.cs
@@ -493,6 +493,12 @@ public void CreateMockCache()
m_mapPartsOfSpeech.Clear();
m_mapAcademicDomains.Clear();
m_mapPublications.Clear();
+ // NUnit shares one fixture instance across the tests, so these outlive the cache they index.
+ m_customFieldEntryIds.Clear();
+ m_customFieldSenseIds.Clear();
+ m_customFieldAllomorphsIds.Clear();
+ m_customFieldExampleSentencesIds.Clear();
+ m_customListsGuids.Clear();
var mockProjectName = "xxyyzProjectFolderForLIFTTest";
MockProjectFolder = Path.Combine(Path.GetTempPath(), mockProjectName);
var mockProjectPath = Path.Combine(MockProjectFolder, mockProjectName + ".fwdata");
@@ -1135,6 +1141,81 @@ public void LiftExport()
VerifyExportRanges(xdocRangeFile);
}
+ ///--------------------------------------------------------------------------------------
+ ///
+ /// A custom list name reaches the ranges file as a range id, which is an attribute, so a name
+ /// holding a quotation mark has to be escaped by the attribute rules for the document to parse.
+ ///
+ ///--------------------------------------------------------------------------------------
+ [Test]
+ public void LiftExportRanges_CustomListRangeIdWithQuoteIsEscapedForAnAttribute()
+ {
+ const string ksListName = "So-called \"words\"";
+ XmlDocument xdocLift;
+ XmlDocument xdocRangeFile;
+ var customList = ExportWithCustomList(ksListName, out xdocLift, out xdocRangeFile);
+
+ var range = xdocRangeFile.SelectSingleNode(string.Format("//range[@guid='{0}']", customList.Guid));
+ Assert.That(range, Is.Not.Null, "the custom list must be written as a range");
+ Assert.That(range.Attributes["id"].Value, Is.EqualTo(ksListName),
+ "the range id must read back as the name stored");
+ }
+
+ ///--------------------------------------------------------------------------------------
+ ///
+ /// A custom list name is written as a range id in both the .lift header and the ranges file,
+ /// escaped once in each, so that the two files name the range the same way.
+ ///
+ ///--------------------------------------------------------------------------------------
+ [Test]
+ public void LiftExportRanges_CustomListRangeIdWithAmpersandIsEscapedExactlyOnce()
+ {
+ const string ksListName = "Birds & Beasts";
+ XmlDocument xdocLift;
+ XmlDocument xdocRangeFile;
+ var customList = ExportWithCustomList(ksListName, out xdocLift, out xdocRangeFile);
+
+ var rangesRange = xdocRangeFile.SelectSingleNode(string.Format("//range[@guid='{0}']", customList.Guid));
+ Assert.That(rangesRange, Is.Not.Null);
+ Assert.That(rangesRange.Attributes["id"].Value, Is.EqualTo(ksListName),
+ "one level of escaping, undone by the parser, must give back the name stored");
+ var headerIds = xdocLift.SelectNodes("//header/ranges/range/@id");
+ Assert.That(headerIds, Is.Not.Null);
+ Assert.That(headerIds.Cast().Any(id => id.Value == ksListName),
+ "the .lift header must name the range the same way the ranges file does");
+ }
+
+ ///
+ /// Exports both files from one exporter, so that the custom list is discovered by the .lift
+ /// pass and is therefore written by the ranges pass.
+ ///
+ private ICmPossibilityList ExportWithCustomList(string listName, out XmlDocument xdocLift, out XmlDocument xdocRangeFile)
+ {
+ ICmPossibilityList customList = null;
+ NonUndoableUnitOfWorkHelper.Do(m_cache.ActionHandlerAccessor, () =>
+ {
+ customList = AddCustomList(listName);
+ MakeCustomField("CustomFieldForCustomList", LexEntryTags.kClassId,
+ WritingSystemServices.kwsAnal, CustomFieldType.ListRefAtomic, customList.Guid);
+ });
+ var exporter = new LiftExporter(m_cache);
+ xdocLift = new XmlDocument();
+ using (var w = new StringWriter())
+ {
+ // SUT: the .lift pass populates the map the ranges pass writes from.
+ exporter.ExportLift(w, LiftFolder);
+ xdocLift.LoadXml(w.ToString());
+ }
+ xdocRangeFile = new XmlDocument();
+ using (var w = new StringWriter())
+ {
+ // SUT
+ exporter.ExportLiftRanges(w);
+ xdocRangeFile.LoadXml(w.ToString());
+ }
+ return customList;
+ }
+
///
/// LT-15467 documents a Flex to WeSay S/R which had pronunciation audio files multiplying like bunny rabbits.
/// Make sure the export doesn't make new files when two different references point to the same file.
diff --git a/Src/LexText/LexTextControls/LiftExporter.cs b/Src/LexText/LexTextControls/LiftExporter.cs
index dfb2807aa5..eba666d8b6 100644
--- a/Src/LexText/LexTextControls/LiftExporter.cs
+++ b/Src/LexText/LexTextControls/LiftExporter.cs
@@ -56,8 +56,14 @@ public class LiftExporter
private readonly int m_wsBestVernAnal;
///
/// This contains the possibility lists that are custom or that are referenced by a custom field.
+ /// Range names are held unescaped, so that each is escaped by the rules of the context it is
+ /// written into.
///
private Dictionary m_CmPossListsReferencedOrCustom = new Dictionary();
+ ///
+ /// Maps a possibility list to the name of its LIFT range. Range names are held unescaped, so
+ /// that each is escaped by the rules of the context it is written into.
+ ///
private Dictionary m_ListsGuidToRangeName = new Dictionary();
private readonly ICmPossibilityListRepository m_repoCmPossibilityLists;
private readonly ISilDataAccessManaged m_sda;
@@ -1979,10 +1985,11 @@ private void WritePossibilityListAsRange(TextWriter w, string rangeId, ICmPossib
{
if (list.PossibilitiesOS.Count == 0)
return;
+ var sRangeId = MakeSafeAndNormalizedAttribute(rangeId);
if (String.IsNullOrEmpty(guid)) //only output the guid for custom lists
- w.WriteLine("", rangeId);
+ w.WriteLine("", sRangeId);
else
- w.WriteLine("", rangeId, MakeSafeAndNormalizedAttribute(guid));
+ w.WriteLine("", sRangeId, MakeSafeAndNormalizedAttribute(guid));
foreach (var poss in list.ReallyReallyAllPossibilities)
{
var liftId = poss.Name.BestAnalysisVernacularAlternative.Text;
@@ -2348,7 +2355,7 @@ private Dictionary GetCustomListsAndReferencedLists(IEnumerable cmPoss
var rangeName = RangeNames.GetRangeNameForLiftExport(m_mdc, possList);
if (!cmPossibilityListsReferencedByFields.ContainsKey(possListGuid))
{
- cmPossibilityListsReferencedByFields.Add(possListGuid, MakeSafeAndNormalizedXml(rangeName));
+ cmPossibilityListsReferencedByFields.Add(possListGuid, rangeName);
}
}
}
@@ -2463,7 +2470,7 @@ private void MapCmPossibilityListGuidsToLiftRangeNames(Dictionary
//by a custom field and which are not already included yet.
foreach (var list in cmPossibilityListsReferencedByFields)
{
- m_ListsGuidToRangeName.Add(list.Key, MakeSafeAndNormalizedXml(list.Value));
+ m_ListsGuidToRangeName.Add(list.Key, list.Value);
}
}