Showing preview only (646K chars total). Download the full file or copy to clipboard to get everything.
Repository: avisi-cloud/structurizr-site-generatr
Branch: main
Commit: da9aed1b5b79
Files: 272
Total size: 559.3 KB
Directory structure:
gitextract_0itw0606/
├── .dockerignore
├── .editorconfig
├── .gitattributes
├── .github/
│ └── workflows/
│ ├── pr.yml
│ └── release.yml
├── .gitignore
├── .markdownlint.json
├── .prettierrc
├── .run/
│ ├── all unit tests.run.xml
│ ├── generate site for example model (from git repo all branches) .run.xml
│ ├── generate site for example model (from git repo).run.xml
│ ├── generate site for example model (local).run.xml
│ └── serve example model.run.xml
├── .tool-versions
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── build.gradle.kts
├── cosign.pub
├── docs/
│ └── example/
│ ├── .adr-dir
│ ├── assets/
│ │ └── site/
│ │ └── custom.css
│ ├── internet-banking-system/
│ │ ├── adr/
│ │ │ └── 0001-record-architecture-decisions.md
│ │ ├── api-application/
│ │ │ ├── email-component/
│ │ │ │ ├── adr/
│ │ │ │ │ ├── 0001-record-architecture-decisions.md
│ │ │ │ │ ├── 0002-implement-feature-1.md
│ │ │ │ │ └── 0003-another-realisation-of-feature-1.md
│ │ │ │ └── docs/
│ │ │ │ └── 0001-inner-workings.md
│ │ │ └── mainframe-banking-system-facade/
│ │ │ ├── adr/
│ │ │ │ └── 0001-record-architecture-decisions.md
│ │ │ └── docs/
│ │ │ ├── 0000-introduction.md
│ │ │ └── 0001-inner-workings.md
│ │ ├── database/
│ │ │ ├── adr/
│ │ │ │ └── 0004-using-oracle-database-schema.md
│ │ │ └── docs/
│ │ │ └── 0002-guide.md
│ │ └── docs/
│ │ ├── 0000-introduction.md
│ │ ├── 0001-history.md
│ │ └── 0002-guide.md
│ ├── workspace-adrs/
│ │ ├── 0001-record-architecture-decisions.md
│ │ ├── 0002-implement-feature-1.md
│ │ ├── 0003-another-realisation-of-feature-1.md
│ │ └── 0004-using-oracle-database-schema.md
│ ├── workspace-docs/
│ │ ├── 00-index.md
│ │ ├── 01-embedding-diagrams-and-images.md
│ │ ├── 02-markdown-features.md
│ │ └── 03-asciidoc-features.adoc
│ └── workspace.dsl
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── package.json
├── renovate.json
├── settings.gradle.kts
└── src/
├── main/
│ ├── kotlin/
│ │ └── nl/
│ │ └── avisi/
│ │ └── structurizr/
│ │ └── site/
│ │ └── generatr/
│ │ ├── App.kt
│ │ ├── ClonedRepository.kt
│ │ ├── CreateStructurizrWorkspace.kt
│ │ ├── GenerateSiteCommand.kt
│ │ ├── ServeCommand.kt
│ │ ├── StringUtilities.kt
│ │ ├── StructurizrUtilities.kt
│ │ ├── VersionCommand.kt
│ │ └── site/
│ │ ├── DateFormatter.kt
│ │ ├── DiagramGenerator.kt
│ │ ├── GeneratorContext.kt
│ │ ├── PlantUmlExporter.kt
│ │ ├── RelativeUrl.kt
│ │ ├── SiteGenerator.kt
│ │ ├── model/
│ │ │ ├── Asciidoctor.kt
│ │ │ ├── BranchHomeLinkViewModel.kt
│ │ │ ├── ComponentTabViewModel.kt
│ │ │ ├── ComponentsTabViewModel.kt
│ │ │ ├── ContainerTabViewModel.kt
│ │ │ ├── ContainersCodeTabViewModel.kt
│ │ │ ├── ContainersComponentTabViewModel.kt
│ │ │ ├── ContentText.kt
│ │ │ ├── ContentTitle.kt
│ │ │ ├── CustomStylesheetViewModel.kt
│ │ │ ├── DecisionTabViewModel.kt
│ │ │ ├── DecisionsTableViewModel.kt
│ │ │ ├── DiagramIndexViewModel.kt
│ │ │ ├── DiagramViewModel.kt
│ │ │ ├── ExternalLinkViewModel.kt
│ │ │ ├── FaviconViewModel.kt
│ │ │ ├── FlexmarkConfig.kt
│ │ │ ├── HeaderBarViewModel.kt
│ │ │ ├── HomePageViewModel.kt
│ │ │ ├── ImageViewModel.kt
│ │ │ ├── ImageViewViewModel.kt
│ │ │ ├── LinkViewModel.kt
│ │ │ ├── MenuNodeViewModel.kt
│ │ │ ├── MenuViewModel.kt
│ │ │ ├── PageViewModel.kt
│ │ │ ├── PropertiesTableViewModel.kt
│ │ │ ├── SearchViewModel.kt
│ │ │ ├── SectionTabViewModel.kt
│ │ │ ├── SectionsTableViewModel.kt
│ │ │ ├── SoftwareSystemContainerComponentCodePageViewModel.kt
│ │ │ ├── SoftwareSystemContainerComponentDecisionPageViewModel.kt
│ │ │ ├── SoftwareSystemContainerComponentDecisionsPageViewModel.kt
│ │ │ ├── SoftwareSystemContainerComponentSectionPageViewModel.kt
│ │ │ ├── SoftwareSystemContainerComponentSectionsPageViewModel.kt
│ │ │ ├── SoftwareSystemContainerComponentsPageViewModel.kt
│ │ │ ├── SoftwareSystemContainerDecisionPageViewModel.kt
│ │ │ ├── SoftwareSystemContainerDecisionsPageViewModel.kt
│ │ │ ├── SoftwareSystemContainerPageViewModel.kt
│ │ │ ├── SoftwareSystemContainerSectionPageViewModel.kt
│ │ │ ├── SoftwareSystemContainerSectionsPageViewModel.kt
│ │ │ ├── SoftwareSystemContextPageViewModel.kt
│ │ │ ├── SoftwareSystemDecisionPageViewModel.kt
│ │ │ ├── SoftwareSystemDecisionsPageViewModel.kt
│ │ │ ├── SoftwareSystemDependenciesPageViewModel.kt
│ │ │ ├── SoftwareSystemDeploymentPageViewModel.kt
│ │ │ ├── SoftwareSystemDynamicPageViewModel.kt
│ │ │ ├── SoftwareSystemHomePageViewModel.kt
│ │ │ ├── SoftwareSystemPageViewModel.kt
│ │ │ ├── SoftwareSystemSectionPageViewModel.kt
│ │ │ ├── SoftwareSystemSectionsPageViewModel.kt
│ │ │ ├── SoftwareSystemTableUtilities.kt
│ │ │ ├── SoftwareSystemsPageViewModel.kt
│ │ │ ├── TableViewModel.kt
│ │ │ ├── Theme.kt
│ │ │ ├── ToHtml.kt
│ │ │ ├── WorkspaceDecisionPageViewModel.kt
│ │ │ ├── WorkspaceDecisionsPageViewModel.kt
│ │ │ ├── WorkspaceDocumentationSectionPageViewModel.kt
│ │ │ └── indexing/
│ │ │ ├── Document.kt
│ │ │ ├── Home.kt
│ │ │ ├── SoftwareSystemComponents.kt
│ │ │ ├── SoftwareSystemContainerDecisions.kt
│ │ │ ├── SoftwareSystemContainerSections.kt
│ │ │ ├── SoftwareSystemContainers.kt
│ │ │ ├── SoftwareSystemContext.kt
│ │ │ ├── SoftwareSystemDecisions.kt
│ │ │ ├── SoftwareSystemHome.kt
│ │ │ ├── SoftwareSystemRelationships.kt
│ │ │ ├── SoftwareSystemSections.kt
│ │ │ ├── WorkspaceDecisions.kt
│ │ │ └── WorkspaceSections.kt
│ │ └── views/
│ │ ├── AutoReloading.kt
│ │ ├── CDN.kt
│ │ ├── ContentDiv.kt
│ │ ├── Diagram.kt
│ │ ├── DiagramIndex.kt
│ │ ├── ExternalLink.kt
│ │ ├── Favicon.kt
│ │ ├── HomePage.kt
│ │ ├── Image.kt
│ │ ├── Link.kt
│ │ ├── MarkdownExtension.kt
│ │ ├── Menu.kt
│ │ ├── Modal.kt
│ │ ├── Page.kt
│ │ ├── PageHeader.kt
│ │ ├── RawHtml.kt
│ │ ├── RedirectRelative.kt
│ │ ├── RedirectUpPage.kt
│ │ ├── SearchPage.kt
│ │ ├── SoftwareSystemContainerComponentCodePage.kt
│ │ ├── SoftwareSystemContainerComponentDecisionPage.kt
│ │ ├── SoftwareSystemContainerComponentDecisionsPage.kt
│ │ ├── SoftwareSystemContainerComponentSectionPage.kt
│ │ ├── SoftwareSystemContainerComponentSectionsPage.kt
│ │ ├── SoftwareSystemContainerComponentsPage.kt
│ │ ├── SoftwareSystemContainerDecisionPage.kt
│ │ ├── SoftwareSystemContainerDecisionsPage.kt
│ │ ├── SoftwareSystemContainerPage.kt
│ │ ├── SoftwareSystemContainerSectionPage.kt
│ │ ├── SoftwareSystemContainerSectionsPage.kt
│ │ ├── SoftwareSystemContextPage.kt
│ │ ├── SoftwareSystemDecisionPage.kt
│ │ ├── SoftwareSystemDecisionsPage.kt
│ │ ├── SoftwareSystemDependenciesPage.kt
│ │ ├── SoftwareSystemDeploymentPage.kt
│ │ ├── SoftwareSystemDynamicPage.kt
│ │ ├── SoftwareSystemHomePage.kt
│ │ ├── SoftwareSystemPage.kt
│ │ ├── SoftwareSystemSectionPage.kt
│ │ ├── SoftwareSystemSectionsPage.kt
│ │ ├── SoftwareSystemsPage.kt
│ │ ├── Table.kt
│ │ ├── WorkspaceDecisionPage.kt
│ │ ├── WorkspaceDecisionsPage.kt
│ │ └── WorkspaceDocumentationSectionPage.kt
│ └── resources/
│ └── assets/
│ ├── css/
│ │ ├── admonition.css
│ │ ├── style.css
│ │ └── treeview.css
│ └── js/
│ ├── admonition.js
│ ├── auto-reload.js
│ ├── header.js
│ ├── katex-render.js
│ ├── modal.js
│ ├── reformat-mermaid.js
│ ├── search.js
│ ├── svg-modal.js
│ ├── toggle-theme.js
│ └── treeview.js
└── test/
└── kotlin/
└── nl/
└── avisi/
└── structurizr/
└── site/
└── generatr/
└── site/
├── BranchComparatorTest.kt
├── PlantUmlExporterTest.kt
├── StringUtilitiesTest.kt
├── StructurizrUtilitiesTest.kt
├── e2e/
│ ├── E2ETestFixture.kt
│ ├── PageTestHelper.kt
│ ├── RetryExtension.kt
│ ├── SearchPageTest.kt
│ ├── SoftwareSystemDependenciesPageTest.kt
│ ├── SoftwareSystemHomePageTest.kt
│ ├── SoftwareSystemsPageTest.kt
│ ├── WorkspaceHomePageTest.kt
│ ├── decisions/
│ │ ├── DecisionPageTestHelper.kt
│ │ ├── DecisionsPageTestHelper.kt
│ │ ├── SoftwareSystemContainerComponentDecisionPageTest.kt
│ │ ├── SoftwareSystemContainerComponentDecisionsPageTest.kt
│ │ ├── SoftwareSystemContainerDecisionPage.kt
│ │ ├── SoftwareSystemContainerDecisionsPageTest.kt
│ │ ├── SoftwareSystemDecisionPageTest.kt
│ │ ├── SoftwareSystemDecisionsPageTest.kt
│ │ ├── WorkspaceDecisionPageTest.kt
│ │ └── WorkspaceDecisionsPageTest.kt
│ └── sections/
│ ├── SectionPageTestHelper.kt
│ ├── SectionsPageTestHelper.kt
│ ├── SoftwareSystemContainerComponentSectionPageTest.kt
│ ├── SoftwareSystemContainerComponentSectionsPageTest.kt
│ ├── SoftwareSystemContainerSectionPageTest.kt
│ ├── SoftwareSystemContainerSectionsPageTest.kt
│ ├── SoftwareSystemSectionPageTest.kt
│ └── SoftwareSystemSectionsPageTest.kt
├── model/
│ ├── AsciidocToHtmlTest.kt
│ ├── BranchHomeLinkViewModelTest.kt
│ ├── ContentTextTest.kt
│ ├── ContentTitleTest.kt
│ ├── CustomStylesheetViewModelTest.kt
│ ├── DecisionTabViewModelTest.kt
│ ├── DecisionsTableViewModelTest.kt
│ ├── DiagramIndexViewModelTest.kt
│ ├── FaviconViewModelTest.kt
│ ├── HeaderBarViewModelTest.kt
│ ├── HomePageViewModelTest.kt
│ ├── ImageViewModelTest.kt
│ ├── ImageViewViewModelTest.kt
│ ├── IndexingTest.kt
│ ├── LinkViewModelTest.kt
│ ├── MarkdownToHtmlTest.kt
│ ├── MenuViewModelTest.kt
│ ├── PageViewModelTest.kt
│ ├── PropertiesTableViewModelTest.kt
│ ├── SearchViewModelTest.kt
│ ├── SoftwareSystemContainerComponentCodePageViewModelTest.kt
│ ├── SoftwareSystemContainerComponentDecisionPageViewModelTest.kt
│ ├── SoftwareSystemContainerComponentDecisionsPageViewModelTest.kt
│ ├── SoftwareSystemContainerComponentSectionPageViewModelTest.kt
│ ├── SoftwareSystemContainerComponentSectionsPageViewModelTest.kt
│ ├── SoftwareSystemContainerComponentsPageViewModelTest.kt
│ ├── SoftwareSystemContainerDecisionPageViewModelTest.kt
│ ├── SoftwareSystemContainerDecisionsPageViewModelTest.kt
│ ├── SoftwareSystemContainerPageViewModelTest.kt
│ ├── SoftwareSystemContainerSectionPageViewModelTest.kt
│ ├── SoftwareSystemContainerSectionsPageViewModelTest.kt
│ ├── SoftwareSystemContextPageViewModelTest.kt
│ ├── SoftwareSystemDecisionPageViewModelTest.kt
│ ├── SoftwareSystemDecisionsPageViewModelTest.kt
│ ├── SoftwareSystemDependenciesPageViewModelTest.kt
│ ├── SoftwareSystemDeploymentPageViewModelTest.kt
│ ├── SoftwareSystemDynamicPageViewModelTest.kt
│ ├── SoftwareSystemHomePageViewModelTest.kt
│ ├── SoftwareSystemPageViewModelTest.kt
│ ├── SoftwareSystemSectionPageViewModelTest.kt
│ ├── SoftwareSystemSectionsPageViewModelTest.kt
│ ├── SoftwareSystemsPageViewModelTest.kt
│ ├── TableViewModelTest.kt
│ ├── ViewModelTest.kt
│ ├── WorkspaceDecisionPageViewModelTest.kt
│ ├── WorkspaceDecisionsPageViewModelTest.kt
│ └── WorkspaceDocumentationSectionPageViewModelTest.kt
└── views/
└── CDNTest.kt
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
*
!build/install
================================================
FILE: .editorconfig
================================================
# EditorConfig is awesome:
# - https://EditorConfig.org
# - https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties
# - https://blog.logrocket.com/using-prettier-eslint-automate-formatting-fixing-javascript/
# - https://prettier.io/docs/en/options.html
# top-most EditorConfig file
root = true
[*]
charset = utf-8
# end_of_line = lf | crlf
# indent_size = 2
# indent_style = tab
insert_final_newline = true
# supported in only some editors; instead use prettier.printWidth
max_line_length = 120
trim_trailing_whitespace = true
[*.{markdown,md,mdx}]
indent_style = space
trim_trailing_whitespace = false
[*.{json,yaml,yml,toml}]
# indent_size = 2
indent_style = space
[Dockerfile]
# indent_size = 4
indent_style = space
[*.{kt,kts}]
max_line_length = 160
ij_kotlin_allow_trailing_comma_on_call_site = false
ij_kotlin_allow_trailing_comma = false
ktlint_standard_no-wildcard-imports = disabled
ktlint_standard_argument-list-wrapping = disabled
ktlint_standard_if-else-wrapping = disabled
ktlint_standard_if-else-bracing = disabled
ktlint_standard_multiline-if-else = disabled
ktlint_standard_function-signature = disabled
ktlint_standard_blank-line-before-declaration = disabled
ktlint_standard_multiline-expression-wrapping = disabled
ktlint_standard_string-template-indent = disabled
================================================
FILE: .gitattributes
================================================
#
# https://help.github.com/articles/dealing-with-line-endings/
#
# These are explicitly windows files and should use crlf
*.bat text eol=crlf
================================================
FILE: .github/workflows/pr.yml
================================================
name: pr
on:
pull_request:
branches:
- main
push:
branches:
- main
env:
IMAGE_NAME: ${{ github.event.repository.name }}
VERSION: '0.0.1'
jobs:
build-gradle:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
- name: Setup Gradle
uses: gradle/gradle-build-action@v3.5.0
- name: Ensure browsers are installed
run: ./gradlew playwright -Pargs="install --with-deps"
- name: Execute Gradle build
run: ./gradlew test installDist
- name: Archive production artifacts
uses: actions/upload-artifact@v7
with:
name: binaries
path: build/install
retention-days: 1
build-docker:
runs-on: ubuntu-latest
needs: build-gradle
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v4
- name: Download binaries
uses: actions/download-artifact@v8
with:
name: binaries
path: build/install
- name: Build container image
uses: docker/build-push-action@v7
with:
push: false
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64/v8
tags: |
ghcr.io/avisi-cloud/${{ env.IMAGE_NAME }}:${{ env.VERSION }}
ghcr.io/avisi-cloud/${{ env.IMAGE_NAME }}:latest
labels: |
org.opencontainers.image.title=${{ github.event.repository.name }}
org.opencontainers.image.description=${{ github.event.repository.description }}
org.opencontainers.image.url=${{ github.event.repository.html_url }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.version=${{ env.VERSION }}
================================================
FILE: .github/workflows/release.yml
================================================
name: release
on:
push:
tags:
- '*'
env:
IMAGE_NAME: ${{ github.event.repository.name }}
jobs:
build-gradle:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
- name: Setup Gradle
uses: gradle/gradle-build-action@v3.5.0
- name: Ensure browsers are installed
run: ./gradlew playwright -Pargs="install --with-deps"
- name: Execute Gradle build
run: ./gradlew -PprojectVersion=${{ github.ref_name }} test assemble installDist
- name: Archive binaries
uses: actions/upload-artifact@v7
with:
name: binaries
path: build/install
retention-days: 1
- name: Archive distribution
uses: actions/upload-artifact@v7
with:
name: distribution
path: build/distributions
retention-days: 1
build-push-docker:
runs-on: ubuntu-latest
needs: build-gradle
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v4
- name: Set up cosign
uses: sigstore/cosign-installer@main
- name: Download binaries
uses: actions/download-artifact@v8
with:
name: binaries
path: build/install
- name: Download distribution
uses: actions/download-artifact@v8
with:
name: distribution
path: build/distributions
- name: Build and publish container image
uses: docker/build-push-action@v7
id: build_push
with:
push: true
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64/v8
tags: |
ghcr.io/avisi-cloud/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
ghcr.io/avisi-cloud/${{ env.IMAGE_NAME }}:latest
labels: |
org.opencontainers.image.title=${{ github.event.repository.name }}
org.opencontainers.image.description=${{ github.event.repository.description }}
org.opencontainers.image.url=${{ github.event.repository.html_url }}
org.opencontainers.image.source=${{ github.event.repository.html_url }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.version=${{ github.ref_name }}
- name: sign container image
run: |
cosign sign --yes --key env://COSIGN_KEY ghcr.io/avisi-cloud/${{ env.IMAGE_NAME }}:${{ github.ref_name }}@${{ steps.build_push.outputs.digest }}
shell: bash
env:
COSIGN_KEY: ${{secrets.COSIGN_KEY}}
COSIGN_PASSWORD: ${{secrets.COSIGN_PASSWORD}}
- name: Check images
run: |
docker buildx imagetools inspect ghcr.io/avisi-cloud/${IMAGE_NAME}:${{ github.ref_name }}
docker pull ghcr.io/avisi-cloud/${IMAGE_NAME}:${{ github.ref_name }}
cosign verify --key cosign.pub ghcr.io/avisi-cloud/${IMAGE_NAME}:${{ github.ref_name }}
- uses: anchore/sbom-action@v0
with:
image: ghcr.io/avisi-cloud/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
create-draft-release:
runs-on: ubuntu-latest
needs: build-gradle
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Download distribution
uses: actions/download-artifact@v8
with:
name: distribution
path: build/distributions
- name: Create Release Draft
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create ${{ github.ref_name }} \
--draft \
--title "${{ github.ref_name }}"
- name: Upload Release Assets
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload ${{ github.ref_name }} ./build/distributions/*
generate-example-site:
runs-on: ubuntu-latest
needs: build-gradle
steps:
- name: Checkout
uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
- name: Download binaries
uses: actions/download-artifact@v8
with:
name: binaries
path: build/install
- name: Set up environment
run: |
sudo apt-get install -y graphviz
chmod +x build/install/structurizr-site-generatr/bin/structurizr-site-generatr
- name: Generate example site
run: >
build/install/structurizr-site-generatr/bin/structurizr-site-generatr generate-site
--git-url https://github.com/avisi-cloud/structurizr-site-generatr.git
--workspace-file docs/example/workspace.dsl
--assets-dir docs/example/assets
--branches main
--default-branch main
--version ${{ github.ref_name }}
- name: Upload example site as GitHub Pages artifact
uses: actions/upload-pages-artifact@v5
with:
path: ./build/site
publish-example-site:
runs-on: ubuntu-latest
needs: generate-example-site
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy example site to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5
================================================
FILE: .gitignore
================================================
.gradle
.idea
build
out
*.md_
.vscode
bin
================================================
FILE: .markdownlint.json
================================================
{
"default": true,
"MD013": false,
"MD041": false
}
================================================
FILE: .prettierrc
================================================
{ "printWidth": 120 }
================================================
FILE: .run/all unit tests.run.xml
================================================
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="all unit tests" type="JUnit" factoryName="JUnit">
<module name="structurizr-site-generatr.test" />
<option name="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="package" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>
================================================
FILE: .run/generate site for example model (from git repo all branches) .run.xml
================================================
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="generate site for example model (from git repo all branches) " type="JetRunConfigurationType">
<option name="MAIN_CLASS_NAME" value="nl.avisi.structurizr.site.generatr.AppKt" />
<module name="structurizr-site-generatr.main" />
<option name="PROGRAM_PARAMETERS" value="generate-site --git-url https://github.com/avisi-cloud/structurizr-site-generatr.git --workspace-file docs/example/workspace.dsl --all-branches --default-branch main --exclude-branches gh-pages" />
<shortenClasspath name="NONE" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>
================================================
FILE: .run/generate site for example model (from git repo).run.xml
================================================
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="generate site for example model (from git repo)" type="JetRunConfigurationType">
<option name="MAIN_CLASS_NAME" value="nl.avisi.structurizr.site.generatr.AppKt" />
<module name="structurizr-site-generatr.main" />
<option name="PROGRAM_PARAMETERS" value="generate-site --git-url https://github.com/avisi-cloud/structurizr-site-generatr.git --workspace-file docs/example/workspace.dsl --branches main,fix-diagram-size-on-pages --default-branch main" />
<shortenClasspath name="NONE" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>
================================================
FILE: .run/generate site for example model (local).run.xml
================================================
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="generate site for example model (local)" type="JetRunConfigurationType">
<option name="MAIN_CLASS_NAME" value="nl.avisi.structurizr.site.generatr.AppKt" />
<module name="structurizr-site-generatr.main" />
<option name="PROGRAM_PARAMETERS" value="generate-site --workspace-file docs/example/workspace.dsl --assets-dir docs/example/assets" />
<shortenClasspath name="NONE" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>
================================================
FILE: .run/serve example model.run.xml
================================================
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="serve example model" type="JetRunConfigurationType">
<option name="MAIN_CLASS_NAME" value="nl.avisi.structurizr.site.generatr.AppKt" />
<module name="structurizr-site-generatr.main" />
<option name="PROGRAM_PARAMETERS" value="serve --workspace-file docs/example/workspace.dsl --assets-dir docs/example/assets" />
<shortenClasspath name="NONE" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>
================================================
FILE: .tool-versions
================================================
java temurin-21.0.10+7.0.LTS
================================================
FILE: CONTRIBUTING.md
================================================
# How to contribute
We welcome contributions! You can contribute
by [filing an issue](https://github.com/avisi-cloud/structurizr-site-generatr/issues), or by filing a pull request.
For those who'd like to help out by filing a pull request, here's some information to help you get started quickly.
## Recommended development tooling
We recommend using IntelliJ Community or Ultimate edition as your IDE. Additionally, you will need the following tools:
- Git for version control
- Java Development Kit (JDK) version 18 for building the source code
- (optional) [asdf-vm](https://asdf-vm.com/). We have provided a `.tool-versions` file in this repository, which is used
by `asdf` to help you to install the correct JDK version for building this tool.
## Working from the command line
If you prefer working with a terminal, here's a few commands you can use for some common tasks. For all these commands,
we assume that the current working directory is the root of this repository. For all these common tasks, we use the
Gradle CLI. Please refer to Gradle's [user manual](https://docs.gradle.org/current/userguide/userguide.html) if you're
not familiar with Gradle.
### Running tests
```shell
./gradlew test
```
### Running the application from source
```shell
./gradlew run --args "<program arguments>"
```
#### Example: Start a development server from source
```shell
./gradlew run --args "serve --workspace-file docs/example/workspace.dsl --assets-dir docs/example/assets"
```
## Working with IntelliJ
For those working with IntelliJ, we've provided some run configurations for running the program from source:
| Name | Description |
|---------------------------------------------------|-----------------------------------------------------------------------------------------------------|
| `all unit tests` | Runs all unit tests |
| `generate site for example model (from git repo)` | Generates a site for the example model in `docs/example` from the remote Git repository on GitHub |
| `generate site for example model (local)` | Generates a site for the example model in `docs/example` from the local clone of the Git repository |
| `serve example model` | Starts a development server for the example model in `docs/example` |
## Updating documentation
At the time of writing this document, the documentation of this tool is sparse. Please provide updated documentation
with your PR's, where applicable.
This is what's currently available:
- There's some basic documentation available in [README.md](README.md), which describes the basic usage of this tool.
- The default homepage (located in the `HomePageViewModel` class) provides some basic documentation on customizing the
homepage.
- Finally, there's the example model, which contains some documentation on embedding diagrams in documentation Markdown
files.
================================================
FILE: Dockerfile
================================================
FROM eclipse-temurin:21.0.10_7-jre-jammy
USER root
RUN apt update && apt install graphviz --yes && rm -rf /var/lib/apt/lists/*
RUN mkdir -p /var/model \
&& chown 65532:65532 /var/model
RUN useradd -d /home/generatr -u 65532 --create-home generatr
ENTRYPOINT ["/opt/structurizr-site-generatr/bin/structurizr-site-generatr"]
WORKDIR /opt/structurizr-site-generatr
COPY build/install/structurizr-site-generatr ./
RUN chmod +x /opt/structurizr-site-generatr/bin/structurizr-site-generatr
USER generatr
VOLUME ["/var/model"]
WORKDIR /var/model
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
<!-- TOC -->
* [Structurizr Site Generatr](#structurizr-site-generatr)
* [Features](#features)
* [Getting Started](#getting-started)
* [Installation using Homebrew - Recommended](#installation-using-homebrew---recommended)
* [Manual installation](#manual-installation)
* [Docker](#docker)
* [[Optional] Verify the Structurizr Site Generatr image with CoSign](#optional-verify-the-structurizr-site-generatr-image-with-cosign)
* [Usage](#usage)
* [Help](#help)
* [Version](#version)
* [Generate a website](#generate-a-website)
* [From a C4 Workspace](#from-a-c4-workspace)
* [For those taking the Docker approach](#for-those-taking-the-docker-approach)
* [Generate a website from a Git repository](#generate-a-website-from-a-git-repository)
* [Start a development web server around the generated website](#start-a-development-web-server-around-the-generated-website)
* [For those taking the Docker approach](#for-those-taking-the-docker-approach-1)
* [Customizing the generated website](#customizing-the-generated-website)
* [Contributing](#contributing)
* [Background](#background)
<!-- TOC -->
# Structurizr Site Generatr
A static site generator for [C4 architecture models](https://c4model.com/) created with [Structrizr DSL](https://docs.structurizr.com/dsl).
See [Background](#background) for the story behind this tool.
[Click here to see an example of a generated site](https://avisi-cloud.github.io/structurizr-site-generatr) based on
the [Big Bank plc example](https://structurizr.com/dsl?example=big-bank-plc) from <https://structurizr.com>. This site
is generated from the example workspace in this repository.
## Features
- Generate a static HTML site, based on a Structurizr DSL workspace.
- Generates diagrams in SVG, PNG and PlantUML format, which can be viewed and downloaded from the generated site.
- Easy browsing through the site by clicking on software system and container elements in the diagrams. Note that
external software systems are excluded from the menu. A software system is considered external when it lives outside
the (deprecated) enterprise boundary or when it contains a specific tag, see [Customizing the generated website](#customizing-the-generated-website).
- Start a development server which generates a site, serves it and updates the site automatically whenever a file that's
part of the Structurizr workspace changes.
- Include documentation (in Markdown or AsciiDoc format) in the generated site. Both workspace level documentation and software
system level documentation are included in the site.
- Include ADR's in the generated site. Again, both workspace level ADR's and software system level ADR's are included in
the site.
- Include static assets in the generated site, which can be used in ADR's and documentation.
- Generate a site from a Structurizr DSL model in a Git repository. Supports multiple branches, which makes it possible
to for example maintain an actual state in `master` and one or more future states in feature branches. The generated
site includes diagrams for all valid configured or detected branches.
- Include a version number in the generated site.
## Getting Started
To get started with the Structurizr Site Generatr, you can either:
- Install it to your local machine (recommended for the best experience), or
- Execute it on your local machine via a container (requires Docker)
**Please note**: The intended use of the Docker image is to generate a site from a CI pipeline. Using it for model
development is possible, but not a usage scenario that's actively supported.
### Installation using Homebrew - Recommended
As this approach relies on [Homebrew](https://brew.sh/), ensure this is already installed. For Windows and other
operating systems not supported by Homebrew, please use the [Docker approach](#docker) instead.
To install Structurizr Site Generatr execute the following commands in your terminal:
```shell
brew tap avisi-cloud/tools
brew install structurizr-site-generatr
structurizr-site-generatr --help
```
Periodically, you would have to update your local installation to take advantage of any new
[Structurizr Site Generatr releases](https://github.com/avisi-cloud/structurizr-site-generatr/releases).
### Manual installation
If using Homebrew is not an option for you, it's also possible to install Structurizr Site Generatr manually. This can
be done as follows:
- Consult the
[Structurizr Site Generatr releases](https://github.com/avisi-cloud/structurizr-site-generatr/releases) and choose
the version you wish to use
- Download the `.tar.gz` or `.zip` distribution
- Extract the archive using your favourite tool
- For ease of use, it's recommended to add Structurizr Site Generatr's `bin` directory to your `PATH`
### Docker
Though local installation is recommended for development where possible, Structurizr Site Generatr is also a packaged
as a Docker image. Therefore, to use this approach, ensure [Docker](https://www.docker.com/) is already installed.
Additionally, for Windows 10+ users, you may want to take advantage of
[WSL2](https://docs.microsoft.com/en-us/windows/wsl/install) (Windows Subsystem for Linux). Both Docker and WSL2
are topics too vast to repeat here, so you are invited to study these as prerequisite learning for this approach.
Then to download our packaged image, consider the
[Structurizr Site Generatr releases](https://github.com/avisi-cloud/structurizr-site-generatr/releases) and choose
the version you wish to use. Then, in your terminal, execute the following:
```shell
docker pull ghcr.io/avisi-cloud/structurizr-site-generatr
```
Once downloaded, you can execute Structurizr Site Generatr via a temporary Docker container by executing the
following in your terminal:
```shell
docker run -it --rm ghcr.io/avisi-cloud/structurizr-site-generatr --help
```
#### [Optional] Verify the Structurizr Site Generatr image with [CoSign](https://github.com/sigstore/cosign)
```shell
cat cosign.pub
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzezKl0vAWSHosQ0JLEsDzNBd2nGm
08KqX+imYqq2avlbH+ehprJFMqKK0/I/bY0q5W9hQC8SLzTRJ9Q5dB9UiQ==
-----END PUBLIC KEY-----
cosign verify --key cosign.pub ghcr.io/avisi-cloud/structurizr-site-generatr
```
Or by using the Github repo url:
```shell
cosign verify --key https://github.com/avisi-cloud/structurizr-site-generatr ghcr.io/avisi-cloud/structurizr-site-generatr
```
## Usage
These examples use the [example workspace](docs/example) in this repository.
Once installed, Structurizr Site Generatr is operated via your terminal by issuing commands. Each command is
explained here:
### Help
To learn about available commands, or parameters for individual commands, call Structurizr Site Generatr with the
`--help` argument.
```shell
installed> structurizr-site-generatr --help
docker> docker run -it --rm ghcr.io/avisi-cloud/structurizr-site-generatr --help
Usage: structurizr-site-generatr options_list
Subcommands:
serve - Start a development server
generate-site - Generate a site for the selected workspace.
version - Print version information
Options:
--help, -h -> Usage info
```
### Version
To query the version of Structurizr Site Generatr installed / used.
```shell
installed> structurizr-site-generatr version
docker> docker run -it --rm ghcr.io/avisi-cloud/structurizr-site-generatr version
Structurizr Site Generatr v1.1.3
```
### Generate a website
#### From a [C4 Workspace](https://docs.structurizr.com/dsl)
This is the primary use case of Structurizr Site Generatr -- to generate a website from a
[C4 Workspace](https://docs.structurizr.com/dsl).
```shell
installed> structurizr-site-generatr generate-site --workspace-file workspace.dsl --assets-dir assets
docker> docker run -it --rm -v c:/projects/c4:/var/model ghcr.io/avisi-cloud/structurizr-site-generatr generate-site --workspace-file workspace.dsl --assets-dir assets
```
Here, the `--workspace-file` or `-w` parameter specifies the input
[C4 Workspace DSL file](https://docs.structurizr.com/dsl) to the `generate-site` command. The `--assets-dir` or `-a` parameter is not
required, but usually needed as well. Additional parameters that affect website generation can be reviewed
using the `--help` operator.
By default, the generated website will be placed in `./build`, which is overwritten if it already exisits.
#### For those taking the Docker approach
When using the Docker approach, the local file system must be made available to the temporary Structurizr Site
Generatr container via a
[Docker file system volume mount](https://docs.docker.com/engine/reference/commandline/run/#mount-volume--v---read-only).
This is achieved by specifying the `-v` parameter with a linux-like **absolute** path to the folder containing
the .dsl file specified via `-w`. See how `C:\Projects\C4` has become `-v c:/projects/c4:/var/model` in the above
example?
#### Generate a website from a Git repository
Instead of relying on local .dsl files only, the `generate-site` command can also retrieve input files from a
Git repository as follows. This is particularly advantageous for demos, documentation, or CI/CD pipelines.
To explicitly name the branches that you want to build sites from you can use the --branches option.
```shell
structurizr-site-generatr generate-site
--git-url https://github.com/avisi-cloud/structurizr-site-generatr.git
--workspace-file docs/example/workspace.dsl
--assets-dir docs/example/assets
--branches main,future,old
--default-branch main
```
or you can choose to build all branches that are found in the repository and exclude specific ones by using the --all-branches and --exclude-branches options.
```shell
structurizr-site-generatr generate-site
--git-url https://github.com/avisi-cloud/structurizr-site-generatr.git
--workspace-file docs/example/workspace.dsl
--assets-dir docs/example/assets
--all-branches
--exclude-branches gh-pages
--default-branch main
```
Both the --branches and --exclude-branches options are comma separated lists and can contain multiple branch names.
### Start a development web server around the generated website
To aid composition of [C4 Workspace DSL files](https://docs.structurizr.com/dsl), the `serve` command will
generate a website from the input .dsl specified with `-w` _and_ start a web server to view it. **Default port** for the web server is **8080**.
A different port for the web server can be specified with `-p PORT`. Additional parameters that affect website generation and the development
web server can be reviewed using the `--help` operator.
```shell
installed> structurizr-site-generatr serve -w workspace.dsl
docker> docker run -it --rm -v c:/projects/c4:/var/model -p 8080:8080 ghcr.io/avisi-cloud/structurizr-site-generatr serve --workspace-file workspace.dsl --assets-dir assets
```
By default, a development web server will be started and accessible at http://localhost:8080/ (if available).
#### For those taking the Docker approach
However, when using the Docker approach, this development web server is within the temporary Structurizr Site
Generatr container. So
[Docker port mapping](https://docs.docker.com/engine/reference/commandline/run/#publish-or-expose-port--p---expose)
is needed to expose the container's port 8080 to the host (web browser). In the example above, the
`-p 8080:8080` argument tells Docker to bind the local machine / host's port 8080 to the container's port 8080.
## Customizing the generated website
By default, the site generator uses the
[C4PlantUmlExporter](https://docs.structurizr.com/export/plantuml#c4plantumlexporter)
to generate the diagrams. When using this exporter, all properties available for the C4PlantUMLExporter, e.g. `c4plantuml.tags`, can be applied
and affect the diagrams in the generate site. See also [Diagram notation](https://docs.structurizr.com/export/comparison) for an overview of supported features
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` |
| `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` |
| `generatr.site.theme` | Experimental: allows to force a light or dark theme or allows to switch between light and dark mode on the website with browser preference or menu item. Possible values are 'light', 'dark' or 'auto'. Note that the 'structurizr' exporter (see 'generatr.site.exporter' setting) generally works better for the dark theme. | `light` | `auto` |
To control the behavior of views, apply the following properties:
| Property name | Description | Value | Scope |
|--------------------------------------|------------------------------------------------------------------------------|----------------------|-----------------|
| `generatr.view.deployment.belongsTo` | Associate the diagram into the Deployment tab of a specific software system. | software system name | deployment view |
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).
## Contributing
We welcome contributions! Please refer to [CONTRIBUTING.md](CONTRIBUTING.md) for more information on how you can help.
## Background
At Avisi, we're big fans of the [C4 model](https://c4model.com). We use it in many projects, big and small, to document
the architecture of the systems and system landscapes we're working on.
We started out by using PlantUML, combined with the [C4 extension](https://github.com/plantuml-stdlib/C4-PlantUML). This
works well for small systems. However, for larger application landscapes, maintained by multiple development teams, this
lead to duplication and inconsistency across diagrams.
To solve this problem, we needed a model based approach. Rather than maintaining separate diagrams and trying to keep
them consistent, we needed to have a single model, from which diagrams could be generated. This is why we started using
the [Structurizr for Java](https://github.com/structurizr/java) library. About a year later, we migrated our models to
Structurizr DSL.
We created custom tooling to generate diagrams and check them in to our Git repository. All diagrams could be seen by
navigating to our GitLab repository. This is less than ideal, because we like to make these diagrams easily accessible
to everyone, including our customers. This is why we decided that we needed a way of publishing our models to an easily
accessible website.
This tool is the result. It's still a work in progress, but it has already proven to be very useful to us.
================================================
FILE: build.gradle.kts
================================================
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.api.tasks.testing.logging.TestLogEvent
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
kotlin("jvm") version "2.3.20"
kotlin("plugin.serialization") version "2.3.20"
application
}
description = "Structurizr Site Generatr"
group = "cloud.avisi"
version = project.properties["projectVersion"] ?: "0.0.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
implementation("org.jetbrains.kotlin:kotlin-stdlib")
implementation("org.jetbrains.kotlinx:kotlinx-cli:0.3.6")
implementation("org.eclipse.jgit:org.eclipse.jgit:7.6.0.202603022253-r")
implementation("com.structurizr:structurizr-core:6.1.0")
implementation("com.structurizr:structurizr-dsl:6.1.0")
implementation("com.structurizr:structurizr-export:6.1.0")
implementation("net.sourceforge.plantuml:plantuml:1.2026.2")
implementation("com.vladsch.flexmark:flexmark-all:0.64.8")
implementation("org.asciidoctor:asciidoctorj:3.0.1")
implementation("org.jsoup:jsoup:1.22.1")
implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.12.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")
implementation("org.eclipse.jetty:jetty-server:12.1.8")
implementation("org.eclipse.jetty.websocket:jetty-websocket-jetty-server:12.1.8")
runtimeOnly("org.slf4j:slf4j-simple:2.0.17")
// Support for Structurizr scripting languages
runtimeOnly("org.jetbrains.kotlin:kotlin-scripting-jsr223:2.3.20")
runtimeOnly("org.codehaus.groovy:groovy-jsr223:3.0.25")
runtimeOnly("org.jruby:jruby-core:9.4.14.0")
testImplementation(kotlin("test"))
testImplementation("org.junit.jupiter:junit-jupiter-params")
testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.28.1")
testImplementation("com.microsoft.playwright:playwright:1.59.0")
}
application {
mainClass.set("nl.avisi.structurizr.site.generatr.AppKt")
}
kotlin {
jvmToolchain(21)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}
java {
targetCompatibility = JavaVersion.VERSION_17
}
tasks {
processResources {
from("package.json")
}
register<JavaExec>("playwright") {
classpath(sourceSets["test"].runtimeClasspath)
mainClass = "com.microsoft.playwright.CLI"
args = (project.properties["args"] as String?)?.split(" ") ?: emptyList()
}
test {
useJUnitPlatform()
systemProperty("file.encoding", "UTF-8")
testLogging.events = setOf(TestLogEvent.PASSED, TestLogEvent.FAILED, TestLogEvent.STANDARD_OUT, TestLogEvent.STANDARD_ERROR)
testLogging.exceptionFormat = TestExceptionFormat.FULL
}
jar {
manifest {
attributes["Implementation-Title"] = project.description
attributes["Implementation-Version"] = project.version
}
}
startScripts {
// This is a workaround for an issue with the generated .bat file,
// reported in https://github.com/avisi-cloud/structurizr-site-generatr/issues/463.
// Instead of listing each and every .jar file in the lib directory, we just use a
// wildcard to include everything in the lib directory in the classpath.
classpath = files("lib/*")
}
withType<Tar> {
archiveExtension.set("tar.gz")
compression = Compression.GZIP
}
}
================================================
FILE: cosign.pub
================================================
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzezKl0vAWSHosQ0JLEsDzNBd2nGm
08KqX+imYqq2avlbH+ehprJFMqKK0/I/bY0q5W9hQC8SLzTRJ9Q5dB9UiQ==
-----END PUBLIC KEY-----
================================================
FILE: docs/example/.adr-dir
================================================
workspace-adrs
================================================
FILE: docs/example/assets/site/custom.css
================================================
svg g g[id^="link"]:hover path {
stroke:#FF0000 !important;
stroke-width: 2.0;
}
svg g g[id^="link"]:hover polygon {
stroke:#FF0000 !important;
}
svg g g[id^="link"]:hover text {
fill:#FF0000 !important;
}
================================================
FILE: docs/example/internet-banking-system/adr/0001-record-architecture-decisions.md
================================================
# 1. Record Internet Banking System architecture decisions
Date: 2022-06-21
## Status
Accepted
## Context
We need to record the architectural decisions made on this project.
## Decision
We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions).
## Consequences
See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools).
================================================
FILE: docs/example/internet-banking-system/api-application/email-component/adr/0001-record-architecture-decisions.md
================================================
# 1. Record Email Component architecture decision
Date: 2022-06-21
## Status
Accepted
## Context
We need to record the architectural decisions made on this project.
## Decision
We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions).
## Consequences
See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools).
================================================
FILE: docs/example/internet-banking-system/api-application/email-component/adr/0002-implement-feature-1.md
================================================
# 2. Implement Feature 1
Date: 2024-12-17
## Status
Superseded by [3. Another Realisation of Feature 1](0003-another-realisation-of-feature-1.md)
## Context
The issue motivating this decision, and any context that influences or constrains the decision.
## Decision
The change that we're proposing or have agreed to implement.
## Consequences
What becomes easier or more difficult to do and any risks introduced by the change that will need to be mitigated.
================================================
FILE: docs/example/internet-banking-system/api-application/email-component/adr/0003-another-realisation-of-feature-1.md
================================================
# 3. Another Realisation of Feature 1
Date: 2024-12-17
## Status
Accepted
Supersedes [2. Implement Feature 1](0002-implement-feature-1.md)
## Context
The issue motivating this decision, and any context that influences or constrains the decision.
## Decision
The change that we're proposing or have agreed to implement.
## Consequences
What becomes easier or more difficult to do and any risks introduced by the change that will need to be mitigated.
================================================
FILE: docs/example/internet-banking-system/api-application/email-component/docs/0001-inner-workings.md
================================================
# Email Component inner workings
This is how the e-mail component works.
================================================
FILE: docs/example/internet-banking-system/api-application/mainframe-banking-system-facade/adr/0001-record-architecture-decisions.md
================================================
# 1. Record Mainframe Banking System Facade architecture decision
Date: 2022-06-21
## Status
Accepted
## Context
We need to record the architectural decisions made on this project.
## Decision
We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions).
## Consequences
See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools).
================================================
FILE: docs/example/internet-banking-system/api-application/mainframe-banking-system-facade/docs/0000-introduction.md
================================================
# Mainframe Banking System Facade
This is the Mainframe Banking System Facade.
================================================
FILE: docs/example/internet-banking-system/api-application/mainframe-banking-system-facade/docs/0001-inner-workings.md
================================================
# Inner workings of Mainframe Banking System Facade
This is how the facade works.
================================================
FILE: docs/example/internet-banking-system/database/adr/0004-using-oracle-database-schema.md
================================================
# 4. Using Oracle database schema
Date: 2025-11-13
## Status
Accepted
## Context
The issue motivating this decision, and any context that influences or constrains the decision.
## Decision
The change that we're proposing or have agreed to implement.
## Consequences
What becomes easier or more difficult to do and any risks introduced by the change that will need to be mitigated.
================================================
FILE: docs/example/internet-banking-system/database/docs/0002-guide.md
================================================
# Usage
This is how we use this thing.
================================================
FILE: docs/example/internet-banking-system/docs/0000-introduction.md
================================================
# Description
This is our fancy Internet Banking System.
## Vision
One system to rule them all!
## References
- Source Code on GitHub: [https://github.com/avisi-cloud/structurizr-site-generatr]
================================================
FILE: docs/example/internet-banking-system/docs/0001-history.md
================================================
# History
Some notes how we got to the current state.
================================================
FILE: docs/example/internet-banking-system/docs/0002-guide.md
================================================
# Usage
This is how we use this thing.
================================================
FILE: docs/example/workspace-adrs/0001-record-architecture-decisions.md
================================================
# 1. Record architecture decisions
Date: 2022-06-21
## Status
Accepted
## Context
We need to record the architectural decisions made on this project.
## Decision
We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions).
## Consequences
See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools).
================================================
FILE: docs/example/workspace-adrs/0002-implement-feature-1.md
================================================
# 2. Implement Feature 1
Date: 2024-12-17
## Status
Superseded by [3. Another Realisation of Feature 1](0003-another-realisation-of-feature-1.md)
## Context
The issue motivating this decision, and any context that influences or constrains the decision.
## Decision
The change that we're proposing or have agreed to implement.
## Consequences
What becomes easier or more difficult to do and any risks introduced by the change that will need to be mitigated.
================================================
FILE: docs/example/workspace-adrs/0003-another-realisation-of-feature-1.md
================================================
# 3. Another Realisation of Feature 1
Date: 2024-12-17
## Status
Accepted
Supersedes [2. Implement Feature 1](0002-implement-feature-1.md)
## Context
The issue motivating this decision, and any context that influences or constrains the decision.
## Decision
The change that we're proposing or have agreed to implement.
## Consequences
What becomes easier or more difficult to do and any risks introduced by the change that will need to be mitigated.
================================================
FILE: docs/example/workspace-adrs/0004-using-oracle-database-schema.md
================================================
# 4. Using Oracle database schema
Date: 2025-11-13
## Status
Accepted
## Context
The issue motivating this decision, and any context that influences or constrains the decision.
## Decision
The change that we're proposing or have agreed to implement.
## Consequences
What becomes easier or more difficult to do and any risks introduced by the change that will need to be mitigated.
================================================
FILE: docs/example/workspace-docs/00-index.md
================================================
## Big Bank plc architecture
This site contains the C4 architecture model for Big Bank plc.
This page is the home page, because it is the first file in the `workspace-docs` directory, when the files are sorted
alphabetically.
================================================
FILE: docs/example/workspace-docs/01-embedding-diagrams-and-images.md
================================================
## Embedding diagrams and images
This page showcases the ability to embed diagrams and static images in documentation.
### Embedding diagrams
Diagrams can be embedded using the `embed:` syntax:
```markdown

```
See also: <https://www.structurizr.com/help/documentation/diagrams>
#### Example: Embedded diagram

### Embedding static images
Static assets can be included in the generated site, using the `--assets-dir` command-line flag. This flag can be used
with the `serve` command and the `generate-site` command.
When, for example, you would like to embed a nice picture which is located in the `pictures` directory under the assets
directory, you can do that as follows:
```markdown

```
#### Example: Embedded picture
[Sun](https://www.flickr.com/photos/schmollmolch/4937297813/), by Christian Scheja

### Embedding PlantUML diagrams
Structurizr Site Generatr supports rendering PlantUML embedded in Markdown files.
#### Sequence diagram example
````markdown
```puml
@startuml
Foo -> Bar: doSomething()
@enduml
```
````
```puml
@startuml
Foo -> Bar: doSomething()
@enduml
```
### Class diagram example
````markdown
```puml
@startuml
class Foo {
+property: String
+foo()
}
class Bar {
-privateProperty: String
+bar()
}
Foo ..> Bar: Uses
@enduml
```
````
```puml
@startuml
class Foo {
+property: String
+foo()
}
class Bar {
-privateProperty: String
+bar()
}
Foo ..> Bar: Uses
@enduml
```
### Embedding mermaid diagrams
Structurizr Site Generatr is supporting mermaid diagrams in markdown pages using the actual mermaid.js version.
Therefore every diagram type, supported by mermaid may be used in markdown documentation files.
* flowchart
* sequence diagram
* class diagram
* state diagram
* entity-relationship diagram
* user journey
* gantt chart
* pie chart
* requirement diagram
* and some more
Please find the full list of supported chart types
on [mermaid.js.org/intro](https://mermaid.js.org/intro/#diagram-types)
#### Flowchart Diagram Example
````markdown
```mermaid
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
```
````
```mermaid
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
```
#### Sequence Diagram Example
````markdown
```mermaid
sequenceDiagram
participant Alice
participant Bob
Alice->>John: Hello John, how are you?
loop Healthcheck
John->>John: Fight against hypochondria
end
Note right of John: Rational thoughts <br/>prevail!
John-->>Alice: Great!
John->>Bob: How about you?
Bob-->>John: Jolly good!
```
````
```mermaid
sequenceDiagram
participant Alice
participant Bob
Alice->>John: Hello John, how are you?
loop Healthcheck
John->>John: Fight against hypochondria
end
Note right of John: Rational thoughts <br/>prevail!
John-->>Alice: Great!
John->>Bob: How about you?
Bob-->>John: Jolly good!
```
================================================
FILE: docs/example/workspace-docs/02-markdown-features.md
================================================
## Extended Markdown features
This page showcases the ability to use extended Markdown formating features in workspace documentation files. The full list of available extensions to standard commonmark markdown features is documented in the flexmark wiki [Extensions page](https://github.com/vsch/flexmark-java/wiki/Extensions).
Most of these extended features have to be activated in your architecture model as a property in workspace views.
```DSL
workspace {
...
views {
...
properties {
...
// full list of available "generatr.markdown.flexmark.extensions":
// - Abbreviation
// - Admonition
// - AnchorLink
// - Aside
// - Attributes
// - Autolink
// - Definition
// - Emoji
// - EnumeratedReference
// - Footnotes
// - GfmIssues
// - GfmStrikethroughSubscript
// - GfmTaskList
// - GfmUsers
// - GitLab
// - Ins
// - Macros
// - MediaTags
// - ResizableImage
// - Superscript
// - Tables
// - TableOfContents
// - SimulatedTableOfContents
// - Typographic
// - WikiLinks
// - XWikiMacro
// - YAMLFrontMatter
// - YouTubeLink
// see https://github.com/vsch/flexmark-java/wiki/Extensions
// ATTENTION:
// * "generatr.markdown.flexmark.extensions" values must be separated by comma
// * it's not possible to use "GitLab" and "ResizableImage" extensions together
// default behaviour, if no generatr.markdown.flexmark.extensions property is specified, is to load the Tables extension only
"generatr.markdown.flexmark.extensions" "Abbreviation,Admonition,AnchorLink,Attributes,Autolink,Definition,Emoji,Footnotes,GfmTaskList,GitLab,MediaTags,Tables,TableOfContents,Typographic"
...
}
...
}
...
}
```
### Table of Contents
`[TOC]` element which renders a table of contents
```markdown
[TOC]
```
will render into
[TOC]
#### Usage info
"TableOfContents" is an optional feature, that has to be activated in workspace views properties
```DSL
"generatr.markdown.flexmark.extensions" "TableOfContents"
```
### Render Tables
Standard tables can be embedded in markdown text syntax:
```markdown
| header1 | header2 |
| ------- | ------- |
| content | content |
```
This will be rendered as
| header1 | header2 |
| ------- | ------- |
| content | content |
#### Usage info
"Render Tables" is an optional feature, that is enabled by default. If other optional markdown features are activated in workspace views properties, than you have to ensure, that Tables is listed in the extension list as well.
```DSL
"generatr.markdown.flexmark.extensions" "Tables"
```
### Admonition Blocks
Admonitions create block-styled side content.
```markdown
!!! faq "FAQ"
This is a FAQ.
!!! attention "Warning"
This is a warning message
!!! info "information"
this is an additional information
```
This will be rendered as
!!! faq "FAQ"
This is a FAQ.
!!! attention "Warning"
This is a warning message
!!! info "information"
this is an additional information
#### Usage info
"Admonition Blocks" is an optional feature, that has to be activated in workspace views properties
```DSL
"generatr.markdown.flexmark.extensions" "Admonition"
```
### GitLab flavored markdown extensions
Please see [GitLab flavored markdown features](https://docs.gitlab.com/ee/user/markdown.html?tab=Rendered+Markdown) for a detailed description.
Unfortunately only the following features are supported by Flexmark markdown renderer, that is used here.
#### Multiline Block quote delimiters
```markdown
>>>
If you paste a message from somewhere else
that spans multiple lines,
you can quote that without having to manually prepend `>` to every line!
>>>
```
>>>
If you paste a message from somewhere else
that spans multiple lines,
you can quote that without having to manually prepend `>` to every line!
>>>
#### Inline diff (deletions and additions)
With inline diff tags, you can display `{+ additions +}` or `[- deletions -]`.
The wrapping tags can be either curly braces or square brackets:
```markdown
- {+ addition 1 +}
- [+ addition 2 +]
- {- deletion 3 -}
- [- deletion 4 -]
```
- {+ addition 1 +}
- [+ addition 2 +]
- {- deletion 3 -}
- [- deletion 4 -]
#### Math
Math written in LaTeX syntax is rendered with [KaTeX](https://github.com/KaTeX/KaTeX).
_KaTeX only supports a [subset](https://katex.org/docs/supported.html) of LaTeX._
Math written between dollar signs with backticks (``$`...`$``) or single dollar signs (`$...$`)
is rendered inline with the text.
Math written between double dollar signs (`$$...$$`) or in a code block with
the language declared as `math` is rendered on a separate line:
````markdown
This math is inline: $`a^2+b^2=c^2`$.
This math is on a separate line using a ```` ```math ```` block:
```math
a^2+b^2=c^2
```
````
This math is inline: $`a^2+b^2=c^2`$.
This math is on a separate line using a ```` ```math ```` block:
```math
a^2+b^2=c^2
```
#### Usage info
"GitLab Flavored Markdown" is an optional feature, that has to be activated in workspace views properties
```DSL
"generatr.markdown.flexmark.extensions" "GitLab"
```
### AnchorLink
Automatically adds anchor links to headings, using GitHub id generation algorithm
#### Usage info
"AnchorLink" is an optional feature, that has to be activated in workspace views properties
```DSL
"generatr.markdown.flexmark.extensions" "AnchorLink"
```
### Definition Lists
Converts definition syntax of Php Markdown Extra Definition List to `<dl></dl>` HTML and corresponding AST nodes.
```markdown
Definition Term
: Definition of above term
: Another definition of above term
```
Definition Term
: Definition of above term
: Another definition of above term
#### Usage info
"Definition Lists" is an optional feature, that has to be activated in workspace views properties
```DSL
"generatr.markdown.flexmark.extensions" "Definition"
```
### Emoji
Allows to create image link to emoji images from emoji shortcuts using [Emoji-Cheat-Sheet.com](https://www.webfx.com/tools/emoji-cheat-sheet) and optionally to replace with its unicode equivalent character with mapping by Mark Wunsch found at [mwunsch/rumoji](https://github.com/mwunsch/rumoji)
```markdown
thumbsup :thumbsup:
calendar :calendar:
warning :warning:
```
thumbsup :thumbsup:
calendar :calendar:
warning :warning:
#### Usage info
"Emoji" is an optional feature, that has to be activated in workspace views properties
```DSL
"generatr.markdown.flexmark.extensions" "Emoji"
```
### GfmTaskList
Enables list items based task lists whose text begins with: `[ ]`, `[x]` or `[X]`
```markdown
- [x] Completed task
- [ ] Incomplete task
- [x] Sub-task 1
- [ ] Sub-task 3
1. [x] Completed task
1. [ ] Incomplete task
1. [x] Sub-task 1
1. [ ] Sub-task 3
```
will be rendered as
- [x] Completed task
- [ ] Incomplete task
- [x] Sub-task 1
- [ ] Sub-task 3
1. [x] Completed task
1. [ ] Incomplete task
1. [x] Sub-task 1
1. [ ] Sub-task 3
#### Usage info
"GfmTaskList" is an optional feature, that has to be activated in workspace views properties
```DSL
"generatr.markdown.flexmark.extensions" "GfmTaskList"
```
### Links to Markdown documents
Placing links on markdown pages is easy. You can link to:
* an overview page like [Workspace decisions](/decisions/),
* an individual decision [ADR-0001. record architecture decisions](/decisions/1/)
* or some system documentation like [Internet Banking](/internet-banking-system/).
================================================
FILE: docs/example/workspace-docs/03-asciidoc-features.adoc
================================================
= AsciiDoc features
:toc: macro
:imagesdir: ../assets
:tip-caption: 💡Tip
== AsciiDoc features 📌
This page showcases the ability to use AsciiDoc formating features in workspace documentation files. The full list of AsciiDoc features is documented in the https://docs.asciidoctor.org/asciidoc/latest/syntax-quick-reference/[Asciidoctor Syntax Reference].
toc::[]
=== Embedding diagrams
Diagrams can be embedded using the `embed:` syntax:
[source, asciidoc]
----
image::embed:SystemLandscape[System Landscape Diagram]
----
See also: https://www.structurizr.com/help/documentation/diagrams
==== Example: Embedded diagram
image::embed:SystemLandscape[System Landscape Diagram]
=== Embedding static images
==== Example Embedded picture
When, for example, you would like to embed a nice picture which is located in the `pictures` directory under the assets directory, you can do that as follows:
[source, asciidoc]
----
image::/pictures/nice-picture.png[A nice picture]
----
https://www.flickr.com/photos/schmollmolch/4937297813/[Sun], by Christian Scheja
image::/pictures/nice-picture.png[A nice picture]
=== Embedding PlantUML diagrams
==== Sequence Diagram Example
[source, asciidoc]
-----
[plantuml]
----
@startuml
Foo -> Bar: doSomething()
@enduml
----
-----
[plantuml]
----
@startuml
Foo -> Bar: doSomething()
@enduml
----
==== Class Diagram Example
[source, asciidoc]
-----
[plantuml]
----
@startuml
class Foo {
+property: String
+foo()
}
class Bar {
-privateProperty: String
+bar()
}
Foo ..> Bar: Uses
@enduml
----
-----
[plantuml]
----
@startuml
class Foo {
+property: String
+foo()
}
class Bar {
-privateProperty: String
+bar()
}
Foo ..> Bar: Uses
@enduml
----
=== Embedding mermaid diagrams
==== Flowchart Diagram Example
[source, asciidoc]
-----
[source, mermaid]
----
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
----
-----
[source, mermaid]
----
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
----
==== Sequence Diagram Example
[source, asciidoc]
-----
[source, mermaid]
----
sequenceDiagram
participant Alice
participant Bob
Alice->>John: Hello John, how are you?
loop Healthcheck
John->>John: Fight against hypochondria
end
Note right of John: Rational thoughts <br/>prevail!
John-->>Alice: Great!
John->>Bob: How about you?
Bob-->>John: Jolly good!
----
-----
[source, mermaid]
----
sequenceDiagram
participant Alice
participant Bob
Alice->>John: Hello John, how are you?
loop Healthcheck
John->>John: Fight against hypochondria
end
Note right of John: Rational thoughts <br/>prevail!
John-->>Alice: Great!
John->>Bob: How about you?
Bob-->>John: Jolly good!
----
=== Tables
[source, asciidoc]
----
|===
|Column 1, Header Row |Column 2, Header Row
|Cell in column 1, row 1
|Cell in column 2, row 1
|Cell in column 1, row 2
|Cell in column 2, row 2
|===
----
This will be rendered as
|===
|Column 1, Header Row |Column 2, Header Row
|Cell in column 1, row 1
|Cell in column 2, row 1
|Cell in column 1, row 2
|Cell in column 2, row 2
|===
=== Admonition Blocks
Admonitions create block-styled side content.
NOTE: This is a note.
[TIP]
.Info
=====
Go to this URL to learn more about it:
* https://docs.asciidoctor.org/asciidoc/latest/blocks/admonitions/
CAUTION: This is Caution message!
WARNING: This is a Warning message!
=====
[IMPORTANT]
One more thing. Happy documenting!
=== Block quotes
[quote,attribution,citation title and information]
Quote or excerpt text
=== Checklist
[source, asciidoc]
----
* [*] checked
* [x] also checked
* [ ] not checked
* normal list item
----
will be rendered as:
* [*] checked
* [x] also checked
* [ ] not checked
* normal list item
================================================
FILE: docs/example/workspace.dsl
================================================
/*
* This is a combined version of the following workspaces:
*
* - "Big Bank plc - System Landscape" (https://structurizr.com/share/28201/)
* - "Big Bank plc - Internet Banking System" (https://structurizr.com/share/36141/)
*/
workspace "Big Bank plc" "This is an example workspace to illustrate the key features of Structurizr, via the DSL, based around a fictional online banking system." {
!docs workspace-docs
!adrs workspace-adrs
model {
properties {
"structurizr.groupSeparator" "/"
}
customer = person "Personal Banking Customer" "A customer of the bank, with personal bank accounts." "Customer"
acquirer = softwaresystem "Acquirer" "Facilitates PIN transactions for merchants." "External System"
group "Big Bank plc" {
supportStaff = person "Customer Service Staff" "Customer service staff within the bank." "Bank Staff" {
properties {
"Location" "Customer Services"
}
}
backoffice = person "Back Office Staff" "Administration and support staff within the bank." "Bank Staff" {
properties {
"Location" "Internal Services"
}
}
mainframe = softwaresystem "Mainframe Banking System" "Stores all of the core banking information about customers, accounts, transactions, etc." "Existing System"
email = softwaresystem "E-mail System" "The internal Microsoft Exchange e-mail system." "Existing System"
atm = softwaresystem "ATM" "Allows customers to withdraw cash." "Existing System"
internetBankingSystem = softwaresystem "Internet Banking System" "Allows customers to view information about their bank accounts, and make payments." {
!adrs internet-banking-system/adr
!docs internet-banking-system/docs
properties {
"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"
webApplication = container "Web Application" "Delivers the static content and the Internet banking single page application." "Java and Spring MVC"
apiApplication = container "API Application" "Provides Internet banking functionality via a JSON/HTTPS API." "Java and Spring MVC" {
properties {
Owner "Team 1"
}
signinController = component "Sign In Controller" "Allows users to sign in to the Internet Banking System." "Spring MVC Rest Controller"
accountsSummaryController = component "Accounts Summary Controller" "Provides customers with a summary of their bank accounts." "Spring MVC Rest Controller"
resetPasswordController = component "Reset Password Controller" "Allows users to reset their passwords with a single use URL." "Spring MVC Rest Controller"
securityComponent = component "Security Component" "Provides functionality related to signing in, changing passwords, etc." "Spring Bean"
mainframeBankingSystemFacade = component "Mainframe Banking System Facade" "A facade onto the mainframe banking system." "Spring Bean" {
!adrs internet-banking-system/api-application/mainframe-banking-system-facade/adr
!docs internet-banking-system/api-application/mainframe-banking-system-facade/docs
}
emailComponent = component "E-mail Component" "Sends e-mails to users." "Spring Bean" {
!adrs internet-banking-system/api-application/email-component/adr
!docs internet-banking-system/api-application/email-component/docs
}
}
database = container "Database" "Stores user registration information, hashed authentication credentials, access logs, etc." "Oracle Database Schema" "Database" {
!adrs internet-banking-system/database/adr
!docs internet-banking-system/database/docs
}
}
}
# relationships between people and software systems
customer -> internetBankingSystem "Views account balances, and makes payments using"
internetBankingSystem -> mainframe "Gets account information from, and makes payments using"
internetBankingSystem -> email "Sends e-mail using"
email -> customer "Sends e-mails to"
customer -> supportStaff "Asks questions to" "Telephone"
supportStaff -> mainframe "Uses"
customer -> atm "Withdraws cash using"
atm -> mainframe "Uses"
backoffice -> mainframe "Uses"
acquirer -> mainframe "Peforms clearing and settlement"
# relationships to/from containers
customer -> webApplication "Visits bigbank.com/ib using" "HTTPS"
customer -> singlePageApplication "Views account balances, and makes payments using"
customer -> mobileApp "Views account balances, and makes payments using"
webApplication -> singlePageApplication "Delivers to the customer's web browser"
# relationships to/from components
singlePageApplication -> signinController "Makes API calls to" "JSON/HTTPS"
singlePageApplication -> accountsSummaryController "Makes API calls to" "JSON/HTTPS"
singlePageApplication -> resetPasswordController "Makes API calls to" "JSON/HTTPS"
mobileApp -> signinController "Makes API calls to" "JSON/HTTPS"
mobileApp -> accountsSummaryController "Makes API calls to" "JSON/HTTPS"
mobileApp -> resetPasswordController "Makes API calls to" "JSON/HTTPS"
signinController -> securityComponent "Uses"
accountsSummaryController -> mainframeBankingSystemFacade "Uses"
resetPasswordController -> securityComponent "Uses"
resetPasswordController -> emailComponent "Uses"
securityComponent -> database "Reads from and writes to" "JDBC"
mainframeBankingSystemFacade -> mainframe "Makes API calls to" "XML/HTTPS"
emailComponent -> email "Sends e-mail using"
deploymentEnvironment "Development" {
deploymentNode "Developer Laptop" "" "Microsoft Windows 10 or Apple macOS" {
deploymentNode "Web Browser" "" "Chrome, Firefox, Safari, or Edge" {
developerSinglePageApplicationInstance = containerInstance singlePageApplication
}
deploymentNode "Docker Container - Web Server" "" "Docker" {
deploymentNode "Apache Tomcat" "" "Apache Tomcat 8.x" {
developerWebApplicationInstance = containerInstance webApplication
developerApiApplicationInstance = containerInstance apiApplication
}
}
deploymentNode "Docker Container - Database Server" "" "Docker" {
deploymentNode "Database Server" "" "Oracle 12c" {
developerDatabaseInstance = containerInstance database
}
}
}
deploymentNode "Big Bank plc" "" "Big Bank plc data center" "" {
deploymentNode "bigbank-dev001" "" "" "" {
softwareSystemInstance mainframe
}
}
}
deploymentEnvironment "Live" {
deploymentNode "Customer's mobile device" "" "Apple iOS or Android" {
liveMobileAppInstance = containerInstance mobileApp
}
deploymentNode "Customer's computer" "" "Microsoft Windows or Apple macOS" {
deploymentNode "Web Browser" "" "Chrome, Firefox, Safari, or Edge" {
liveSinglePageApplicationInstance = containerInstance singlePageApplication
}
}
deploymentNode "Big Bank plc" "" "Big Bank plc data center" {
deploymentNode "bigbank-web***" "" "Ubuntu 16.04 LTS" "" 4 {
deploymentNode "Apache Tomcat" "" "Apache Tomcat 8.x" {
liveWebApplicationInstance = containerInstance webApplication
}
}
deploymentNode "bigbank-api***" "" "Ubuntu 16.04 LTS" "" 8 {
deploymentNode "Apache Tomcat" "" "Apache Tomcat 8.x" {
liveApiApplicationInstance = containerInstance apiApplication
}
}
deploymentNode "bigbank-db01" "" "Ubuntu 16.04 LTS" {
primaryDatabaseServer = deploymentNode "Oracle - Primary" "" "Oracle 12c" {
livePrimaryDatabaseInstance = containerInstance database
}
}
deploymentNode "bigbank-db02" "" "Ubuntu 16.04 LTS" "Failover" {
secondaryDatabaseServer = deploymentNode "Oracle - Secondary" "" "Oracle 12c" "Failover" {
liveSecondaryDatabaseInstance = containerInstance database "Failover"
}
}
deploymentNode "bigbank-prod001" "" "" "" {
softwareSystemInstance mainframe
}
}
primaryDatabaseServer -> secondaryDatabaseServer "Replicates data to"
}
deploymentEnvironment "Environment Landscape" {
deploymentNode "bigbank-prod001" {
softwareSystemInstance mainframe
}
deploymentNode "bigbank-preprod001" {
softwareSystemInstance mainframe
}
deploymentNode "bigbank-test001" {
softwareSystemInstance mainframe
}
deploymentNode "bigbank-staging1" {
softwareSystemInstance email
}
deploymentNode "bigbank-prod1" {
softwareSystemInstance email
}
}
}
views {
properties {
"c4plantuml.elementProperties" "true"
"c4plantuml.tags" "true"
"generatr.style.colors.primary" "#485fc7"
"generatr.style.colors.secondary" "#ffffff"
"generatr.style.faviconPath" "site/favicon.ico"
"generatr.style.logoPath" "site/logo.png"
// Absolute URL's like "https://example.com/custom.css" are also supported
"generatr.style.customStylesheet" "site/custom.css"
"generatr.svglink.target" "_self"
// Full list of available "generatr.markdown.flexmark.extensions"
// "Abbreviation,Admonition,AnchorLink,Aside,Attributes,Autolink,Definition,Emoji,EnumeratedReference,Footnotes,GfmIssues,GfmStrikethroughSubscript,GfmTaskList,GfmUsers,GitLab,Ins,Macros,MediaTags,ResizableImage,Superscript,Tables,TableOfContents,SimulatedTableOfContents,Typographic,WikiLinks,XWikiMacro,YAMLFrontMatter,YouTubeLink"
// see https://github.com/vsch/flexmark-java/wiki/Extensions
// ATTENTION:
// * "generatr.markdown.flexmark.extensions" values must be separated by comma
// * it's not possible to use "GitLab" and "ResizableImage" extensions together
// default behaviour, if no generatr.markdown.flexmark.extensions property is specified, is to load the Tables extension only
"generatr.markdown.flexmark.extensions" "Abbreviation,Admonition,AnchorLink,Attributes,Autolink,Definition,Emoji,Footnotes,GfmTaskList,GitLab,MediaTags,Tables,TableOfContents,Typographic"
"generatr.site.exporter" "structurizr"
"generatr.site.externalTag" "External System"
"generatr.site.nestGroups" "false"
"generatr.site.cdn" "https://cdn.jsdelivr.net/npm"
"generatr.site.theme" "auto"
}
systemlandscape "SystemLandscape" {
include *
autoLayout
}
image atm {
image atm/atm-example.png
title "ATM System"
description "Image View to show how the ATM system works internally"
}
systemcontext internetBankingSystem "SystemContext" {
include *
animation {
internetBankingSystem
customer
mainframe
email
}
autoLayout
title "System Context of Internet Banking System"
description "Describes the overall context"
}
container internetBankingSystem "Containers" {
include *
animation {
customer mainframe email
webApplication
singlePageApplication
mobileApp
apiApplication
database
}
autoLayout
}
component apiApplication "Components" {
include *
animation {
singlePageApplication mobileApp database email mainframe
signinController securityComponent
accountsSummaryController mainframeBankingSystemFacade
resetPasswordController emailComponent
}
autoLayout
}
image database {
image internet-banking-system/database-erd-example.jpg
title "Entity Relationship Diagram"
description "Image View to show the ERD diagram for the database container"
}
image accountsSummaryController {
image internet-banking-system/uml-class-diagram.png
title "AccountsSummaryController Zoom-In"
description "This is a sample imageView for code of a component"
}
dynamic apiApplication "SignIn" "Summarises how the sign in feature works in the single-page application." {
singlePageApplication -> signinController "Submits credentials to"
signinController -> securityComponent "Validates credentials using"
securityComponent -> database "select * from users where username = ?"
database -> securityComponent "Returns user data to"
securityComponent -> signinController "Returns true if the hashed password matches"
signinController -> singlePageApplication "Sends back an authentication token to"
autoLayout
}
deployment internetBankingSystem "Development" "DevelopmentDeployment" {
include *
animation {
developerSinglePageApplicationInstance
developerWebApplicationInstance developerApiApplicationInstance
developerDatabaseInstance
}
autoLayout
}
deployment internetBankingSystem "Live" "LiveDeployment" {
include *
animation {
liveSinglePageApplicationInstance
liveMobileAppInstance
liveWebApplicationInstance liveApiApplicationInstance
livePrimaryDatabaseInstance
liveSecondaryDatabaseInstance
}
autoLayout
}
deployment * "Environment Landscape" "EnvLandscapeMainframe" {
include mainframe
autoLayout
properties {
"generatr.view.deployment.belongsTo" "Mainframe Banking System"
}
}
styles {
element "Person" {
color #ffffff
shape Person
}
element "Customer" {
background #686868
}
element "Bank Staff" {
background #08427B
}
element "Software System" {
background #1168bd
color #ffffff
}
element "External System" {
background #686868
}
element "Existing System" {
background #999999
color #ffffff
}
element "Container" {
background #438dd5
color #ffffff
}
element "Web Browser" {
shape WebBrowser
}
element "Mobile App" {
shape MobileDeviceLandscape
}
element "Database" {
shape Cylinder
}
element "Component" {
background #85bbf0
color #000000
}
element "Failover" {
opacity 25
}
element "Group" {
background #f1f1f1
color #444444
}
element "Boundary" {
background #f1f1f1
color #444444
}
// default style
element "Element" {
fontSize 24
}
}
}
}
================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: gradlew
================================================
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
================================================
FILE: gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: package.json
================================================
{
"name": "structurizr-site-generatr",
"dependencies": {
"bulma": "1.0.4",
"katex": "0.16.33",
"lunr": "2.3.9",
"lunr-languages": "1.14.0",
"mermaid": "11.12.3",
"svg-pan-zoom": "3.6.2",
"webfontloader": "1.6.28"
}
}
================================================
FILE: renovate.json
================================================
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"prHourlyLimit": 2,
"prConcurrentLimit": 6,
"packageRules": [
{
"groupName": "kotlin",
"matchPackageNames": [
"org.jetbrains.kotlin.jvm{/,}**",
"org.jetbrains.kotlin.plugin.serialization{/,}**",
"org.jetbrains.kotlin:{/,}**"
]
},
{
"groupName": "kotlinx",
"matchPackageNames": ["org.jetbrains.kotlinx:{/,}**"]
},
{
"groupName": "structurizr",
"matchPackageNames": ["com.structurizr:{/,}**"]
},
{
"groupName": "jetty",
"matchPackageNames": ["org.eclipse.jetty:{/,}**", "org.eclipse.jetty.websocket:{/,}**"]
},
{
"matchDepNames": "net.sourceforge.plantuml:plantuml",
"allowedVersions": "/^[0-9]+\\.[0-9]+\\.[0-9]+$/"
},
{
"matchDepNames": "org.jruby:jruby-core",
"allowedVersions": "^9"
},
{
"groupName": "actions",
"matchDepTypes": ["action"]
}
]
}
================================================
FILE: settings.gradle.kts
================================================
rootProject.name = "structurizr-site-generatr"
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/App.kt
================================================
@file:Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@file:OptIn(ExperimentalCli::class)
package nl.avisi.structurizr.site.generatr
import kotlinx.cli.ArgParser
import kotlinx.cli.ExperimentalCli
fun main(args: Array<String>) {
val parser = ArgParser("structurizr-site-generatr")
parser.subcommands(ServeCommand(), GenerateSiteCommand(), VersionCommand())
parser.parse(args)
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/ClonedRepository.kt
================================================
package nl.avisi.structurizr.site.generatr
import org.eclipse.jgit.api.CreateBranchCommand
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.ListBranchCommand
import org.eclipse.jgit.storage.file.FileRepositoryBuilder
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider
import java.io.File
class ClonedRepository(
val cloneDir: File,
private val url: String,
private val username: String?,
private val password: String?
) {
private val repo = FileRepositoryBuilder.create(File(cloneDir, ".git"))
fun refreshLocalClone() {
val credentialsProvider = if (username != null)
UsernamePasswordCredentialsProvider(username, password)
else
null
if (cloneDir.isDirectory) {
Git(repo).pull()
.setCredentialsProvider(credentialsProvider)
.call()
} else {
cloneDir.deleteRecursively()
val cloneCommand = Git.cloneRepository()
.setURI(url)
.setDirectory(cloneDir)
if (credentialsProvider != null)
cloneCommand.setCredentialsProvider(credentialsProvider)
cloneCommand.call()
}
}
fun checkoutBranch(branch: String) {
val git = Git(repo)
git.branchCreate()
.setName(branch)
.setUpstreamMode(CreateBranchCommand.SetupUpstreamMode.SET_UPSTREAM)
.setStartPoint("origin/$branch")
.setForce(true)
.call()
git.checkout()
.setName(branch)
.call()
}
fun getBranchNames(excludeBranches: List<String>) =
Git(repo).branchList().setListMode(ListBranchCommand.ListMode.REMOTE).call()
.map { it.name.toString().substringAfter("/remotes/origin/") }
.onEach { println("Found the following branch: $it") }
.filter { it !in excludeBranches }
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/CreateStructurizrWorkspace.kt
================================================
package nl.avisi.structurizr.site.generatr
import com.structurizr.dsl.StructurizrDslParser
import com.structurizr.model.Element
import com.structurizr.view.ThemeUtils
import java.io.File
fun createStructurizrWorkspace(workspaceFile: File) =
StructurizrDslParser()
.apply { parse(workspaceFile) }
.workspace
.apply {
ThemeUtils.loadThemes(this)
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
}
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/GenerateSiteCommand.kt
================================================
@file:OptIn(ExperimentalCli::class)
package nl.avisi.structurizr.site.generatr
import kotlinx.cli.*
import nl.avisi.structurizr.site.generatr.site.*
import java.io.File
class GenerateSiteCommand : Subcommand(
"generate-site",
"Generate a site for the selected workspace."
) {
private val gitUrl by option(
ArgType.String, "git-url", "g",
"The URL of the Git repository which contains the Structurizr model. " +
"If a Git repository is provided, it will be cloned and" +
"--workspace-file and --assets-dir will be treated as paths within the cloned repository. " +
"If no Git repository is provided, --workspace-file and --assets-dir will be used as-is, and the site" +
"will only contain one branch, named after the --default-branch option."
)
private val gitUsername by option(
ArgType.String, "git-username", "u",
"Username for the Git repository"
)
private val gitPassword by option(
ArgType.String, "git-password", "p",
"Password for the Git repository"
)
private val workspaceFile by option(
ArgType.String, "workspace-file", "w",
"Relative path within the Git repository of the workspace file"
).required()
private val assetsDir by option(
ArgType.String, "assets-dir", "a",
"Relative path within the Git repository where static assets are located"
)
private val branches by option(
ArgType.String, "branches", "b",
"Comma-separated list of branches to include in the generated site. Not used if '--all-branches' option is set to true"
).default("master")
private val defaultBranch by option(
ArgType.String, "default-branch", "d",
"The default branch"
).default("master")
private val version by option(
ArgType.String, "version", "v",
"The version of the site"
).default("0.0.0")
private val outputDir by option(
ArgType.String, "output-dir", "o",
"Directory where the generated site will be stored. Will be created if it doesn't exist yet."
).default("build/site")
private val allBranches by option(
ArgType.Boolean, "all-branches", "all",
"When set, generate a site for every branch in the git repository"
).default(value = false)
private val excludeBranches by option(
ArgType.String, "exclude-branches", "ex",
"Comma-separated list of branches to exclude from the generated site"
).default("")
override fun execute() {
val siteDir = File(outputDir).apply { mkdirs() }
val gitUrl = gitUrl
generateRedirectingIndexPage(siteDir, defaultBranch)
copySiteWideAssets(siteDir)
if (gitUrl != null)
generateSiteForModelInGitRepository(gitUrl, siteDir)
else
generateSiteForModel(siteDir)
}
private fun generateSiteForModelInGitRepository(gitUrl: String, siteDir: File) {
val cloneDir = File("build/model-clone")
val clonedRepository = ClonedRepository(cloneDir, gitUrl, gitUsername, gitPassword).apply {
refreshLocalClone()
}
val branchNames = if (allBranches)
clonedRepository.getBranchNames(excludeBranches.split(","))
else
branches.split(",")
println("The following branches will be checked for Structurizr Workspaces: $branchNames")
val workspaceFileInRepo = File(clonedRepository.cloneDir, workspaceFile)
val branchesToGenerate = branchNames.filter { branch ->
println("Checking branch $branch")
try {
clonedRepository.checkoutBranch(branch)
createStructurizrWorkspace(workspaceFileInRepo)
true
} catch (e: Exception) {
val errorMessage = e.message ?: "Unknown error"
println("Bad Branch $branch: $errorMessage")
false
}
}.sortedWith(branchComparator(defaultBranch))
println("The following branches contain a valid Structurizr workspace: $branchesToGenerate")
if (!branchesToGenerate.contains(defaultBranch)) {
throw Exception("$defaultBranch does not contain a valid structurizr workspace. Site generation halted.")
}
branchesToGenerate.forEach { branch ->
println("Generating site for branch $branch")
clonedRepository.checkoutBranch(branch)
val workspace = createStructurizrWorkspace(workspaceFileInRepo)
writeStructurizrJson(workspace, File(siteDir, branch))
generateDiagrams(workspace, File(siteDir, branch))
generateSite(
version,
workspace,
assetsDir?.let { File(cloneDir, it) },
siteDir,
branchesToGenerate,
branch
)
}
}
private fun generateSiteForModel(siteDir: File) {
val workspace = createStructurizrWorkspace(File(workspaceFile))
writeStructurizrJson(workspace, File(siteDir, defaultBranch))
generateDiagrams(workspace, File(siteDir, defaultBranch))
generateSite(
version,
workspace,
assetsDir?.let { File(it) },
siteDir,
listOf(defaultBranch),
defaultBranch
)
}
}
fun branchComparator(defaultBranch: String) = Comparator<String> { a, b ->
if (a == defaultBranch) -1
else if (b == defaultBranch) 1
else a.compareTo(b, ignoreCase = true)
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/ServeCommand.kt
================================================
@file:OptIn(ExperimentalCli::class)
package nl.avisi.structurizr.site.generatr
import kotlinx.cli.*
import nl.avisi.structurizr.site.generatr.site.*
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.server.handler.ContextHandler
import org.eclipse.jetty.server.handler.ContextHandlerCollection
import org.eclipse.jetty.server.handler.ResourceHandler
import org.eclipse.jetty.util.resource.ResourceFactory
import org.eclipse.jetty.websocket.api.Callback
import org.eclipse.jetty.websocket.api.Session
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen
import org.eclipse.jetty.websocket.api.annotations.WebSocket
import org.eclipse.jetty.websocket.server.WebSocketUpgradeHandler
import java.io.File
import java.nio.file.*
import java.time.Duration
import kotlin.io.path.absolutePathString
import kotlin.io.path.extension
import kotlin.io.path.isDirectory
import kotlin.io.path.isHidden
import kotlin.io.path.isRegularFile
import kotlin.system.measureTimeMillis
class ServeCommand : Subcommand("serve", "Start a development server") {
private val workspaceFile by option(
ArgType.String, "workspace-file", "w", "The workspace file"
).required()
private val assetsDir by option(
ArgType.String, "assets-dir", "a", "Directory where static assets are located"
)
private val siteDir by option(
ArgType.String, "site-dir", "s", "Directory for the generated site"
).default("build/serve")
private val port by option(
ArgType.Int, "port", "p", "Port the site is served on"
).default(8080)
private val eventSockets = mutableListOf<EventSocket>()
private val eventSocketsLock = Any()
private var updateSiteError: String? = null
override fun execute() {
updateSite()
val server = runServer()
val watchService = startWatchService()
try {
server.join()
} finally {
println("Stop watching for changes")
watchService.close()
println("Stopping server...")
server.stop()
println("Server stopped")
}
}
private fun updateSite() {
val branch = "master"
val exportDir = File(siteDir, branch).apply { mkdirs() }
try {
val ms = measureTimeMillis {
broadcast("site-updating")
val workspace = createStructurizrWorkspace(File(workspaceFile))
println("Generating index page...")
generateRedirectingIndexPage(File(siteDir), branch)
println("Copying assets...")
copySiteWideAssets(File(siteDir))
println("Writing workspace.json...")
writeStructurizrJson(workspace, exportDir)
println("Generating diagrams...")
generateDiagrams(workspace, exportDir)
println("Generating site...")
generateSite(
"0.0.0",
workspace,
assetsDir?.let { File(it) },
File(siteDir),
listOf(branch),
branch,
serving = true
)
updateSiteError = null
broadcast("site-updated")
}
println("Successfully generated diagrams and site in ${ms.toDouble() / 1000} seconds")
} catch (e: Exception) {
updateSiteError = e.message ?: "Unknown error"
broadcast(updateSiteError!!)
e.printStackTrace()
}
}
private fun runServer(): Server =
Server(port).also { server ->
println("Starting server...")
server.handler = createRootContextHandler(server)
server.start()
println("Server started")
println("Open http://localhost:$port in your browser to view the site")
}
private fun createRootContextHandler(server: Server) = ContextHandlerCollection(
ContextHandler(createStaticResourceHandler(), "/"),
ContextHandler("/").apply {
handler = createWebSocketHandler(server, this, "/_events")
}
)
private fun createStaticResourceHandler() =
ResourceHandler().apply {
baseResource = ResourceFactory.of(this).newResource(File(siteDir).absolutePath)
isUseFileMapping = false
}
private fun createWebSocketHandler(
server: Server,
context: ContextHandler,
@Suppress("SameParameterValue") pathSpec: String
) =
WebSocketUpgradeHandler.from(server, context) { container ->
container.idleTimeout = Duration.ZERO
container.addMapping(pathSpec) { _, _, _ -> EventSocket() }
}
private fun startWatchService(): WatchService {
val path = File(workspaceFile).absoluteFile.parentFile.toPath()
val watchService = FileSystems.getDefault().newWatchService()
val absoluteSiteDir = File(siteDir).absolutePath
path.watch(watchService)
Files.walk(path)
.filter { it.isDirectory() && !it.isHidden() && !it.absolutePathString().startsWith(absoluteSiteDir) }
.forEach { it.watch(watchService) }
Thread {
try {
monitorFileChanges(watchService)
} catch (_: ClosedWatchServiceException) {
// ignore, server shutdown
}
}
.apply { isDaemon = true }
.start()
return watchService
}
private fun monitorFileChanges(watchService: WatchService) {
while (true) {
val watchKey = watchService.take()
val parentPath = watchKey.watchable() as Path
Thread.sleep(100) // Throttle the file watcher to give editors time for cleaning up temporary files
val events = watchKey.pollEvents().filterNot { isHashFile(it) }
val fileModified = events.map { parentPath.resolve(it.context() as Path) }
.any { it.isRegularFile() }
val fileOrDirectoryDeleted = events
.any { it.kind() == StandardWatchEventKinds.ENTRY_DELETE }
events.filter { it.kind() == StandardWatchEventKinds.ENTRY_CREATE }
.map { parentPath.resolve(it.context() as Path) }
.filter { it.isDirectory() && !it.isHidden() }
.forEach { it.watch(watchService) }
if (fileModified || fileOrDirectoryDeleted) {
println("Detected a change in ${watchKey.watchable()}, updating site...")
try {
updateSite()
} catch (e: Exception) {
System.err.println("An error occurred while updating the site")
e.printStackTrace()
}
}
watchKey.reset()
}
}
private fun isHashFile(it: WatchEvent<*>) = (it.context() as Path).extension == "md5"
private fun Path.watch(watchService: WatchService) {
register(
watchService,
arrayOf(
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.ENTRY_MODIFY
)
)
}
private fun broadcast(message: String) = synchronized(eventSocketsLock) {
eventSockets.forEach { it.send(message) }
}
@Suppress("UNUSED_PARAMETER", "unused")
@WebSocket
inner class EventSocket {
private var session: Session? = null
@OnWebSocketOpen
fun onWebSocketConnect(session: Session?) {
synchronized(eventSocketsLock) { eventSockets.add(this) }
this.session = session
updateSiteError?.let { send(it) }
}
@OnWebSocketClose
fun onWebSocketClose(statusCode: Int, reason: String?) {
session = null
synchronized(eventSocketsLock) { eventSockets.remove(this) }
}
@OnWebSocketError
fun onWebSocketError(cause: Throwable?) {
session = null
synchronized(eventSocketsLock) { eventSockets.remove(this) }
}
fun send(message: String) {
session?.let {
if (it.isOpen)
it.sendText(message, Callback.NOOP)
}
}
}
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/StringUtilities.kt
================================================
package nl.avisi.structurizr.site.generatr
// based on https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names
private const val reservedChars = "|\\?*<\":>+[]/'"
private val reservedNames = setOf(
"CON", "PRN", "AUX", "NUL",
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"
)
fun String.normalize(): String =
lowercase()
.replace("\\s+".toRegex(), "-")
.filterNot { reservedChars.contains(it) }
.trim()
.let {
if (reservedNames.contains(it.uppercase()))
"${it}-"
else
it
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/StructurizrUtilities.kt
================================================
package nl.avisi.structurizr.site.generatr
import com.structurizr.Workspace
import com.structurizr.model.Component
import com.structurizr.model.Container
import com.structurizr.model.SoftwareSystem
import com.structurizr.model.StaticStructureElement
import com.structurizr.view.ViewSet
import nl.avisi.structurizr.site.generatr.site.GeneratorContext
val Workspace.includedSoftwareSystems: List<SoftwareSystem>
get() = model.softwareSystems.filter {
val externalTag = views.configuration.properties.getOrDefault("generatr.site.externalTag", null)
if (externalTag != null) !it.tags.contains(externalTag) else true
}
fun Workspace.hasImageViews(id: String) = views.imageViews.any { it.elementId == id }
fun Workspace.hasComponentDiagrams(container: Container) = views.componentViews.any { it.container == container}
val SoftwareSystem.hasContainers
get() = this.containers.isNotEmpty()
val StaticStructureElement.includedProperties
get() = this.properties.filterNot { (name, _) -> name.startsWith("structurizr.") or name.startsWith("generatr.") }
val Container.hasComponents
get() = this.components.isNotEmpty()
fun SoftwareSystem.firstContainer(generatorContext: GeneratorContext) = containers
.sortedBy { it.name }.firstOrNull { container ->
generatorContext.workspace.hasComponentDiagrams(container) or
generatorContext.workspace.hasImageViews(container.id) }
fun SoftwareSystem.hasDecisions(recursive: Boolean = false) = documentation.decisions.isNotEmpty() || (recursive && hasContainerDecisions(recursive = true))
fun SoftwareSystem.hasContainerDecisions(recursive: Boolean = false) = containers.any { it.hasDecisions() || (recursive && hasComponentDecisions()) }
fun SoftwareSystem.hasComponentDecisions() = containers.any { it.hasComponentDecisions() }
fun SoftwareSystem.hasDocumentationSections(recursive: Boolean = false) = documentation.sections.size >= 2 || (recursive && hasContainerDocumentationSections(recursive))
fun SoftwareSystem.hasContainerDocumentationSections(recursive: Boolean = false) = containers.any { it.hasSections(recursive) } || (recursive && hasComponentDocumentationSections())
fun SoftwareSystem.hasComponentDocumentationSections() = containers.any { it.hasComponentsSections() }
fun Container.firstComponent(generatorContext: GeneratorContext) = components
.sortedBy { it.name }.firstOrNull {
component -> generatorContext.workspace.hasImageViews(component.id) }
fun Container.hasDecisions(recursive: Boolean = false) = documentation.decisions.isNotEmpty() || (recursive && hasComponentDecisions())
fun Container.hasComponentDecisions() = components.any { it.hasDecisions() }
fun Container.hasSections(recursive: Boolean = false) = documentation.sections.isNotEmpty() || (recursive && hasComponentsSections())
fun Container.hasComponentsSections() = components.any { it.hasSections() }
fun Component.hasDecisions() = documentation.decisions.isNotEmpty()
fun Component.hasSections() = documentation.sections.isNotEmpty()
fun ViewSet.hasSystemContextViews(softwareSystem: SoftwareSystem) =
systemContextViews.any { it.softwareSystem == softwareSystem }
fun ViewSet.hasContainerViews(workspace: Workspace, softwareSystem: SoftwareSystem) =
containerViews.any { it.softwareSystem == softwareSystem } ||
workspace.views.imageViews.any { it.elementId == softwareSystem.id }
fun ViewSet.hasComponentViews(workspace: Workspace, softwareSystem: SoftwareSystem) =
componentViews.any { it.softwareSystem == softwareSystem } ||
workspace.views.imageViews.any { it.elementId in softwareSystem.containers.map { cn -> cn.id } }
fun ViewSet.hasCodeViews(workspace: Workspace, softwareSystem: SoftwareSystem) =
componentViews.any { it.softwareSystem == softwareSystem } &&
workspace.views.imageViews.any { it.elementId in softwareSystem.containers.flatMap { cn -> cn.components.map { com -> com.id } } }
fun ViewSet.hasDynamicViews(softwareSystem: SoftwareSystem) =
dynamicViews.any { it.softwareSystem == softwareSystem }
fun ViewSet.hasDeploymentViews(softwareSystem: SoftwareSystem) = with(deploymentViews) {
any { it.softwareSystem == softwareSystem } || any { it.properties["generatr.view.deployment.belongsTo"] == softwareSystem.name }
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/VersionCommand.kt
================================================
@file:OptIn(ExperimentalCli::class)
package nl.avisi.structurizr.site.generatr
import kotlinx.cli.ExperimentalCli
import kotlinx.cli.Subcommand
class VersionCommand : Subcommand("version", "Print version information") {
override fun execute() {
val pkg = javaClass.`package`
println("${pkg.implementationTitle} v${pkg.implementationVersion}")
}
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/DateFormatter.kt
================================================
package nl.avisi.structurizr.site.generatr.site
import java.time.ZoneId
import java.util.*
fun formatDate(date: Date): String {
val localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()
return String.format("%02d-%02d-%04d", localDate.dayOfMonth, localDate.monthValue, localDate.year)
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/DiagramGenerator.kt
================================================
package nl.avisi.structurizr.site.generatr.site
import com.structurizr.Workspace
import com.structurizr.export.Diagram
import com.structurizr.export.plantuml.PlantUMLDiagram
import com.structurizr.view.ModelView
import com.structurizr.view.View
import net.sourceforge.plantuml.FileFormat
import net.sourceforge.plantuml.FileFormatOption
import net.sourceforge.plantuml.SourceStringReader
import java.io.ByteArrayOutputStream
import java.io.File
import java.net.URI
import java.util.concurrent.ConcurrentHashMap
fun generateDiagrams(workspace: Workspace, exportDir: File) {
val pumlDir = pumlDir(exportDir)
val svgDir = svgDir(exportDir)
val pngDir = pngDir(exportDir)
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}...")
saveAsSvg(diagram, svgDir)
saveAsPng(diagram, pngDir)
saveAsPUML(diagram, plantUMLFile)
} else {
println("${diagram.key} UP-TO-DATE")
}
}
}
fun generateDiagramWithElementLinks(
workspace: Workspace,
view: View,
url: String,
diagramCache: ConcurrentHashMap<String, String>
): String {
val diagram = generatePlantUMLDiagramWithElementLinks(workspace, view, url)
val name = "${diagram.key}-${view.key}"
return diagramCache.getOrPut(name) {
val reader = SourceStringReader(diagram.withCachedIncludes().definition)
val stream = ByteArrayOutputStream()
reader.outputImage(stream, FileFormatOption(FileFormat.SVG, false))
stream.toString(Charsets.UTF_8)
}
}
private fun generatePlantUMLDiagrams(workspace: Workspace): Collection<Diagram> {
val plantUMLExporter = PlantUmlExporter(workspace)
return plantUMLExporter.export()
}
private fun saveAsPUML(diagram: Diagram, plantUMLFile: File) {
plantUMLFile.writeText(diagram.definition)
}
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")
pngFile.outputStream().use {
reader.outputImage(it)
}
}
private fun generatePlantUMLDiagramWithElementLinks(workspace: Workspace, view: View, url: String): Diagram {
val plantUMLExporter = PlantUmlExporterWithElementLinks(workspace, url)
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])
"!include $cachedInclude"
}
return PlantUMLDiagram(view as ModelView, def)
}
private object IncludeCache {
private val cache = mutableMapOf<String, String>()
private val cacheDir = File("build/puml-cache").apply { mkdirs() }
fun cachedInclude(includedFile: String): String {
if (!includedFile.startsWith("http"))
return includedFile
return cache.getOrPut(includedFile) {
val fileName = includedFile.split("/").last()
val cachedFile = File(cacheDir, fileName)
if (!cachedFile.exists())
downloadIncludedFile(includedFile, cachedFile)
cachedFile.absolutePath
}
}
private fun downloadIncludedFile(includedFile: String, cachedFile: File) {
URI(includedFile).toURL().openStream().use { inputStream ->
cachedFile.outputStream().use { outputStream ->
inputStream.copyTo(outputStream)
}
}
}
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/GeneratorContext.kt
================================================
package nl.avisi.structurizr.site.generatr.site
import com.structurizr.Workspace
data class GeneratorContext(
val version: String,
val workspace: Workspace,
val branches: List<String>,
val currentBranch: String,
val serving: Boolean,
val svgFactory: (key: String, url: String) -> String?
)
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/PlantUmlExporter.kt
================================================
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.export.plantuml.StructurizrPlantUMLExporter
import com.structurizr.model.Component
import com.structurizr.model.Container
import com.structurizr.model.Element
import com.structurizr.model.SoftwareSystem
import com.structurizr.view.*
import nl.avisi.structurizr.site.generatr.*
enum class ExporterType { C4, STRUCTURIZR }
class PlantUmlExporter(val workspace: Workspace) {
private val exporter = when (workspace.exporterType()) {
ExporterType.C4 -> C4PlantUMLExporter(ColorScheme.Light)
ExporterType.STRUCTURIZR -> StructurizrPlantUMLExporter(ColorScheme.Light)
}
fun export(): Collection<Diagram> = exporter.export(workspace)
}
class PlantUmlExporterWithElementLinks(workspace: Workspace, url: String) {
private val exporter = when (workspace.exporterType()) {
ExporterType.C4 -> C4PlantUmlExporterWithElementLinks(workspace, url, ColorScheme.Light)
ExporterType.STRUCTURIZR -> StructurizrPlantUmlExporterWithElementLinks(workspace, url, ColorScheme.Light)
}
init {
if (workspace.views.configuration.properties.containsKey("generatr.svglink.target")) {
exporter.addSkinParam(
"svgLinkTarget",
workspace.views.configuration.properties.getValue("generatr.svglink.target")
)
}
}
fun export(view: View): Diagram = when (view) {
is CustomView -> exporter.export(view)
is SystemLandscapeView -> exporter.export(view)
is SystemContextView -> exporter.export(view)
is ContainerView -> exporter.export(view)
is ComponentView -> exporter.export(view)
is DynamicView -> exporter.export(view)
is DeploymentView -> exporter.export(view)
else -> throw IllegalStateException("View ${view.name} has a non-exportable type")
}
}
fun Workspace.exporterType() = views.configuration.properties
.getOrDefault("generatr.site.exporter", "c4")
.let { ExporterType.valueOf(it.uppercase()) }
private class WriterWithElementLinks(
private val workspace: Workspace,
private val url: String
) {
companion object {
const val TEMP_URI = "https://will-be-changed-to-relative/"
}
fun writeHeader(
view: ModelView,
writer: IndentingWriter,
writeHeaderFn: (view: ModelView, writer: IndentingWriter) -> Unit
) {
writeHeaderFn(view, writer)
writer.writeLine("skinparam svgDimensionStyle false")
writer.writeLine("skinparam preserveAspectRatio meet")
}
fun writeElement(
view: ModelView?,
element: Element?,
writer: IndentingWriter?,
writeElementFn: (view: ModelView?, element: Element?, writer: IndentingWriter?) -> Unit
) {
val url = when {
needsLinkToSoftwareSystem(element, view) -> getUrlToElement(element, "context")
needsLinkToContainerViews(element, view, workspace) -> getUrlToElement(element)
needsLinkToComponentViews(element, view, workspace) -> getUrlToElement(element)
needsLinkToCodeViews(element, workspace) -> getUrlToElement(element)
else -> null
}
if (url != null)
writeElementWithCustomUrl(element, url, view, writer, writeElementFn)
else
writeElementFn(view, element, writer)
}
private fun needsLinkToSoftwareSystem(element: Element?, view: ModelView?) =
element is SoftwareSystem && workspace.includedSoftwareSystems.contains(element) && element != view?.softwareSystem
private fun needsLinkToContainerViews(element: Element?, view: ModelView?, workspace: Workspace) =
element is SoftwareSystem && workspace.includedSoftwareSystems.contains(element) && element == view?.softwareSystem &&
(element.hasContainers || workspace.hasImageViews(element.id))
private fun needsLinkToComponentViews(element: Element?, view: ModelView?, workspace: Workspace) =
element is Container && (element.hasComponents || workspace.hasImageViews(element.id)) && view !is ComponentView
private fun needsLinkToCodeViews(element: Element?, workspace: Workspace) =
element is Component && workspace.hasImageViews(element.id)
private fun getUrlToElement(element: Element?, page: String? = null): String {
val path = when (element) {
is SoftwareSystem -> "/${element.name?.normalize()}/${page?.let { page } ?: "container"}/".asUrlToDirectory(url)
is Container -> "/${element.parent?.name?.normalize()}/${page?.let { page } ?: "component"}/${element.name?.normalize()}".asUrlToDirectory(url)
is Component -> "/${element.parent?.parent?.name?.normalize()}/${page?.let { page } ?: "code"}/${element.container?.name?.normalize()}/${element.name?.normalize()}".asUrlToDirectory(url)
else -> throw IllegalStateException("Not supported element")
}
return "$TEMP_URI$path"
}
private fun writeElementWithCustomUrl(
element: Element?,
url: String?,
view: ModelView?,
writer: IndentingWriter?,
writeElementFn: (view: ModelView?, element: Element?, writer: IndentingWriter?) -> Unit
) {
element?.url = url
writeModifiedElement(view, element, writer, writeElementFn)
element?.url = null
}
private fun writeModifiedElement(
view: ModelView?,
element: Element?,
writer: IndentingWriter?,
writeElement: (view: ModelView?, element: Element?, writer: IndentingWriter?) -> Unit
) = IndentingWriter().let {
writeElement(view, element, it)
it.toString()
.replace(TEMP_URI, "")
.lines()
.forEach { line -> writer?.writeLine(line) }
}
}
class C4PlantUmlExporterWithElementLinks(workspace: Workspace, url: String, colorScheme: ColorScheme = ColorScheme.Light) : C4PlantUMLExporter(colorScheme) {
private val withElementLinks = WriterWithElementLinks(workspace, url)
override fun writeHeader(view: ModelView, writer: IndentingWriter) {
withElementLinks.writeHeader(view, writer) { v, w -> super.writeHeader(v, w) }
}
override fun writeElement(view: ModelView?, element: Element?, writer: IndentingWriter?) {
withElementLinks.writeElement(view, element, writer) { v, e, w -> super.writeElement(v, e, w) }
}
}
class StructurizrPlantUmlExporterWithElementLinks(workspace: Workspace, url: String, colorScheme: ColorScheme = ColorScheme.Light) : StructurizrPlantUMLExporter(colorScheme) {
private val withElementLinks = WriterWithElementLinks(workspace, url)
override fun writeHeader(view: ModelView, writer: IndentingWriter) {
withElementLinks.writeHeader(view, writer) { v, w -> super.writeHeader(v, w) }
}
override fun writeElement(view: ModelView?, element: Element?, writer: IndentingWriter?) {
withElementLinks.writeElement(view, element, writer) { v, e, w -> super.writeElement(v, e, w) }
}
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/RelativeUrl.kt
================================================
package nl.avisi.structurizr.site.generatr.site
import java.io.File
fun String.asUrlToDirectory(otherUrl: String) = "${this.asUrlToFile(otherUrl)}/"
fun String.asUrlToFile(relativeTo: String): String =
if (relativeTo == this) "."
else File(this)
.relativeTo(File(relativeTo))
.invariantSeparatorsPath
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/SiteGenerator.kt
================================================
package nl.avisi.structurizr.site.generatr.site
import com.structurizr.Workspace
import com.structurizr.util.WorkspaceUtils
import kotlinx.html.*
import kotlinx.html.stream.appendHTML
import nl.avisi.structurizr.site.generatr.*
import nl.avisi.structurizr.site.generatr.site.model.*
import nl.avisi.structurizr.site.generatr.site.views.*
import java.io.File
import java.math.BigInteger
import java.security.MessageDigest
import java.util.concurrent.ConcurrentHashMap
fun copySiteWideAssets(exportDir: File) {
copySiteWideAsset(exportDir, "/css/style.css")
copySiteWideAsset(exportDir, "/js/header.js")
copySiteWideAsset(exportDir, "/js/svg-modal.js")
copySiteWideAsset(exportDir, "/js/modal.js")
copySiteWideAsset(exportDir, "/js/search.js")
copySiteWideAsset(exportDir, "/js/auto-reload.js")
copySiteWideAsset(exportDir, "/css/admonition.css")
copySiteWideAsset(exportDir, "/js/admonition.js")
copySiteWideAsset(exportDir, "/js/reformat-mermaid.js")
copySiteWideAsset(exportDir, "/css/treeview.css")
copySiteWideAsset(exportDir, "/js/treeview.js")
copySiteWideAsset(exportDir, "/js/katex-render.js")
copySiteWideAsset(exportDir, "/js/toggle-theme.js")
}
private fun copySiteWideAsset(exportDir: File, asset: String) {
val content = object {}.javaClass.getResource("/assets$asset")?.readText()
?: throw IllegalStateException("File $asset not found on classpath")
val file = File(exportDir, asset.substringAfterLast('/'))
file.writeText(content)
}
fun generateRedirectingIndexPage(exportDir: File, defaultBranch: String) {
val htmlFile = File(exportDir, "index.html")
htmlFile.writeText(
buildString {
appendLine("<!doctype html>")
appendHTML().html {
attributes["lang"] = "en"
head {
meta {
httpEquiv = "refresh"
content = "0; url=$defaultBranch/"
}
title { +"Structurizr site generatr" }
}
body()
}
}
)
}
fun writeStructurizrJson(workspace: Workspace, exportDir: File) {
val json = WorkspaceUtils.toJson(workspace, true)
File(exportDir, "workspace.json")
.apply { parentFile.mkdirs() }
.writeText(json)
}
fun generateSite(
version: String,
workspace: Workspace,
assetsDir: File?,
exportDir: File,
branches: List<String>,
currentBranch: String,
serving: Boolean = false
) {
val generatorContext = GeneratorContext(version, workspace, branches, currentBranch, serving) { key, url ->
val diagramCache = ConcurrentHashMap<String, String>()
workspace.views.views.singleOrNull { view -> view.key == key }
?.let { generateDiagramWithElementLinks(workspace, it, url, diagramCache) }
}
val branchDir = File(exportDir, currentBranch)
deleteOldHashes(branchDir)
if (assetsDir != null) copyAssets(assetsDir, branchDir)
generateStyle(generatorContext, branchDir)
generateHtmlFiles(generatorContext, branchDir)
}
private fun deleteOldHashes(branchDir: File) = branchDir.walk().filter { it.extension == "md5" }
.forEach { it.delete() }
private fun copyAssets(assetsDir: File, branchDir: File) {
assetsDir.copyRecursively(branchDir, overwrite = true)
}
private fun generateStyle(context: GeneratorContext, branchDir: File) {
val configuration = context.workspace.views.configuration.properties
val primary = configuration.getOrDefault("generatr.style.colors.primary", "#333333")
val secondary = configuration.getOrDefault("generatr.style.colors.secondary", "#cccccc")
val file = File(branchDir, "style-branding.css")
val content = """
.navbar .has-site-branding {
background-color: $primary!important;
color: $secondary!important;
}
.navbar .has-site-branding::after {
border-color: $secondary!important;
}
.menu .has-site-branding a.is-active {
color: $secondary!important;
background-color: $primary!important;
}
.input.has-site-branding:focus {
border-color: $secondary!important;
box-shadow: 0 0 0 0.125em $secondary;
}
""".trimIndent()
file.writeText(content)
}
private fun generateHtmlFiles(context: GeneratorContext, branchDir: File) {
buildList {
add { writeHtmlFile(branchDir, HomePageViewModel(context)) }
add { writeHtmlFile(branchDir, WorkspaceDecisionsPageViewModel(context)) }
add { writeHtmlFile(branchDir, SoftwareSystemsPageViewModel(context)) }
add { writeHtmlFile(branchDir, SearchViewModel(context)) }
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.includedSoftwareSystems.forEach {
add { writeHtmlFile(branchDir, SoftwareSystemHomePageViewModel(context, it)) }
add { writeHtmlFile(branchDir, SoftwareSystemContextPageViewModel(context, it)) }
add { writeHtmlFile(branchDir, SoftwareSystemContainerPageViewModel(context, it)) }
add { writeHtmlFile(branchDir, SoftwareSystemDynamicPageViewModel(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 ->
add { writeHtmlFile(branchDir, SoftwareSystemDecisionPageViewModel(context, it, decision)) }
}
it.containers
.filter { container -> container.hasDecisions(recursive = true) }
.onEach { container ->
add { writeHtmlFile(branchDir, SoftwareSystemContainerDecisionsPageViewModel(context, container)) }
container.documentation.decisions.forEach { decision ->
add { writeHtmlFile(branchDir, SoftwareSystemContainerDecisionPageViewModel(context, container, decision)) }
}
}
.flatMap { container -> container.components }
.filter { component -> component.hasDecisions() }
.forEach { component ->
add { writeHtmlFile(branchDir, SoftwareSystemContainerComponentDecisionsPageViewModel(context, component)) }
component.documentation.decisions.forEach { decision ->
add { writeHtmlFile(branchDir, SoftwareSystemContainerComponentDecisionPageViewModel(context, component, decision)) }
}
}
it.containers
.filter { container ->
context.workspace.hasComponentDiagrams(container) or
container.includedProperties.isNotEmpty() or
context.workspace.hasImageViews(container.id) }
.forEach { container ->
add { writeHtmlFile(branchDir, SoftwareSystemContainerComponentsPageViewModel(context, container)) } }
it.containers
.filter { container ->
( context.workspace.hasComponentDiagrams(container) or
context.workspace.hasImageViews(container.id)) }
.forEach { container ->
container.components.filter { component ->
context.workspace.hasImageViews(component.id) }
.forEach { component ->
add { writeHtmlFile(branchDir, SoftwareSystemContainerComponentCodePageViewModel(context, container, component)) }
}
}
it.documentation.sections
.filter { section -> section.order != 1 }
.forEach { section ->
add { writeHtmlFile(branchDir, SoftwareSystemSectionPageViewModel(context, it, section)) }
}
it.containers
.filter { container -> container.hasSections(recursive = true) }
.onEach { container ->
add { writeHtmlFile(branchDir, SoftwareSystemContainerSectionsPageViewModel(context, container)) }
container.documentation.sections.forEach { section ->
add { writeHtmlFile(branchDir, SoftwareSystemContainerSectionPageViewModel(context, container, section)) }
}
}
.flatMap { container -> container.components }
.filter { component -> component.hasSections() }
.forEach { component ->
add { writeHtmlFile(branchDir, SoftwareSystemContainerComponentSectionsPageViewModel(context, component)) }
component.documentation.sections.forEach { section ->
add { writeHtmlFile(branchDir, SoftwareSystemContainerComponentSectionPageViewModel(context, component, section)) }
}
}
}
}
.parallelStream()
.forEach { it.invoke() }
}
private fun writeHtmlFile(exportDir: File, viewModel: PageViewModel) {
val html = buildString {
appendLine("<!doctype html>")
appendHTML().html {
when (viewModel) {
is HomePageViewModel -> homePage(viewModel)
is SearchViewModel -> searchPage(viewModel)
is SoftwareSystemsPageViewModel -> softwareSystemsPage(viewModel)
is SoftwareSystemHomePageViewModel -> softwareSystemHomePage(viewModel)
is SoftwareSystemContextPageViewModel -> softwareSystemContextPage(viewModel)
is SoftwareSystemContainerPageViewModel -> softwareSystemContainerPage(viewModel)
is SoftwareSystemContainerDecisionPageViewModel -> softwareSystemContainerDecisionPage(viewModel)
is SoftwareSystemContainerDecisionsPageViewModel -> softwareSystemContainerDecisionsPage(viewModel)
is SoftwareSystemContainerSectionPageViewModel -> softwareSystemContainerSectionPage(viewModel)
is SoftwareSystemContainerSectionsPageViewModel -> softwareSystemContainerSectionsPage(viewModel)
is SoftwareSystemContainerComponentsPageViewModel -> softwareSystemContainerComponentsPage(viewModel)
is SoftwareSystemContainerComponentCodePageViewModel -> softwareSystemContainerComponentCodePage(viewModel)
is SoftwareSystemContainerComponentDecisionPageViewModel -> softwareSystemContainerComponentDecisionPage(viewModel)
is SoftwareSystemContainerComponentDecisionsPageViewModel -> softwareSystemContainerComponentDecisionsPage(viewModel)
is SoftwareSystemContainerComponentSectionPageViewModel -> softwareSystemContainerComponentSectionPage(viewModel)
is SoftwareSystemContainerComponentSectionsPageViewModel -> softwareSystemContainerComponentSectionsPage(viewModel)
is SoftwareSystemDynamicPageViewModel -> softwareSystemDynamicPage(viewModel)
is SoftwareSystemDeploymentPageViewModel -> softwareSystemDeploymentPage(viewModel)
is SoftwareSystemDependenciesPageViewModel -> softwareSystemDependenciesPage(viewModel)
is SoftwareSystemDecisionPageViewModel -> softwareSystemDecisionPage(viewModel)
is SoftwareSystemDecisionsPageViewModel -> softwareSystemDecisionsPage(viewModel)
is SoftwareSystemSectionPageViewModel -> softwareSystemSectionPage(viewModel)
is SoftwareSystemSectionsPageViewModel -> softwareSystemSectionsPage(viewModel)
is WorkspaceDecisionPageViewModel -> workspaceDecisionPage(viewModel)
is WorkspaceDecisionsPageViewModel -> workspaceDecisionsPage(viewModel)
is WorkspaceDocumentationSectionPageViewModel -> workspaceDocumentationSectionPage(viewModel)
}
}
}
val subDirectory = File(exportDir, viewModel.url)
val htmlFile = File(subDirectory, "index.html")
htmlFile.parentFile.mkdirs()
htmlFile.writeText(html)
val hash = MessageDigest.getInstance("MD5").digest(html.toByteArray())
.let { BigInteger(1, it).toString(16) }
val hashFile = File("${htmlFile.absolutePath}.md5")
hashFile.writeText(hash)
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/Asciidoctor.kt
================================================
package nl.avisi.structurizr.site.generatr.site.model
import net.sourceforge.plantuml.FileFormat
import net.sourceforge.plantuml.FileFormatOption
import net.sourceforge.plantuml.SourceStringReader
import org.asciidoctor.Asciidoctor
import org.asciidoctor.ast.ContentModel
import org.asciidoctor.ast.ContentNode
import org.asciidoctor.ast.Document
import org.asciidoctor.ast.StructuralNode
import org.asciidoctor.converter.ConverterFor
import org.asciidoctor.converter.StringConverter
import org.asciidoctor.extension.BlockProcessor
import org.asciidoctor.extension.Contexts
import org.asciidoctor.extension.Name
import org.asciidoctor.extension.Reader
import java.io.ByteArrayOutputStream
val asciidoctorWithTextConverter: Asciidoctor = Asciidoctor.Factory.create().apply {
javaConverterRegistry().register(AsciiDocTextConverter::class.java)
}
val asciidoctorWithPUMLRenderer: Asciidoctor = Asciidoctor.Factory.create().apply {
javaExtensionRegistry().block(PlantUMLBlockProcessor::class.java)
}
@ConverterFor("text")
class AsciiDocTextConverter(
backend: String?,
opts: Map<String?, Any?>?
) : StringConverter(backend, opts) {
// based on https://docs.asciidoctor.org/asciidoctorj/latest/write-converter/
override fun convert(node: ContentNode, transform: String?, o: Map<Any?, Any?>?): String? {
val transform1 = transform ?: node.nodeName
return if (node is Document)
node.content.toString()
else if (node is org.asciidoctor.ast.Section)
"${node.title}\n${node.content}"
else if (transform1 == "preamble" || transform1 == "paragraph")
(node as StructuralNode).content as String
else
null
}
}
@Name("plantuml")
@Contexts(value = [Contexts.LISTING])
@ContentModel(ContentModel.VERBATIM)
class PlantUMLBlockProcessor : BlockProcessor() {
override fun process(parent: StructuralNode?, reader: Reader, attributes: MutableMap<String, Any>?): Any {
val plantUMLCode = reader.read()
val plantUMLReader = SourceStringReader(plantUMLCode)
val stream = ByteArrayOutputStream()
plantUMLReader.outputImage(stream, FileFormatOption(FileFormat.SVG, false))
return createBlock(parent, "pass", "<div>$stream</div>")
}
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/BranchHomeLinkViewModel.kt
================================================
package nl.avisi.structurizr.site.generatr.site.model
import nl.avisi.structurizr.site.generatr.site.asUrlToDirectory
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
data class BranchHomeLinkViewModel(
private val pageViewModel: PageViewModel,
private val branchName: String
) {
val title get() = branchName
val relativeHref get() = HomePageViewModel.url().asUrlToDirectory(pageViewModel.url) + "../${URLEncoder.encode(branchName, StandardCharsets.UTF_8)}/"
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ComponentTabViewModel.kt
================================================
package nl.avisi.structurizr.site.generatr.site.model
data class ComponentTabViewModel(val pageViewModel: PageViewModel, val title: String, val url: String) {
val link = LinkViewModel(pageViewModel, title, url)
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ComponentsTabViewModel.kt
================================================
package nl.avisi.structurizr.site.generatr.site.model
import com.structurizr.model.Container
import nl.avisi.structurizr.site.generatr.hasImageViews
import nl.avisi.structurizr.site.generatr.site.GeneratorContext
fun SoftwareSystemPageViewModel.createComponentsTabViewModel(
generatorContext: GeneratorContext,
container: Container
) = buildList {
container
.components
.sortedBy { it.name }
.filter { component ->
generatorContext.workspace.hasImageViews(component.id) }
.map {
ComponentTabViewModel(
this@createComponentsTabViewModel,
it.name,
SoftwareSystemContainerComponentCodePageViewModel.url(it.container, it)
)
}
.forEach { add(it) }
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ContainerTabViewModel.kt
================================================
package nl.avisi.structurizr.site.generatr.site.model
data class ContainerTabViewModel(val pageViewModel: SoftwareSystemPageViewModel, val title: String, val url: String, val match: Match = Match.EXACT) {
val link = LinkViewModel(pageViewModel, title, url, match)
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ContainersCodeTabViewModel.kt
================================================
package nl.avisi.structurizr.site.generatr.site.model
import com.structurizr.model.SoftwareSystem
import nl.avisi.structurizr.site.generatr.hasComponentDiagrams
import nl.avisi.structurizr.site.generatr.hasImageViews
import nl.avisi.structurizr.site.generatr.site.GeneratorContext
fun SoftwareSystemPageViewModel.createContainersCodeTabViewModel(
generatorContext: GeneratorContext,
softwareSystem: SoftwareSystem,
) = buildList {
softwareSystem
.containers
.sortedBy { it.name }
.filter { container ->
(generatorContext.workspace.hasComponentDiagrams(container) or
generatorContext.workspace.hasImageViews(container.id)) and
container.components.any { component -> generatorContext.workspace.hasImageViews(component.id) }
}
.map { container ->
ContainerTabViewModel(
this@createContainersCodeTabViewModel,
container.name,
SoftwareSystemContainerComponentCodePageViewModel.url(container, container.components.sortedBy { it.name }.firstOrNull { component -> generatorContext.workspace.hasImageViews(component.id) }),
Match.SIBLING
)
}
.forEach { add(it) }
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ContainersComponentTabViewModel.kt
================================================
package nl.avisi.structurizr.site.generatr.site.model
import com.structurizr.model.SoftwareSystem
import nl.avisi.structurizr.site.generatr.hasComponentDiagrams
import nl.avisi.structurizr.site.generatr.hasImageViews
import nl.avisi.structurizr.site.generatr.includedProperties
import nl.avisi.structurizr.site.generatr.site.GeneratorContext
fun SoftwareSystemPageViewModel.createContainersComponentTabViewModel(
generatorContext: GeneratorContext,
softwareSystem: SoftwareSystem,
) = buildList {
softwareSystem
.containers
.sortedBy { it.name }
.filter { container ->
container.includedProperties.isNotEmpty() or
generatorContext.workspace.hasComponentDiagrams(container) or
generatorContext.workspace.hasImageViews(container.id) }
.map {
ContainerTabViewModel(
this@createContainersComponentTabViewModel,
it.name,
SoftwareSystemContainerComponentsPageViewModel.url(it)
)
}
.forEach { add(it) }
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ContentText.kt
================================================
package nl.avisi.structurizr.site.generatr.site.model
import com.structurizr.documentation.Decision
import com.structurizr.documentation.Format
import com.structurizr.documentation.Section
import com.vladsch.flexmark.ast.Heading
import com.vladsch.flexmark.ast.Paragraph
import com.vladsch.flexmark.parser.Parser
import org.asciidoctor.Options
import org.asciidoctor.SafeMode
private val parser = Parser.builder().build()
fun Decision.contentText(): String = when (format) {
Format.Markdown -> markdownText(content)
Format.AsciiDoc -> asciidocText(content)
else -> ""
}
fun Section.contentText() = when (format) {
Format.Markdown -> markdownText(content)
Format.AsciiDoc -> asciidocText(content)
else -> ""
}
private fun markdownText(content: String): String {
val document = parser.parse(content)
if (!document.hasChildren())
return ""
return document
.children
.filterIsInstance<Heading>()
.drop(1) // ignore title
.joinToString(" ") { it.text.toString() }
.plus(" ")
.plus(
document
.children
.filterIsInstance<Paragraph>()
.joinToString(" ") { it.chars.toString().trim() }
)
.trim()
}
private fun asciidocText(content: String): String {
val options = Options.builder().safe(SafeMode.SERVER).backend("text").build()
val text = asciidoctorWithTextConverter.convert(content, options)
return text.lines().joinToString(" ")
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ContentTitle.kt
================================================
package nl.avisi.structurizr.site.generatr.site.model
import com.structurizr.documentation.Format
import com.structurizr.documentation.Section
import com.vladsch.flexmark.ast.Heading
import com.vladsch.flexmark.parser.Parser
import org.asciidoctor.Options
import org.asciidoctor.SafeMode
private val parser = Parser.builder().build()
fun Section.contentTitle(): String = when (format) {
Format.Markdown -> markdownTitle()
Format.AsciiDoc -> asciidocTitle()
else -> "unsupported document"
}
private fun Section.markdownTitle(): String {
val document = parser.parse(content)
val header = document.children.firstOrNull { it is Heading }?.let { it as Heading }
if (header != null)
return header.text.toString().trim()
return "untitled document"
}
private fun Section.asciidocTitle(): String {
val options = Options.builder().safe(SafeMode.SERVER).backend("text").build()
val document = asciidoctorWithTextConverter.load(content, options)
if (document.title != null && document.title.isNotEmpty())
return document.title.trim()
return "untitled document"
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/CustomStylesheetViewModel.kt
================================================
package nl.avisi.structurizr.site.generatr.site.model
import nl.avisi.structurizr.site.generatr.site.GeneratorContext
import nl.avisi.structurizr.site.generatr.site.asUrlToFile
class CustomStylesheetViewModel(generatorContext: GeneratorContext, pageViewModel: PageViewModel) {
val resourceURI = getResourceURI(generatorContext)?.let {
if (!it.lowercase().startsWith("http"))
"/$it".asUrlToFile(pageViewModel.url)
else
it
}
val includeCustomStylesheet = resourceURI != null
private fun getResourceURI(generatorContext: GeneratorContext) =
generatorContext.workspace.views.configuration.properties
.getOrDefault(
"generatr.style.customStylesheet",
null
)
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/DecisionTabViewModel.kt
================================================
package nl.avisi.structurizr.site.generatr.site.model
data class DecisionTabViewModel(val pageViewModel: SoftwareSystemPageViewModel, val title: String, val url: String, private val match: Match = Match.EXACT) {
val link = LinkViewModel(pageViewModel, title, url, match)
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/DecisionsTableViewModel.kt
================================================
package nl.avisi.structurizr.site.generatr.site.model
import com.structurizr.documentation.Decision
import com.structurizr.model.SoftwareSystem
import com.structurizr.model.StaticStructureElement
import nl.avisi.structurizr.site.generatr.hasDecisions
import nl.avisi.structurizr.site.generatr.site.formatDate
fun PageViewModel.createDecisionsTableViewModel(decisions: Collection<Decision>, hrefFactory: (Decision) -> String) =
TableViewModel.create {
headerRow(
headerCellSmall("ID"),
headerCell("Date"),
headerCell("Status"),
headerCellLarge("Title")
)
decisions
.sortedBy { it.id.toInt() }
.forEach {
bodyRow(
cellWithIndex(it.id),
cell(formatDate(it.date)),
cell(it.status),
cellWithLink(this@createDecisionsTableViewModel, it.title, hrefFactory(it))
)
}
}
fun SoftwareSystemPageViewModel.createDecisionsTabViewModel(
softwareSystem: SoftwareSystem,
tab: SoftwareSystemPageViewModel.Tab,
linkMatch: (StaticStructureElement) -> Match = { Match.EXACT }
) = buildList {
if (softwareSystem.hasDecisions()) {
add(
DecisionTabViewModel(
this@createDecisionsTabViewModel,
"System",
SoftwareSystemPageViewModel.url(softwareSystem, tab),
linkMatch(softwareSystem)
)
)
}
softwareSystem
.containers
.filter { it.hasDecisions(recursive = true) }
.map {
DecisionTabViewModel(
this@createDecisionsTabViewModel,
it.name,
SoftwareSystemContainerDecisionsPageViewModel.url(it),
linkMatch(it)
)
}
.forEach { add(it) }
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/DiagramIndexViewModel.kt
================================================
package nl.avisi.structurizr.site.generatr.site.model
data class IndexEntry(val key: String, val title: String)
data class DiagramIndexViewModel(
private val diagrams: List<DiagramViewModel> = emptyList(),
private val images: List<ImageViewViewModel> = emptyList()) {
val entries = diagrams.map { IndexEntry(it.key, it.indexTitle()) } +
images.map { IndexEntry(it.key, it.indexTitle()) }
val visible: Boolean = (diagrams.count() + images.count()) > 1
private fun DiagramViewModel.indexTitle() = if (description.isNullOrBlank()) title else "$title ($description)"
private fun ImageViewViewModel.indexTitle() = if (description.isNullOrBlank()) title else "$title ($description)"
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/DiagramViewModel.kt
================================================
package nl.avisi.structurizr.site.generatr.site.model
import com.structurizr.view.View
data class DiagramViewModel(
val key: String,
val title: String,
val description: 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?) =
forView(pageViewModel, view.key, view.name, view.title, view.description.ifBlank { null }, svgFactory)
fun forView(
pageViewModel: PageViewModel,
key: String,
name: String,
title: String?,
description: String?,
svgFactory: (key: String, url: String) -> String?
): DiagramViewModel {
val svg = svgFactory(key, pageViewModel.url)
return DiagramViewModel(
key,
title ?: name,
description,
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
}
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ExternalLinkViewModel.kt
================================================
package nl.avisi.structurizr.site.generatr.site.model
data class ExternalLinkViewModel(
val title: String,
val href: String,
)
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/FaviconViewModel.kt
================================================
package nl.avisi.structurizr.site.generatr.site.model
import nl.avisi.structurizr.site.generatr.site.GeneratorContext
import nl.avisi.structurizr.site.generatr.site.asUrlToFile
import java.io.File
class FaviconViewModel(generatorContext: GeneratorContext, pageViewModel: PageViewModel) {
val url = faviconPath(generatorContext)?.let { "/$it".asUrlToFile(pageViewModel.url) }
val type = extractType()
val includeFavicon = url != null
private fun faviconPath(generatorContext: GeneratorContext) =
generatorContext.workspace.views.configuration.properties
.getOrDefault(
"generatr.style.faviconPath",
null
)
private fun extractType(): String? {
return url?.let {
val extension = File(it).extension.lowercase()
if (extension.isBlank() || !arrayOf("ico", "png", "gif").contains(extension))
throw IllegalArgumentException("Favicon must be a valid *.ico, *.png of *.gif file")
"image/$extension"
}
}
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/FlexmarkConfig.kt
================================================
package nl.avisi.structurizr.site.generatr.site.model
import com.structurizr.Workspace
import com.vladsch.flexmark.util.misc.Extension
import com.vladsch.flexmark.util.data.DataHolder
import com.vladsch.flexmark.util.data.MutableDataSet
import com.vladsch.flexmark.ext.emoji.EmojiExtension
import com.vladsch.flexmark.ext.emoji.EmojiImageType
import com.vladsch.flexmark.ext.gitlab.GitLabExtension
import com.vladsch.flexmark.parser.Parser
import nl.avisi.structurizr.site.generatr.site.GeneratorContext
data class FlexmarkConfig(
val flexmarkExtensionsProperty: String,
val selectedExtensionMap: Map<String, Extension>,
val flexmarkOptions: DataHolder
)
val availableExtensionMap: MutableMap<String, Extension> = mutableMapOf(
"Abbreviation" to com.vladsch.flexmark.ext.abbreviation.AbbreviationExtension.create(),
"Admonition" to com.vladsch.flexmark.ext.admonition.AdmonitionExtension.create(),
"AnchorLink" to com.vladsch.flexmark.ext.anchorlink.AnchorLinkExtension.create(),
"Aside" to com.vladsch.flexmark.ext.aside.AsideExtension.create(),
"Attributes" to com.vladsch.flexmark.ext.attributes.AttributesExtension.create(),
"Autolink" to com.vladsch.flexmark.ext.autolink.AutolinkExtension.create(),
"Definition" to com.vladsch.flexmark.ext.definition.DefinitionExtension.create(),
// Docx Converter: render doxc from markdown
"Emoji" to com.vladsch.flexmark.ext.emoji.EmojiExtension.create(),
"EnumeratedReference" to com.vladsch.flexmark.ext.enumerated.reference.EnumeratedReferenceExtension.create(),
"Footnotes" to com.vladsch.flexmark.ext.footnotes.FootnoteExtension.create(),
"GfmIssues" to com.vladsch.flexmark.ext.gfm.issues.GfmIssuesExtension.create(),
"GfmStrikethrough" to com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension.create(),
"GfmSubscript" to com.vladsch.flexmark.ext.gfm.strikethrough.SubscriptExtension.create(),
"GfmStrikethroughSubscript" to com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension.create(),
"GfmTaskList" to com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension.create(),
"GfmUsers" to com.vladsch.flexmark.ext.gfm.users.GfmUsersExtension.create(),
"GitLab" to com.vladsch.flexmark.ext.gitlab.GitLabExtension.create(),
// Html To Markdown: render markdown from html
"Ins" to com.vladsch.flexmark.ext.ins.InsExtension.create(),
// Jekyll Tags: render jekyll tags from markdown
// Jira-Converter: render jira markup from markdown
"Macros" to com.vladsch.flexmark.ext.macros.MacrosExtension.create(),
"MediaTags" to com.vladsch.flexmark.ext.media.tags.MediaTagsExtension.create(),
"ResizableImage" to com.vladsch.flexmark.ext.resizable.image.ResizableImageExtension.create(),
// "SpecExample" to com.vladsch.flexmark.ext.spec.example.SpecExampleExtension.create(),
"Superscript" to com.vladsch.flexmark.ext.superscript.SuperscriptExtension.create(),
"Tables" to com.vladsch.flexmark.ext.tables.TablesExtension.create(),
"TableOfContents" to com.vladsch.flexmark.ext.toc.TocExtension.create(),
"SimulatedTableOfContents" to com.vladsch.flexmark.ext.toc.SimTocExtension.create(),
"Typographic" to com.vladsch.flexmark.ext.typographic.TypographicExtension.create(),
"WikiLinks" to com.vladsch.flexmark.ext.wikilink.WikiLinkExtension.create(),
"XWikiMacro" to com.vladsch.flexmark.ext.xwiki.macros.MacroExtension.create(),
"YAMLFrontMatter" to com.vladsch.flexmark.ext.yaml.front.matter.YamlFrontMatterExtension.create(),
// "YouTrackConverter" to com.vladsch.flexmark.ext.youtrack.converter.YouTrackConverterExtension.create(),
"YouTubeLink" to com.vladsch.flexmark.ext.youtube.embedded.YouTubeLinkExtension.create(),
)
fun buildFlexmarkConfig(context: GeneratorContext): FlexmarkConfig {
val configuration = context.workspace.views.configuration.properties
val flexmarkExtensionString = configuration.getOrDefault("generatr.markdown.flexmark.extensions", "Tables")
val flexmarkExtensionNames = flexmarkExtensionString.split(",")
val selectedExtensionMap: MutableMap<String, Extension> = mutableMapOf();
flexmarkExtensionNames.forEach {
val extensionName = it.trim()
if (availableExtensionMap.containsKey(extensionName)) {
selectedExtensionMap[extensionName] = availableExtensionMap[extensionName] as Extension
} else {
println("Unknown flexmark extension requested in generatr.markdown.flexmark.extensions: Skipping $extensionName.")
}
}
val flexmarkOptions = MutableDataSet()
flexmarkOptions.set(Parser.EXTENSIONS, selectedExtensionMap.values)
if (selectedExtensionMap.containsKey("Emoji")) {
flexmarkOptions.set(EmojiExtension.USE_IMAGE_TYPE, EmojiImageType.UNICODE_ONLY)
}
if (selectedExtensionMap.containsKey("GitLab")) {
flexmarkOptions.set(GitLabExtension.RENDER_BLOCK_MERMAID, false)
}
return FlexmarkConfig(flexmarkExtensionString, selectedExtensionMap, flexmarkOptions)
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/HeaderBarViewModel.kt
================================================
package nl.avisi.structurizr.site.generatr.site.model
import nl.avisi.structurizr.site.generatr.site.GeneratorContext
class HeaderBarViewModel(pageViewModel: PageViewModel, generatorContext: GeneratorContext) {
val url = pageViewModel.url
val logo = logoPath(generatorContext)?.let { ImageViewModel(pageViewModel, "/$it") }
val hasLogo = logo != null
val titleLink = LinkViewModel(pageViewModel, generatorContext.workspace.name, HomePageViewModel.url())
val searchLink = LinkViewModel(pageViewModel, generatorContext.workspace.name, SearchViewModel.url())
val branches = generatorContext.branches
.map { BranchHomeLinkViewModel(pageViewModel, it) }
val currentBranch = generatorContext.currentBranch
val version = generatorContext.version
val allowToggleTheme = pageViewModel.allowToggleTheme
private fun logoPath(generatorContext: GeneratorContext) =
generatorContext.workspace.views.configuration.properties
.getOrDefault(
"generatr.style.logoPath",
null
)
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/HomePageViewModel.kt
================================================
package nl.avisi.structurizr.site.generatr.site.model
import com.structurizr.documentation.Format
import nl.avisi.structurizr.site.generatr.site.GeneratorContext
import org.intellij.lang.annotations.Language
class HomePageViewModel(generatorContext: GeneratorContext) : PageViewModel(generatorContext) {
override val pageSubTitle = if (generatorContext.workspace.name.isNotBlank()) "" else "Home"
override val url = url()
val content = toHtml(
this,
content = generatorContext.workspace.documentation.sections
.firstOrNull { it.order == 1 }?.content ?: DEFAULT_HOMEPAGE_CONTENT,
format = generatorContext.workspace.documentation.sections
.firstOrNull { it.order == 1 }?.format ?: Format.Markdown,
svgFactory = generatorContext.svgFactory
)
companion object {
fun url() = "/"
@Language("markdown")
const val DEFAULT_HOMEPAGE_CONTENT = """
## Home
This is the default home page. You can customize this home page by adding documentation pages to your workspace. For
example (see the [Structurizr DSL](https://github.com/structurizr/dsl/blob/master/docs/language-reference.md#documentation)
documentation for more information):
```text
workspace "Example workspace" {
!docs docs
}
```
In the above example, the first document in the `docs` directory (after sorting alphabetically), will be used as the
homepage of the generated site.
"""
}
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ImageViewModel.kt
================================================
package nl.avisi.structurizr.site.generatr.site.model
import nl.avisi.structurizr.site.generatr.site.asUrlToFile
data class ImageViewModel(val pageViewModel: PageViewModel, val href: String) {
val relativeHref get() = href.asUrlToFile(pageViewModel.url)
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ImageViewViewModel.kt
================================================
package nl.avisi.structurizr.site.generatr.site.model
import com.structurizr.model.Component
import com.structurizr.model.Container
import com.structurizr.model.SoftwareSystem
import com.structurizr.view.ImageView
data class ImageViewViewModel(val imageView: ImageView) {
val key: String = imageView.key
val title: String = imageView.title ?: generateName()
val description: String? = imageView.description
val content: String = imageView.content
private fun generateName(): String {
val elementName = generateSequence(imageView.element) {
it.parent
}.map { it.name }.toList().reversed().joinToString(" - ")
val elementType = when (imageView.element) {
is SoftwareSystem -> "Containers"
is Container -> "Components"
is Component -> "Code"
else -> throw IllegalStateException("Not supported element")
}
return "$elementName - $elementType"
}
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/LinkViewModel.kt
================================================
package nl.avisi.structurizr.site.generatr.site.model
import nl.avisi.structurizr.site.generatr.site.asUrlToDirectory
data class LinkViewModel(
private val pageViewModel: PageViewModel,
val title: String,
val href: String,
val match: Match = Match.EXACT
) {
val relativeHref get() = href.asUrlToDirectory(pageViewModel.url)
val active get() =
when (match) {
Match.EXACT -> isHrefOfContainingPage
Match.CHILD -> isChildHrefOfContainingPage
Match.SIBLING -> isSiblingHrefOfContainingPage
Match.SIBLING_CHILD -> isSiblingChildHrefOfContainingPage
}
private val isHrefOfContainingPage get() = href == pageViewModel.url
private val isChildHrefOfContainingPage get() = pageViewModel.url == href || pageViewModel.url.startsWith("$href/")
private val isSiblingHrefOfContainingPage get() = pageViewModel.url.trimEnd('/').dropLastWhile { it != '/' } == href.trimEnd('/').dropLastWhile { it != '/' }
private val isSiblingChildHrefOfContainingPage get() =
pageViewModel.url.trimEnd('/').dropLastWhile { it != '/' }.trimEnd('/').dropLastWhile { it != '/' } ==
href.trimEnd('/').dropLastWhile { it != '/' }.trimEnd('/').dropLastWhile { it != '/' }
}
enum class Match {
EXACT,
CHILD,
SIBLING,
SIBLING_CHILD
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuNodeViewModel.kt
================================================
package nl.avisi.structurizr.site.generatr.site.model
data class MenuNodeViewModel(val name: String, val children: List<MenuNodeViewModel>)
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModel.kt
================================================
package nl.avisi.structurizr.site.generatr.site.model
import nl.avisi.structurizr.site.generatr.site.GeneratorContext
class MenuViewModel(generatorContext: GeneratorContext, private val pageViewModel: PageViewModel) {
val generalItems = sequence {
yield(createMenuItem("Home", HomePageViewModel.url()))
if (generatorContext.workspace.documentation.decisions.isNotEmpty())
yield(createMenuItem("Decisions", WorkspaceDecisionsPageViewModel.url(), Match.CHILD))
if (generatorContext.workspace.model.softwareSystems.isNotEmpty())
yield(createMenuItem("Software Systems", SoftwareSystemsPageViewModel.url()))
generatorContext.workspace.documentation.sections
.sortedBy { it.order }
.drop(1)
.forEach { yield(createMenuItem(it.contentTitle(), WorkspaceDocumentationSectionPageViewModel.url(it))) }
}.toList()
val softwareSystemItems = pageViewModel.includedSoftwareSystems
.sortedBy { it.name.lowercase() }
.map {
createMenuItem(it.name, SoftwareSystemPageViewModel.url(it, SoftwareSystemPageViewModel.Tab.HOME), Match.CHILD)
}
private val groupSeparator = generatorContext.workspace.model.properties["structurizr.groupSeparator"] ?: "/"
private val softwareSystemPaths = pageViewModel.includedSoftwareSystems
.map { "${it.group ?: ""}$groupSeparator${it.name}" }
.sortedBy { it.lowercase() }
private fun createMenuItem(title: String, href: String, match: Match = Match.EXACT) =
LinkViewModel(pageViewModel, title, href, match)
fun softwareSystemNodes(): MenuNodeViewModel {
data class MutableMenuNode(val name: String, val children: MutableList<MutableMenuNode>) {
fun toMenuNode(): MenuNodeViewModel = MenuNodeViewModel(name, children.map { it.toMenuNode() })
}
val rootNode = MutableMenuNode("", mutableListOf())
softwareSystemPaths.forEach { path ->
var currentNode = rootNode
path.split(groupSeparator).forEach { part ->
val existingNode = currentNode.children.find { it.name == part }
currentNode = if (existingNode == null) {
val newNode = MutableMenuNode(part, mutableListOf())
currentNode.children.add(newNode)
newNode
} else {
existingNode
}
}
}
return rootNode.toMenuNode()
}
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/PageViewModel.kt
================================================
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 {
if (pageSubTitle.isNotBlank() && generatorContext.workspace.name.isNotBlank())
"$pageSubTitle | ${generatorContext.workspace.name}"
else if (generatorContext.workspace.name.isNotBlank())
generatorContext.workspace.name
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) }
val menu by lazy { MenuViewModel(generatorContext, this) }
val includeAutoReloading = generatorContext.serving
val flexmarkConfig by lazy { buildFlexmarkConfig(generatorContext) }
val includeAdmonition = flexmarkConfig.selectedExtensionMap.containsKey("Admonition")
val includeKatex = flexmarkConfig.selectedExtensionMap.containsKey("GitLab")
val includedSoftwareSystems = generatorContext.workspace.includedSoftwareSystems
val configuration = generatorContext.workspace.views.configuration.properties
val includeTreeview = configuration.getOrDefault("generatr.site.nestGroups", "false").toBoolean()
val theme = configuration.getOrDefault("generatr.site.theme", "light").toTheme()
val allowToggleTheme = theme == Theme.AUTO
abstract val url: String
abstract val pageSubTitle: String
}
fun String.toTheme() = when (this) {
"light" -> Theme.LIGHT
"dark" -> Theme.DARK
"auto" -> Theme.AUTO
else -> throw IllegalArgumentException("Unknown theme '$this', allowed values are 'light', 'dark' or 'auto'")
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/PropertiesTableViewModel.kt
================================================
package nl.avisi.structurizr.site.generatr.site.model
import com.structurizr.util.Url
fun createPropertiesTableViewModel(properties: Map<String, String>) =
TableViewModel.create {
headerRow(
headerCellMedium("Name"),
headerCell("Value")
)
properties
.toSortedMap()
.forEach { (name, value) ->
bodyRow(
cell(name),
if (Url.isUrl(value))
cellWithExternalLink(value, value)
else
cell(value)
)
}
}
================================================
FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SearchViewModel.kt
================================================
package nl.avisi.structurizr.site.generatr.site.model
import nl.avisi.structurizr.site.generatr.site.GeneratorContext
import nl.avisi.structurizr.site.generatr.site.model.indexing.*
class SearchViewModel(generatorContext: GeneratorContext) : PageViewModel(generatorContext) {
override val pageSubTitle = "Search results"
override val url = url()
val language: String = generatorContext.workspace.views
.configuration.properties.getOrDefault("generatr.search.language", "")
val documents = buildList {
add(home(generatorContext.workspace.documentation, this@SearchViewModel))
addAll(workspaceDecisions(generatorContext.workspace.documentation, this@SearchViewModel))
addAll(workspaceSections(generatorContext.workspace.documentation, this@SearchViewModel))
addAll(
includedSoftwareSystems
.flatMap {
buildList {
add(softwareSystemHome(it, this@SearchViewModel))
add(softwareSystemContext(it, this@SearchViewModel))
add(softwareSystemContainers(it, this@SearchViewModel))
add(softwareSystemComponents(it, this@SearchViewModel))
add(softwareSystemRelationships(it, this@SearchViewModel))
addAll(softwareSystemDecisions(it, this@SearchViewModel))
addAll(softwareSystemSections(it, this@SearchViewModel))
}
}
.mapNotNull { it }
)
addAll(
includedSoftwareSystems
.flatMap {
buildList {
it.containers.forEach {
addAll(softwareSystemContainerSections(it, this@
gitextract_0itw0606/
├── .dockerignore
├── .editorconfig
├── .gitattributes
├── .github/
│ └── workflows/
│ ├── pr.yml
│ └── release.yml
├── .gitignore
├── .markdownlint.json
├── .prettierrc
├── .run/
│ ├── all unit tests.run.xml
│ ├── generate site for example model (from git repo all branches) .run.xml
│ ├── generate site for example model (from git repo).run.xml
│ ├── generate site for example model (local).run.xml
│ └── serve example model.run.xml
├── .tool-versions
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── build.gradle.kts
├── cosign.pub
├── docs/
│ └── example/
│ ├── .adr-dir
│ ├── assets/
│ │ └── site/
│ │ └── custom.css
│ ├── internet-banking-system/
│ │ ├── adr/
│ │ │ └── 0001-record-architecture-decisions.md
│ │ ├── api-application/
│ │ │ ├── email-component/
│ │ │ │ ├── adr/
│ │ │ │ │ ├── 0001-record-architecture-decisions.md
│ │ │ │ │ ├── 0002-implement-feature-1.md
│ │ │ │ │ └── 0003-another-realisation-of-feature-1.md
│ │ │ │ └── docs/
│ │ │ │ └── 0001-inner-workings.md
│ │ │ └── mainframe-banking-system-facade/
│ │ │ ├── adr/
│ │ │ │ └── 0001-record-architecture-decisions.md
│ │ │ └── docs/
│ │ │ ├── 0000-introduction.md
│ │ │ └── 0001-inner-workings.md
│ │ ├── database/
│ │ │ ├── adr/
│ │ │ │ └── 0004-using-oracle-database-schema.md
│ │ │ └── docs/
│ │ │ └── 0002-guide.md
│ │ └── docs/
│ │ ├── 0000-introduction.md
│ │ ├── 0001-history.md
│ │ └── 0002-guide.md
│ ├── workspace-adrs/
│ │ ├── 0001-record-architecture-decisions.md
│ │ ├── 0002-implement-feature-1.md
│ │ ├── 0003-another-realisation-of-feature-1.md
│ │ └── 0004-using-oracle-database-schema.md
│ ├── workspace-docs/
│ │ ├── 00-index.md
│ │ ├── 01-embedding-diagrams-and-images.md
│ │ ├── 02-markdown-features.md
│ │ └── 03-asciidoc-features.adoc
│ └── workspace.dsl
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── package.json
├── renovate.json
├── settings.gradle.kts
└── src/
├── main/
│ ├── kotlin/
│ │ └── nl/
│ │ └── avisi/
│ │ └── structurizr/
│ │ └── site/
│ │ └── generatr/
│ │ ├── App.kt
│ │ ├── ClonedRepository.kt
│ │ ├── CreateStructurizrWorkspace.kt
│ │ ├── GenerateSiteCommand.kt
│ │ ├── ServeCommand.kt
│ │ ├── StringUtilities.kt
│ │ ├── StructurizrUtilities.kt
│ │ ├── VersionCommand.kt
│ │ └── site/
│ │ ├── DateFormatter.kt
│ │ ├── DiagramGenerator.kt
│ │ ├── GeneratorContext.kt
│ │ ├── PlantUmlExporter.kt
│ │ ├── RelativeUrl.kt
│ │ ├── SiteGenerator.kt
│ │ ├── model/
│ │ │ ├── Asciidoctor.kt
│ │ │ ├── BranchHomeLinkViewModel.kt
│ │ │ ├── ComponentTabViewModel.kt
│ │ │ ├── ComponentsTabViewModel.kt
│ │ │ ├── ContainerTabViewModel.kt
│ │ │ ├── ContainersCodeTabViewModel.kt
│ │ │ ├── ContainersComponentTabViewModel.kt
│ │ │ ├── ContentText.kt
│ │ │ ├── ContentTitle.kt
│ │ │ ├── CustomStylesheetViewModel.kt
│ │ │ ├── DecisionTabViewModel.kt
│ │ │ ├── DecisionsTableViewModel.kt
│ │ │ ├── DiagramIndexViewModel.kt
│ │ │ ├── DiagramViewModel.kt
│ │ │ ├── ExternalLinkViewModel.kt
│ │ │ ├── FaviconViewModel.kt
│ │ │ ├── FlexmarkConfig.kt
│ │ │ ├── HeaderBarViewModel.kt
│ │ │ ├── HomePageViewModel.kt
│ │ │ ├── ImageViewModel.kt
│ │ │ ├── ImageViewViewModel.kt
│ │ │ ├── LinkViewModel.kt
│ │ │ ├── MenuNodeViewModel.kt
│ │ │ ├── MenuViewModel.kt
│ │ │ ├── PageViewModel.kt
│ │ │ ├── PropertiesTableViewModel.kt
│ │ │ ├── SearchViewModel.kt
│ │ │ ├── SectionTabViewModel.kt
│ │ │ ├── SectionsTableViewModel.kt
│ │ │ ├── SoftwareSystemContainerComponentCodePageViewModel.kt
│ │ │ ├── SoftwareSystemContainerComponentDecisionPageViewModel.kt
│ │ │ ├── SoftwareSystemContainerComponentDecisionsPageViewModel.kt
│ │ │ ├── SoftwareSystemContainerComponentSectionPageViewModel.kt
│ │ │ ├── SoftwareSystemContainerComponentSectionsPageViewModel.kt
│ │ │ ├── SoftwareSystemContainerComponentsPageViewModel.kt
│ │ │ ├── SoftwareSystemContainerDecisionPageViewModel.kt
│ │ │ ├── SoftwareSystemContainerDecisionsPageViewModel.kt
│ │ │ ├── SoftwareSystemContainerPageViewModel.kt
│ │ │ ├── SoftwareSystemContainerSectionPageViewModel.kt
│ │ │ ├── SoftwareSystemContainerSectionsPageViewModel.kt
│ │ │ ├── SoftwareSystemContextPageViewModel.kt
│ │ │ ├── SoftwareSystemDecisionPageViewModel.kt
│ │ │ ├── SoftwareSystemDecisionsPageViewModel.kt
│ │ │ ├── SoftwareSystemDependenciesPageViewModel.kt
│ │ │ ├── SoftwareSystemDeploymentPageViewModel.kt
│ │ │ ├── SoftwareSystemDynamicPageViewModel.kt
│ │ │ ├── SoftwareSystemHomePageViewModel.kt
│ │ │ ├── SoftwareSystemPageViewModel.kt
│ │ │ ├── SoftwareSystemSectionPageViewModel.kt
│ │ │ ├── SoftwareSystemSectionsPageViewModel.kt
│ │ │ ├── SoftwareSystemTableUtilities.kt
│ │ │ ├── SoftwareSystemsPageViewModel.kt
│ │ │ ├── TableViewModel.kt
│ │ │ ├── Theme.kt
│ │ │ ├── ToHtml.kt
│ │ │ ├── WorkspaceDecisionPageViewModel.kt
│ │ │ ├── WorkspaceDecisionsPageViewModel.kt
│ │ │ ├── WorkspaceDocumentationSectionPageViewModel.kt
│ │ │ └── indexing/
│ │ │ ├── Document.kt
│ │ │ ├── Home.kt
│ │ │ ├── SoftwareSystemComponents.kt
│ │ │ ├── SoftwareSystemContainerDecisions.kt
│ │ │ ├── SoftwareSystemContainerSections.kt
│ │ │ ├── SoftwareSystemContainers.kt
│ │ │ ├── SoftwareSystemContext.kt
│ │ │ ├── SoftwareSystemDecisions.kt
│ │ │ ├── SoftwareSystemHome.kt
│ │ │ ├── SoftwareSystemRelationships.kt
│ │ │ ├── SoftwareSystemSections.kt
│ │ │ ├── WorkspaceDecisions.kt
│ │ │ └── WorkspaceSections.kt
│ │ └── views/
│ │ ├── AutoReloading.kt
│ │ ├── CDN.kt
│ │ ├── ContentDiv.kt
│ │ ├── Diagram.kt
│ │ ├── DiagramIndex.kt
│ │ ├── ExternalLink.kt
│ │ ├── Favicon.kt
│ │ ├── HomePage.kt
│ │ ├── Image.kt
│ │ ├── Link.kt
│ │ ├── MarkdownExtension.kt
│ │ ├── Menu.kt
│ │ ├── Modal.kt
│ │ ├── Page.kt
│ │ ├── PageHeader.kt
│ │ ├── RawHtml.kt
│ │ ├── RedirectRelative.kt
│ │ ├── RedirectUpPage.kt
│ │ ├── SearchPage.kt
│ │ ├── SoftwareSystemContainerComponentCodePage.kt
│ │ ├── SoftwareSystemContainerComponentDecisionPage.kt
│ │ ├── SoftwareSystemContainerComponentDecisionsPage.kt
│ │ ├── SoftwareSystemContainerComponentSectionPage.kt
│ │ ├── SoftwareSystemContainerComponentSectionsPage.kt
│ │ ├── SoftwareSystemContainerComponentsPage.kt
│ │ ├── SoftwareSystemContainerDecisionPage.kt
│ │ ├── SoftwareSystemContainerDecisionsPage.kt
│ │ ├── SoftwareSystemContainerPage.kt
│ │ ├── SoftwareSystemContainerSectionPage.kt
│ │ ├── SoftwareSystemContainerSectionsPage.kt
│ │ ├── SoftwareSystemContextPage.kt
│ │ ├── SoftwareSystemDecisionPage.kt
│ │ ├── SoftwareSystemDecisionsPage.kt
│ │ ├── SoftwareSystemDependenciesPage.kt
│ │ ├── SoftwareSystemDeploymentPage.kt
│ │ ├── SoftwareSystemDynamicPage.kt
│ │ ├── SoftwareSystemHomePage.kt
│ │ ├── SoftwareSystemPage.kt
│ │ ├── SoftwareSystemSectionPage.kt
│ │ ├── SoftwareSystemSectionsPage.kt
│ │ ├── SoftwareSystemsPage.kt
│ │ ├── Table.kt
│ │ ├── WorkspaceDecisionPage.kt
│ │ ├── WorkspaceDecisionsPage.kt
│ │ └── WorkspaceDocumentationSectionPage.kt
│ └── resources/
│ └── assets/
│ ├── css/
│ │ ├── admonition.css
│ │ ├── style.css
│ │ └── treeview.css
│ └── js/
│ ├── admonition.js
│ ├── auto-reload.js
│ ├── header.js
│ ├── katex-render.js
│ ├── modal.js
│ ├── reformat-mermaid.js
│ ├── search.js
│ ├── svg-modal.js
│ ├── toggle-theme.js
│ └── treeview.js
└── test/
└── kotlin/
└── nl/
└── avisi/
└── structurizr/
└── site/
└── generatr/
└── site/
├── BranchComparatorTest.kt
├── PlantUmlExporterTest.kt
├── StringUtilitiesTest.kt
├── StructurizrUtilitiesTest.kt
├── e2e/
│ ├── E2ETestFixture.kt
│ ├── PageTestHelper.kt
│ ├── RetryExtension.kt
│ ├── SearchPageTest.kt
│ ├── SoftwareSystemDependenciesPageTest.kt
│ ├── SoftwareSystemHomePageTest.kt
│ ├── SoftwareSystemsPageTest.kt
│ ├── WorkspaceHomePageTest.kt
│ ├── decisions/
│ │ ├── DecisionPageTestHelper.kt
│ │ ├── DecisionsPageTestHelper.kt
│ │ ├── SoftwareSystemContainerComponentDecisionPageTest.kt
│ │ ├── SoftwareSystemContainerComponentDecisionsPageTest.kt
│ │ ├── SoftwareSystemContainerDecisionPage.kt
│ │ ├── SoftwareSystemContainerDecisionsPageTest.kt
│ │ ├── SoftwareSystemDecisionPageTest.kt
│ │ ├── SoftwareSystemDecisionsPageTest.kt
│ │ ├── WorkspaceDecisionPageTest.kt
│ │ └── WorkspaceDecisionsPageTest.kt
│ └── sections/
│ ├── SectionPageTestHelper.kt
│ ├── SectionsPageTestHelper.kt
│ ├── SoftwareSystemContainerComponentSectionPageTest.kt
│ ├── SoftwareSystemContainerComponentSectionsPageTest.kt
│ ├── SoftwareSystemContainerSectionPageTest.kt
│ ├── SoftwareSystemContainerSectionsPageTest.kt
│ ├── SoftwareSystemSectionPageTest.kt
│ └── SoftwareSystemSectionsPageTest.kt
├── model/
│ ├── AsciidocToHtmlTest.kt
│ ├── BranchHomeLinkViewModelTest.kt
│ ├── ContentTextTest.kt
│ ├── ContentTitleTest.kt
│ ├── CustomStylesheetViewModelTest.kt
│ ├── DecisionTabViewModelTest.kt
│ ├── DecisionsTableViewModelTest.kt
│ ├── DiagramIndexViewModelTest.kt
│ ├── FaviconViewModelTest.kt
│ ├── HeaderBarViewModelTest.kt
│ ├── HomePageViewModelTest.kt
│ ├── ImageViewModelTest.kt
│ ├── ImageViewViewModelTest.kt
│ ├── IndexingTest.kt
│ ├── LinkViewModelTest.kt
│ ├── MarkdownToHtmlTest.kt
│ ├── MenuViewModelTest.kt
│ ├── PageViewModelTest.kt
│ ├── PropertiesTableViewModelTest.kt
│ ├── SearchViewModelTest.kt
│ ├── SoftwareSystemContainerComponentCodePageViewModelTest.kt
│ ├── SoftwareSystemContainerComponentDecisionPageViewModelTest.kt
│ ├── SoftwareSystemContainerComponentDecisionsPageViewModelTest.kt
│ ├── SoftwareSystemContainerComponentSectionPageViewModelTest.kt
│ ├── SoftwareSystemContainerComponentSectionsPageViewModelTest.kt
│ ├── SoftwareSystemContainerComponentsPageViewModelTest.kt
│ ├── SoftwareSystemContainerDecisionPageViewModelTest.kt
│ ├── SoftwareSystemContainerDecisionsPageViewModelTest.kt
│ ├── SoftwareSystemContainerPageViewModelTest.kt
│ ├── SoftwareSystemContainerSectionPageViewModelTest.kt
│ ├── SoftwareSystemContainerSectionsPageViewModelTest.kt
│ ├── SoftwareSystemContextPageViewModelTest.kt
│ ├── SoftwareSystemDecisionPageViewModelTest.kt
│ ├── SoftwareSystemDecisionsPageViewModelTest.kt
│ ├── SoftwareSystemDependenciesPageViewModelTest.kt
│ ├── SoftwareSystemDeploymentPageViewModelTest.kt
│ ├── SoftwareSystemDynamicPageViewModelTest.kt
│ ├── SoftwareSystemHomePageViewModelTest.kt
│ ├── SoftwareSystemPageViewModelTest.kt
│ ├── SoftwareSystemSectionPageViewModelTest.kt
│ ├── SoftwareSystemSectionsPageViewModelTest.kt
│ ├── SoftwareSystemsPageViewModelTest.kt
│ ├── TableViewModelTest.kt
│ ├── ViewModelTest.kt
│ ├── WorkspaceDecisionPageViewModelTest.kt
│ ├── WorkspaceDecisionsPageViewModelTest.kt
│ └── WorkspaceDocumentationSectionPageViewModelTest.kt
└── views/
└── CDNTest.kt
SYMBOL INDEX (20 symbols across 7 files)
FILE: src/main/resources/assets/js/auto-reload.js
function connectToWs (line 3) | function connectToWs() {
function reloadIfNeeded (line 32) | function reloadIfNeeded() {
function showUpdatingSite (line 59) | function showUpdatingSite() {
function hideUpdatingSite (line 64) | function hideUpdatingSite() {
function showUpdateSiteError (line 69) | function showUpdateSiteError(content) {
function hideUpdateSiteError (line 74) | function hideUpdateSiteError() {
function showSite (line 79) | function showSite() {
function hideSite (line 83) | function hideSite() {
FILE: src/main/resources/assets/js/header.js
function redirect (line 1) | function redirect(event, value, href) {
FILE: src/main/resources/assets/js/modal.js
function openModal (line 1) | function openModal(id) {
function closeModal (line 5) | function closeModal(id) {
FILE: src/main/resources/assets/js/search.js
function search (line 14) | function search(terms) {
function clearResultElements (line 32) | function clearResultElements(div) {
function createResultElement (line 38) | function createResultElement(result) {
function createNoResultsElement (line 56) | function createNoResultsElement() {
FILE: src/main/resources/assets/js/svg-modal.js
function resetPz (line 3) | function resetPz() {
function openSvgModal (line 11) | function openSvgModal(id, svgId) {
function closeSvgModal (line 31) | function closeSvgModal(id) {
FILE: src/main/resources/assets/js/toggle-theme.js
function toggleTheme (line 10) | function toggleTheme() {
FILE: src/main/resources/assets/js/treeview.js
function listree (line 1) | function listree() {
Condensed preview — 272 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (625K chars).
[
{
"path": ".dockerignore",
"chars": 17,
"preview": "*\n!build/install\n"
},
{
"path": ".editorconfig",
"chars": 1310,
"preview": "# EditorConfig is awesome:\n# - https://EditorConfig.org\n# - https://github.com/editorconfig/editorconfig/wiki/EditorConf"
},
{
"path": ".gitattributes",
"chars": 154,
"preview": "#\n# https://help.github.com/articles/dealing-with-line-endings/\n#\n# These are explicitly windows files and should use cr"
},
{
"path": ".github/workflows/pr.yml",
"chars": 2003,
"preview": "name: pr\non:\n pull_request:\n branches:\n - main\n push:\n branches:\n - main\n\nenv:\n IMAGE_NAME: ${{ githu"
},
{
"path": ".github/workflows/release.yml",
"chars": 5768,
"preview": "name: release\non:\n push:\n tags:\n - '*'\n\nenv:\n IMAGE_NAME: ${{ github.event.repository.name }}\n\njobs:\n build-g"
},
{
"path": ".gitignore",
"chars": 42,
"preview": ".gradle\n.idea\nbuild\nout\n*.md_\n.vscode\nbin\n"
},
{
"path": ".markdownlint.json",
"chars": 63,
"preview": "{\n \"default\": true,\n \"MD013\": false,\n \"MD041\": false\n}"
},
{
"path": ".prettierrc",
"chars": 22,
"preview": "{ \"printWidth\": 120 }\n"
},
{
"path": ".run/all unit tests.run.xml",
"chars": 484,
"preview": "<component name=\"ProjectRunConfigurationManager\">\n <configuration default=\"false\" name=\"all unit tests\" type=\"JUnit\" fa"
},
{
"path": ".run/generate site for example model (from git repo all branches) .run.xml",
"chars": 712,
"preview": "<component name=\"ProjectRunConfigurationManager\">\n <configuration default=\"false\" name=\"generate site for example model"
},
{
"path": ".run/generate site for example model (from git repo).run.xml",
"chars": 697,
"preview": "<component name=\"ProjectRunConfigurationManager\">\n <configuration default=\"false\" name=\"generate site for example model"
},
{
"path": ".run/generate site for example model (local).run.xml",
"chars": 587,
"preview": "<component name=\"ProjectRunConfigurationManager\">\n <configuration default=\"false\" name=\"generate site for example model"
},
{
"path": ".run/serve example model.run.xml",
"chars": 559,
"preview": "<component name=\"ProjectRunConfigurationManager\">\n <configuration default=\"false\" name=\"serve example model\" type=\"JetR"
},
{
"path": ".tool-versions",
"chars": 29,
"preview": "java temurin-21.0.10+7.0.LTS\n"
},
{
"path": "CONTRIBUTING.md",
"chars": 3160,
"preview": "# How to contribute\n\nWe welcome contributions! You can contribute\nby [filing an issue](https://github.com/avisi-cloud/st"
},
{
"path": "Dockerfile",
"chars": 547,
"preview": "FROM eclipse-temurin:21.0.10_7-jre-jammy\n\nUSER root\nRUN apt update && apt install graphviz --yes && rm -rf /var/lib/apt/"
},
{
"path": "LICENSE",
"chars": 11357,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 21515,
"preview": "<!-- TOC -->\n* [Structurizr Site Generatr](#structurizr-site-generatr)\n * [Features](#features)\n * [Getting Started](#"
},
{
"path": "build.gradle.kts",
"chars": 3494,
"preview": "import org.gradle.api.tasks.testing.logging.TestExceptionFormat\nimport org.gradle.api.tasks.testing.logging.TestLogEvent"
},
{
"path": "cosign.pub",
"chars": 178,
"preview": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzezKl0vAWSHosQ0JLEsDzNBd2nGm\n08KqX+imYqq2avlbH+ehprJFMqKK"
},
{
"path": "docs/example/.adr-dir",
"chars": 15,
"preview": "workspace-adrs\n"
},
{
"path": "docs/example/assets/site/custom.css",
"chars": 224,
"preview": "svg g g[id^=\"link\"]:hover path {\n stroke:#FF0000 !important;\n stroke-width: 2.0;\n}\n\nsvg g g[id^=\"link\"]:hover poly"
},
{
"path": "docs/example/internet-banking-system/adr/0001-record-architecture-decisions.md",
"chars": 507,
"preview": "# 1. Record Internet Banking System architecture decisions\n\nDate: 2022-06-21\n\n## Status\n\nAccepted\n\n## Context\n\nWe need t"
},
{
"path": "docs/example/internet-banking-system/api-application/email-component/adr/0001-record-architecture-decisions.md",
"chars": 498,
"preview": "# 1. Record Email Component architecture decision\n\nDate: 2022-06-21\n\n## Status\n\nAccepted\n\n## Context\n\nWe need to record "
},
{
"path": "docs/example/internet-banking-system/api-application/email-component/adr/0002-implement-feature-1.md",
"chars": 466,
"preview": "# 2. Implement Feature 1\n\nDate: 2024-12-17\n\n## Status\n\nSuperseded by [3. Another Realisation of Feature 1](0003-another-"
},
{
"path": "docs/example/internet-banking-system/api-application/email-component/adr/0003-another-realisation-of-feature-1.md",
"chars": 460,
"preview": "# 3. Another Realisation of Feature 1\n\nDate: 2024-12-17\n\n## Status\n\nAccepted\n\nSupersedes [2. Implement Feature 1](0002-i"
},
{
"path": "docs/example/internet-banking-system/api-application/email-component/docs/0001-inner-workings.md",
"chars": 74,
"preview": "# Email Component inner workings\n\nThis is how the e-mail component works.\n"
},
{
"path": "docs/example/internet-banking-system/api-application/mainframe-banking-system-facade/adr/0001-record-architecture-decisions.md",
"chars": 514,
"preview": "# 1. Record Mainframe Banking System Facade architecture decision\n\nDate: 2022-06-21\n\n## Status\n\nAccepted\n\n## Context\n\nWe"
},
{
"path": "docs/example/internet-banking-system/api-application/mainframe-banking-system-facade/docs/0000-introduction.md",
"chars": 80,
"preview": "# Mainframe Banking System Facade\n\nThis is the Mainframe Banking System Facade.\n"
},
{
"path": "docs/example/internet-banking-system/api-application/mainframe-banking-system-facade/docs/0001-inner-workings.md",
"chars": 83,
"preview": "# Inner workings of Mainframe Banking System Facade\n\nThis is how the facade works.\n"
},
{
"path": "docs/example/internet-banking-system/database/adr/0004-using-oracle-database-schema.md",
"chars": 390,
"preview": "# 4. Using Oracle database schema\n\nDate: 2025-11-13\n\n## Status\n\nAccepted\n\n## Context\n\nThe issue motivating this decision"
},
{
"path": "docs/example/internet-banking-system/database/docs/0002-guide.md",
"chars": 40,
"preview": "# Usage\n\nThis is how we use this thing.\n"
},
{
"path": "docs/example/internet-banking-system/docs/0000-introduction.md",
"chars": 199,
"preview": "# Description\n\nThis is our fancy Internet Banking System.\n\n## Vision\n\nOne system to rule them all!\n\n## References\n\n- Sou"
},
{
"path": "docs/example/internet-banking-system/docs/0001-history.md",
"chars": 55,
"preview": "# History\n\nSome notes how we got to the current state.\n"
},
{
"path": "docs/example/internet-banking-system/docs/0002-guide.md",
"chars": 40,
"preview": "# Usage\n\nThis is how we use this thing.\n"
},
{
"path": "docs/example/workspace-adrs/0001-record-architecture-decisions.md",
"chars": 483,
"preview": "# 1. Record architecture decisions\n\nDate: 2022-06-21\n\n## Status\n\nAccepted\n\n## Context\n\nWe need to record the architectur"
},
{
"path": "docs/example/workspace-adrs/0002-implement-feature-1.md",
"chars": 466,
"preview": "# 2. Implement Feature 1\n\nDate: 2024-12-17\n\n## Status\n\nSuperseded by [3. Another Realisation of Feature 1](0003-another-"
},
{
"path": "docs/example/workspace-adrs/0003-another-realisation-of-feature-1.md",
"chars": 460,
"preview": "# 3. Another Realisation of Feature 1\n\nDate: 2024-12-17\n\n## Status\n\nAccepted\n\nSupersedes [2. Implement Feature 1](0002-i"
},
{
"path": "docs/example/workspace-adrs/0004-using-oracle-database-schema.md",
"chars": 390,
"preview": "# 4. Using Oracle database schema\n\nDate: 2025-11-13\n\n## Status\n\nAccepted\n\n## Context\n\nThe issue motivating this decision"
},
{
"path": "docs/example/workspace-docs/00-index.md",
"chars": 228,
"preview": "## Big Bank plc architecture\n\nThis site contains the C4 architecture model for Big Bank plc.\n\nThis page is the home page"
},
{
"path": "docs/example/workspace-docs/01-embedding-diagrams-and-images.md",
"chars": 3071,
"preview": "## Embedding diagrams and images\n\nThis page showcases the ability to embed diagrams and static images in documentation.\n"
},
{
"path": "docs/example/workspace-docs/02-markdown-features.md",
"chars": 7875,
"preview": "## Extended Markdown features\n\nThis page showcases the ability to use extended Markdown formating features in workspace "
},
{
"path": "docs/example/workspace-docs/03-asciidoc-features.adoc",
"chars": 3745,
"preview": "= AsciiDoc features\n:toc: macro\n:imagesdir: ../assets\n:tip-caption: 💡Tip\n\n== AsciiDoc features 📌\n\nThis page showcases th"
},
{
"path": "docs/example/workspace.dsl",
"chars": 17448,
"preview": "/*\n * This is a combined version of the following workspaces:\n *\n * - \"Big Bank plc - System Landscape\" (https://structu"
},
{
"path": "gradle/wrapper/gradle-wrapper.properties",
"chars": 252,
"preview": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributi"
},
{
"path": "gradlew",
"chars": 8631,
"preview": "#!/bin/sh\n\n#\n# Copyright © 2015 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\")"
},
{
"path": "gradlew.bat",
"chars": 2896,
"preview": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (th"
},
{
"path": "package.json",
"chars": 285,
"preview": "{\n \"name\": \"structurizr-site-generatr\",\n \"dependencies\": {\n \"bulma\": \"1.0.4\",\n \"katex\": \"0.16.33\",\n "
},
{
"path": "renovate.json",
"chars": 1031,
"preview": "{\n \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n \"extends\": [\"config:recommended\"],\n \"prHourlyLimi"
},
{
"path": "settings.gradle.kts",
"chars": 47,
"preview": "rootProject.name = \"structurizr-site-generatr\"\n"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/App.kt",
"chars": 386,
"preview": "@file:Suppress(\"EXPERIMENTAL_IS_NOT_ENABLED\")\n@file:OptIn(ExperimentalCli::class)\n\npackage nl.avisi.structurizr.site.gen"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/ClonedRepository.kt",
"chars": 1941,
"preview": "package nl.avisi.structurizr.site.generatr\n\nimport org.eclipse.jgit.api.CreateBranchCommand\nimport org.eclipse.jgit.api."
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/CreateStructurizrWorkspace.kt",
"chars": 802,
"preview": "package nl.avisi.structurizr.site.generatr\n\nimport com.structurizr.dsl.StructurizrDslParser\nimport com.structurizr.model"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/GenerateSiteCommand.kt",
"chars": 5598,
"preview": "@file:OptIn(ExperimentalCli::class)\n\npackage nl.avisi.structurizr.site.generatr\n\nimport kotlinx.cli.*\nimport nl.avisi.st"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/ServeCommand.kt",
"chars": 8518,
"preview": "@file:OptIn(ExperimentalCli::class)\n\npackage nl.avisi.structurizr.site.generatr\n\nimport kotlinx.cli.*\nimport nl.avisi.st"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/StringUtilities.kt",
"chars": 737,
"preview": "package nl.avisi.structurizr.site.generatr\n\n// based on https://stackoverflow.com/questions/1976007/what-characters-are-"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/StructurizrUtilities.kt",
"chars": 4324,
"preview": "package nl.avisi.structurizr.site.generatr\n\nimport com.structurizr.Workspace\nimport com.structurizr.model.Component\nimpo"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/VersionCommand.kt",
"chars": 374,
"preview": "@file:OptIn(ExperimentalCli::class)\n\npackage nl.avisi.structurizr.site.generatr\n\nimport kotlinx.cli.ExperimentalCli\nimpo"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/DateFormatter.kt",
"chars": 317,
"preview": "package nl.avisi.structurizr.site.generatr.site\n\nimport java.time.ZoneId\nimport java.util.*\n\nfun formatDate(date: Date):"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/DiagramGenerator.kt",
"chars": 4357,
"preview": "package nl.avisi.structurizr.site.generatr.site\n\nimport com.structurizr.Workspace\nimport com.structurizr.export.Diagram\n"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/GeneratorContext.kt",
"chars": 316,
"preview": "package nl.avisi.structurizr.site.generatr.site\n\nimport com.structurizr.Workspace\n\ndata class GeneratorContext(\n val "
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/PlantUmlExporter.kt",
"chars": 7236,
"preview": "package nl.avisi.structurizr.site.generatr.site\n\nimport com.structurizr.Workspace\nimport com.structurizr.export.Diagram\n"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/RelativeUrl.kt",
"chars": 328,
"preview": "package nl.avisi.structurizr.site.generatr.site\n\nimport java.io.File\n\nfun String.asUrlToDirectory(otherUrl: String) = \"$"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/SiteGenerator.kt",
"chars": 13003,
"preview": "package nl.avisi.structurizr.site.generatr.site\n\nimport com.structurizr.Workspace\nimport com.structurizr.util.WorkspaceU"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/Asciidoctor.kt",
"chars": 2276,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport net.sourceforge.plantuml.FileFormat\nimport net.sourceforge"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/BranchHomeLinkViewModel.kt",
"chars": 498,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport nl.avisi.structurizr.site.generatr.site.asUrlToDirectory\ni"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ComponentTabViewModel.kt",
"chars": 218,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\ndata class ComponentTabViewModel(val pageViewModel: PageViewModel"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ComponentsTabViewModel.kt",
"chars": 791,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.Container\nimport nl.avisi.structuriz"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ContainerTabViewModel.kt",
"chars": 271,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\ndata class ContainerTabViewModel(val pageViewModel: SoftwareSyste"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ContainersCodeTabViewModel.kt",
"chars": 1267,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.struc"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ContainersComponentTabViewModel.kt",
"chars": 1083,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.struc"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ContentText.kt",
"chars": 1513,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Decision\nimport com.structur"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ContentTitle.kt",
"chars": 1120,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Format\nimport com.structuriz"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/CustomStylesheetViewModel.kt",
"chars": 778,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\ni"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/DecisionTabViewModel.kt",
"chars": 278,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\ndata class DecisionTabViewModel(val pageViewModel: SoftwareSystem"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/DecisionsTableViewModel.kt",
"chars": 1893,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Decision\nimport com.structur"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/DiagramIndexViewModel.kt",
"chars": 720,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\ndata class IndexEntry(val key: String, val title: String)\n\ndata c"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/DiagramViewModel.kt",
"chars": 1712,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.view.View\n\ndata class DiagramViewModel(\n "
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ExternalLinkViewModel.kt",
"chars": 136,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\ndata class ExternalLinkViewModel(\n val title: String,\n val "
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/FaviconViewModel.kt",
"chars": 1056,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\ni"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/FlexmarkConfig.kt",
"chars": 5051,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.Workspace\n\nimport com.vladsch.flexmark.uti"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/HeaderBarViewModel.kt",
"chars": 1076,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\n"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/HomePageViewModel.kt",
"chars": 1454,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Format\nimport nl.avisi.struc"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ImageViewModel.kt",
"chars": 262,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport nl.avisi.structurizr.site.generatr.site.asUrlToFile\n\ndata "
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ImageViewViewModel.kt",
"chars": 972,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.Component\nimport com.structurizr.mod"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/LinkViewModel.kt",
"chars": 1337,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport nl.avisi.structurizr.site.generatr.site.asUrlToDirectory\n\n"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuNodeViewModel.kt",
"chars": 141,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\ndata class MenuNodeViewModel(val name: String, val children: List"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModel.kt",
"chars": 2520,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\n"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/PageViewModel.kt",
"chars": 2013,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport nl.avisi.structurizr.site.generatr.includedSoftwareSystems"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/PropertiesTableViewModel.kt",
"chars": 629,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.util.Url\n\nfun createPropertiesTableViewMod"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SearchViewModel.kt",
"chars": 2106,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\ni"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SectionTabViewModel.kt",
"chars": 277,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\ndata class SectionTabViewModel(val pageViewModel: SoftwareSystemP"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SectionsTableViewModel.kt",
"chars": 2100,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Section\nimport com.structuri"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentCodePageViewModel.kt",
"chars": 1156,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.Container\nimport com.structurizr.mod"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentDecisionPageViewModel.kt",
"chars": 1259,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Decision\nimport com.structur"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentDecisionsPageViewModel.kt",
"chars": 1390,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Decision\nimport com.structur"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentSectionPageViewModel.kt",
"chars": 900,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Section\nimport com.structuri"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentSectionsPageViewModel.kt",
"chars": 1355,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Section\nimport com.structuri"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentsPageViewModel.kt",
"chars": 1410,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.Container\nimport nl.avisi.structuriz"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerDecisionPageViewModel.kt",
"chars": 1140,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Decision\nimport com.structur"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerDecisionsPageViewModel.kt",
"chars": 2786,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Decision\nimport com.structur"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerPageViewModel.kt",
"chars": 975,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.struc"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerSectionPageViewModel.kt",
"chars": 846,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Section\nimport com.structuri"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerSectionsPageViewModel.kt",
"chars": 2941,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Section\nimport com.structuri"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContextPageViewModel.kt",
"chars": 781,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.struc"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDecisionPageViewModel.kt",
"chars": 1072,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Decision\nimport com.structur"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDecisionsPageViewModel.kt",
"chars": 1366,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.Container\nimport com.structurizr.mod"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDependenciesPageViewModel.kt",
"chars": 2677,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.Relationship\nimport com.structurizr."
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDeploymentPageViewModel.kt",
"chars": 869,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.struc"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDynamicPageViewModel.kt",
"chars": 756,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.struc"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemHomePageViewModel.kt",
"chars": 1047,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Format\nimport com.structuriz"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemPageViewModel.kt",
"chars": 4258,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.struc"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemSectionPageViewModel.kt",
"chars": 817,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Section\nimport com.structuri"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemSectionsPageViewModel.kt",
"chars": 1609,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.Container\nimport com.structurizr.mod"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemTableUtilities.kt",
"chars": 529,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.SoftwareSystem\n\nfun TableViewModel.T"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemsPageViewModel.kt",
"chars": 866,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\n"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/TableViewModel.kt",
"chars": 4312,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nenum class CellWidth {\n UNSPECIFIED,\n ONE_TENTH,\n ONE_FO"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/Theme.kt",
"chars": 98,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nenum class Theme {\n LIGHT, DARK, AUTO\n}\n"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ToHtml.kt",
"chars": 7188,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Format\nimport com.vladsch.fl"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/WorkspaceDecisionPageViewModel.kt",
"chars": 837,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Decision\nimport nl.avisi.str"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/WorkspaceDecisionsPageViewModel.kt",
"chars": 531,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\n"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/WorkspaceDocumentationSectionPageViewModel.kt",
"chars": 699,
"preview": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Section\nimport nl.avisi.stru"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/Document.kt",
"chars": 214,
"preview": "package nl.avisi.structurizr.site.generatr.site.model.indexing\n\nimport kotlinx.serialization.Serializable\n\n@Serializable"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/Home.kt",
"chars": 807,
"preview": "package nl.avisi.structurizr.site.generatr.site.model.indexing\n\nimport com.structurizr.documentation.Documentation\nimpor"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/SoftwareSystemComponents.kt",
"chars": 1085,
"preview": "package nl.avisi.structurizr.site.generatr.site.model.indexing\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.av"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/SoftwareSystemContainerDecisions.kt",
"chars": 899,
"preview": "package nl.avisi.structurizr.site.generatr.site.model.indexing\n\nimport com.structurizr.model.Container\nimport nl.avisi.s"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/SoftwareSystemContainerSections.kt",
"chars": 739,
"preview": "package nl.avisi.structurizr.site.generatr.site.model.indexing\n\nimport com.structurizr.model.Container\nimport nl.avisi.s"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/SoftwareSystemContainers.kt",
"chars": 964,
"preview": "package nl.avisi.structurizr.site.generatr.site.model.indexing\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.av"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/SoftwareSystemContext.kt",
"chars": 694,
"preview": "package nl.avisi.structurizr.site.generatr.site.model.indexing\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.av"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/SoftwareSystemDecisions.kt",
"chars": 903,
"preview": "package nl.avisi.structurizr.site.generatr.site.model.indexing\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.av"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/SoftwareSystemHome.kt",
"chars": 1331,
"preview": "package nl.avisi.structurizr.site.generatr.site.model.indexing\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.av"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/SoftwareSystemRelationships.kt",
"chars": 1586,
"preview": "package nl.avisi.structurizr.site.generatr.site.model.indexing\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.av"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/SoftwareSystemSections.kt",
"chars": 794,
"preview": "package nl.avisi.structurizr.site.generatr.site.model.indexing\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.av"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/WorkspaceDecisions.kt",
"chars": 792,
"preview": "package nl.avisi.structurizr.site.generatr.site.model.indexing\n\nimport com.structurizr.documentation.Documentation\nimpor"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/WorkspaceSections.kt",
"chars": 976,
"preview": "package nl.avisi.structurizr.site.generatr.site.model.indexing\n\nimport com.structurizr.documentation.Documentation\nimpor"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/AutoReloading.kt",
"chars": 1078,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.s"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/CDN.kt",
"chars": 2421,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport com.structurizr.Workspace\nimport kotlinx.serialization.Ser"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/ContentDiv.kt",
"chars": 181,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\n\nfun DIV.contentDiv(block: DIV.() -> Unit) "
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Diagram.kt",
"chars": 1657,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.s"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/DiagramIndex.kt",
"chars": 542,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.s"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/ExternalLink.kt",
"chars": 376,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.FlowOrPhrasingContent\nimport kotlinx.html.a\ni"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Favicon.kt",
"chars": 324,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HEAD\nimport kotlinx.html.link\nimport nl.avisi"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/HomePage.kt",
"chars": 300,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.s"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Image.kt",
"chars": 922,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.s"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Link.kt",
"chars": 436,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.FlowOrPhrasingContent\nimport kotlinx.html.a\ni"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/MarkdownExtension.kt",
"chars": 2580,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.s"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Menu.kt",
"chars": 2215,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.s"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Modal.kt",
"chars": 705,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.FlowContent\nimport kotlinx.html.button\nimport"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Page.kt",
"chars": 3347,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.s"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/PageHeader.kt",
"chars": 3296,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.s"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/RawHtml.kt",
"chars": 349,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\n\nfun FlowContent.rawHtml(html: String, cont"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/RedirectRelative.kt",
"chars": 439,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport kotlinx.html.body\nimport kotlinx."
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/RedirectUpPage.kt",
"chars": 411,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport kotlinx.html.body\nimport kotlinx."
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SearchPage.kt",
"chars": 2421,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport kotlinx.serialization.encodeToString"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerComponentCodePage.kt",
"chars": 1441,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport kotlinx.html.div\nimport kotlinx.h"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerComponentDecisionPage.kt",
"chars": 389,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generat"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerComponentDecisionsPage.kt",
"chars": 569,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generat"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerComponentSectionPage.kt",
"chars": 483,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generat"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerComponentSectionsPage.kt",
"chars": 533,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generat"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerComponentsPage.kt",
"chars": 1125,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.s"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerDecisionPage.kt",
"chars": 362,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generat"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerDecisionsPage.kt",
"chars": 1349,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.FlowContent\nimport kotlinx.html.HTML\nimport k"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerPage.kt",
"chars": 527,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generat"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerSectionPage.kt",
"chars": 359,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generat"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerSectionsPage.kt",
"chars": 1268,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.s"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContextPage.kt",
"chars": 470,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generat"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemDecisionPage.kt",
"chars": 335,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generat"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemDecisionsPage.kt",
"chars": 1357,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.FlowContent\nimport kotlinx.html.HTML\nimport k"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemDependenciesPage.kt",
"chars": 489,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport kotlinx.html.h2\nimport nl.avisi.s"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemDeploymentPage.kt",
"chars": 479,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generat"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemDynamicPage.kt",
"chars": 470,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generat"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemHomePage.kt",
"chars": 473,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport kotlinx.html.h2\nimport nl.avisi.s"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemPage.kt",
"chars": 883,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.s"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemSectionPage.kt",
"chars": 332,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generat"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemSectionsPage.kt",
"chars": 1162,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.s"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemsPage.kt",
"chars": 424,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport kotlinx.html.h2\nimport nl.avisi.s"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Table.kt",
"chars": 2201,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.s"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/WorkspaceDecisionPage.kt",
"chars": 341,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generat"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/WorkspaceDecisionsPage.kt",
"chars": 415,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport kotlinx.html.h2\nimport nl.avisi.s"
},
{
"path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/WorkspaceDocumentationSectionPage.kt",
"chars": 377,
"preview": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generat"
},
{
"path": "src/main/resources/assets/css/admonition.css",
"chars": 6221,
"preview": ".adm-block {\n display: block;\n width: 99%;\n border-radius: 6px;\n padding-left: 10px;\n margin-bottom: 1em;"
},
{
"path": "src/main/resources/assets/css/style.css",
"chars": 1260,
"preview": "body {\n height: 100vh;\n display: flex;\n flex-direction: column;\n}\n\n.site-layout {\n display: grid;\n grid-t"
},
{
"path": "src/main/resources/assets/css/treeview.css",
"chars": 623,
"preview": ".listree-submenu-heading {\n cursor: pointer;\n padding: .5em .75em;\n}\n\nul.listree {\n list-style: none\n}\n\nul.list"
},
{
"path": "src/main/resources/assets/js/admonition.js",
"chars": 1101,
"preview": "(() => {\n let divs = document.getElementsByClassName(\"adm-block\");\n for (let i = 0; i < divs.length; i++) {\n "
},
{
"path": "src/main/resources/assets/js/auto-reload.js",
"chars": 2714,
"preview": "let hash = undefined;\n\nfunction connectToWs() {\n const ws = new WebSocket(\"ws://\" + location.host + \"/_events\");\n "
},
{
"path": "src/main/resources/assets/js/header.js",
"chars": 137,
"preview": "function redirect(event, value, href) {\n if (event.key === 'Enter' && value.length >= 1) window.location.href = href + "
},
{
"path": "src/main/resources/assets/js/katex-render.js",
"chars": 578,
"preview": "// Script taken from flexmark wiki: https://github.com/vsch/flexmark-java/wiki/Extensions#gitlab-flavoured-markdown\n(fun"
},
{
"path": "src/main/resources/assets/js/modal.js",
"chars": 526,
"preview": "function openModal(id) {\n document.getElementById(id).classList.add('is-active');\n}\n\nfunction closeModal(id) {\n window"
},
{
"path": "src/main/resources/assets/js/reformat-mermaid.js",
"chars": 707,
"preview": "// solution from https://css-tricks.com/making-mermaid-diagrams-in-markdown/\n\n// select <pre class=\"mermaid\"> _and_ <pre"
},
{
"path": "src/main/resources/assets/js/search.js",
"chars": 1567,
"preview": "window.onpageshow = function() {\n const el = document.getElementById('search');\n el.focus();\n\n const params = new URL"
},
{
"path": "src/main/resources/assets/js/svg-modal.js",
"chars": 616,
"preview": "let pz = undefined;\n\nfunction resetPz() {\n if (pz) {\n pz.resize();\n pz.center();\n pz.reset();\n }\n}\n\nfunction "
},
{
"path": "src/main/resources/assets/js/toggle-theme.js",
"chars": 647,
"preview": "if (!localStorage.getItem(\"data-theme\")) {\n const prefersDarkMode = window.matchMedia &&\n window.matchMedia('(prefer"
},
{
"path": "src/main/resources/assets/js/treeview.js",
"chars": 2582,
"preview": "function listree() {\n\n const subMenuHeadingClass = \"listree-submenu-heading\";\n const expandedClass = \"expanded\";\n con"
},
{
"path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/BranchComparatorTest.kt",
"chars": 785,
"preview": "package nl.avisi.structurizr.site.generatr.site\n\nimport assertk.assertThat\nimport assertk.assertions.containsExactly\nimp"
},
{
"path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/PlantUmlExporterTest.kt",
"chars": 20682,
"preview": "package nl.avisi.structurizr.site.generatr.site\n\nimport assertk.assertThat\nimport assertk.assertions.contains\nimport ass"
},
{
"path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/StringUtilitiesTest.kt",
"chars": 695,
"preview": "package nl.avisi.structurizr.site.generatr.site\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport nl"
},
{
"path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/StructurizrUtilitiesTest.kt",
"chars": 2189,
"preview": "package nl.avisi.structurizr.site.generatr.site\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport co"
},
{
"path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/E2ETestFixture.kt",
"chars": 2920,
"preview": "package nl.avisi.structurizr.site.generatr.site.e2e\n\nimport assertk.assertThat\nimport assertk.assertions.isTrue\nimport c"
},
{
"path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/PageTestHelper.kt",
"chars": 2536,
"preview": "package nl.avisi.structurizr.site.generatr.site.e2e\n\nimport com.microsoft.playwright.Locator\nimport com.microsoft.playwr"
},
{
"path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/RetryExtension.kt",
"chars": 903,
"preview": "package nl.avisi.structurizr.site.generatr.site.e2e\n\nimport org.junit.jupiter.api.extension.ExtensionContext\nimport org."
}
]
// ... and 72 more files (download for full content)
About this extraction
This page contains the full source code of the avisi-cloud/structurizr-site-generatr GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 272 files (559.3 KB), approximately 136.9k tokens, and a symbol index with 20 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.