Skip to content

Commit 91041d0

Browse files
imnasnainaecclaude
andauthored
LT-22697: Normalize the range-element ids written to LIFT ranges (#1063)
FieldWorks holds strings as NFD and normalizes them to NFC on LIFT export, but several writes in the ranges file bypass the normalizing helper. The part-of-speech and lexical-relation range-element ids went out through MakeSafeXmlAttribute while the parent attribute of the same element, two lines away, was normalized -- one name in two encodings inside one element, which a consumer resolving a range value by string equality fails to match. The morph-type id was not escaped at all, so a name holding an ampersand left the document unparseable. Route those six writes through MakeSafeAndNormalizedAttribute, which escapes as well as normalizes. The two writes left alone are writing system tags, where normalizing is a no-op. Two regression tests cover the two defects. Fixes LT-22697. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 81283d9 commit 91041d0

2 files changed

Lines changed: 88 additions & 8 deletions

File tree

Src/LexText/LexTextControls/LexTextControlsTests/LiftExportTests.cs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2064,6 +2064,86 @@ public void LiftExportRanges_PartOfSpeechCatalogIdIsExported()
20642064
AssertThatXmlIn.Dom(xdocRangeFile).HasAtLeastOneMatchForXpath("//range[@id='grammatical-info']/range-element/trait[@name='catalog-source-id']");
20652065
}
20662066

2067+
///--------------------------------------------------------------------------------------
2068+
/// <summary>
2069+
/// LT-22697: FLEx holds names decomposed and normalizes them on export. A
2070+
/// range-element id must be normalized like the parent attribute naming it and like
2071+
/// its own label, or a consumer comparing raw strings cannot resolve the reference.
2072+
/// </summary>
2073+
///--------------------------------------------------------------------------------------
2074+
[Test]
2075+
public void LiftExportRanges_PartOfSpeechIdIsNormalizedLikeItsParentAndLabel()
2076+
{
2077+
const string ksDecomposed = "Comple\u0301ments"; // e + U+0301
2078+
const string ksComposed = "Compl\u00e9ments"; // U+00E9
2079+
IPartOfSpeech parentPos = null;
2080+
IPartOfSpeech childPos = null;
2081+
NonUndoableUnitOfWorkHelper.Do(m_cache.ActionHandlerAccessor, () =>
2082+
{
2083+
var posFactory = m_cache.ServiceLocator.GetInstance<IPartOfSpeechFactory>();
2084+
parentPos = posFactory.Create();
2085+
m_cache.LangProject.PartsOfSpeechOA.PossibilitiesOS.Add(parentPos);
2086+
parentPos.Name.set_String(m_cache.DefaultAnalWs, ksDecomposed);
2087+
childPos = posFactory.Create();
2088+
parentPos.SubPossibilitiesOS.Add(childPos);
2089+
childPos.Name.set_String(m_cache.DefaultAnalWs, "Comple\u0301ment du lieu");
2090+
});
2091+
var xdocRangeFile = new XmlDocument();
2092+
using (var w = new StringWriter())
2093+
{
2094+
// SUT
2095+
new LiftExporter(m_cache).ExportLiftRanges(w);
2096+
xdocRangeFile.LoadXml(w.ToString());
2097+
}
2098+
2099+
var parentElement = xdocRangeFile.SelectSingleNode(string.Format(
2100+
"//range[@id='grammatical-info']/range-element[@guid='{0}']", parentPos.Guid));
2101+
var childElement = xdocRangeFile.SelectSingleNode(string.Format(
2102+
"//range[@id='grammatical-info']/range-element[@guid='{0}']", childPos.Guid));
2103+
Assert.That(parentElement, Is.Not.Null);
2104+
Assert.That(childElement, Is.Not.Null);
2105+
var sId = parentElement.Attributes["id"].Value;
2106+
Assert.That(sId, Is.EqualTo(ksComposed),
2107+
"the id must be normalized, not the decomposed form held in memory");
2108+
Assert.That(childElement.Attributes["parent"].Value, Is.EqualTo(sId),
2109+
"the parent attribute must match the id it names");
2110+
Assert.That(parentElement.SelectSingleNode("label/form/text").InnerText, Is.EqualTo(sId),
2111+
"the label must match the id of its own element");
2112+
}
2113+
2114+
///--------------------------------------------------------------------------------------
2115+
/// <summary>
2116+
/// LT-22697: the morph-type id was written raw, so a name holding a markup character
2117+
/// left the whole ranges document unparseable.
2118+
/// </summary>
2119+
///--------------------------------------------------------------------------------------
2120+
[Test]
2121+
public void LiftExportRanges_MorphTypeIdWithMarkupCharacterIsEscaped()
2122+
{
2123+
const string ksName = "prefix & suffix";
2124+
IMoMorphType morphType = null;
2125+
NonUndoableUnitOfWorkHelper.Do(m_cache.ActionHandlerAccessor, () =>
2126+
{
2127+
morphType = m_cache.ServiceLocator.GetInstance<IMoMorphTypeFactory>().Create();
2128+
m_cache.LangProject.LexDbOA.MorphTypesOA.PossibilitiesOS.Add(morphType);
2129+
morphType.Name.set_String(m_cache.DefaultAnalWs, ksName);
2130+
});
2131+
var xdocRangeFile = new XmlDocument();
2132+
using (var w = new StringWriter())
2133+
{
2134+
// SUT
2135+
new LiftExporter(m_cache).ExportLiftRanges(w);
2136+
Assert.That(() => xdocRangeFile.LoadXml(w.ToString()), Throws.Nothing,
2137+
"an unescaped id leaves the whole ranges document unparseable");
2138+
}
2139+
2140+
var element = xdocRangeFile.SelectSingleNode(string.Format(
2141+
"//range[@id='morph-type']/range-element[@guid='{0}']", morphType.Guid));
2142+
Assert.That(element, Is.Not.Null);
2143+
Assert.That(element.Attributes["id"].Value, Is.EqualTo(ksName),
2144+
"the parsed id must be the name as stored, escaping undone");
2145+
}
2146+
20672147
private int m_flidLongText;
20682148

