Skip to content

Commit

Permalink
Make element links in embedded SVGs relative
Browse files Browse the repository at this point in the history
This fixes broken links when the generate site is not hosted as root
web site.
As a consequence we no longer generate embedded SVG's up-front,
but generate them as we need them with links relative to the link of
the current page.
  • Loading branch information
jp7677 committed Nov 14, 2022
1 parent b09a3ae commit b3fe5bf
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -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?) {
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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<Diagram> {
val plantUMLExporter = C4PlantUMLExporter()

Expand All @@ -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])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ data class GeneratorContext(
val workspace: Workspace,
val branches: List<String>,
val currentBranch: String,
val svgFactory: (name: String) -> String
val svgFactory: (key: String, url: String) -> String
)
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,9 @@ fun generateSite(
branches: List<String>,
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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down Expand Up @@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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<Workspace, SoftwareSystem> {
@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<Workspace, SystemContextView> {
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<Workspace, SystemContextView> {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import java.time.ZoneId
import java.util.*

abstract class ViewModelTest {
protected val svgFactory = { _: String -> "<svg></svg>" }
protected val svgFactory = { _: String, _: String -> "<svg></svg>" }

protected fun generatorContext(
workspaceName: String = "workspace name",
Expand Down

0 comments on commit b3fe5bf

Please sign in to comment.