Precompute matched-transition link keys for HTML export
Embed linkKey on matchedTransitions in HTML metadata so explorer highlighting uses the same SVG #link_* suffix as PlantUML, avoiding JS/Java format drift while keeping golden JSON unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,11 +2,14 @@ package click.kamil.springstatemachineexporter.html.exporter;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.LinkResolution;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.exporter.ExportOptions;
|
||||
import click.kamil.springstatemachineexporter.exporter.PlantUml;
|
||||
import click.kamil.springstatemachineexporter.exporter.StateMachineExporter;
|
||||
import click.kamil.springstatemachineexporter.exporter.TransitionLinkKey;
|
||||
import click.kamil.springstatemachineexporter.model.Transition;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
@@ -58,8 +61,9 @@ public class HtmlExporter implements StateMachineExporter {
|
||||
// 4. Load Template
|
||||
String template = loadTemplate();
|
||||
|
||||
// 5. Prepare Metadata JSON
|
||||
String metadataJson = objectMapper.writeValueAsString(result);
|
||||
// 5. Prepare Metadata JSON (precompute link keys so HTML JS matches SVG identifiers)
|
||||
AnalysisResult htmlMetadata = enrichLinkKeys(result, options);
|
||||
String metadataJson = objectMapper.writeValueAsString(htmlMetadata);
|
||||
|
||||
// Check if we should render metadata pane
|
||||
boolean hideMetadataProp = Boolean.getBoolean("html.hideMetadataPane");
|
||||
@@ -147,6 +151,41 @@ public class HtmlExporter implements StateMachineExporter {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static AnalysisResult enrichLinkKeys(AnalysisResult result, ExportOptions options) {
|
||||
CodebaseMetadata metadata = result.getMetadata();
|
||||
if (metadata == null || metadata.getCallChains() == null || metadata.getCallChains().isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
List<CallChain> enrichedChains = metadata.getCallChains().stream()
|
||||
.map(chain -> enrichChainLinkKeys(chain, options))
|
||||
.toList();
|
||||
return result.toBuilder()
|
||||
.metadata(metadata.toBuilder().callChains(enrichedChains).build())
|
||||
.build();
|
||||
}
|
||||
|
||||
private static CallChain enrichChainLinkKeys(CallChain chain, ExportOptions options) {
|
||||
if (chain.getMatchedTransitions() == null || chain.getMatchedTransitions().isEmpty()) {
|
||||
return chain;
|
||||
}
|
||||
List<MatchedTransition> enriched = chain.getMatchedTransitions().stream()
|
||||
.map(mt -> enrichMatchedTransitionLinkKey(mt, options))
|
||||
.toList();
|
||||
return chain.toBuilder().matchedTransitions(enriched).build();
|
||||
}
|
||||
|
||||
private static MatchedTransition enrichMatchedTransitionLinkKey(MatchedTransition mt, ExportOptions options) {
|
||||
if (mt.getLinkKey() != null && !mt.getLinkKey().isBlank()) {
|
||||
return mt;
|
||||
}
|
||||
String linkKey = TransitionLinkKey.build(
|
||||
mt.getSourceState(),
|
||||
mt.getEvent(),
|
||||
options.getStateFormat(),
|
||||
options.getEventFormat());
|
||||
return mt.toBuilder().linkKey(linkKey).build();
|
||||
}
|
||||
|
||||
private String loadTemplate() {
|
||||
try (InputStream is = getClass().getResourceAsStream("/html/template.html")) {
|
||||
if (is == null) throw new RuntimeException("HTML template not found");
|
||||
|
||||
@@ -492,12 +492,13 @@
|
||||
let steps = [];
|
||||
chains.forEach(c => {
|
||||
c.matchedTransitions.forEach(t => {
|
||||
const key = buildLinkKey(t.sourceState, t.event);
|
||||
const key = t.linkKey || buildLinkKey(t.sourceState, t.event);
|
||||
if (!key || seen.has(key)) return;
|
||||
seen.add(key);
|
||||
steps.push({
|
||||
event: t.event,
|
||||
source: t.sourceState,
|
||||
linkKey: t.linkKey,
|
||||
ambiguous: c.linkResolution === 'AMBIGUOUS_WIDEN'
|
||||
|| (c.triggerPoint && c.triggerPoint.ambiguous)
|
||||
});
|
||||
@@ -528,7 +529,7 @@
|
||||
let isAmbiguous = false;
|
||||
|
||||
if (typeof step === 'object') {
|
||||
targetLinkKey = buildLinkKey(step.source, step.event);
|
||||
targetLinkKey = step.linkKey || buildLinkKey(step.source, step.event);
|
||||
isAmbiguous = step.ambiguous;
|
||||
} else if (step.includes("->")) {
|
||||
const parts = step.split("->");
|
||||
|
||||
@@ -412,6 +412,7 @@ class HtmlTransitionLinkConsistencyTest {
|
||||
|
||||
assertThat(html).contains("Ambiguous link");
|
||||
assertThat(html).contains("#link_OrderState_NEW__OrderEvent_PAY");
|
||||
assertThat(html).contains("\"linkKey\" : \"OrderState_NEW__OrderEvent_PAY\"");
|
||||
assertThat(html).contains("com.example.web.AmbiguousController");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user