Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make element links in embedded SVGs relative #86

Merged
merged 4 commits into from
Nov 15, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class GenerateSiteCommand : Subcommand(
clonedRepository.checkoutBranch(branch)

val workspace = createStructurizrWorkspace(workspaceFileInRepo)
generateDiagrams(workspace, File(siteDir, branch), branch)
generateDiagrams(workspace, File(siteDir, branch))
generateSite(
version,
workspace,
Expand All @@ -95,7 +95,7 @@ class GenerateSiteCommand : Subcommand(

private fun generateSiteForModel(siteDir: File) {
val workspace = createStructurizrWorkspace(File(workspaceFile))
generateDiagrams(workspace, File(siteDir, defaultBranch), defaultBranch)
generateDiagrams(workspace, File(siteDir, defaultBranch))
generateSite(
version,
workspace,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class ServeCommand : Subcommand("serve", "Start a development server") {
val exportDir = File(siteDir, branch)

println("Generating diagrams...")
generateDiagrams(workspace, exportDir, branch)
generateDiagrams(workspace, exportDir)

println("Generating site...")
copySiteWideAssets(File(siteDir))
Expand Down
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
jp7677 marked this conversation as resolved.
Show resolved Hide resolved

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 @@ -2,35 +2,57 @@ package nl.avisi.structurizr.site.generatr.site

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.File
import java.net.URL

fun generateDiagrams(workspace: Workspace, exportDir: File, branch: String) {
val pumlDir = File(exportDir, "puml").apply { mkdirs() }
val pngDir = File(exportDir, "png").apply { mkdirs() }
val svgDir = File(exportDir, "svg").apply { mkdirs() }
fun generateDiagrams(workspace: Workspace, exportDir: File) {
val pumlDir = pumlDir(exportDir)
val svgDir = svgDir(exportDir)
val pngDir = pngDir(exportDir)

val plantUMLDiagrams = generatePlantUMLDiagrams(workspace, branch)
val plantUMLDiagrams = generatePlantUMLDiagrams(workspace)

plantUMLDiagrams.parallelStream()
.forEach { diagram ->
val plantUMLFile = File(pumlDir, "${diagram.key}.puml")
if (!plantUMLFile.exists() || plantUMLFile.readText() != diagram.definition) {
println("${diagram.key}...")
saveAsPUML(diagram, plantUMLFile)
saveImages(diagram, pngDir, svgDir)
saveAsSvg(diagram, svgDir)
saveAsPng(diagram, pngDir)
} else {
println("${diagram.key} UP-TO-DATE")
}
}
}

private fun generatePlantUMLDiagrams(workspace: Workspace, branch: String): Collection<Diagram> {
val plantUMLExporter = C4PlantUmlExporterWithElementLinks(workspace, branch)
fun generateDiagramWithElementLinks(workspace: Workspace, exportDir: File, view: View, baseUrl: String): String {
val pumlDir = pumlDir(exportDir)
val svgDir = svgDir(exportDir)

val diagram = generatePlantUMLDiagramWithElementLinks(workspace, baseUrl, view)

val name = "${diagram.key}-${view.key}"
val plantUMLFile = File(pumlDir, "$name.puml")
if (!plantUMLFile.exists() || plantUMLFile.readText() != diagram.definition) {
saveAsPUML(diagram, plantUMLFile)
saveAsSvg(diagram, svgDir, name)
} else {
println("$name UP-TO-DATE")
}

return readSvg(svgDir, name)
}

private fun generatePlantUMLDiagrams(workspace: Workspace): Collection<Diagram> {
val plantUMLExporter = C4PlantUMLExporter()

return plantUMLExporter.export(workspace)
}
Expand All @@ -39,19 +61,39 @@ private fun saveAsPUML(diagram: Diagram, plantUMLFile: File) {
plantUMLFile.writeText(diagram.definition)
}

private fun saveImages(diagram: Diagram, pngDir: File, svgDir: File) {
private fun saveAsSvg(diagram: Diagram, svgDir: File, name: String = diagram.key) {
val reader = SourceStringReader(diagram.withCachedIncludes().definition)
val svgFile = File(svgDir, "$name.svg")

svgFile.outputStream().use {
reader.outputImage(it, FileFormatOption(FileFormat.SVG, false))
}
}

private fun saveAsPng(diagram: Diagram, pngDir: File) {
val reader = SourceStringReader(diagram.withCachedIncludes().definition)
val pngFile = File(pngDir, "${diagram.key}.png")
val svgFile = File(svgDir, "${diagram.key}.svg")

pngFile.outputStream().use {
reader.outputImage(it)
}
svgFile.outputStream().use {
reader.outputImage(it, FileFormatOption(FileFormat.SVG, false))
}
}

private fun readSvg(svgDir: File, name: String): String {
val svgFile = File(svgDir, "$name.svg")
return svgFile.readText()
}

private fun generatePlantUMLDiagramWithElementLinks(workspace: Workspace, baseUrl: String, view: View ): Diagram {
val plantUMLExporter = C4PlantUmlExporterWithElementLinks(workspace, baseUrl)

return plantUMLExporter.export(view)
}

private fun pumlDir(exportDir: File) = File(exportDir, "puml").apply { mkdirs() }
private fun svgDir(exportDir: File) = File(exportDir, "svg").apply { mkdirs() }
private fun pngDir(exportDir: File) = File(exportDir, "png").apply { mkdirs() }

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, exportDir, view, baseUrl)
}

if (assetsDir != null) copyAssets(assetsDir, File(exportDir, currentBranch))
Expand All @@ -63,34 +60,42 @@ private fun copyAssets(assetsDir: File, exportDir: File) {

private fun generateHtmlFiles(context: GeneratorContext, exportDir: File) {
val branchDir = File(exportDir, context.currentBranch)
writeHtmlFile(branchDir, HomePageViewModel(context))
writeHtmlFile(branchDir, WorkspaceDecisionsPageViewModel(context))
writeHtmlFile(branchDir, SoftwareSystemsPageViewModel(context))
buildList {
add { writeHtmlFile(branchDir, HomePageViewModel(context)) }
add { writeHtmlFile(branchDir, WorkspaceDecisionsPageViewModel(context)) }
add { writeHtmlFile(branchDir, SoftwareSystemsPageViewModel(context)) }

context.workspace.documentation.sections
.filter { it.order != 1 }
.forEach { writeHtmlFile(branchDir, WorkspaceDocumentationSectionPageViewModel(context, it)) }
context.workspace.documentation.decisions
.forEach { writeHtmlFile(branchDir, WorkspaceDecisionPageViewModel(context, it)) }
context.workspace.documentation.sections
.filter { it.order != 1 }
.forEach {
add { writeHtmlFile(branchDir, WorkspaceDocumentationSectionPageViewModel(context, it)) }
}
context.workspace.documentation.decisions
.forEach {
add { writeHtmlFile(branchDir, WorkspaceDecisionPageViewModel(context, it)) }
}

context.workspace.model.includedSoftwareSystems.forEach {
writeHtmlFile(branchDir, SoftwareSystemHomePageViewModel(context, it))
writeHtmlFile(branchDir, SoftwareSystemContextPageViewModel(context, it))
writeHtmlFile(branchDir, SoftwareSystemContainerPageViewModel(context, it))
writeHtmlFile(branchDir, SoftwareSystemComponentPageViewModel(context, it))
writeHtmlFile(branchDir, SoftwareSystemDeploymentPageViewModel(context, it))
writeHtmlFile(branchDir, SoftwareSystemDependenciesPageViewModel(context, it))
writeHtmlFile(branchDir, SoftwareSystemDecisionsPageViewModel(context, it))
writeHtmlFile(branchDir, SoftwareSystemSectionsPageViewModel(context, it))
context.workspace.model.includedSoftwareSystems.forEach {
add { writeHtmlFile(branchDir, SoftwareSystemHomePageViewModel(context, it)) }
add { writeHtmlFile(branchDir, SoftwareSystemContextPageViewModel(context, it)) }
add { writeHtmlFile(branchDir, SoftwareSystemContainerPageViewModel(context, it)) }
add { writeHtmlFile(branchDir, SoftwareSystemComponentPageViewModel(context, it)) }
add { writeHtmlFile(branchDir, SoftwareSystemDeploymentPageViewModel(context, it)) }
add { writeHtmlFile(branchDir, SoftwareSystemDependenciesPageViewModel(context, it)) }
add { writeHtmlFile(branchDir, SoftwareSystemDecisionsPageViewModel(context, it)) }
add { writeHtmlFile(branchDir, SoftwareSystemSectionsPageViewModel(context, it)) }

it.documentation.decisions.forEach { decision ->
writeHtmlFile(branchDir, SoftwareSystemDecisionPageViewModel(context, it, decision))
}
it.documentation.decisions.forEach { decision ->
add { writeHtmlFile(branchDir, SoftwareSystemDecisionPageViewModel(context, it, decision)) }
}

it.documentation.sections.filter { section -> section.order != 1 }.forEach { section ->
writeHtmlFile(branchDir, SoftwareSystemSectionPageViewModel(context, it, section))
it.documentation.sections.filter { section -> section.order != 1 }.forEach { section ->
add { writeHtmlFile(branchDir, SoftwareSystemSectionPageViewModel(context, it, section)) }
}
}
}
.parallelStream()
.forEach { it.invoke() }
}

private fun writeHtmlFile(exportDir: File, viewModel: PageViewModel) {
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()
}
Loading