Skip to content

Commit

Permalink
Merge pull request #95 from jp7677/markdown-diagrams
Browse files Browse the repository at this point in the history
Streamline diagram presentation in markdowns
  • Loading branch information
dirkgroot authored Nov 28, 2022
2 parents 377a633 + 4467131 commit d5c7837
Show file tree
Hide file tree
Showing 31 changed files with 216 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ data class GeneratorContext(
val branches: List<String>,
val currentBranch: String,
val serving: Boolean,
val svgFactory: (key: String, url: String) -> String
val svgFactory: (key: String, url: String) -> String?
)
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ fun generateSite(
serving: Boolean = false
) {
val generatorContext = GeneratorContext(version, workspace, branches, currentBranch, serving) { key, url ->
val view = workspace.views.views.single { view -> view.key == key }
generateDiagramWithElementLinks(view, url, exportDir)
workspace.views.views.singleOrNull { view -> view.key == key }
?.let { generateDiagramWithElementLinks(it, url, exportDir) }
}

deleteOldHashes(exportDir)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,41 @@ package nl.avisi.structurizr.site.generatr.site.model
import com.structurizr.view.View

data class DiagramViewModel(
val key: String,
val name: String,
val svg: String,
val svg: String?,
val diagramWidthInPixels: Int?,
val svgLocation: ImageViewModel,
val pngLocation: ImageViewModel,
val pumlLocation: ImageViewModel
) {
companion object {
fun forView(pageViewModel: PageViewModel, view: View, svgFactory: (key: String, url: String) -> String) = DiagramViewModel(
view.name,
svgFactory(view.key, pageViewModel.url),
ImageViewModel(pageViewModel, "/svg/${view.key}.svg"),
ImageViewModel(pageViewModel, "/png/${view.key}.png"),
ImageViewModel(pageViewModel, "/puml/${view.key}.puml")
)
fun forView(pageViewModel: PageViewModel, view: View, svgFactory: (key: String, url: String) -> String?) =
forView(pageViewModel, view.key, view.name, svgFactory)

fun forView(
pageViewModel: PageViewModel,
key: String, name: String,
svgFactory: (key: String, url: String) -> String?
): DiagramViewModel {
val svg = svgFactory(key, pageViewModel.url)
return DiagramViewModel(
key,
name,
svg,
extractDiagramWidthInPixels(svg),
ImageViewModel(pageViewModel, "/svg/${key}.svg"),
ImageViewModel(pageViewModel, "/png/${key}.png"),
ImageViewModel(pageViewModel, "/puml/${key}.puml")
)
}

private fun extractDiagramWidthInPixels(svg: String?) =
if (svg != null)
"viewBox=\"\\d+ \\d+ (\\d+) \\d+\"".toRegex()
.find(svg)
?.let { it.groupValues[1].toInt() }
?: throw IllegalStateException("No viewBox attribute found in SVG!")
else null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ class HomePageViewModel(generatorContext: GeneratorContext) : PageViewModel(gene
override val pageSubTitle = "Home"
override val url = url()

val content = MarkdownViewModel(
val content = markdownToHtml(
this,
markdown = generatorContext.workspace.documentation.sections
.firstOrNull { it.order == 1 }?.content ?: DEFAULT_HOMEPAGE_CONTENT,
svgFactory = generatorContext.svgFactory
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package nl.avisi.structurizr.site.generatr.site.views
package nl.avisi.structurizr.site.generatr.site.model

import com.vladsch.flexmark.ext.tables.TablesExtension
import com.vladsch.flexmark.html.HtmlRenderer
Expand All @@ -10,24 +10,14 @@ import com.vladsch.flexmark.html.renderer.ResolvedLink
import com.vladsch.flexmark.parser.Parser
import com.vladsch.flexmark.util.ast.Node
import com.vladsch.flexmark.util.data.MutableDataSet
import kotlinx.html.FlowContent
import kotlinx.html.div
import kotlinx.html.unsafe
import kotlinx.html.stream.createHTML
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 nl.avisi.structurizr.site.generatr.site.views.diagram
import org.jsoup.Jsoup
import org.jsoup.nodes.Element

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

private fun markdownToHtml(pageViewModel: PageViewModel, markdownViewModel: MarkdownViewModel): String {
fun markdownToHtml(pageViewModel: PageViewModel, markdown: String, svgFactory: (key: String, url: String) -> String?): String {
val options = MutableDataSet()

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

return Jsoup.parse(html)
.apply { body().transformEmbeddedDiagramElements(markdownViewModel.svgFactory, pageViewModel.url) }
.apply { body().transformEmbeddedDiagramElements(pageViewModel, svgFactory) }
.body()
.html()
}

private class CustomLinkResolver(private val pageViewModel: PageViewModel) : LinkResolver {
override fun resolveLink(node: Node, context: LinkResolverBasicContext, link: ResolvedLink): ResolvedLink {
if (link.url.startsWith("embed:")) {
if (link.url.startsWith(embedPrefix)) {
return link
.withStatus(LinkStatus.VALID)
.withUrl(link.url)
Expand All @@ -72,13 +63,20 @@ private class CustomLinkResolver(private val pageViewModel: PageViewModel) : Lin
}

private fun Element.transformEmbeddedDiagramElements(
svgFactory: (key: String, url: String) -> String,
url: String
pageViewModel: PageViewModel,
svgFactory: (key: String, url: String) -> String?
) = this.allElements
.toList()
.filter { it.tag().name == "img" && it.attr("src").startsWith("embed:") }
.filter { it.tag().name == "img" && it.attr("src").startsWith(embedPrefix) }
.forEach {
val diagramId = it.attr("src").substring(6)
it.parent()?.append(svgFactory(diagramId, url))
val key = it.attr("src").substring(embedPrefix.length)
val name = it.attr("alt").ifBlank { key }
val html = createHTML().div {
diagram(DiagramViewModel.forView(pageViewModel, key, name, svgFactory))
}

it.parent()?.append(html)
it.remove()
}

private const val embedPrefix = "embed:"

This file was deleted.

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 content = MarkdownViewModel(decision.content, generatorContext.svgFactory)
val content = markdownToHtml(this, 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 @@ -7,11 +7,10 @@ class SoftwareSystemHomePageViewModel(generatorContext: GeneratorContext, softwa
SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME) {
val hasProperties = softwareSystem.properties.any()
val propertiesTable = createPropertiesTableViewModel(softwareSystem.properties)
val content = softwareSystem.documentation.sections
val content = markdownToHtml(this, softwareSystem.info(), generatorContext.svgFactory)

private fun SoftwareSystem.info() = documentation.sections
.minByOrNull { it.order }
?.let { MarkdownViewModel(it.content, generatorContext.svgFactory) }
?: MarkdownViewModel(
"# Description${System.lineSeparator()}${softwareSystem.description}",
generatorContext.svgFactory
)
?.content
?: "# Description${System.lineSeparator()}${description}"
}
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 content = MarkdownViewModel(section.content, generatorContext.svgFactory)
val content = markdownToHtml(this, 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 content = MarkdownViewModel(decision.content, generatorContext.svgFactory)
val content = markdownToHtml(this, 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 content = MarkdownViewModel(section.content, generatorContext.svgFactory)
val content = markdownToHtml(this, 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 @@ -4,26 +4,26 @@ import kotlinx.html.*
import nl.avisi.structurizr.site.generatr.site.model.DiagramViewModel

fun FlowContent.diagram(viewModel: DiagramViewModel) {
val diagramWidthInPixels = "viewBox=\"\\d+ \\d+ (\\d+) \\d+\"".toRegex()
.find(viewModel.svg)
?.let { it.groupValues[1].toInt() }
?: throw IllegalStateException("No viewBox attribute found in SVG!")
if (viewModel.svg != null)
figure {
style = "width: min(100%, ${viewModel.diagramWidthInPixels}px);"

figure {
style = "width: min(100%, ${diagramWidthInPixels}px);"

unsafe {
+viewModel.svg
rawHtml(viewModel.svg)
figcaption {
+viewModel.name
+" ["
a(href = viewModel.svgLocation.relativeHref) { +"svg" }
+"|"
a(href = viewModel.pngLocation.relativeHref) { +"png" }
+"|"
a(href = viewModel.pumlLocation.relativeHref) { +"puml" }
+"]"
}
}
figcaption {
+viewModel.name
+" ["
a(href = viewModel.svgLocation.relativeHref) { +"svg" }
+"|"
a(href = viewModel.pngLocation.relativeHref) { +"png" }
+"|"
a(href = viewModel.pumlLocation.relativeHref) { +"puml" }
+"]"
else
div(classes = "notification is-danger") {
+"No view with key"
span(classes = "has-text-weight-bold") { +" ${viewModel.key} " }
+"found!"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import nl.avisi.structurizr.site.generatr.site.model.HomePageViewModel
fun HTML.homePage(viewModel: HomePageViewModel) {
page(viewModel) {
contentDiv {
markdown(viewModel, viewModel.content)
rawHtml(viewModel.content)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package nl.avisi.structurizr.site.generatr.site.views

import kotlinx.html.FlowContent
import kotlinx.html.div
import kotlinx.html.unsafe

fun FlowContent.rawHtml(html: String) {
div {
unsafe {
+html
}
}
}
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.content)
rawHtml(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.SoftwareSystemHomePageViewM

fun HTML.softwareSystemHomePage(viewModel: SoftwareSystemHomePageViewModel) {
softwareSystemPage(viewModel) {
markdown(viewModel, viewModel.content)
rawHtml(viewModel.content)
if (viewModel.hasProperties) {
h2 { +"Properties" }
table(viewModel.propertiesTable)
Expand Down
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.content)
rawHtml(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.content)
rawHtml(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.content)
rawHtml(viewModel.content)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class HomePageViewModelTest : ViewModelTest() {
val viewModel = HomePageViewModel(generatorContext)

assertThat(viewModel.content)
.isEqualTo(MarkdownViewModel(DEFAULT_HOMEPAGE_CONTENT, svgFactory))
.isEqualTo(markdownToHtml(viewModel, DEFAULT_HOMEPAGE_CONTENT, svgFactory))
}

@Test
Expand All @@ -35,7 +35,7 @@ class HomePageViewModelTest : ViewModelTest() {
val viewModel = HomePageViewModel(generatorContext)

assertThat(viewModel.content)
.isEqualTo(MarkdownViewModel("Section content", svgFactory))
.isEqualTo(markdownToHtml(viewModel, "Section content", svgFactory))
}

@ParameterizedTest
Expand Down
Loading

0 comments on commit d5c7837

Please sign in to comment.