Skip to content

Commit

Permalink
Merge pull request #80 from jp7677/diagram-links
Browse files Browse the repository at this point in the history
Make diagram elements clickable
  • Loading branch information
dirkgroot authored Nov 11, 2022
2 parents e6adff1 + b73f937 commit b6433e1
Show file tree
Hide file tree
Showing 39 changed files with 219 additions and 48 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependencies {
implementation("net.sourceforge.plantuml:plantuml:1.2022.12")

implementation("com.vladsch.flexmark:flexmark-all:0.64.0")
implementation("org.jsoup:jsoup:1.15.3")

implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.8.0")

Expand Down
1 change: 1 addition & 0 deletions docs/example/workspace.dsl
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ workspace "Big Bank plc" "This is an example workspace to illustrate the key fea
"Owner" "Customer Services"
"Development Team" "Dev/Internet Services"
}
url https://en.wikipedia.org/wiki/Online_banking

singlePageApplication = container "Single-Page Application" "Provides all of the Internet banking functionality to customers via their web browser." "JavaScript and Angular" "Web Browser"
mobileApp = container "Mobile App" "Provides a limited subset of the Internet banking functionality to customers via their mobile device." "Xamarin" "Mobile App"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,15 @@ fun createStructurizrWorkspace(workspaceFile: File) =
.workspace
.apply {
views.configuration.addProperty(C4PlantUMLExporter.C4PLANTUML_ELEMENT_PROPERTIES_PROPERTY, true.toString())
model.elements.forEach {
moveUrlToProperty(it) // We need the URL later for our own links, preserve the original in a property
}
}
?: throw IllegalStateException("Workspace could not be parsed")

