diff --git a/src/RolemasterDb.App/rolemaster.db b/src/RolemasterDb.App/rolemaster.db index 8934083..598b2ed 100644 Binary files a/src/RolemasterDb.App/rolemaster.db and b/src/RolemasterDb.App/rolemaster.db differ diff --git a/src/RolemasterDb.ImportTool.Tests/StandardCriticalTableParserIntegrationTests.cs b/src/RolemasterDb.ImportTool.Tests/StandardCriticalTableParserIntegrationTests.cs index 71cd0f2..3c1b018 100644 --- a/src/RolemasterDb.ImportTool.Tests/StandardCriticalTableParserIntegrationTests.cs +++ b/src/RolemasterDb.ImportTool.Tests/StandardCriticalTableParserIntegrationTests.cs @@ -136,6 +136,25 @@ public sealed class StandardCriticalTableParserIntegrationTests Assert.Equal(1, bleedEffects.Single().PerRound); } + [Fact] + public async Task Puncture_91_95_keeps_a_and_b_prose_in_separate_columns() + { + var entry = LoadManifest().Tables.Single(item => string.Equals(item.Slug, "puncture", StringComparison.Ordinal)); + var parseResult = await LoadParseResultAsync(entry); + var row91A = parseResult.Table.Results.Single(item => + item.GroupKey is null && + string.Equals(item.RollBandLabel, "91-95", StringComparison.Ordinal) && + string.Equals(item.ColumnKey, "A", StringComparison.Ordinal)); + var row91B = parseResult.Table.Results.Single(item => + item.GroupKey is null && + string.Equals(item.RollBandLabel, "91-95", StringComparison.Ordinal) && + string.Equals(item.ColumnKey, "B", StringComparison.Ordinal)); + + Assert.Equal("Strike to foe's ear. Foe hears at -50.", row91A.DescriptionText); + Assert.DoesNotContain("Strike to foe's hip.", row91A.DescriptionText, StringComparison.Ordinal); + Assert.Contains("Strike to foe's hip.", row91B.DescriptionText, StringComparison.Ordinal); + } + [Fact] public async Task Slash_boundary_repair_keeps_56_60_a_prose_first() { @@ -460,6 +479,30 @@ public sealed class StandardCriticalTableParserIntegrationTests Assert.Contains(result.Branches.SelectMany(item => item.Effects), item => item.EffectCode == CriticalEffectCodes.BleedPerRound); } + [Fact] + public async Task Loader_persists_puncture_b_91_95_description_text() + { + var entry = LoadManifest().Tables.Single(item => string.Equals(item.Slug, "puncture", StringComparison.Ordinal)); + var parseResult = await LoadParseResultAsync(entry); + var databasePath = CreateTemporaryDatabaseCopy(); + var loader = new CriticalImportLoader(databasePath); + + await loader.LoadAsync(parseResult.Table); + + await using var dbContext = CreateDbContext(databasePath); + var result = await dbContext.CriticalResults + .Include(item => item.CriticalTable) + .Include(item => item.CriticalColumn) + .Include(item => item.CriticalRollBand) + .SingleAsync(item => + item.CriticalTable.Slug == "puncture" && + item.CriticalColumn.ColumnKey == "B" && + item.CriticalRollBand.Label == "91-95"); + + Assert.Contains("Strike to foe's hip.", result.DescriptionText, StringComparison.Ordinal); + Assert.StartsWith("Strike to foe's hip.", result.RawCellText, StringComparison.Ordinal); + } + [Fact] public async Task Lookup_service_returns_effects_for_results_and_branches() { diff --git a/src/RolemasterDb.ImportTool/Parsing/CriticalTableParserSupport.cs b/src/RolemasterDb.ImportTool/Parsing/CriticalTableParserSupport.cs index a6666ab..acc92d3 100644 --- a/src/RolemasterDb.ImportTool/Parsing/CriticalTableParserSupport.cs +++ b/src/RolemasterDb.ImportTool/Parsing/CriticalTableParserSupport.cs @@ -15,6 +15,7 @@ internal static class CriticalTableParserSupport internal const int TopGroupingTolerance = 2; private static readonly Regex MultiFragmentSplitRegex = new(@"\S(?:.*?\S)?(?=(?:\s{2,}|$))", RegexOptions.Compiled); + private static readonly Regex SentenceFragmentSplitRegex = new(@"\S.*?(?:[.!?](?:['"")\]]*)|$)", RegexOptions.Compiled); private static readonly Regex NumericAffixLineRegex = new(@"^\d+(?:H|∑|∏|π|∫|\s*[–-])", RegexOptions.Compiled); private static readonly Regex StandaloneModifierAffixLineRegex = new(@"^(?:\d+)?\((?:\+|-|–)\d+\)$", RegexOptions.Compiled); @@ -373,7 +374,7 @@ internal static class CriticalTableParserSupport supportsPowerPointModifier: footerText.Contains("powerpoint modification", StringComparison.OrdinalIgnoreCase)); } - internal static List SplitBoundaryCrossingAffixFragments( + internal static List SplitBoundaryCrossingFragments( IReadOnlyList bodyFragments, IReadOnlyList<(string Key, double CenterX)> columnCenters, IReadOnlySet affixLegendSymbols) @@ -382,7 +383,7 @@ internal static class CriticalTableParserSupport foreach (var fragment in bodyFragments) { - splitFragments.AddRange(SplitBoundaryCrossingAffixFragment(fragment, columnCenters, affixLegendSymbols)); + splitFragments.AddRange(SplitBoundaryCrossingFragment(fragment, columnCenters, affixLegendSymbols)); } return splitFragments; @@ -467,7 +468,7 @@ internal static class CriticalTableParserSupport !excludedFragments.Contains(item)) .ToList(); - return SplitBoundaryCrossingAffixFragments(bodyFragments, columnCenters, affixLegendSymbols); + return SplitBoundaryCrossingFragments(bodyFragments, columnCenters, affixLegendSymbols); } internal static void RepairLeadingAffixLeakage(List cellEntries, IReadOnlySet affixLegendSymbols) @@ -612,17 +613,17 @@ internal static class CriticalTableParserSupport return true; } - private static IReadOnlyList SplitBoundaryCrossingAffixFragment( + private static IReadOnlyList SplitBoundaryCrossingFragment( XmlTextFragment fragment, IReadOnlyList<(string Key, double CenterX)> columnCenters, IReadOnlySet affixLegendSymbols) { - if (!LooksLikeBoundaryCrossingAffixFragment(fragment, columnCenters, affixLegendSymbols)) + if (!TryGetBoundaryCrossingPattern(fragment, columnCenters, affixLegendSymbols, out var splitPattern)) { return [fragment]; } - var matches = MultiFragmentSplitRegex.Matches(fragment.Text); + var matches = splitPattern.Matches(fragment.Text); if (matches.Count < 2) { return [fragment]; @@ -667,17 +668,40 @@ internal static class CriticalTableParserSupport : [fragment]; } - private static bool LooksLikeBoundaryCrossingAffixFragment( + private static bool TryGetBoundaryCrossingPattern( XmlTextFragment fragment, IReadOnlyList<(string Key, double CenterX)> columnCenters, - IReadOnlySet affixLegendSymbols) + IReadOnlySet affixLegendSymbols, + out Regex splitPattern) { - if (!IsAffixLikeLine(fragment.Text, affixLegendSymbols) || - !fragment.Text.Contains(" ", StringComparison.Ordinal)) + splitPattern = null!; + + if (!CrossesColumnBoundary(fragment, columnCenters)) { return false; } + if (IsAffixLikeLine(fragment.Text, affixLegendSymbols) && + fragment.Text.Contains(" ", StringComparison.Ordinal)) + { + splitPattern = MultiFragmentSplitRegex; + return true; + } + + if (!IsAffixLikeLine(fragment.Text, affixLegendSymbols) && + CountSentenceLikeSegments(fragment.Text) >= 2) + { + splitPattern = SentenceFragmentSplitRegex; + return true; + } + + return false; + } + + private static bool CrossesColumnBoundary( + XmlTextFragment fragment, + IReadOnlyList<(string Key, double CenterX)> columnCenters) + { var fragmentRight = fragment.Left + fragment.Width; for (var index = 0; index < columnCenters.Count - 1; index++) @@ -692,6 +716,11 @@ internal static class CriticalTableParserSupport return false; } + private static int CountSentenceLikeSegments(string text) => + SentenceFragmentSplitRegex.Matches(text) + .Select(match => CollapseWhitespace(match.Value)) + .Count(value => !string.IsNullOrWhiteSpace(value)); + private static void AddLegendMatch( IDictionary symbolEffects, string value,