Skip to content

Commit a76ffa6

Browse files
hahn-kevmyieye
andauthored
change RichMultiString.cs to store RichStrings instead of just strings (#1692)
* change RichMultiString.cs to store RichStrings instead of just strings * migrate IRichMultiString to use IRichString, make a rich multi ws input editor based off lcm-rich-text-editor.svelte * only fire onchange on blur and not on keystroke * Add test for property changes in RichMultiString diffs This test ensures that the diff logic correctly captures property changes, such as toggling text formatting attributes in RichMultiStrings. It validates the generation of precise replace operations when properties are updated. * don't allow RichSpan.Ws to be null * handle null strings better, ensure ws it not null where possible. Note, in the case of ExampleSentence.Reference we don't have a writing system to fallback to like we do with MultiString, so it may be null in some old data, however, it shouldn't trigger any issues due to sync order. * handle db serialization issues due to ExampleSentence.Reference being a RichString * add migration for Reference column * Refactor activity view to use runes * ensure a default WsId has the Code "default" rather than null * add ExampleSentenceTestsBase.cs to catch issues creating examples without references * change RichSpan.Colors to use the C# Color struct to avoid issues of invalid colors * globally configure FluentAssert to compare RichString and RichSpan by members and no their equals method to improve assert messages * try to restore cursor location/selection on rich text state change, this avoids issues when typing in a field which was undefined * ensure all tests projects use the global fluent assert config in MiniLcmTests * fix issue round tripping RichText tags where the guid would change * merge consecutive RichSpans that have the same props * Replicate input tab-selection handling and remove custom key handlers. * fix issue where `newDoc.eq(editor.state.doc)` was never true because node.attr.richSpan.text was out of date * don't use inline span nodes to avoid issues related to deleting a whole span in one block --------- Co-authored-by: Tim Haasdyk <tim_haasdyk@sil.org>
1 parent 4ad1f24 commit a76ffa6

93 files changed

Lines changed: 2297 additions & 585 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/Directory.Packages.props

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
<PackageVersion Include="HotChocolate.Data.EntityFramework" Version="14.0.0" />
2525
<PackageVersion Include="HotChocolate.Diagnostics" Version="14.0.0" />
2626
<PackageVersion Include="HotChocolate.Types.Analyzers" Version="14.0.0" />
27-
<PackageVersion Include="HtmlAgilityPack" Version="1.12.0" />
2827
<PackageVersion Include="Humanizer.Core" Version="2.14.1" />
2928
<PackageVersion Include="icu.net" Version="3.0.1" />
3029
<PackageVersion Include="linq2db.AspNet" Version="5.4.1" />
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using FluentAssertions.Extensibility;
2+
3+
[assembly: AssertionEngineInitializer(typeof(FluentAssertGlobalConfig), nameof(FluentAssertGlobalConfig.Initialize))]
4+
5+
namespace FwDataMiniLcmBridge.Tests;
6+
7+
public static class FluentAssertGlobalConfig
8+
{
9+
public static void Initialize()
10+
{
11+
MiniLcm.Tests.FluentAssertGlobalConfig.Initialize();
12+
}
13+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using FwDataMiniLcmBridge.Tests.Fixtures;
2+
3+
namespace FwDataMiniLcmBridge.Tests.MiniLcmTests;
4+
5+
[Collection(ProjectLoaderFixture.Name)]
6+
public class ExampleSentenceTests(ProjectLoaderFixture fixture) : ExampleSentenceTestsBase
7+
{
8+
protected override Task<IMiniLcmApi> NewApi()
9+
{
10+
return Task.FromResult<IMiniLcmApi>(fixture.NewProjectApi("ExampleSentenceTests", "en", "en"));
11+
}
12+
}

backend/FwLite/FwDataMiniLcmBridge.Tests/MiniLcmTests/UpdateEntryTests.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,18 @@ public async Task UpdateEntry_CanUpdateExampleSentenceTranslations_WhenNoTransla
5252
var entry = await Api.CreateEntry(new Entry
5353
{
5454
LexemeForm = { { "en", "test" } },
55-
Note = { { "en", "this is a test note" } },
55+
Note = { { "en", new RichString("this is a test note") } },
5656
CitationForm = { { "en", "test" } },
57-
LiteralMeaning = { { "en", "test" } },
57+
LiteralMeaning = { { "en", new RichString("test") } },
5858
Senses =
5959
[
6060
new Sense
6161
{
6262
Gloss = { { "en", "test" } },
63-
Definition = { { "en", "test" } },
63+
Definition = { { "en", new RichString("test") } },
6464
ExampleSentences =
6565
[
66-
new ExampleSentence { Sentence = { { "en", "testing is good" } } }
66+
new ExampleSentence { Sentence = { { "en", new RichString("testing is good") } } }
6767
]
6868
}
6969
]
@@ -86,15 +86,15 @@ public async Task UpdateEntry_CanUpdateExampleSentenceTranslations_WhenNoTransla
8686

8787
var before = entry.Copy();
8888
var exampleSentence = entry.Senses[0].ExampleSentences[0];
89-
exampleSentence.Translation = new() { { "en", "updated" } };
89+
exampleSentence.Translation = new() { { "en", new RichString("updated") } };
9090

9191
// Act
9292
var updatedEntry = await Api.UpdateEntry(before, entry);
9393
var updatedExampleSentence = updatedEntry.Senses[0].ExampleSentences[0];
9494

9595
// Assert
9696
updatedExampleSentence.Translation.Should().ContainSingle();
97-
updatedExampleSentence.Translation["en"].Should().Be("updated");
97+
updatedExampleSentence.Translation["en"].Should().BeEquivalentTo(new RichString("updated", "en"));
9898
updatedEntry.Should().BeEquivalentTo(entry, options => options);
9999
}
100100
}

backend/FwLite/FwDataMiniLcmBridge.Tests/RichTextTests.cs

Lines changed: 139 additions & 53 deletions
Large diffs are not rendered by default.

backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -655,7 +655,11 @@ private ExampleSentence FromLexExampleSentence(Guid senseGuid, ILexExampleSenten
655655
Id = sentence.Guid,
656656
SenseId = senseGuid,
657657
Sentence = FromLcmMultiString(sentence.Example),
658-
Reference = sentence.Reference.Text,
658+
Reference =
659+
sentence.Reference.Length == 0
660+
? null
661+
: RichTextMapping.FromTsString(sentence.Reference,
662+
h => h is null ? null : (WritingSystemId?)GetWritingSystemId(h.Value)),
659663
Translation = translation is null ? new() : FromLcmMultiString(translation),
660664
};
661665
}
@@ -680,7 +684,11 @@ private RichMultiString FromLcmMultiString(IMultiString multiString)
680684
{
681685
var tsString = multiString.GetStringFromIndex(i, out var ws);
682686

683-
result.Add(GetWritingSystemId(ws), tsString.Text);
687+
result.Add(GetWritingSystemId(ws), RichTextMapping.FromTsString(tsString, h =>
688+
{
689+
if (h is null) return null;
690+
return GetWritingSystemId(h.Value);
691+
}));
684692
}
685693

686694
return result;
@@ -1065,7 +1073,7 @@ private void UpdateLcmMultiString(ITsMultiString multiString, RichMultiString ne
10651073
foreach (var (ws, value) in newMultiString)
10661074
{
10671075
var writingSystemHandle = GetWritingSystemHandle(ws);
1068-
multiString.set_String(writingSystemHandle, TsStringUtils.MakeString(value, writingSystemHandle));
1076+
multiString.set_String(writingSystemHandle, RichTextMapping.ToTsString(value, id => GetWritingSystemHandle(id)));
10691077
}
10701078
}
10711079

@@ -1329,8 +1337,9 @@ internal void CreateExampleSentence(ILexSense lexSense, ExampleSentence exampleS
13291337
UpdateLcmMultiString(lexExampleSentence.Example, exampleSentence.Sentence);
13301338
var translation = CreateExampleSentenceTranslation(lexExampleSentence);
13311339
UpdateLcmMultiString(translation.Translation, exampleSentence.Translation);
1332-
lexExampleSentence.Reference = TsStringUtils.MakeString(exampleSentence.Reference,
1333-
lexExampleSentence.Reference.get_WritingSystem(0));
1340+
lexExampleSentence.Reference = exampleSentence.Reference is null
1341+
? null
1342+
: RichTextMapping.ToTsString(exampleSentence.Reference, id => GetWritingSystemHandle(id));
13341343
}
13351344

13361345
public ICmTranslation CreateExampleSentenceTranslation(ILexExampleSentence parent)

backend/FwLite/FwDataMiniLcmBridge/Api/LcmHelpers.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,10 @@ internal static int GetWritingSystemHandle(this LcmCache cache, WritingSystemId
9090
};
9191
}
9292