private fun moveUrlToProperty(element: Element) {
if (element.url != null) {
element.addProperty("Url", element.url)
element.url = null
}
}
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))
generateDiagrams(workspace, File(siteDir, branch), 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))
generateDiagrams(workspace, File(siteDir, defaultBranch), defaultBranch)
generateSite(
version,
workspace,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,22 @@ class ServeCommand : Subcommand("serve", "Start a development server") {

private fun updateSite() {
val workspace = createStructurizrWorkspace(File(workspaceFile))
val exportDir = File(siteDir, "master")
val branch = "master"
val exportDir = File(siteDir, branch)

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

println("Generating site...")
copySiteWideAssets(File(siteDir))
generateRedirectingIndexPage(File(siteDir), "master")
generateRedirectingIndexPage(File(siteDir), branch)
generateSite(
"0.0.0",
workspace,
assetsDir?.let { File(it) },
File(siteDir),
listOf("master"),
"master"
listOf(branch),
branch
)

println("Successfully generated diagrams and site")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package nl.avisi.structurizr.site.generatr.site

import com.structurizr.Workspace
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.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
): C4PlantUMLExporter() {
companion object {
const val TEMP_URI = "https://will-be-changed-to-relative"
}

override fun writeElement(view: View?, element: Element?, writer: IndentingWriter?) {
if (element !is SoftwareSystem || !element.linkNeeded(view))
return super.writeElement(view, element, writer)

setElementUrl(element)
writeModifiedElement(view, element, writer)
restoreElement(element)
}

private fun Element.linkNeeded(view: View?) =
workspace.model.includedSoftwareSystems.contains(this) && this != view?.softwareSystem

private fun setElementUrl(element: Element) {
element.url = "${TEMP_URI}/$branch/${element.name.normalize()}/context/"
}

private fun writeModifiedElement(
view: View?,
element: Element?,
writer: IndentingWriter?
) = IndentingWriter().let {
super.writeElement(view, element, it)
it.toString()
.replace(TEMP_URI, "")
.split(System.lineSeparator())
.forEach { line -> writer?.writeLine(line) }
}

private fun restoreElement(element: Element) {
element.url = null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,19 @@ 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 net.sourceforge.plantuml.FileFormat
import net.sourceforge.plantuml.FileFormatOption
import net.sourceforge.plantuml.SourceStringReader
import java.io.File
import java.net.URL

fun generateDiagrams(workspace: Workspace, exportDir: File) {
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() }

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

plantUMLDiagrams.parallelStream()
.forEach { diagram ->
Expand All @@ -30,8 +29,8 @@ fun generateDiagrams(workspace: Workspace, exportDir: File) {
}
}

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

return plantUMLExporter.export(workspace)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ data class GeneratorContext(
val version: String,
val workspace: Workspace,
val branches: List<String>,
val currentBranch: String
val currentBranch: String,
val svgFactory: (name: String) -> String
)
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,13 @@ fun generateSite(
branches: List<String>,
currentBranch: String
) {
val generatorContext = GeneratorContext(version, workspace, branches, currentBranch)
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"
}
}

if (assetsDir != null) copyAssets(assetsDir, exportDir)
generateHtmlFiles(generatorContext, exportDir)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import com.structurizr.view.View

data class DiagramViewModel(
val name: String,
val svg: String,
val svgLocation: ImageViewModel,
val pngLocation: ImageViewModel,
val pumlLocation: ImageViewModel
) {
companion object {
fun forView(pageViewModel: PageViewModel, view: View) = DiagramViewModel(
fun forView(pageViewModel: PageViewModel, view: View, svgFactory: (name: String) -> String) = DiagramViewModel(
view.name,
svgFactory(view.key),
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
Expand Up @@ -9,7 +9,8 @@ class HomePageViewModel(generatorContext: GeneratorContext) : PageViewModel(gene

val content = MarkdownViewModel(
markdown = generatorContext.workspace.documentation.sections
.firstOrNull { it.order == 1 }?.content ?: DEFAULT_HOMEPAGE_CONTENT
.firstOrNull { it.order == 1 }?.content ?: DEFAULT_HOMEPAGE_CONTENT,
svgFactory = generatorContext.svgFactory
)

companion object {
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)
data class MarkdownViewModel(val markdown: String, val svgFactory: (name: String) -> String)
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ class SoftwareSystemComponentPageViewModel(generatorContext: GeneratorContext, s
val diagrams = generatorContext.workspace.views.componentViews
.filter { it.softwareSystem == softwareSystem }
.sortedBy { it.key }
.map { DiagramViewModel.forView(this, it) }
.map { DiagramViewModel.forView(this, it, generatorContext.svgFactory) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ class SoftwareSystemContainerPageViewModel(generatorContext: GeneratorContext, s
val diagrams = generatorContext.workspace.views.containerViews
.filter { it.softwareSystem == softwareSystem }
.sortedBy { it.key }
.map { DiagramViewModel.forView(this, it) }
.map { DiagramViewModel.forView(this, it, generatorContext.svgFactory) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ class SoftwareSystemContextPageViewModel(generatorContext: GeneratorContext, sof
val diagrams = generatorContext.workspace.views.systemContextViews
.filter { it.softwareSystem == softwareSystem }
.sortedBy { it.key }
.map { DiagramViewModel.forView(this, it) }
.map { DiagramViewModel.forView(this, it, generatorContext.svgFactory) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class SoftwareSystemDecisionPageViewModel(
) : SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.DECISIONS) {
override val url = url(softwareSystem, decision)

val markdown = MarkdownViewModel(decision.content)
val content = MarkdownViewModel(decision.content, generatorContext.svgFactory)

companion object {
fun url(softwareSystem: SoftwareSystem, decision: Decision) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ class SoftwareSystemDeploymentPageViewModel(generatorContext: GeneratorContext,
val diagrams = generatorContext.workspace.views.deploymentViews
.filter { it.softwareSystem == softwareSystem }
.sortedBy { it.key }
.map { DiagramViewModel.forView(this, it) }
.map { DiagramViewModel.forView(this, it, generatorContext.svgFactory) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ class SoftwareSystemHomePageViewModel(generatorContext: GeneratorContext, softwa
val propertiesTable = createPropertiesTableViewModel(softwareSystem.properties)
val content = softwareSystem.documentation.sections
.minByOrNull { it.order }
?.let { MarkdownViewModel(it.content) }
?: MarkdownViewModel("# Description${System.lineSeparator()}${softwareSystem.description}")
?.let { MarkdownViewModel(it.content, generatorContext.svgFactory) }
?: MarkdownViewModel(
"# Description${System.lineSeparator()}${softwareSystem.description}",
generatorContext.svgFactory
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class SoftwareSystemSectionPageViewModel(
) : SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.DECISIONS) {
override val url = url(softwareSystem, section)

val markdown = MarkdownViewModel(section.content)
val content = MarkdownViewModel(section.content, generatorContext.svgFactory)

companion object {
fun url(softwareSystem: SoftwareSystem, section: Section) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class WorkspaceDecisionPageViewModel(generatorContext: GeneratorContext, decisio
override val url = url(decision)
override val pageSubTitle: String = decision.title

val markdown = MarkdownViewModel(decision.content)
val content = MarkdownViewModel(decision.content, generatorContext.svgFactory)

companion object {
fun url(decision: Decision) = "/decisions/${decision.id}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class WorkspaceDocumentationSectionPageViewModel(generatorContext: GeneratorCont
override val url = url(section)
override val pageSubTitle: String = section.title

val markdown = MarkdownViewModel(section.content)
val content = MarkdownViewModel(section.content, generatorContext.svgFactory)

companion object {
fun url(section: Section): String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import nl.avisi.structurizr.site.generatr.site.model.DiagramViewModel

fun FlowContent.diagram(viewModel: DiagramViewModel) {
figure {
img(src = viewModel.svgLocation.relativeHref, alt = viewModel.name)
unsafe {
+viewModel.svg
}
figcaption {
+viewModel.name
+" ["
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,18 @@ import kotlinx.html.unsafe
import nl.avisi.structurizr.site.generatr.site.asUrlRelativeTo
import nl.avisi.structurizr.site.generatr.site.model.MarkdownViewModel
import nl.avisi.structurizr.site.generatr.site.model.PageViewModel
import org.jsoup.Jsoup
import org.jsoup.nodes.Element

fun FlowContent.markdown(pageViewModel: PageViewModel, markdown: MarkdownViewModel) {
fun FlowContent.markdown(pageViewModel: PageViewModel, markdownViewModel: MarkdownViewModel) {
div {
unsafe {
+markdownToHtml(pageViewModel, markdown)
+markdownToHtml(pageViewModel, markdownViewModel)
}
}
}

private fun markdownToHtml(pageViewModel: PageViewModel, markdown: MarkdownViewModel): String {
private fun markdownToHtml(pageViewModel: PageViewModel, markdownViewModel: MarkdownViewModel): String {
val options = MutableDataSet()

options.set(Parser.EXTENSIONS, listOf(TablesExtension.create()))
Expand All @@ -34,18 +36,20 @@ private fun markdownToHtml(pageViewModel: PageViewModel, markdown: MarkdownViewM
val renderer = HtmlRenderer.builder(options)
.linkResolverFactory(CustomLinkResolver.Factory(pageViewModel))
.build()
val document = parser.parse(markdown.markdown)
val markDownDocument = parser.parse(markdownViewModel.markdown)
val html = renderer.render(markDownDocument)

return renderer.render(document)
return Jsoup.parse(html)
.apply { body().transformEmbeddedDiagramElements(markdownViewModel.svgFactory) }
.html()
}

private class CustomLinkResolver(private val pageViewModel: PageViewModel) : LinkResolver {
override fun resolveLink(node: Node, context: LinkResolverBasicContext, link: ResolvedLink): ResolvedLink {
if (link.url.startsWith("embed:")) {
val diagramId = link.url.substring(6)
return link
.withStatus(LinkStatus.VALID)
.withUrl("/svg/$diagramId.svg".asUrlRelativeTo(pageViewModel.url))
.withUrl(link.url)
}
if (link.url.matches("https?://.*".toRegex()))
return link
Expand All @@ -66,3 +70,12 @@ private class CustomLinkResolver(private val pageViewModel: PageViewModel) : Lin
override fun affectsGlobalScope() = false
}
}

private fun Element.transformEmbeddedDiagramElements(svgFactory: (name: String) -> 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.remove()
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemDecisionPageV

fun HTML.softwareSystemDecisionPage(viewModel: SoftwareSystemDecisionPageViewModel) {
softwareSystemPage(viewModel) {
markdown(viewModel, viewModel.markdown)
markdown(viewModel, viewModel.content)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemSectionPageVi

fun HTML.softwareSystemSectionPage(viewModel: SoftwareSystemSectionPageViewModel) {
softwareSystemPage(viewModel) {
markdown(viewModel, viewModel.markdown)
markdown(viewModel, viewModel.content)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import nl.avisi.structurizr.site.generatr.site.model.WorkspaceDecisionPageViewMo
fun HTML.workspaceDecisionPage(viewModel: WorkspaceDecisionPageViewModel) {
page(viewModel) {
contentDiv {
markdown(viewModel, viewModel.markdown)
markdown(viewModel, viewModel.content)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import nl.avisi.structurizr.site.generatr.site.model.WorkspaceDocumentationSecti
fun HTML.workspaceDocumentationSectionPage(viewModel: WorkspaceDocumentationSectionPageViewModel) {
page(viewModel) {
contentDiv {
markdown(viewModel, viewModel.markdown)
markdown(viewModel, viewModel.content)
}
}
}
Loading

0 comments on commit b6433e1

Please sign in to comment.