From a800a492f012d811ec14aa94e2ba7cb97dc67d20 Mon Sep 17 00:00:00 2001 From: LunarN0va Date: Wed, 10 Apr 2024 15:56:26 +0200 Subject: [PATCH] Add customizable CDN link --- README.md | 27 ++++--- docs/example/workspace.dsl | 1 + .../site/generatr/site/model/PageViewModel.kt | 2 + .../site/generatr/site/views/CDN.kt | 80 ++++++++++--------- .../generatr/site/views/MarkdownExtension.kt | 14 ++-- .../site/generatr/site/views/Page.kt | 10 +-- .../site/generatr/site/views/SearchPage.kt | 6 +- .../site/generatr/site/views/CDNTest.kt | 23 +++--- 8 files changed, 87 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index e739dbd5..89826bb8 100644 --- a/README.md +++ b/README.md @@ -256,19 +256,20 @@ and limitations for this exporter. The look and feel of the generated site can be customized with several additional view properties in the C4 architecture model: -| Property name | Description | Default | Example | -|-----------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------|------------------------------------------------------| -| `generatr.style.colors.primary` | Primary site color, used for header bar background and active menu background. | `#333333` | `#485fc7` | -| `generatr.style.colors.secondary` | Secondary site color, used for font color in header bar and for active menu. | `#cccccc` | `#ffffff` | -| `generatr.style.faviconPath` | Site logo location relative to the configured `assets` folder. When configured, the logo image will be place on the left side in the header bar. This requires the `--assets-dir` switch when generating the site and the corresponding file to be available in the `assets` folder. | | `site/favicon.ico` | -| `generatr.style.logoPath` | Site favicon location relative to the configured `assets` folder. When configured, the favicon will be set for all generated pages. This requires the `--assets-dir` switch when generating the site and the corresponding file to be available in the `assets` folder. | | `site/logo.png` | -| `generatr.style.customStylesheet` | URL to hosted custom stylesheet or path to custom stylesheet file (location relative to the configured `assets` folder). When configured this css file will be loaded for all pages. When using a path to a file the `--assets-dir` switch must be used when generating the site and the corresponding file is available in the `assets` folder. | | `site/custom.css` or 'https://uri.example/custom.css | -| `generatr.search.language` | Indexing/stemming language for the search index. See [Lunr language support](https://github.com/olivernn/lunr-languages) | `en` | `nl` | -| `generatr.markdown.flexmark.extensions` | Additional extensions to the markdown generator to add new markdown capabilities. [More Details](https://avisi-cloud.github.io/structurizr-site-generatr/main/extended-markdown-features/) | Tables | `Tables,Admonition` | -| `generatr.svglink.target` | Specifies the link target for element links in the exported svg | `_top` | `_self` | -| `generatr.site.exporter` | Specifies the UML exporter, can be `c4` (uses the `C4PlantUMLExporter`) or `structurizr` (uses the `StructurizrPlantUMLExporter`) | `c4` | `structurizr` | -| `generatr.site.externalTag` | Software systems containing this tag will be considered external | | | -| `generatr.site.nestGroups` | Will show software systems in the left side navigator in collapsable groups | `false` | `true` | +| Property name | Description | Default | Example | +|-----------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------|------------------------------------------------------| +| `generatr.style.colors.primary` | Primary site color, used for header bar background and active menu background. | `#333333` | `#485fc7` | +| `generatr.style.colors.secondary` | Secondary site color, used for font color in header bar and for active menu. | `#cccccc` | `#ffffff` | +| `generatr.style.faviconPath` | Site logo location relative to the configured `assets` folder. When configured, the logo image will be place on the left side in the header bar. This requires the `--assets-dir` switch when generating the site and the corresponding file to be available in the `assets` folder. | | `site/favicon.ico` | +| `generatr.style.logoPath` | Site favicon location relative to the configured `assets` folder. When configured, the favicon will be set for all generated pages. This requires the `--assets-dir` switch when generating the site and the corresponding file to be available in the `assets` folder. | | `site/logo.png` | +| `generatr.style.customStylesheet` | URL to hosted custom stylesheet or path to custom stylesheet file (location relative to the configured `assets` folder). When configured this css file will be loaded for all pages. When using a path to a file the `--assets-dir` switch must be used when generating the site and the corresponding file is available in the `assets` folder. | | `site/custom.css` or 'https://uri.example/custom.css | +| `generatr.search.language` | Indexing/stemming language for the search index. See [Lunr language support](https://github.com/olivernn/lunr-languages) | `en` | `nl` | +| `generatr.markdown.flexmark.extensions` | Additional extensions to the markdown generator to add new markdown capabilities. [More Details](https://avisi-cloud.github.io/structurizr-site-generatr/main/extended-markdown-features/) | Tables | `Tables,Admonition` | +| `generatr.svglink.target` | Specifies the link target for element links in the exported svg | `_top` | `_self` | +| `generatr.site.exporter` | Specifies the UML exporter, can be `c4` (uses the `C4PlantUMLExporter`) or `structurizr` (uses the `StructurizrPlantUMLExporter`) | `c4` | `structurizr` | +| `generatr.site.externalTag` | Software systems containing this tag will be considered external | | | +| `generatr.site.nestGroups` | Will show software systems in the left side navigator in collapsable groups | `false` | `true` | +| `generatr.site.cdn` | Specifies the CDN base location for fetching NPM packages for browser runtime dependencies. Defaults to jsDelivr, but can be changed to e.g. an on-premise location. | `https://cdn.jsdelivr.net/npm` | `https://cdn.my-company/npm` | See the included example for usage of some those properties in the [C4 architecture model example](https://github.com/avisi-cloud/structurizr-site-generatr/blob/main/docs/example/workspace.dsl#L163). diff --git a/docs/example/workspace.dsl b/docs/example/workspace.dsl index 9859835f..ce0c94d6 100644 --- a/docs/example/workspace.dsl +++ b/docs/example/workspace.dsl @@ -185,6 +185,7 @@ workspace "Big Bank plc" "This is an example workspace to illustrate the key fea "generatr.site.exporter" "c4" "generatr.site.externalTag" "External System" "generatr.site.nestGroups" "false" + "generatr.site.cdn" "https://cdn.jsdelivr.net/npm" } systemlandscape "SystemLandscape" { diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/PageViewModel.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/PageViewModel.kt index f591720f..9d79b8af 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/PageViewModel.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/PageViewModel.kt @@ -2,6 +2,7 @@ package nl.avisi.structurizr.site.generatr.site.model import nl.avisi.structurizr.site.generatr.includedSoftwareSystems import nl.avisi.structurizr.site.generatr.site.GeneratorContext +import nl.avisi.structurizr.site.generatr.site.views.CDN abstract class PageViewModel(protected val generatorContext: GeneratorContext) { val pageTitle: String by lazy { @@ -12,6 +13,7 @@ abstract class PageViewModel(protected val generatorContext: GeneratorContext) { else pageSubTitle } + val cdn by lazy { CDN(generatorContext.workspace) } val favicon by lazy { FaviconViewModel(generatorContext, this) } val customStylesheet by lazy { CustomStylesheetViewModel(generatorContext, this) } val headerBar by lazy { HeaderBarViewModel(this, generatorContext) } diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/CDN.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/CDN.kt index 441a8f3f..4df63559 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/CDN.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/CDN.kt @@ -1,63 +1,65 @@ package nl.avisi.structurizr.site.generatr.site.views +import com.structurizr.Workspace import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import java.lang.IllegalStateException -class CDN { - companion object { - private const val CDN_BASE = "https://cdn.jsdelivr.net/npm/" - private val json = Json { ignoreUnknownKeys = true } +class CDN(val workspace: Workspace) { + private val cdnBaseURL = workspace.views.configuration.properties.getOrDefault("generatr.site.cdn", "https://cdn.jsdelivr.net/npm") + private val json = Json { ignoreUnknownKeys = true } - private val packageJson = object {}.javaClass.getResource("/package.json")?.readText() - ?: throw IllegalStateException("package.json not found") + private val packageJson = object {}.javaClass.getResource("/package.json")?.readText() + ?: throw IllegalStateException("package.json not found") - private val dependencies = json.parseToJsonElement(packageJson).jsonObject["dependencies"] - ?.jsonObject - ?.map { Dependency(it.key, it.value.jsonPrimitive.content) } - ?: throw IllegalStateException("dependencies element not found in package.json") + private val dependencies = json.parseToJsonElement(packageJson).jsonObject["dependencies"] + ?.jsonObject + ?.map { Dependency(cdnBaseURL, it.key, it.value.jsonPrimitive.content) } + ?: throw IllegalStateException("dependencies element not found in package.json") - fun bulmaCss() = dependencies.single { it.name == "bulma" }.let { - "${it.baseUrl()}/css/bulma.min.css" - } + fun bulmaCss() = dependencies.single { it.name == "bulma" }.let { + "${it.baseUrl()}/css/bulma.min.css" + } - fun katexJs() = dependencies.single { it.name == "katex" }.let { - "${it.baseUrl()}/dist/katex.min.js" - } + fun katexJs() = dependencies.single { it.name == "katex" }.let { + "${it.baseUrl()}/dist/katex.min.js" + } - fun katexCss() = dependencies.single { it.name == "katex" }.let { - "${it.baseUrl()}/dist/katex.min.css" - } + fun katexCss() = dependencies.single { it.name == "katex" }.let { + "${it.baseUrl()}/dist/katex.min.css" + } - fun lunrJs() = dependencies.single { it.name == "lunr" }.let { - "${it.baseUrl()}/lunr.min.js" - } + fun lunrJs() = dependencies.single { it.name == "lunr" }.let { + "${it.baseUrl()}/lunr.min.js" + } - fun lunrLanguagesStemmerJs() = dependencies.single { it.name == "lunr-languages" }.let { - "${it.baseUrl()}/min/lunr.stemmer.support.min.js" - } + fun lunrLanguagesStemmerJs() = dependencies.single { it.name == "lunr-languages" }.let { + "${it.baseUrl()}/min/lunr.stemmer.support.min.js" + } - fun lunrLanguagesJs(language: String) = dependencies.single { it.name == "lunr-languages" }.let { - "${it.baseUrl()}/min/lunr.$language.min.js" - } + fun lunrLanguagesJs(language: String) = dependencies.single { it.name == "lunr-languages" }.let { + "${it.baseUrl()}/min/lunr.$language.min.js" + } - fun mermaidJs() = dependencies.single { it.name == "mermaid" }.let { - "${it.baseUrl()}/dist/mermaid.esm.min.mjs" - } + fun mermaidJs() = dependencies.single { it.name == "mermaid" }.let { + "${it.baseUrl()}/dist/mermaid.esm.min.mjs" + } - fun svgpanzoomJs() = dependencies.single { it.name == "svg-pan-zoom" }.let { - "${it.baseUrl()}/dist/svg-pan-zoom.min.js" - } + fun svgpanzoomJs() = dependencies.single { it.name == "svg-pan-zoom" }.let { + "${it.baseUrl()}/dist/svg-pan-zoom.min.js" + } - fun webfontloaderJs() = dependencies.single { it.name == "webfontloader" }.let { - "${it.baseUrl()}/webfontloader.js" - } + fun webfontloaderJs() = dependencies.single { it.name == "webfontloader" }.let { + "${it.baseUrl()}/webfontloader.js" } @Serializable - data class Dependency(val name: String, val version: String) { - fun baseUrl() = "$CDN_BASE$name@$version" + data class Dependency(val cdnBaseURL: String, val name: String, val version: String) { + fun baseUrl() = when (cdnBaseURL.last().toString()) { + "/" -> "$cdnBaseURL$name@$version" + else -> "$cdnBaseURL/$name@$version" + } } } diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/MarkdownExtension.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/MarkdownExtension.kt index 60dee7d8..8122d11e 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/MarkdownExtension.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/MarkdownExtension.kt @@ -18,25 +18,25 @@ fun BODY.markdownAdmonitionScript(viewModel: PageViewModel) { ) { } } -fun HEAD.katexStylesheet() { +fun HEAD.katexStylesheet(cdn: CDN) { // loading KaTeX as global on a webpage: https://katex.org/docs/browser.html#loading-as-global unsafe { raw(""" - + """) } } -fun HEAD.katexScript() { +fun HEAD.katexScript(cdn: CDN) { // loading KaTeX as global on a webpage: https://katex.org/docs/browser.html#loading-as-global unsafe { raw(""" - + """) } } -fun HEAD.katexFonts() { +fun HEAD.katexFonts(cdn: CDN) { // loading KaTeX as global on a webpage: https://katex.org/docs/browser.html#loading-as-global unsafe { raw(""" @@ -50,7 +50,7 @@ fun HEAD.katexFonts() { }, }; - + """) } } @@ -72,7 +72,7 @@ fun BODY.mermaidScript(viewModel: PageViewModel) { // Simple full example, how to include Mermaid: https://mermaid.js.org/config/usage.html#simple-full-example script(type = "module") { unsafe { - raw("import mermaid from '${CDN.mermaidJs()}';") + raw("import mermaid from '${viewModel.cdn.mermaidJs()}';") } } } diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Page.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Page.kt index 726eef8a..6ade394d 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Page.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Page.kt @@ -17,12 +17,12 @@ private fun HTML.headFragment(viewModel: PageViewModel) { meta(charset = "utf-8") meta(name = "viewport", content = "width=device-width, initial-scale=1") title { +viewModel.pageTitle } - link(rel = "stylesheet", href = CDN.bulmaCss()) + link(rel = "stylesheet", href = viewModel.cdn.bulmaCss()) link(rel = "stylesheet", href = "../" + "/style.css".asUrlToFile(viewModel.url)) link(rel = "stylesheet", href = "./" + "/style-branding.css".asUrlToFile(viewModel.url)) script(type = ScriptType.textJavaScript, src = "../" + "/modal.js".asUrlToFile(viewModel.url)) { } script(type = ScriptType.textJavaScript, src = "../" + "/svg-modal.js".asUrlToFile(viewModel.url)) { } - script(type = ScriptType.textJavaScript, src = CDN.svgpanzoomJs()) { } + script(type = ScriptType.textJavaScript, src = viewModel.cdn.svgpanzoomJs()) { } if (viewModel.includeTreeview) link(rel = "stylesheet", href = "../" + "/treeview.css".asUrlToFile(viewModel.url)) @@ -31,11 +31,11 @@ private fun HTML.headFragment(viewModel: PageViewModel) { markdownAdmonitionStylesheet(viewModel) if (viewModel.includeKatex) - katexStylesheet() + katexStylesheet(viewModel.cdn) if (viewModel.includeKatex) - katexScript() + katexScript(viewModel.cdn) if (viewModel.includeKatex) - katexFonts() + katexFonts(viewModel.cdn) if (viewModel.favicon.includeFavicon) favicon(viewModel.favicon) diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SearchPage.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SearchPage.kt index 7d17c7f4..43f6a97f 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SearchPage.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SearchPage.kt @@ -18,16 +18,16 @@ fun HTML.searchPage(viewModel: SearchViewModel) { h2 { +viewModel.pageSubTitle } script( type = ScriptType.textJavaScript, - src = CDN.lunrJs() + src = viewModel.cdn.lunrJs() ) { } script( type = ScriptType.textJavaScript, - src = CDN.lunrLanguagesStemmerJs() + src = viewModel.cdn.lunrLanguagesStemmerJs() ) { } if (language.isNotBlank()) script( type = ScriptType.textJavaScript, - src = CDN.lunrLanguagesJs(language) + src = viewModel.cdn.lunrLanguagesJs(language) ) { } script(type = ScriptType.textJavaScript) { diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/views/CDNTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/views/CDNTest.kt index 3cad299c..67f158ca 100644 --- a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/views/CDNTest.kt +++ b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/views/CDNTest.kt @@ -5,21 +5,26 @@ import assertk.assertThat import assertk.assertions.doesNotContain import assertk.assertions.endsWith import assertk.assertions.startsWith +import com.structurizr.Workspace import org.junit.jupiter.api.DynamicTest import org.junit.jupiter.api.TestFactory class CDNTest { + + private val workspace = Workspace("workspace name", "") + private val cdn = CDN(workspace) + @TestFactory fun `cdn locations`() = listOf( - CDN.bulmaCss() to "/css/bulma.min.css", - CDN.katexJs() to "/dist/katex.min.js", - CDN.katexCss() to "/dist/katex.min.css", - CDN.lunrJs() to "/lunr.min.js", - CDN.lunrLanguagesStemmerJs() to "/min/lunr.stemmer.support.min.js", - CDN.lunrLanguagesJs("en") to "/min/lunr.en.min.js", - CDN.mermaidJs() to "/dist/mermaid.esm.min.mjs", - CDN.svgpanzoomJs() to "/dist/svg-pan-zoom.min.js", - CDN.webfontloaderJs() to "/webfontloader.js" + cdn.bulmaCss() to "/css/bulma.min.css", + cdn.katexJs() to "/dist/katex.min.js", + cdn.katexCss() to "/dist/katex.min.css", + cdn.lunrJs() to "/lunr.min.js", + cdn.lunrLanguagesStemmerJs() to "/min/lunr.stemmer.support.min.js", + cdn.lunrLanguagesJs("en") to "/min/lunr.en.min.js", + cdn.mermaidJs() to "/dist/mermaid.esm.min.mjs", + cdn.svgpanzoomJs() to "/dist/svg-pan-zoom.min.js", + cdn.webfontloaderJs() to "/webfontloader.js" ).map { (url, suffix) -> DynamicTest.dynamicTest(url) { assertThat(url).all {