93-
var lcmWs = cache.ServiceLocator.WritingSystemManager.Get(ws.Code);
93+
if (!cache.ServiceLocator.WritingSystemManager.TryGet(ws.Code, out var lcmWs))
94+
{
95+
throw new NullReferenceException($"unable to find writing system with id '{ws.Code}'");
96+
}
9497
if (lcmWs is not null && type is not null)
9598
{
9699
var validWs = type switch

backend/FwLite/FwDataMiniLcmBridge/Api/RichTextMapping.cs

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using System.Collections.Frozen;
2+
using System.Drawing;
23
using System.Globalization;
34
using System.Text;
45
using MiniLcm.Models;
6+
using MiniLcm.RichText;
57
using Mono.Unix.Native;
68
using SIL.LCModel.Core.KernelInterfaces;
79
using SIL.LCModel.Core.Text;
@@ -125,6 +127,23 @@ public static class RichTextMapping
125127
};
126128
}
127129

130+
public static RichString FromTsString(ITsString tsString, Func<int?, WritingSystemId?> wsIdLookup)
131+
{
132+
var spans = new List<RichSpan>(tsString.RunCount);
133+
for (int i = 0; i < tsString.RunCount; i++)
134+
{
135+
var props = tsString.FetchRunInfo(i, out _);
136+
var span = new RichSpan
137+
{
138+
Text = tsString.get_RunText(i)
139+
};
140+
WriteToSpan(span, props, wsIdLookup);
141+
spans.Add(span);
142+
}
143+
144+
return new RichString([..spans]);
145+
}
146+
128147
public static void WriteToSpan(RichSpan span, ITsTextProps textProps, Func<int?, WritingSystemId?> wsIdLookup)
129148
{
130149
for (int i = 0; i < textProps.IntPropCount; i++)
@@ -176,7 +195,8 @@ private static void SetFromInt(RichSpan span, ITsTextProps textProps, FwTextProp
176195
switch (type)
177196
{
178197
case FwTextPropType.ktptWs:
179-
span.Ws = wsIdLookup(GetNullableIntProp(textProps, type));
198+
var wsHandle = GetNullableIntProp(textProps, type);
199+
span.Ws = wsIdLookup(wsHandle) ?? throw new ArgumentException($"ws handle {wsHandle} is not valid");
180200
break;
181201
case FwTextPropType.ktptBaseWs:
182202
span.WsBase = wsIdLookup(GetNullableIntProp(textProps, type));
@@ -231,6 +251,20 @@ private static void SetFromInt(RichSpan span, ITsTextProps textProps, FwTextProp
231251
}
232252
}
233253

254+
public static ITsString ToTsString(RichString richString, Func<WritingSystemId, int> wsHandleLookup)
255+
{
256+
var stringBuilder = TsStringUtils.MakeIncStrBldr();
257+
var propsBldr = TsStringUtils.MakePropsBldr();
258+
foreach (var span in richString.Spans)
259+
{
260+
WriteToTextProps(span, propsBldr, wsHandleLookup);
261+
stringBuilder.AppendTsString(TsStringUtils.MakeString(span.Text, propsBldr.GetTextProps()));
262+
propsBldr.Clear();
263+
}
264+
265+
return stringBuilder.GetString();
266+
}
267+
234268
public static void WriteToTextProps(RichSpan span,
235269
ITsPropsBldr builder,
236270
Func<WritingSystemId, int> wsHandleLookup)
@@ -253,8 +287,8 @@ public static void WriteToTextProps(RichSpan span,
253287
}
254288
}
255289

256-
if (span.Ws is not null)
257-
SetInt(builder, FwTextPropType.ktptWs, wsHandleLookup(span.Ws.Value));
290+
291+
SetInt(builder, FwTextPropType.ktptWs, wsHandleLookup(span.Ws));
258292
if (span.WsBase is not null)
259293
SetInt(builder, FwTextPropType.ktptBaseWs, wsHandleLookup(span.WsBase.Value));
260294
SetInt(builder, FwTextPropType.ktptItalic, ReverseMapToggle(span.Italic));
@@ -393,12 +427,11 @@ private static int ReverseSizeUnit(RichTextSizeUnit? unit)
393427
{
394428
if (string.IsNullOrEmpty(value))
395429
return null;
396-
const int guidLength = 16;
397-
var bytes = Encoding.Unicode.GetBytes(value).AsSpan();
398-
Guid[] guids = new Guid[bytes.Length / guidLength];
430+
const int guidLength = 8;
431+
Guid[] guids = new Guid[value.Length / guidLength];
399432
for (int i = 0; i < guids.Length; i++)
400433
{
401-
guids[i] = new Guid(bytes.Slice(i * guidLength, guidLength));
434+
guids[i] = MiscUtils.GetGuidFromObjData(value.Substring(i * guidLength, guidLength));
402435
}
403436
return guids;
404437
}
@@ -518,26 +551,26 @@ private static string ReverseTags(Guid[] tags)
518551
};
519552
}
520553

