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 diagram elements clickable #80

Merged
merged 8 commits into from
Nov 11, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
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