Add quick parse notation for critical editor
This commit is contained in:
@@ -71,16 +71,26 @@
|
||||
<section class="critical-editor-section">
|
||||
<div class="critical-editor-section-header">
|
||||
<div>
|
||||
<h4>Raw Text</h4>
|
||||
<p class="muted">Update the source text, then correct the visible result rows below.</p>
|
||||
<h4>Quick Parse Input</h4>
|
||||
<p class="muted">First line is the result prose. Later lines are base affixes or <code>condition: ...</code> lines with comma-separated shorthand.</p>
|
||||
</div>
|
||||
<button type="button" class="btn-ritual" @onclick="OnReparse" disabled="@IsSaving || IsReparsing">
|
||||
@(IsReparsing ? "Re-Parsing..." : "Re-Parse Raw Text")
|
||||
@(IsReparsing ? "Generating..." : "Generate From Quick Input")
|
||||
</button>
|
||||
</div>
|
||||
<div class="field-shell">
|
||||
<label>Raw Cell Text</label>
|
||||
<InputTextArea class="input-shell critical-editor-textarea tall" @bind-Value="Model.RawCellText" />
|
||||
<label>Quick Parse Input</label>
|
||||
<InputTextArea class="input-shell critical-editor-textarea tall" @bind-Value="Model.QuickParseInput" />
|
||||
</div>
|
||||
<p class="muted critical-editor-advanced-hint">Example: <code>Foe brings his guard up, frightened by your display.</code> then <code>+5, 1mp</code> or <code>w/o shield: glancing blow, +15, 3s, 3np</code>.</p>
|
||||
<div class="critical-editor-quick-legend">
|
||||
@foreach (var entry in QuickParseLegendEntries)
|
||||
{
|
||||
<div class="critical-editor-quick-legend-item">
|
||||
<code>@entry.Token</code>
|
||||
<span class="muted">@entry.Meaning</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (Model.ValidationMessages.Count > 0)
|
||||
{
|
||||
@@ -88,7 +98,7 @@
|
||||
}
|
||||
@if (HasComparisonDifferences(Model, ComparisonBaseline))
|
||||
{
|
||||
<p class="muted critical-editor-advanced-hint">Fresh parsing differs from the current edited card. Review Generated Compare before saving if you want to keep the overrides.</p>
|
||||
<p class="muted critical-editor-advanced-hint">Fresh generation differs from the current edited card. Review Generated Compare before saving if you want to keep the overrides.</p>
|
||||
}
|
||||
<div class="field-shell">
|
||||
<label>Result Text</label>
|
||||
@@ -203,7 +213,7 @@
|
||||
<div class="critical-editor-card-header">
|
||||
<div>
|
||||
<strong>Generated Compare</strong>
|
||||
<p class="muted critical-editor-inline-copy">Compare the current edited card against the fresh parser output from the raw text.</p>
|
||||
<p class="muted critical-editor-inline-copy">Compare the current edited card against the fresh generated result from the quick parse input.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -222,8 +232,8 @@
|
||||
Effects="@BuildPreviewEffects(GetComparisonSourceModel(Model, ComparisonBaseline))"
|
||||
Branches="@BuildPreviewBranches(GetComparisonSourceModel(Model, ComparisonBaseline))" />
|
||||
<CriticalResultPreviewCard
|
||||
Title="Generated From Raw Text"
|
||||
Caption="This is the fresh parser output before override preservation."
|
||||
Title="Generated From Quick Input"
|
||||
Caption="This is the fresh generated output before override preservation."
|
||||
Description="@Model.GeneratedState.DescriptionText"
|
||||
Effects="@Model.GeneratedState.Effects"
|
||||
Branches="@Model.GeneratedState.Branches"
|
||||
@@ -250,6 +260,10 @@
|
||||
</div>
|
||||
|
||||
<dl class="critical-editor-diagnostic-grid">
|
||||
<div>
|
||||
<dt>OCR Source</dt>
|
||||
<dd>@(string.IsNullOrWhiteSpace(Model.RawCellText) ? "—" : Model.RawCellText)</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Parse Status</dt>
|
||||
<dd>@Model.ParseStatus</dd>
|
||||
@@ -371,6 +385,18 @@
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
private static readonly IReadOnlyList<(string Token, string Meaning)> QuickParseLegendEntries =
|
||||
[
|
||||
("+15", "Direct hits"),
|
||||
("3s", "Stunned 3 rounds"),
|
||||
("1mp", "Must parry 1 round"),
|
||||
("3np", "No parry 3 rounds"),
|
||||
("1hpr", "Bleed 1 hit per round"),
|
||||
("-20", "Foe penalty"),
|
||||
("+20b", "Attacker bonus next round"),
|
||||
("+2d10-3pp", "Power-point modifier"),
|
||||
("w/o shield: +15, 3s", "Conditional line")
|
||||
];
|
||||
private IJSObjectReference? jsModule;
|
||||
private bool isBackdropPointerDown;
|
||||
|
||||
@@ -610,8 +636,8 @@
|
||||
|
||||
private static string GetParserNoteSummary(int noteCount) =>
|
||||
noteCount == 1
|
||||
? "1 parser note is available under Advanced Diagnostics."
|
||||
: $"{noteCount} parser notes are available under Advanced Diagnostics.";
|
||||
? "1 parser note is available under Advanced Review & Diagnostics."
|
||||
: $"{noteCount} parser notes are available under Advanced Review & Diagnostics.";
|
||||
|
||||
private static string FormatJson(string json)
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@ public sealed class CriticalCellEditorModel
|
||||
public string ColumnLabel { get; set; } = string.Empty;
|
||||
public string ColumnRole { get; set; } = string.Empty;
|
||||
public string RawCellText { get; set; } = string.Empty;
|
||||
public string QuickParseInput { get; set; } = string.Empty;
|
||||
public string DescriptionText { get; set; } = string.Empty;
|
||||
public string? RawAffixText { get; set; }
|
||||
public string ParseStatus { get; set; } = string.Empty;
|
||||
@@ -43,6 +44,7 @@ public sealed class CriticalCellEditorModel
|
||||
ColumnLabel = response.ColumnLabel,
|
||||
ColumnRole = response.ColumnRole,
|
||||
RawCellText = response.RawCellText,
|
||||
QuickParseInput = response.QuickParseInput,
|
||||
DescriptionText = response.DescriptionText,
|
||||
RawAffixText = response.RawAffixText,
|
||||
ParseStatus = response.ParseStatus,
|
||||
@@ -61,6 +63,7 @@ public sealed class CriticalCellEditorModel
|
||||
{
|
||||
var request = new CriticalCellUpdateRequest(
|
||||
RawCellText,
|
||||
QuickParseInput,
|
||||
DescriptionText,
|
||||
RawAffixText,
|
||||
ResolveParseStatus(Effects, Branches),
|
||||
@@ -99,6 +102,7 @@ public sealed class CriticalCellEditorModel
|
||||
ColumnLabel = ColumnLabel,
|
||||
ColumnRole = ColumnRole,
|
||||
RawCellText = RawCellText,
|
||||
QuickParseInput = QuickParseInput,
|
||||
DescriptionText = DescriptionText,
|
||||
RawAffixText = RawAffixText,
|
||||
ParseStatus = ParseStatus,
|
||||
|
||||
@@ -14,6 +14,7 @@ public sealed record CriticalCellEditorResponse(
|
||||
string ColumnLabel,
|
||||
string ColumnRole,
|
||||
string RawCellText,
|
||||
string QuickParseInput,
|
||||
string DescriptionText,
|
||||
string? RawAffixText,
|
||||
string ParseStatus,
|
||||
|
||||
@@ -9,7 +9,8 @@ public sealed record CriticalCellEditorSnapshot(
|
||||
bool AreEffectsOverridden,
|
||||
bool AreBranchesOverridden,
|
||||
IReadOnlyList<CriticalEffectEditorItem> Effects,
|
||||
IReadOnlyList<CriticalBranchEditorItem> Branches)
|
||||
IReadOnlyList<CriticalBranchEditorItem> Branches,
|
||||
string? QuickParseInput)
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
@@ -21,7 +22,8 @@ public sealed record CriticalCellEditorSnapshot(
|
||||
request.AreEffectsOverridden,
|
||||
request.AreBranchesOverridden,
|
||||
request.Effects,
|
||||
request.Branches);
|
||||
request.Branches,
|
||||
request.QuickParseInput);
|
||||
|
||||
public string ToJson() =>
|
||||
JsonSerializer.Serialize(this, JsonOptions);
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace RolemasterDb.App.Features;
|
||||
|
||||
public sealed record CriticalCellUpdateRequest(
|
||||
string RawCellText,
|
||||
string QuickParseInput,
|
||||
string DescriptionText,
|
||||
string? RawAffixText,
|
||||
string ParseStatus,
|
||||
|
||||
100
src/RolemasterDb.App/Features/CriticalQuickNotationFormatter.cs
Normal file
100
src/RolemasterDb.App/Features/CriticalQuickNotationFormatter.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace RolemasterDb.App.Features;
|
||||
|
||||
public static class CriticalQuickNotationFormatter
|
||||
{
|
||||
public static string Format(
|
||||
string descriptionText,
|
||||
IReadOnlyList<CriticalEffectEditorItem> effects,
|
||||
IReadOnlyList<CriticalBranchEditorItem> branches)
|
||||
{
|
||||
var lines = new List<string>
|
||||
{
|
||||
CollapseWhitespace(descriptionText)
|
||||
};
|
||||
|
||||
var baseAffixLine = FormatEffects(effects);
|
||||
if (!string.IsNullOrWhiteSpace(baseAffixLine))
|
||||
{
|
||||
lines.Add(baseAffixLine);
|
||||
}
|
||||
|
||||
foreach (var branch in branches.OrderBy(item => item.SortOrder))
|
||||
{
|
||||
var payloadParts = new List<string>();
|
||||
var branchDescription = CollapseWhitespace(branch.DescriptionText);
|
||||
if (!string.IsNullOrWhiteSpace(branchDescription))
|
||||
{
|
||||
payloadParts.Add(branchDescription);
|
||||
}
|
||||
|
||||
var branchAffixLine = FormatEffects(branch.Effects);
|
||||
if (!string.IsNullOrWhiteSpace(branchAffixLine))
|
||||
{
|
||||
payloadParts.Add(branchAffixLine);
|
||||
}
|
||||
|
||||
if (payloadParts.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
lines.Add($"{CollapseWhitespace(branch.ConditionText)}: {string.Join(", ", payloadParts)}");
|
||||
}
|
||||
|
||||
return string.Join(Environment.NewLine, lines.Where(line => !string.IsNullOrWhiteSpace(line)));
|
||||
}
|
||||
|
||||
private static string? FormatEffects(IReadOnlyList<CriticalEffectEditorItem> effects)
|
||||
{
|
||||
var tokens = effects
|
||||
.Select(FormatEffect)
|
||||
.Where(token => !string.IsNullOrWhiteSpace(token))
|
||||
.ToList();
|
||||
|
||||
return tokens.Count == 0 ? null : string.Join(", ", tokens);
|
||||
}
|
||||
|
||||
private static string? FormatEffect(CriticalEffectEditorItem effect) =>
|
||||
effect.EffectCode switch
|
||||
{
|
||||
Domain.CriticalEffectCodes.DirectHits => effect.ValueInteger is int hits ? $"+{hits}" : effect.SourceText,
|
||||
Domain.CriticalEffectCodes.StunnedRounds => effect.DurationRounds is int rounds ? $"{rounds}s" : effect.SourceText,
|
||||
Domain.CriticalEffectCodes.MustParryRounds => effect.DurationRounds is int rounds ? $"{rounds}mp" : effect.SourceText,
|
||||
Domain.CriticalEffectCodes.NoParryRounds => effect.DurationRounds is int rounds ? $"{rounds}np" : effect.SourceText,
|
||||
Domain.CriticalEffectCodes.BleedPerRound => effect.PerRound is int bleed ? $"{bleed}hpr" : effect.SourceText,
|
||||
Domain.CriticalEffectCodes.FoePenalty => effect.Modifier is int penalty ? penalty.ToString(System.Globalization.CultureInfo.InvariantCulture) : effect.SourceText,
|
||||
Domain.CriticalEffectCodes.AttackerBonusNextRound => effect.Modifier is int bonus ? $"+{bonus}b" : effect.SourceText,
|
||||
Domain.CriticalEffectCodes.PowerPointModifier => FormatPowerPointModifier(effect.ValueExpression, effect.SourceText),
|
||||
_ => effect.SourceText ?? effect.EffectCode
|
||||
};
|
||||
|
||||
private static string? FormatPowerPointModifier(string? valueExpression, string? sourceText)
|
||||
{
|
||||
var expression = CollapseWhitespace(valueExpression);
|
||||
if (string.IsNullOrWhiteSpace(expression))
|
||||
{
|
||||
return sourceText;
|
||||
}
|
||||
|
||||
expression = expression.Trim();
|
||||
if (expression.StartsWith('+'))
|
||||
{
|
||||
expression = expression[1..];
|
||||
}
|
||||
|
||||
if (expression.StartsWith('(') && expression.EndsWith(')') && expression.Length > 2)
|
||||
{
|
||||
expression = expression[1..^1].Trim();
|
||||
}
|
||||
|
||||
expression = Regex.Replace(expression, @"\s+", string.Empty);
|
||||
return $"+{expression}pp";
|
||||
}
|
||||
|
||||
private static string CollapseWhitespace(string? value) =>
|
||||
string.IsNullOrWhiteSpace(value)
|
||||
? string.Empty
|
||||
: Regex.Replace(value.Trim(), @"\s+", " ");
|
||||
}
|
||||
@@ -296,7 +296,7 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
||||
}
|
||||
|
||||
var currentState = CreateCurrentEditorState(result);
|
||||
var generatedContent = await ParseCriticalCellContentAsync(dbContext, result.CriticalTableId, currentState.RawCellText, cancellationToken);
|
||||
var generatedContent = await ParseCriticalCellContentAsync(dbContext, result.CriticalTableId, currentState.QuickParseInput, cancellationToken);
|
||||
return CreateCellEditorResponse(result, currentState, generatedContent.ValidationErrors, CreateComparisonState(generatedContent));
|
||||
}
|
||||
|
||||
@@ -325,8 +325,7 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
||||
return null;
|
||||
}
|
||||
|
||||
var affixLegend = await BuildSharedAffixLegendAsync(dbContext, result.CriticalTableId, cancellationToken);
|
||||
var content = SharedParsing.CriticalCellTextParser.Parse(currentState.RawCellText, affixLegend);
|
||||
var content = await ParseCriticalCellContentAsync(dbContext, result.CriticalTableId, currentState.QuickParseInput, cancellationToken);
|
||||
var generatedState = CreateGeneratedEditorState(content);
|
||||
var mergedState = MergeGeneratedState(currentState, generatedState);
|
||||
return CreateCellEditorResponse(result, mergedState, content.ValidationErrors, CreateComparisonState(content));
|
||||
@@ -370,7 +369,7 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var generatedContent = await ParseCriticalCellContentAsync(dbContext, result.CriticalTableId, request.RawCellText, cancellationToken);
|
||||
var generatedContent = await ParseCriticalCellContentAsync(dbContext, result.CriticalTableId, request.QuickParseInput, cancellationToken);
|
||||
return CreateCellEditorResponse(result, request, generatedContent.ValidationErrors, CreateComparisonState(generatedContent));
|
||||
}
|
||||
|
||||
@@ -452,6 +451,7 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
||||
result.CriticalColumn.Label,
|
||||
result.CriticalColumn.Role,
|
||||
state.RawCellText,
|
||||
state.QuickParseInput,
|
||||
state.DescriptionText,
|
||||
state.RawAffixText,
|
||||
state.ParseStatus,
|
||||
@@ -575,8 +575,13 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
||||
{
|
||||
if (CriticalCellEditorSnapshot.TryParse(result.ParsedJson, out var snapshot) && snapshot is not null)
|
||||
{
|
||||
var snapshotQuickParseInput = string.IsNullOrWhiteSpace(snapshot.QuickParseInput)
|
||||
? CriticalQuickNotationFormatter.Format(result.DescriptionText, snapshot.Effects, snapshot.Branches)
|
||||
: snapshot.QuickParseInput;
|
||||
|
||||
return new CriticalCellUpdateRequest(
|
||||
result.RawCellText,
|
||||
snapshotQuickParseInput,
|
||||
result.DescriptionText,
|
||||
result.RawAffixText,
|
||||
result.ParseStatus,
|
||||
@@ -600,6 +605,7 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
||||
|
||||
return new CriticalCellUpdateRequest(
|
||||
result.RawCellText,
|
||||
CriticalQuickNotationFormatter.Format(result.DescriptionText, effects, branches),
|
||||
result.DescriptionText,
|
||||
result.RawAffixText,
|
||||
result.ParseStatus,
|
||||
@@ -623,17 +629,18 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
||||
.ToList();
|
||||
|
||||
return new CriticalCellUpdateRequest(
|
||||
content.RawCellText,
|
||||
content.DescriptionText,
|
||||
content.RawAffixText,
|
||||
ResolveParseStatus(content.Effects, content.Branches),
|
||||
SerializeParsedEffects(content.Effects),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
effects,
|
||||
branches);
|
||||
RawCellText: string.Empty,
|
||||
QuickParseInput: content.RawCellText,
|
||||
DescriptionText: content.DescriptionText,
|
||||
RawAffixText: content.RawAffixText,
|
||||
ParseStatus: ResolveParseStatus(content.Effects, content.Branches),
|
||||
ParsedJson: SerializeParsedEffects(content.Effects),
|
||||
IsDescriptionOverridden: false,
|
||||
IsRawAffixTextOverridden: false,
|
||||
AreEffectsOverridden: false,
|
||||
AreBranchesOverridden: false,
|
||||
Effects: effects,
|
||||
Branches: branches);
|
||||
}
|
||||
|
||||
private static CriticalCellUpdateRequest MergeGeneratedState(
|
||||
@@ -641,6 +648,7 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
||||
CriticalCellUpdateRequest generatedState) =>
|
||||
new(
|
||||
currentState.RawCellText,
|
||||
currentState.QuickParseInput,
|
||||
currentState.IsDescriptionOverridden ? currentState.DescriptionText : generatedState.DescriptionText,
|
||||
currentState.IsRawAffixTextOverridden ? currentState.RawAffixText : generatedState.RawAffixText,
|
||||
generatedState.ParseStatus,
|
||||
@@ -922,11 +930,11 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
||||
private static async Task<SharedParsing.CriticalCellParseContent> ParseCriticalCellContentAsync(
|
||||
RolemasterDbContext dbContext,
|
||||
int tableId,
|
||||
string rawCellText,
|
||||
string quickParseInput,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var affixLegend = await BuildSharedAffixLegendAsync(dbContext, tableId, cancellationToken);
|
||||
return SharedParsing.CriticalCellTextParser.Parse(rawCellText, affixLegend);
|
||||
return SharedParsing.CriticalQuickNotationParser.Parse(quickParseInput, affixLegend);
|
||||
}
|
||||
|
||||
private static bool IsLegendSymbolEffectCode(string effectCode) =>
|
||||
|
||||
@@ -843,6 +843,26 @@ textarea {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.critical-editor-quick-legend {
|
||||
display: grid;
|
||||
gap: 0.55rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
}
|
||||
|
||||
.critical-editor-quick-legend-item {
|
||||
display: grid;
|
||||
gap: 0.2rem;
|
||||
padding: 0.65rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: 1px solid rgba(127, 96, 55, 0.1);
|
||||
}
|
||||
|
||||
.critical-editor-quick-legend-item code {
|
||||
font-size: 0.9rem;
|
||||
color: #5b4327;
|
||||
}
|
||||
|
||||
.critical-editor-compare-grid {
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
|
||||
Reference in New Issue
Block a user