521-
private static string? GetNullableColorProp(ITsTextProps textProps, FwTextPropType type)
554+
private static Color? GetNullableColorProp(ITsTextProps textProps, FwTextPropType type)
522555
{
523556
if (!textProps.TryGetIntValue(type, out _, out var value))
524557
{
525558
return null;
526559
}
527-
if (value == (int)FwTextColor.kclrTransparent) return "#00000000";
560+
if (value == (int)FwTextColor.kclrTransparent) return ColorJsonConverter.UnnamedTransparent;
528561
int blue = (value >> 16) & 0xff;
529562
int green = (value >> 8) & 0xff;
530563
int red = value & 0xff;
531-
return $"#{red:x2}{green:x2}{blue:x2}";
564+
return Color.FromArgb(red, green, blue);
532565
}
533566

534-
private static int? ReverseColor(string? rgb)
567+
private static int? ReverseColor(Color? rgb)
535568
{
536-
if (string.IsNullOrEmpty(rgb))
569+
if (rgb is null || rgb.Value == default)
537570
return null;
538-
if (rgb == "#00000000")
571+
if (rgb.Value.A == 0)
539572
return (int)FwTextColor.kclrTransparent;
540-
return (int)ColorUtil.ConvertRGBtoBGR(uint.Parse(rgb.AsSpan()[1..], NumberStyles.HexNumber));
573+
return (int)ColorUtil.ConvertRGBtoBGR(uint.Parse(ColorTranslator.ToHtml(rgb.Value).AsSpan()[1..], NumberStyles.HexNumber));
541574
}
542575

