diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/C4PlantUmlExporterWithElementLinks.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/C4PlantUmlExporterWithElementLinks.kt index e8b1fe18..dfd03984 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/C4PlantUmlExporterWithElementLinks.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/C4PlantUmlExporterWithElementLinks.kt @@ -1,20 +1,39 @@ package nl.avisi.structurizr.site.generatr.site import com.structurizr.Workspace +import com.structurizr.export.Diagram import com.structurizr.export.IndentingWriter import com.structurizr.export.plantuml.C4PlantUMLExporter import com.structurizr.model.Element import com.structurizr.model.SoftwareSystem +import com.structurizr.view.ComponentView +import com.structurizr.view.ContainerView +import com.structurizr.view.CustomView +import com.structurizr.view.DeploymentView +import com.structurizr.view.DynamicView +import com.structurizr.view.SystemContextView +import com.structurizr.view.SystemLandscapeView import com.structurizr.view.View import nl.avisi.structurizr.site.generatr.includedSoftwareSystems import nl.avisi.structurizr.site.generatr.normalize class C4PlantUmlExporterWithElementLinks( private val workspace: Workspace, - private val branch: String + private val url: String ): C4PlantUMLExporter() { companion object { - const val TEMP_URI = "https://will-be-changed-to-relative" + const val TEMP_URI = "https://will-be-changed-to-relative/" + + fun C4PlantUMLExporter.export(view: View): Diagram = when (view) { + is CustomView -> export(view) + is SystemLandscapeView -> export(view) + is SystemContextView -> export(view) + is ContainerView -> export(view) + is ComponentView -> export(view) + is DynamicView -> export(view) + is DeploymentView -> export(view) + else -> throw IllegalStateException("View ${view.name} has a non-exportable type") + } } override fun writeElement(view: View?, element: Element?, writer: IndentingWriter?) { @@ -30,7 +49,8 @@ class C4PlantUmlExporterWithElementLinks( workspace.model.includedSoftwareSystems.contains(this) && this != view?.softwareSystem private fun setElementUrl(element: Element) { - element.url = "${TEMP_URI}/$branch/${element.name.normalize()}/context/" + val path = "/${element.name.normalize()}/context/".asUrlRelativeTo(url) + element.url = "${TEMP_URI}$path" } private fun writeModifiedElement( diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/DiagramGenerator.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/DiagramGenerator.kt index 2fd6ad29..3d88b21e 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/DiagramGenerator.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/DiagramGenerator.kt @@ -4,9 +4,12 @@ import com.structurizr.Workspace import com.structurizr.export.Diagram import com.structurizr.export.plantuml.C4PlantUMLExporter import com.structurizr.export.plantuml.PlantUMLDiagram +import com.structurizr.view.View import net.sourceforge.plantuml.FileFormat import net.sourceforge.plantuml.FileFormatOption import net.sourceforge.plantuml.SourceStringReader +import nl.avisi.structurizr.site.generatr.site.C4PlantUmlExporterWithElementLinks.Companion.export +import java.io.ByteArrayOutputStream import java.io.File import java.net.URL @@ -30,6 +33,12 @@ fun generateDiagrams(workspace: Workspace, exportDir: File) { } } +fun generateDiagramWithElementLinks(workspace: Workspace, view: View, baseUrl: String): String { + val diagram = generatePlantUMLDiagramWithElementLinks(workspace, baseUrl, view) + + return outputToEmbeddableSvg(diagram) +} + private fun generatePlantUMLDiagrams(workspace: Workspace): Collection { val plantUMLExporter = C4PlantUMLExporter() @@ -53,6 +62,18 @@ private fun saveImages(diagram: Diagram, pngDir: File, svgDir: File) { } } +private fun generatePlantUMLDiagramWithElementLinks(workspace: Workspace, baseUrl: String, view: View ): Diagram { + val plantUMLExporter = C4PlantUmlExporterWithElementLinks(workspace, baseUrl) + + return plantUMLExporter.export(view) +} + +private fun outputToEmbeddableSvg(diagram: Diagram): String { + val stream = ByteArrayOutputStream() + SourceStringReader(diagram.withCachedIncludes().definition).outputImage(stream, FileFormatOption(FileFormat.SVG, false)) + return stream.toByteArray().decodeToString() +} + private fun Diagram.withCachedIncludes(): Diagram { val def = definition.replace("!include\\s+(.*)".toRegex()) { val cachedInclude = IncludeCache.cachedInclude(it.groupValues[1]) diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/GeneratorContext.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/GeneratorContext.kt index 041a20b4..ee053c45 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/GeneratorContext.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/GeneratorContext.kt @@ -7,5 +7,5 @@ data class GeneratorContext( val workspace: Workspace, val branches: List, val currentBranch: String, - val svgFactory: (name: String) -> String + val svgFactory: (key: String, url: String) -> String ) diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/SiteGenerator.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/SiteGenerator.kt index 6f130c0b..1c87fbd9 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/SiteGenerator.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/SiteGenerator.kt @@ -45,12 +45,9 @@ fun generateSite( branches: List, currentBranch: String ) { - val generatorContext = GeneratorContext(version, workspace, branches, currentBranch) { - val pathname = "${exportDir.absolutePath}/${currentBranch}/svg/${it}.svg" - File(pathname) - .let { file -> - if (file.exists()) file.readText() else "$pathname not found" - } + val generatorContext = GeneratorContext(version, workspace, branches, currentBranch) { name, baseUrl -> + val view = workspace.views.views.single { view -> view.key == name } + generateDiagramWithElementLinks(workspace, view, baseUrl) } if (assetsDir != null) copyAssets(assetsDir, File(exportDir, currentBranch)) diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/DiagramViewModel.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/DiagramViewModel.kt index c45d8297..a8f829ee 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/DiagramViewModel.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/DiagramViewModel.kt @@ -10,9 +10,9 @@ data class DiagramViewModel( val pumlLocation: ImageViewModel ) { companion object { - fun forView(pageViewModel: PageViewModel, view: View, svgFactory: (name: String) -> String) = DiagramViewModel( + fun forView(pageViewModel: PageViewModel, view: View, svgFactory: (key: String, url: String) -> String) = DiagramViewModel( view.name, - svgFactory(view.key), + svgFactory(view.key, pageViewModel.url), ImageViewModel(pageViewModel, "/svg/${view.key}.svg"), ImageViewModel(pageViewModel, "/png/${view.key}.png"), ImageViewModel(pageViewModel, "/puml/${view.key}.puml") diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/MarkdownViewModel.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/MarkdownViewModel.kt index 4335b2b9..7fc3044d 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/MarkdownViewModel.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/MarkdownViewModel.kt @@ -1,3 +1,3 @@ package nl.avisi.structurizr.site.generatr.site.model -data class MarkdownViewModel(val markdown: String, val svgFactory: (name: String) -> String) +data class MarkdownViewModel(val markdown: String, val svgFactory: (key: String, url: String) -> String) diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Markdown.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Markdown.kt index 12686060..7b04f978 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Markdown.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Markdown.kt @@ -40,7 +40,7 @@ private fun markdownToHtml(pageViewModel: PageViewModel, markdownViewModel: Mark val html = renderer.render(markDownDocument) return Jsoup.parse(html) - .apply { body().transformEmbeddedDiagramElements(markdownViewModel.svgFactory) } + .apply { body().transformEmbeddedDiagramElements(markdownViewModel.svgFactory, pageViewModel.url) } .html() } @@ -71,11 +71,14 @@ private class CustomLinkResolver(private val pageViewModel: PageViewModel) : Lin } } -private fun Element.transformEmbeddedDiagramElements(svgFactory: (name: String) -> String) = this.allElements +private fun Element.transformEmbeddedDiagramElements( + svgFactory: (key: String, url: String) -> String, + url: String +) = this.allElements .toList() .filter { it.tag().name == "img" && it.attr("src").startsWith("embed:") } .forEach { val diagramId = it.attr("src").substring(6) - it.parent()?.append(svgFactory(diagramId)) + it.parent()?.append(svgFactory(diagramId, url)) it.remove() } diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/C4PlantUmlExporterWithElementLinksTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/C4PlantUmlExporterWithElementLinksTest.kt index e23a5b02..c3c0cc27 100644 --- a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/C4PlantUmlExporterWithElementLinksTest.kt +++ b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/C4PlantUmlExporterWithElementLinksTest.kt @@ -3,18 +3,16 @@ package nl.avisi.structurizr.site.generatr.site import assertk.assertThat import assertk.assertions.isEqualTo import com.structurizr.Workspace -import com.structurizr.model.SoftwareSystem +import com.structurizr.view.SystemContextView import org.junit.jupiter.api.Test class C4PlantUmlExporterWithElementLinksTest { @Test fun `renders diagram`() { - val (workspace, system) = createWorkspace() - val view = workspace.views.createSystemContextView(system, "Context1", "") - .apply { addAllElements() } + val (workspace, view) = createWorkspaceWithOneSystem() - val diagram = C4PlantUmlExporterWithElementLinks(workspace, "master") + val diagram = C4PlantUmlExporterWithElementLinks(workspace, "/landscape/") .export(view) assertThat(diagram.definition.withoutHeaderAndFooter()).isEqualTo( @@ -26,28 +24,55 @@ class C4PlantUmlExporterWithElementLinksTest { @Test fun `link to other software system`() { - val (workspace, system) = createWorkspace() - workspace.model.addSoftwareSystem("System 2").apply { uses(system, "uses") } - val view = workspace.views.createSystemContextView(system, "Context 1", "") - .apply { addAllElements() } + val (workspace, view) = createWorkspaceWithTwoSystems() - val diagram = C4PlantUmlExporterWithElementLinks(workspace, "master") + val diagram = C4PlantUmlExporterWithElementLinks(workspace, "/landscape/") .export(view) assertThat(diagram.definition.withoutHeaderAndFooter()).isEqualTo( """ System(System1, "System 1", "", ${'$'}tags="") - System(System2, "System 2", "", ${'$'}tags="")[[/master/system-2/context/]] + System(System2, "System 2", "", ${'$'}tags="")[[../system-2/context]] Rel_D(System2, System1, "uses", ${'$'}tags="") """.withoutTrailingSpaces() ) } - private fun createWorkspace(): Pair { + @Test + fun `link to other software system from two path segments deep`() { + val (workspace, view) = createWorkspaceWithTwoSystems() + + val diagram = C4PlantUmlExporterWithElementLinks(workspace, "/system-1/context/") + .export(view) + + assertThat(diagram.definition.withoutHeaderAndFooter()).isEqualTo( + """ + System(System1, "System 1", "", ${'$'}tags="") + System(System2, "System 2", "", ${'$'}tags="")[[../../system-2/context]] + + Rel_D(System2, System1, "uses", ${'$'}tags="") + """.withoutTrailingSpaces() + ) + } + + private fun createWorkspaceWithOneSystem(): Pair { + val workspace = Workspace("workspace name", "") + val system = workspace.model.addSoftwareSystem("System 1") + val view = workspace.views.createSystemContextView(system, "Context1", "") + .apply { addAllElements() } + + return workspace to view + } + + private fun createWorkspaceWithTwoSystems(): Pair { val workspace = Workspace("workspace name", "") val system = workspace.model.addSoftwareSystem("System 1") - return workspace to system + workspace.model.addSoftwareSystem("System 2").apply { uses(system, "uses") } + val view = workspace.views.createSystemContextView(system, "Context 1", "") + .apply { addAllElements() } + + return workspace to view } private fun String.withoutHeaderAndFooter() = this diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/ViewModelTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/ViewModelTest.kt index 572fcd05..62c031e0 100644 --- a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/ViewModelTest.kt +++ b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/ViewModelTest.kt @@ -10,7 +10,7 @@ import java.time.ZoneId import java.util.* abstract class ViewModelTest { - protected val svgFactory = { _: String -> "" } + protected val svgFactory = { _: String, _: String -> "" } protected fun generatorContext( workspaceName: String = "workspace name",