20692149
private void AddStTextCustomFieldAndData()

Src/LexText/LexTextControls/LiftExporter.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1741,13 +1741,13 @@ private void WritePartOfSpeechRangeElement(TextWriter w, IPartOfSpeech pos)
17411741
{
17421742
var liftIdOwner = ((IPartOfSpeech)(pos.Owner)).Name.BestAnalysisVernacularAlternative.Text;
17431743
w.WriteLine("<range-element id=\"{0}\" guid=\"{1}\" parent=\"{2}\">",
1744-
XmlUtils.MakeSafeXmlAttribute(liftId), pos.Guid,
1744+
MakeSafeAndNormalizedAttribute(liftId), pos.Guid,
17451745
MakeSafeAndNormalizedAttribute(liftIdOwner));
17461746
}
17471747
else
17481748
{
17491749
w.WriteLine("<range-element id=\"{0}\" guid=\"{1}\">",
1750-
XmlUtils.MakeSafeXmlAttribute(liftId), pos.Guid);
1750+
MakeSafeAndNormalizedAttribute(liftId), pos.Guid);
17511751
}
17521752
WriteAllForms(w, "label", null, "form", pos.Name);
17531753
WriteAllForms(w, "abbrev", null, "form", pos.Abbreviation);
@@ -1778,13 +1778,13 @@ private void WriteLexRefType(TextWriter w, ILexRefType refer)
17781778
{
17791779
var liftIdOwner = ((ILexRefType)refer.Owner).Name.BestAnalysisVernacularAlternative.Text;
17801780
w.WriteLine("<range-element id=\"{0}\" guid=\"{1}\" parent=\"{2}\">",
1781-
XmlUtils.MakeSafeXmlAttribute(liftId), refer.Guid,
1781+
MakeSafeAndNormalizedAttribute(liftId), refer.Guid,
17821782
MakeSafeAndNormalizedAttribute(liftIdOwner));
17831783
}
17841784
else
17851785
{
17861786
w.WriteLine("<range-element id=\"{0}\" guid=\"{1}\">",
1787-
XmlUtils.MakeSafeXmlAttribute(liftId), refer.Guid);
1787+
MakeSafeAndNormalizedAttribute(liftId), refer.Guid);
17881788
}
17891789
WriteAllForms(w, "label", null, "form", refer.Name);
17901790
WriteAllForms(w, "abbrev", null, "form", refer.Abbreviation);
@@ -2177,19 +2177,19 @@ void WriteMorphTypeRange(TextWriter w)
21772177
var liftId = type.Name.get_String(m_wsEn).Text;
21782178
if (String.IsNullOrEmpty(liftId))
21792179
liftId = type.Name.BestAnalysisVernacularAlternative.Text;
2180-
w.WriteLine("<range-element id=\"{0}\" guid=\"{1}\">", liftId, type.Guid);
2180+
w.WriteLine("<range-element id=\"{0}\" guid=\"{1}\">", MakeSafeAndNormalizedAttribute(liftId), type.Guid);
21812181
WriteAllForms(w, "label", null, "form", type.Name);
21822182
WriteAllForms(w, "abbrev", null, "form", type.Abbreviation);
21832183
WriteAllForms(w, "description", null, "form", type.Description);
21842184
if (type.Prefix != null)
21852185
{
21862186
w.WriteLine("<trait name=\"leading-symbol\" value=\"{0}\"/>",
2187-
XmlUtils.MakeSafeXmlAttribute(type.Prefix));
2187+
MakeSafeAndNormalizedAttribute(type.Prefix));
21882188
}
21892189
if (type.Postfix != null)
21902190
{
21912191
w.WriteLine("<trait name=\"trailing-symbol\" value=\"{0}\"/>",
2192-
XmlUtils.MakeSafeXmlAttribute(type.Postfix));
2192+
MakeSafeAndNormalizedAttribute(type.Postfix));
21932193
}
21942194
w.WriteLine("</range-element>");
21952195
}
@@ -2298,7 +2298,7 @@ void WriteStemNameRanges(TextWriter w)
22982298
foreach (var region in stem.RegionsOC)
22992299
{
23002300
w.WriteLine("<trait name=\"feature-set\" value=\"{0}\"/>",
2301-
XmlUtils.MakeSafeXmlAttribute(region.LiftName));
2301+
MakeSafeAndNormalizedAttribute(region.LiftName));
23022302
}
23032303
w.WriteLine("</range-element>");
23042304
}

0 commit comments

Comments
 (0)