543576
private static RichTextAlign? GetNullableRichTextAlign(ITsTextProps textProps)

backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateDictionaryProxy.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ public void Add(WritingSystemId key, string value)
1919
multiString.set_String(writingSystemHandle, TsStringUtils.MakeString(value, writingSystemHandle));
2020
}
2121

22+
public void Add(WritingSystemId key, RichString value)
23+
{
24+
var writingSystemHandle = lexboxLcmApi.GetWritingSystemHandle(key);
25+
multiString.set_String(writingSystemHandle, RichTextMapping.ToTsString(value, ws => lexboxLcmApi.GetWritingSystemHandle(ws)));
26+
}
27+
2228
public bool ContainsKey(WritingSystemId key)
2329
{
2430
if (multiString.StringCount == 0) return false;

backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateEntryProxy.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,12 @@ public override MultiString Copy()
8484

8585
public class UpdateRichMultiStringProxy(ITsMultiString multiString, FwDataMiniLcmApi lexboxLcmApi) : RichMultiString, IDictionary
8686
{
87-
private IDictionary<WritingSystemId, string> proxy => new UpdateDictionaryProxy(multiString, lexboxLcmApi);
87+
private UpdateDictionaryProxy proxy = new(multiString, lexboxLcmApi);
8888

8989
void IDictionary.Add(object key, object? value)
9090
{
91-
var valStr = value as string ??
92-
throw new ArgumentException($"unable to convert value {value?.GetType().Name ?? "null"} to string",
91+
var valStr = value as RichString ??
92+
throw new ArgumentException($"unable to convert value {value?.GetType().Name ?? "null"} to RichString",
9393
nameof(value));
9494
if (key is WritingSystemId keyWs)
9595
{

0 commit comments

Comments
 (0)