html exporter fixed

This commit is contained in:
2026-06-18 22:06:57 +02:00
parent 5894303510
commit d93d36e8ad
4 changed files with 72 additions and 38 deletions

View File

@@ -12,6 +12,8 @@ public class ExportOptions {
boolean renderChoicesAsDiamonds = true; boolean renderChoicesAsDiamonds = true;
@Builder.Default @Builder.Default
boolean embedIdentifiers = false; boolean embedIdentifiers = false;
@Builder.Default
boolean includeMetadataPane = true;
@Builder.Default @Builder.Default
EnumFormat eventFormat = EnumFormat.fn; EnumFormat eventFormat = EnumFormat.fn;

View File

@@ -56,6 +56,9 @@ public class HtmlExporterCommand implements Callable<Integer> {
@Option(names = {"--no-diamonds"}, description = "Disable rendering choice pseudostates as PlantUML diamonds (render them as normal state nodes instead).") @Option(names = {"--no-diamonds"}, description = "Disable rendering choice pseudostates as PlantUML diamonds (render them as normal state nodes instead).")
private boolean noDiamonds; private boolean noDiamonds;
@Option(names = {"--no-metadata-pane"}, description = "Disable rendering the left metadata pane (entry points, flows) in the HTML viewer.")
private boolean noMetadataPane;
@Override @Override
public Integer call() throws Exception { public Integer call() throws Exception {
var out = spec.commandLine().getOut(); var out = spec.commandLine().getOut();
@@ -78,12 +81,17 @@ public class HtmlExporterCommand implements Callable<Integer> {
List<String> formats = List.of("html", "json"); List<String> formats = List.of("html", "json");
List<String> profiles = activeProfiles != null ? activeProfiles : java.util.Collections.emptyList(); List<String> profiles = activeProfiles != null ? activeProfiles : java.util.Collections.emptyList();
if (noMetadataPane) {
System.setProperty("html.hideMetadataPane", "true");
}
if (jsonFile != null) { if (jsonFile != null) {
exportService.runJsonExporter(jsonFile, outputDir, formats, profiles); exportService.runJsonExporter(jsonFile, outputDir, formats, profiles);
} else { } else {
boolean renderChoicesAsDiamonds = !noDiamonds; boolean renderChoicesAsDiamonds = !noDiamonds;
exportService.runExporter(inputDir, outputDir, formats, renderChoicesAsDiamonds, profiles, flowsFile, machineFilter); exportService.runExporter(inputDir, outputDir, formats, renderChoicesAsDiamonds, profiles, flowsFile, machineFilter);
} }
System.clearProperty("html.hideMetadataPane");
out.println(CommandLine.Help.Ansi.AUTO.string("%n@|bold,green SUCCESS:|@ Interactive documentation generated successfully.")); out.println(CommandLine.Help.Ansi.AUTO.string("%n@|bold,green SUCCESS:|@ Interactive documentation generated successfully."));

View File

@@ -56,12 +56,51 @@ public class HtmlExporter implements StateMachineExporter {
// 5. Prepare Metadata JSON // 5. Prepare Metadata JSON
String metadataJson = objectMapper.writeValueAsString(result); String metadataJson = objectMapper.writeValueAsString(result);
// Check if we should render metadata pane
boolean hideMetadataProp = Boolean.getBoolean("html.hideMetadataPane");
boolean hasEntryPoints = result.getMetadata() != null && result.getMetadata().getEntryPoints() != null && !result.getMetadata().getEntryPoints().isEmpty();
boolean hasFlows = result.getFlows() != null && !result.getFlows().isEmpty();
boolean shouldRenderPane = !hideMetadataProp && options.isIncludeMetadataPane() && (hasEntryPoints || hasFlows);
// 6. Inject Data // 6. Inject Data
return template String finalHtml = template
.replace("{{MACHINE_NAME}}", result.getName()) .replace("{{MACHINE_NAME}}", result.getName())
.replace("{{SVG_CONTENT}}", decoratedSvg) .replace("{{SVG_CONTENT}}", decoratedSvg)
.replace("{{METADATA_JSON}}", metadataJson); .replace("{{METADATA_JSON}}", metadataJson);
if (shouldRenderPane) {
// If template.html uses {{SIDEBAR_HTML}} and {{TOGGLE_BUTTON_HTML}}, replace with actual HTML
finalHtml = finalHtml.replace("{{SIDEBAR_HTML}}", "<div id=\"sidebar\">\n" +
" <div class=\"sidebar-header\">\n" +
" <header>\n" +
" <h1>Explorer</h1>\n" +
" <p>{{MACHINE_NAME}}</p>\n" +
" </header>\n" +
" </div>\n" +
" \n" +
" <div class=\"tabs\">\n" +
" <div class=\"tab active\" data-tab=\"explorer\">Explorer</div>\n" +
" <div class=\"tab\" data-tab=\"flows\">Business Flows</div>\n" +
" </div>\n" +
"\n" +
" <div id=\"tab-explorer\" class=\"tab-content active\">\n" +
" <div id=\"ep-list\"></div>\n" +
" </div>\n" +
"\n" +
" <div id=\"tab-flows\" class=\"tab-content\">\n" +
" <div id=\"flow-list\"></div>\n" +
" </div>\n" +
"</div>").replace("{{MACHINE_NAME}}", result.getName());
finalHtml = finalHtml.replace("{{TOGGLE_BUTTON_HTML}}", " <button id=\"toggle-sidebar\" aria-label=\"Toggle Sidebar\" title=\"Toggle Sidebar\">\n" +
" <svg viewBox=\"0 0 24 24\"><path d=\"M4 6h16M4 12h16M4 18h16\"></path></svg>\n" +
" </button>");
} else {
finalHtml = finalHtml.replace("{{SIDEBAR_HTML}}", "").replace("{{TOGGLE_BUTTON_HTML}}", "");
}
return finalHtml;
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to export to HTML", e); log.error("Failed to export to HTML", e);
throw new RuntimeException("HTML export failed", e); throw new RuntimeException("HTML export failed", e);

View File

@@ -261,32 +261,10 @@
</head> </head>
<body> <body>
<div id="sidebar"> {{SIDEBAR_HTML}}
<div class="sidebar-header">
<header>
<h1>Explorer</h1>
<p>{{MACHINE_NAME}}</p>
</header>
</div>
<div class="tabs">
<div class="tab active" data-tab="explorer">Explorer</div>
<div class="tab" data-tab="flows">Business Flows</div>
</div>
<div id="tab-explorer" class="tab-content active">
<div id="ep-list"></div>
</div>
<div id="tab-flows" class="tab-content">
<div id="flow-list"></div>
</div>
</div>
<div id="main"> <div id="main">
<button id="toggle-sidebar" aria-label="Toggle Sidebar" title="Toggle Sidebar"> {{TOGGLE_BUTTON_HTML}}
<svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16"></path></svg>
</button>
<div id="svg-container"> <div id="svg-container">
{{SVG_CONTENT}} {{SVG_CONTENT}}
</div> </div>
@@ -304,18 +282,21 @@
const svg = document.querySelector('#svg-container svg'); const svg = document.querySelector('#svg-container svg');
panZoomInstance = svgPanZoom(svg, { zoomEnabled: true, controlIconsEnabled: true, fit: true, center: true }); panZoomInstance = svgPanZoom(svg, { zoomEnabled: true, controlIconsEnabled: true, fit: true, center: true });
document.getElementById('toggle-sidebar').addEventListener('click', () => { const toggleSidebar = document.getElementById('toggle-sidebar');
const sidebar = document.getElementById('sidebar'); if (toggleSidebar) {
sidebar.classList.toggle('collapsed'); toggleSidebar.addEventListener('click', () => {
const sidebar = document.getElementById('sidebar');
// Re-center SVG pan-zoom after transition if (sidebar) sidebar.classList.toggle('collapsed');
setTimeout(() => {
if (panZoomInstance) { // Re-center SVG pan-zoom after transition
panZoomInstance.resize(); setTimeout(() => {
panZoomInstance.center(); if (panZoomInstance) {
} panZoomInstance.resize();
}, 300); panZoomInstance.center();
}); }
}, 300);
});
}
setupTabs(); setupTabs();
populateSidebar(); populateSidebar();
@@ -325,8 +306,10 @@
function setupTabs() { function setupTabs() {
const flows = metadata.flows || []; const flows = metadata.flows || [];
const tabsEl = document.querySelector('.tabs');
if (!tabsEl) return;
if (flows.length === 0) { if (flows.length === 0) {
document.querySelector('.tabs').style.display = 'none'; tabsEl.style.display = 'none';
document.querySelector('.tab-content[data-tab="flows"]')?.remove(); document.querySelector('.tab-content[data-tab="flows"]')?.remove();
return; return;
} }
@@ -342,6 +325,7 @@
function populateSidebar() { function populateSidebar() {
const list = document.getElementById('ep-list'); const list = document.getElementById('ep-list');
if (!list) return;
const entryPoints = metadata.metadata.entryPoints || []; const entryPoints = metadata.metadata.entryPoints || [];
const machineEvents = new Set(metadata.transitions.map(t => t.event).filter(Boolean)); const machineEvents = new Set(metadata.transitions.map(t => t.event).filter(Boolean));
@@ -372,6 +356,7 @@
function populateFlows() { function populateFlows() {
const list = document.getElementById('flow-list'); const list = document.getElementById('flow-list');
if (!list) return;
const flows = metadata.flows || []; const flows = metadata.flows || [];
if (flows.length === 0) { if (flows.length === 0) {