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:
2026-07-14 06:46:23 +02:00
parent 32fe96041c
commit 00d1f24709
6 changed files with 142 additions and 5 deletions

View File

@@ -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");

View File

@@ -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("->");

View File

@@ -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");
}