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 ================================================ ================================================ FILE: .run/generate site for example model (from git repo all branches) .run.xml ================================================ ================================================ FILE: .run/generate site for example model (from git repo).run.xml ================================================ ================================================ FILE: .run/generate site for example model (local).run.xml ================================================ ================================================ FILE: .run/serve example model.run.xml ================================================ ================================================ 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 "" ``` #### 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 ================================================ * [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) # 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 . 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("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 { 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 ![System Landscape Diagram](embed:SystemLandscape) ``` See also: #### Example: Embedded diagram ![System Landscape Diagram](embed:SystemLandscape) ### 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 ![A nice picture](/pictures/nice-picture.png) ``` #### Example: Embedded picture [Sun](https://www.flickr.com/photos/schmollmolch/4937297813/), by Christian Scheja ![A nice picture](/pictures/nice-picture.png) ### 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
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
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 `
` 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
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
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) { 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) = 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 { 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() 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 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 { 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 { 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() 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, 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 = 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("") 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, currentBranch: String, serving: Boolean = false ) { val generatorContext = GeneratorContext(version, workspace, branches, currentBranch, serving) { key, url -> val diagramCache = ConcurrentHashMap() 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("") 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? ) : StringConverter(backend, opts) { // based on https://docs.asciidoctor.org/asciidoctorj/latest/write-converter/ override fun convert(node: ContentNode, transform: String?, o: Map?): 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?): Any { val plantUMLCode = reader.read() val plantUMLReader = SourceStringReader(plantUMLCode) val stream = ByteArrayOutputStream() plantUMLReader.outputImage(stream, FileFormatOption(FileFormat.SVG, false)) return createBlock(parent, "pass", "
$stream
") } } ================================================ 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() .drop(1) // ignore title .joinToString(" ") { it.text.toString() } .plus(" ") .plus( document .children .filterIsInstance() .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, 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 = emptyList(), private val images: List = 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, val flexmarkOptions: DataHolder ) val availableExtensionMap: MutableMap = 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 = 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) ================================================ 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) { 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) = 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@SearchViewModel)) addAll(softwareSystemContainerDecisions(it, this@SearchViewModel)) } } } .mapNotNull { it } ) }.mapNotNull { it } companion object { fun url() = "/search" } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SectionTabViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model data class SectionTabViewModel(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/SectionsTableViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import com.structurizr.documentation.Section import com.structurizr.model.SoftwareSystem import com.structurizr.model.StaticStructureElement import nl.avisi.structurizr.site.generatr.hasDocumentationSections import nl.avisi.structurizr.site.generatr.hasSections fun PageViewModel.createSectionsTableViewModel( sections: Collection
, dropFirst: Boolean = true, hrefFactory: (Section) -> String ): TableViewModel { val rows = sections .sortedBy { it.order } .drop(if (dropFirst) 1 else 0) .associateWith { if (dropFirst) it.order - 1 else it.order } return if (rows.isEmpty()) { TableViewModel(emptyList(), emptyList()) } else { TableViewModel.create { headerRow( headerCellSmall("#"), headerCell("Title") ) rows.forEach { (section, index) -> bodyRow( cellWithIndex(index.toString()), cellWithLink(this@createSectionsTableViewModel, section.contentTitle(), hrefFactory(section)) ) } } } } fun BaseSoftwareSystemSectionsPageViewModel.createSectionsTabViewModel( softwareSystem: SoftwareSystem, tab: SoftwareSystemPageViewModel.Tab, linkMatch: (StaticStructureElement) -> Match = { Match.EXACT } ) = buildList { if (softwareSystem.hasDocumentationSections()) { add( SectionTabViewModel( this@createSectionsTabViewModel, "System", SoftwareSystemPageViewModel.url(softwareSystem, tab), linkMatch(softwareSystem) ) ) } softwareSystem .containers .filter { it.hasSections(recursive = true) } .map { SectionTabViewModel( this@createSectionsTabViewModel, it.name, SoftwareSystemContainerSectionsPageViewModel.url(it), linkMatch(it) ) } .forEach { add(it) } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentCodePageViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import com.structurizr.model.Container import com.structurizr.model.Component import nl.avisi.structurizr.site.generatr.normalize import nl.avisi.structurizr.site.generatr.site.GeneratorContext class SoftwareSystemContainerComponentCodePageViewModel(generatorContext: GeneratorContext, container: Container, component: Component) : SoftwareSystemPageViewModel(generatorContext, container.softwareSystem, Tab.CODE) { override val url = url(container, component) val images = generatorContext.workspace.views.imageViews .filter { it.element == component } .map { ImageViewViewModel(it) } val visible = images.isNotEmpty() val containerTabs = createContainersCodeTabViewModel(generatorContext, container.softwareSystem) val componentTabs = createComponentsTabViewModel(generatorContext, container) val diagramIndex = DiagramIndexViewModel(images = images) companion object { fun url(container: Container, component: Component?) = "${url(container.softwareSystem, Tab.CODE)}/${container.name.normalize()}/${component?.name?.normalize()}" } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentDecisionPageViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import com.structurizr.documentation.Decision import com.structurizr.model.Component import nl.avisi.structurizr.site.generatr.normalize import nl.avisi.structurizr.site.generatr.site.GeneratorContext class SoftwareSystemContainerComponentDecisionPageViewModel( generatorContext: GeneratorContext, component: Component, decision: Decision ) : SoftwareSystemPageViewModel(generatorContext, component.container.softwareSystem, Tab.DECISIONS) { override val url = url(component, decision) val content = toHtml(this, transformADRLinks(decision.content, component), decision.format, generatorContext.svgFactory) private fun transformADRLinks(content: String, component: Component) = content.replace("\\[(.*)]\\(#(\\d+)\\)".toRegex()) { "[${it.groupValues[1]}](${url(component.container.softwareSystem, Tab.DECISIONS)}/${component.container.name.normalize()}/${component.name.normalize()}/${it.groupValues[2]})" } companion object { fun url(component: Component, decision: Decision) = "${url(component.container.softwareSystem, Tab.DECISIONS)}/${component.container.name.normalize()}/${component.name.normalize()}/${decision.id}" } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentDecisionsPageViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import com.structurizr.documentation.Decision import com.structurizr.model.Component import com.structurizr.model.Container import nl.avisi.structurizr.site.generatr.hasDecisions import nl.avisi.structurizr.site.generatr.normalize import nl.avisi.structurizr.site.generatr.site.GeneratorContext class SoftwareSystemContainerComponentDecisionsPageViewModel( generatorContext: GeneratorContext, component: Component ) : BaseSoftwareSystemContainerDecisionsPageViewModel( generatorContext, component.container ) { val visible = component.hasDecisions() override val url = url(component) val decisionsTable = createDecisionsTableViewModel(component.documentation.decisions) { "$url/${it.id}" } override fun decisionTableItemUrl(container: Container, decision: Decision?) = decision?.let { "${url(container)}/${decision.id}" } ?: url(container) override fun componentDecisionItemUrl(component: Component) = "${url(component.container)}/${component.name.normalize()}" companion object { fun url(container: Container) = "${url(container.softwareSystem, Tab.DECISIONS)}/${container.name.normalize()}" fun url(component: Component) = "${url(component.container.softwareSystem, Tab.DECISIONS)}/${component.container.name.normalize()}/${component.name.normalize()}" } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentSectionPageViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import com.structurizr.documentation.Section import com.structurizr.model.Component import com.structurizr.model.Container import nl.avisi.structurizr.site.generatr.normalize import nl.avisi.structurizr.site.generatr.site.GeneratorContext class SoftwareSystemContainerComponentSectionPageViewModel( generatorContext: GeneratorContext, component: Component, section: Section ) : SoftwareSystemPageViewModel(generatorContext, component.container.softwareSystem, Tab.SECTIONS) { override val url = url(component, section) val content = toHtml(this, section.content, section.format, generatorContext.svgFactory) companion object { fun url(component: Component, section: Section) = "${SoftwareSystemContainerComponentSectionsPageViewModel.url(component)}/${section.contentTitle().normalize()}" } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentSectionsPageViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import com.structurizr.documentation.Section import com.structurizr.model.Component import com.structurizr.model.Container import nl.avisi.structurizr.site.generatr.hasComponentsSections import nl.avisi.structurizr.site.generatr.hasSections import nl.avisi.structurizr.site.generatr.normalize import nl.avisi.structurizr.site.generatr.site.GeneratorContext class SoftwareSystemContainerComponentSectionsPageViewModel(generatorContext: GeneratorContext, component: Component) : BaseSoftwareSystemContainerSectionsPageViewModel(generatorContext, component.container) { val visible = component.hasSections() override val url = url(component) override fun sectionTableItemUrl(container: Container, section: Section?): String = SoftwareSystemContainerSectionsPageViewModel.url(container, section) override fun componentSectionItemUrl(component: Component): String = url(component) val componentSectionTable = createSectionsTableViewModel(component.documentation.sections, dropFirst = false) { SoftwareSystemContainerComponentSectionPageViewModel.url(component, it) } companion object { fun url(component: Component) = "${SoftwareSystemContainerSectionsPageViewModel.url(component.container)}/${component.name.normalize()}" } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentsPageViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import com.structurizr.model.Container import nl.avisi.structurizr.site.generatr.includedProperties import nl.avisi.structurizr.site.generatr.normalize import nl.avisi.structurizr.site.generatr.site.GeneratorContext class SoftwareSystemContainerComponentsPageViewModel(generatorContext: GeneratorContext, container: Container) : SoftwareSystemPageViewModel(generatorContext, container.softwareSystem, Tab.COMPONENT) { override val url = url(container) val diagrams = generatorContext.workspace.views.componentViews .filter { it.container == container } .map { DiagramViewModel.forView(this, it, generatorContext.svgFactory) } val images = generatorContext.workspace.views.imageViews .filter { it.element == container } .map { ImageViewViewModel(it) } val hasProperties = container.includedProperties.isNotEmpty() val propertiesTable = createPropertiesTableViewModel(container.includedProperties) val visible = diagrams.isNotEmpty() or images.isNotEmpty() or hasProperties val containerTabs = createContainersComponentTabViewModel(generatorContext, container.softwareSystem) val diagramIndex = DiagramIndexViewModel(diagrams, images) companion object { fun url(container: Container) = "${url(container.softwareSystem, Tab.COMPONENT)}/${container.name.normalize()}" } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerDecisionPageViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import com.structurizr.documentation.Decision import com.structurizr.model.Container import nl.avisi.structurizr.site.generatr.normalize import nl.avisi.structurizr.site.generatr.site.GeneratorContext class SoftwareSystemContainerDecisionPageViewModel( generatorContext: GeneratorContext, container: Container, decision: Decision ) : SoftwareSystemPageViewModel(generatorContext, container.softwareSystem, Tab.DECISIONS) { override val url = url(container, decision) val content = toHtml(this, transformADRLinks(decision.content, container), decision.format, generatorContext.svgFactory) private fun transformADRLinks(content: String, container: Container) = content.replace("\\[(.*)]\\(#(\\d+)\\)".toRegex()) { "[${it.groupValues[1]}](${url(container.softwareSystem, Tab.DECISIONS)}/${container.name.normalize()}/${it.groupValues[2]})" } companion object { fun url(container: Container, decision: Decision) = "${url(container.softwareSystem, Tab.DECISIONS)}/${container.name.normalize()}/${decision.id}" } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerDecisionsPageViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import com.structurizr.documentation.Decision import com.structurizr.model.Component import com.structurizr.model.Container import nl.avisi.structurizr.site.generatr.hasComponentDecisions import nl.avisi.structurizr.site.generatr.hasDecisions import nl.avisi.structurizr.site.generatr.normalize import nl.avisi.structurizr.site.generatr.site.GeneratorContext abstract class BaseSoftwareSystemContainerDecisionsPageViewModel( generatorContext: GeneratorContext, container: Container ) : BaseSoftwareSystemDecisionsPageViewModel( generatorContext, container.softwareSystem ) { val componentDecisionsTabs by lazy { val components = container.components.filter { it.hasDecisions() } buildList(components.size) { if (container.hasDecisions()) add( DecisionTabViewModel( this@BaseSoftwareSystemContainerDecisionsPageViewModel, "Container", decisionTableItemUrl(container), Match.EXACT ) ) addAll( components.map { component -> DecisionTabViewModel( this@BaseSoftwareSystemContainerDecisionsPageViewModel, component.name, componentDecisionItemUrl(component) ) } ) } } abstract fun decisionTableItemUrl(container: Container, decision: Decision? = null): String abstract fun componentDecisionItemUrl(component: Component): String } class SoftwareSystemContainerDecisionsPageViewModel(generatorContext: GeneratorContext, container: Container) : BaseSoftwareSystemContainerDecisionsPageViewModel(generatorContext, container) { override val url = url(container) val decisionsTable = createDecisionsTableViewModel(container.documentation.decisions) { "$url/${it.id}" } private val componentDecisionsVisible = container.hasComponentDecisions() val containerDecisionsVisible = container.hasDecisions() val visible = componentDecisionsVisible or containerDecisionsVisible val onlyComponentDecisionsVisible = !containerDecisionsVisible and componentDecisionsVisible override fun decisionTableItemUrl(container: Container, decision: Decision?) = decision?.let { "${url(container)}/${decision.id}" } ?: url(container) override fun componentDecisionItemUrl(component: Component) = "${url(component.container)}/${component.name.normalize()}" companion object { fun url(container: Container) = "${url(container.softwareSystem, Tab.DECISIONS)}/${container.name.normalize()}" } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerPageViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import com.structurizr.model.SoftwareSystem import nl.avisi.structurizr.site.generatr.hasContainerViews import nl.avisi.structurizr.site.generatr.site.GeneratorContext class SoftwareSystemContainerPageViewModel(generatorContext: GeneratorContext, softwareSystem: SoftwareSystem) : SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.CONTAINER) { val diagrams = generatorContext.workspace.views.containerViews .filter { it.softwareSystem == softwareSystem } .map { DiagramViewModel.forView(this, it, generatorContext.svgFactory) } val images = generatorContext.workspace.views.imageViews .filter { it.element == softwareSystem } .map { ImageViewViewModel(it) } val visible = generatorContext.workspace.views.hasContainerViews(generatorContext.workspace, softwareSystem) || images.isNotEmpty() val diagramIndex = DiagramIndexViewModel(diagrams, images) } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerSectionPageViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import com.structurizr.documentation.Section import com.structurizr.model.Container import nl.avisi.structurizr.site.generatr.normalize import nl.avisi.structurizr.site.generatr.site.GeneratorContext class SoftwareSystemContainerSectionPageViewModel( generatorContext: GeneratorContext, container: Container, section: Section ) : SoftwareSystemPageViewModel(generatorContext, container.softwareSystem, Tab.SECTIONS) { override val url = url(container, section) val content = toHtml(this, section.content, section.format, generatorContext.svgFactory) companion object { fun url(container: Container, section: Section) = "${url(container.softwareSystem, Tab.SECTIONS)}/${container.name.normalize()}/${section.contentTitle().normalize()}" } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerSectionsPageViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import com.structurizr.documentation.Section import com.structurizr.model.Component import com.structurizr.model.Container import nl.avisi.structurizr.site.generatr.hasComponentsSections import nl.avisi.structurizr.site.generatr.hasSections import nl.avisi.structurizr.site.generatr.normalize import nl.avisi.structurizr.site.generatr.site.GeneratorContext abstract class BaseSoftwareSystemContainerSectionsPageViewModel(generatorContext: GeneratorContext, container: Container) : BaseSoftwareSystemSectionsPageViewModel(generatorContext, container.softwareSystem) { val sectionsTable: TableViewModel = createSectionsTableViewModel(container.documentation.sections, dropFirst = false) { sectionTableItemUrl(container, it) } val componentSectionsTabs by lazy { val components = container.components.filter { it.hasSections() } buildList(components.size) { if (container.hasSections()) { add( SectionTabViewModel( this@BaseSoftwareSystemContainerSectionsPageViewModel, "Container", sectionTableItemUrl(container), Match.EXACT ) ) } addAll( components.map { component -> SectionTabViewModel(this@BaseSoftwareSystemContainerSectionsPageViewModel, component.name, componentSectionItemUrl(component)) } ) } } abstract fun sectionTableItemUrl(container: Container, section: Section? = null): String abstract fun componentSectionItemUrl(component: Component): String } class SoftwareSystemContainerSectionsPageViewModel(generatorContext: GeneratorContext, container: Container) : BaseSoftwareSystemContainerSectionsPageViewModel(generatorContext, container) { private val containerDocumentationSectionsVisible = container.hasSections() private val componentsDocumentationSectionsVisible = container.hasComponentsSections() val visible = containerDocumentationSectionsVisible or componentsDocumentationSectionsVisible val onlyComponentsDocumentationSectionsVisible = !containerDocumentationSectionsVisible and componentsDocumentationSectionsVisible override val url = url(container) override fun sectionTableItemUrl(container: Container, section: Section?): String = url(container, section) override fun componentSectionItemUrl(component: Component): String = SoftwareSystemContainerComponentSectionsPageViewModel.url(component) companion object { fun url(container: Container) = "${url(container.softwareSystem, Tab.SECTIONS)}/${container.name.normalize()}" fun url(container: Container, section: Section?) = section?.let { "${url(container)}/${it.contentTitle().normalize()}" } ?: url(container) } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContextPageViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import com.structurizr.model.SoftwareSystem import nl.avisi.structurizr.site.generatr.hasSystemContextViews import nl.avisi.structurizr.site.generatr.site.GeneratorContext class SoftwareSystemContextPageViewModel(generatorContext: GeneratorContext, softwareSystem: SoftwareSystem) : SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.SYSTEM_CONTEXT) { val diagrams = generatorContext.workspace.views.systemContextViews .filter { it.softwareSystem == softwareSystem } .map { DiagramViewModel.forView(this, it, generatorContext.svgFactory) } val visible = generatorContext.workspace.views.hasSystemContextViews(softwareSystem) val diagramIndex = DiagramIndexViewModel(diagrams) } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDecisionPageViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import com.structurizr.documentation.Decision import com.structurizr.model.SoftwareSystem import nl.avisi.structurizr.site.generatr.site.GeneratorContext class SoftwareSystemDecisionPageViewModel( generatorContext: GeneratorContext, softwareSystem: SoftwareSystem, decision: Decision ) : SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.DECISIONS) { override val url = url(softwareSystem, decision) val content = toHtml( this, transformADRLinks(decision.content, softwareSystem), decision.format, generatorContext.svgFactory ) private fun transformADRLinks(content: String, softwareSystem: SoftwareSystem) = content.replace("\\[(.*)]\\(#(\\d+)\\)".toRegex()) { "[${it.groupValues[1]}](${url(softwareSystem, Tab.DECISIONS)}/${it.groupValues[2]})" } companion object { fun url(softwareSystem: SoftwareSystem, decision: Decision) = "${url(softwareSystem, Tab.DECISIONS)}/${decision.id}" } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDecisionsPageViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import com.structurizr.model.Container import com.structurizr.model.SoftwareSystem import nl.avisi.structurizr.site.generatr.hasContainerDecisions import nl.avisi.structurizr.site.generatr.hasDecisions import nl.avisi.structurizr.site.generatr.site.GeneratorContext abstract class BaseSoftwareSystemDecisionsPageViewModel( generatorContext: GeneratorContext, softwareSystem: SoftwareSystem ) : SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.DECISIONS) { val decisionTabs = createDecisionsTabViewModel(softwareSystem, Tab.DECISIONS) { if (it is Container) Match.CHILD else Match.EXACT } } class SoftwareSystemDecisionsPageViewModel(generatorContext: GeneratorContext, softwareSystem: SoftwareSystem) : BaseSoftwareSystemDecisionsPageViewModel(generatorContext, softwareSystem) { val decisionsTable = createDecisionsTableViewModel(softwareSystem.documentation.decisions) { "$url/${it.id}" } private val containerDecisionsVisible = softwareSystem.hasContainerDecisions(recursive = true) val softwareSystemDecisionsVisible = softwareSystem.hasDecisions() val visible = softwareSystemDecisionsVisible or containerDecisionsVisible val onlyContainersDecisionsVisible = !softwareSystemDecisionsVisible and containerDecisionsVisible } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDependenciesPageViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import com.structurizr.model.Relationship import com.structurizr.model.SoftwareSystem import nl.avisi.structurizr.site.generatr.site.GeneratorContext class SoftwareSystemDependenciesPageViewModel( generatorContext: GeneratorContext, private val softwareSystem: SoftwareSystem ) : SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.DEPENDENCIES) { val dependenciesInboundTable = TableViewModel.create { header() incomingRelationships.forEach {inboundBodyRow(it)} } val dependenciesOutboundTable = TableViewModel.create { header() outgoingRelationships.forEach {outboundBodyRow(it)} } private val incomingRelationships get() = generatorContext.workspace.model.relationships.asSequence() .filter { it.destination == softwareSystem } .plus(softwareSystem.relationships) .filter { it.source is SoftwareSystem && it.destination is SoftwareSystem} .filter { it.destination == softwareSystem} .sortedBy { it.source.name.lowercase() } private val outgoingRelationships get() = generatorContext.workspace.model.relationships.asSequence() .filter { it.destination == softwareSystem } .plus(softwareSystem.relationships) .filter { it.source is SoftwareSystem && it.destination is SoftwareSystem } .filter { it.source == softwareSystem} .sortedBy { it.destination.name.lowercase() } private fun TableViewModel.TableViewInitializerContext.header() { headerRow( headerCellMedium("System"), headerCellLarge("Description"), headerCell("Technology") ) } private fun TableViewModel.TableViewInitializerContext.inboundBodyRow(relationship: Relationship) { bodyRow( softwareSystemDependencyCell(relationship.source as SoftwareSystem), cell(relationship.description), cell(relationship.technology) ) } private fun TableViewModel.TableViewInitializerContext.outboundBodyRow(relationship: Relationship) { bodyRow( softwareSystemDependencyCell(relationship.destination as SoftwareSystem), cell(relationship.description), cell(relationship.technology) ) } private fun TableViewModel.TableViewInitializerContext.softwareSystemDependencyCell(system: SoftwareSystem) = if (system == softwareSystem) headerCell(system.name) else softwareSystemCell(this@SoftwareSystemDependenciesPageViewModel, system) } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDeploymentPageViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import com.structurizr.model.SoftwareSystem import nl.avisi.structurizr.site.generatr.hasDeploymentViews import nl.avisi.structurizr.site.generatr.site.GeneratorContext class SoftwareSystemDeploymentPageViewModel(generatorContext: GeneratorContext, softwareSystem: SoftwareSystem) : SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.DEPLOYMENT) { val diagrams = generatorContext.workspace.views.deploymentViews .filter { it.softwareSystem == softwareSystem || it.properties["generatr.view.deployment.belongsTo"] == softwareSystem.name } .map { DiagramViewModel.forView(this, it, generatorContext.svgFactory) } val visible = generatorContext.workspace.views.hasDeploymentViews(softwareSystem) val diagramIndex = DiagramIndexViewModel(diagrams) } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDynamicPageViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import com.structurizr.model.SoftwareSystem import nl.avisi.structurizr.site.generatr.hasDynamicViews import nl.avisi.structurizr.site.generatr.site.GeneratorContext class SoftwareSystemDynamicPageViewModel(generatorContext: GeneratorContext, softwareSystem: SoftwareSystem) : SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.DYNAMIC) { val diagrams = generatorContext.workspace.views.dynamicViews .filter { it.softwareSystem == softwareSystem } .map { DiagramViewModel.forView(this, it, generatorContext.svgFactory) } val visible = generatorContext.workspace.views.hasDynamicViews(softwareSystem) val diagramIndex = DiagramIndexViewModel(diagrams) } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemHomePageViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import com.structurizr.documentation.Format import com.structurizr.model.SoftwareSystem import nl.avisi.structurizr.site.generatr.includedProperties import nl.avisi.structurizr.site.generatr.site.GeneratorContext class SoftwareSystemHomePageViewModel(generatorContext: GeneratorContext, softwareSystem: SoftwareSystem) : SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME) { val hasProperties = softwareSystem.includedProperties.any() val propertiesTable = createPropertiesTableViewModel(softwareSystem.includedProperties) val content = toHtml(this, softwareSystem.info(), softwareSystem.format(), generatorContext.svgFactory) private fun SoftwareSystem.info() = documentation.sections .minByOrNull { it.order } ?.content ?: "# Description${System.lineSeparator()}${description}" private fun SoftwareSystem.format() = documentation.sections .minByOrNull { it.order } ?.format ?: Format.Markdown } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemPageViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import com.structurizr.model.SoftwareSystem import nl.avisi.structurizr.site.generatr.* import nl.avisi.structurizr.site.generatr.site.GeneratorContext open class SoftwareSystemPageViewModel( generatorContext: GeneratorContext, private val softwareSystem: SoftwareSystem, tab: Tab ) : PageViewModel(generatorContext) { enum class Tab { HOME, SYSTEM_CONTEXT, CONTAINER, COMPONENT, CODE, DYNAMIC, DEPLOYMENT, DEPENDENCIES, DECISIONS, SECTIONS } inner class TabViewModel(val tab: Tab, match: Match = Match.EXACT) { val link = when (tab) { Tab.COMPONENT -> LinkViewModel( this@SoftwareSystemPageViewModel, title, "${url(softwareSystem, tab)}/${softwareSystem.firstContainer(generatorContext)?.name?.normalize()}", match ) Tab.CODE -> LinkViewModel( this@SoftwareSystemPageViewModel, title, "${url(softwareSystem, tab)}/${softwareSystem.firstContainer(generatorContext)?.name?.normalize()}/${softwareSystem.firstContainer(generatorContext)?.firstComponent(generatorContext)?.name?.normalize()}", match ) else -> LinkViewModel(this@SoftwareSystemPageViewModel, title, url(softwareSystem, tab), match) } private val title get() = when (tab) { Tab.HOME -> "Info" Tab.SYSTEM_CONTEXT -> "Context views" Tab.CONTAINER -> "Container views" Tab.COMPONENT -> "Component views" Tab.CODE -> "Code views" Tab.DYNAMIC -> "Dynamic views" Tab.DEPLOYMENT -> "Deployment views" Tab.DEPENDENCIES -> "Dependencies" Tab.DECISIONS -> "Decisions" Tab.SECTIONS -> "Documentation" } val visible get() = when (tab) { Tab.HOME -> true Tab.DEPENDENCIES -> true Tab.SYSTEM_CONTEXT -> generatorContext.workspace.views.hasSystemContextViews(softwareSystem) Tab.CONTAINER -> generatorContext.workspace.views.hasContainerViews(generatorContext.workspace, softwareSystem) Tab.COMPONENT -> generatorContext.workspace.views.hasComponentViews(generatorContext.workspace, softwareSystem) Tab.CODE -> generatorContext.workspace.views.hasCodeViews(generatorContext.workspace, softwareSystem) Tab.DYNAMIC -> generatorContext.workspace.views.hasDynamicViews(softwareSystem) Tab.DEPLOYMENT -> generatorContext.workspace.views.hasDeploymentViews(softwareSystem) Tab.DECISIONS -> softwareSystem.hasDecisions(recursive = true) Tab.SECTIONS -> softwareSystem.hasDocumentationSections(recursive = true) } } override val url: String = url(softwareSystem, tab) override val pageSubTitle: String = softwareSystem.name val tabs = listOf( TabViewModel(Tab.HOME), TabViewModel(Tab.SYSTEM_CONTEXT), TabViewModel(Tab.CONTAINER), TabViewModel(Tab.COMPONENT, Match.SIBLING), TabViewModel(Tab.CODE, Match.SIBLING_CHILD), TabViewModel(Tab.DYNAMIC), TabViewModel(Tab.DEPLOYMENT), TabViewModel(Tab.DEPENDENCIES), TabViewModel(Tab.DECISIONS, Match.CHILD), TabViewModel(Tab.SECTIONS, Match.CHILD) ) val description: String = softwareSystem.description companion object { fun url(softwareSystem: SoftwareSystem, tab: Tab): String { val home = "/${softwareSystem.name.normalize()}" return when (tab) { Tab.HOME -> home Tab.SYSTEM_CONTEXT -> "$home/context" Tab.CONTAINER -> "$home/container" Tab.COMPONENT -> "$home/component" Tab.CODE -> "$home/code" Tab.DYNAMIC -> "$home/dynamic" Tab.DEPLOYMENT -> "$home/deployment" Tab.DEPENDENCIES -> "$home/dependencies" Tab.DECISIONS -> "$home/decisions" Tab.SECTIONS -> "$home/sections" } } } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemSectionPageViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import com.structurizr.documentation.Section import com.structurizr.model.SoftwareSystem import nl.avisi.structurizr.site.generatr.normalize import nl.avisi.structurizr.site.generatr.site.GeneratorContext class SoftwareSystemSectionPageViewModel( generatorContext: GeneratorContext, softwareSystem: SoftwareSystem, section: Section ) : SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.SECTIONS) { override val url = url(softwareSystem, section) val content = toHtml(this, section.content, section.format, generatorContext.svgFactory) companion object { fun url(softwareSystem: SoftwareSystem, section: Section) = "${url(softwareSystem, Tab.SECTIONS)}/${section.contentTitle().normalize()}" } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemSectionsPageViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import com.structurizr.model.Container import com.structurizr.model.SoftwareSystem import nl.avisi.structurizr.site.generatr.hasContainerDocumentationSections import nl.avisi.structurizr.site.generatr.hasDocumentationSections import nl.avisi.structurizr.site.generatr.normalize import nl.avisi.structurizr.site.generatr.site.GeneratorContext abstract class BaseSoftwareSystemSectionsPageViewModel(generatorContext: GeneratorContext, softwareSystem: SoftwareSystem) : SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.SECTIONS) { val sectionsTabs: List = createSectionsTabViewModel(softwareSystem, Tab.SECTIONS) { if (it is Container) Match.CHILD else Match.EXACT } } class SoftwareSystemSectionsPageViewModel( generatorContext: GeneratorContext, softwareSystem: SoftwareSystem ) : BaseSoftwareSystemSectionsPageViewModel( generatorContext, softwareSystem ) { private val softwareSystemDocumentationSectionsVisible = softwareSystem.hasDocumentationSections() private val containerDocumentationSectionsVisible = softwareSystem.hasContainerDocumentationSections(recursive = true) val visible = softwareSystemDocumentationSectionsVisible or containerDocumentationSectionsVisible val onlyContainersDocumentationSectionsVisible = !softwareSystemDocumentationSectionsVisible and containerDocumentationSectionsVisible val sectionsTable = createSectionsTableViewModel(softwareSystem.documentation.sections) { "$url/${it.contentTitle().normalize()}" } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemTableUtilities.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import com.structurizr.model.SoftwareSystem fun TableViewModel.TableViewInitializerContext.softwareSystemCell( pageViewModel: PageViewModel, system: SoftwareSystem ) = if (pageViewModel.includedSoftwareSystems.contains(system)) cellWithSoftwareSystemLink( pageViewModel, system.name, SoftwareSystemPageViewModel.url(system, SoftwareSystemPageViewModel.Tab.HOME) ) else cellWithExternalSoftwareSystem("${system.name} (External)") ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemsPageViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import nl.avisi.structurizr.site.generatr.site.GeneratorContext class SoftwareSystemsPageViewModel(generatorContext: GeneratorContext) : PageViewModel(generatorContext) { override val url = url() override val pageSubTitle = "Software Systems" val softwareSystemsTable: TableViewModel = TableViewModel.create { headerRow( headerCellMedium("Name"), headerCell("Description") ) generatorContext.workspace.model.softwareSystems .sortedBy { it.name.lowercase() } .forEach { bodyRow( softwareSystemCell(this@SoftwareSystemsPageViewModel, it), cell(it.description) ) } } companion object { fun url() = "/software-systems" } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/TableViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model enum class CellWidth { UNSPECIFIED, ONE_TENTH, ONE_FOURTH, TWO_FOURTH, } data class TableViewModel(val headerRows: List, val bodyRows: List) { sealed interface CellViewModel { val isHeader: Boolean } data class TextCellViewModel( val title: String, override val isHeader: Boolean, val greyText: Boolean = false, val boldText: Boolean = false, val width: CellWidth = CellWidth.UNSPECIFIED ) : CellViewModel { override fun toString() = if (isHeader) "headerCell($title, greyText=$greyText)" else "cell($title, greyText=$greyText, boldText=$boldText, width=${width.name})" } data class LinkCellViewModel( val link: LinkViewModel, override val isHeader: Boolean, val boldText: Boolean = false ) : CellViewModel { override fun toString() = if (isHeader) "headerCell($link), boldText=$boldText," else "cell($link), boldText=$boldText," } data class ExternalLinkCellViewModel( val link: ExternalLinkViewModel, override val isHeader: Boolean ) : CellViewModel { override fun toString() = if (isHeader) "headerCell($link)" else "cell($link)" } data class RowViewModel(val columns: List) { override fun toString() = columns.joinToString(separator = ", ", prefix = "row { ", postfix = " }") { it.toString() } } class TableViewInitializerContext( private val headerRows: MutableList, private val bodyRows: MutableList ) { fun headerRow(vararg cells: TextCellViewModel) { headerRows.add(RowViewModel(cells.toList())) } fun bodyRow(vararg cells: CellViewModel) { bodyRows.add(RowViewModel(cells.toList())) } fun headerCell(title: String) = TextCellViewModel(title, true) fun headerCellSmall(title: String): TextCellViewModel = TextCellViewModel(title, isHeader = true, width = CellWidth.ONE_TENTH) fun headerCellMedium(title: String): TextCellViewModel = TextCellViewModel(title, isHeader = true, width = CellWidth.ONE_FOURTH) fun headerCellLarge(title: String): TextCellViewModel = TextCellViewModel(title, isHeader = true, width = CellWidth.TWO_FOURTH) fun cell(title: String, greyText: Boolean = false) = TextCellViewModel(title, false, greyText, false) fun cellWithIndex(title: String) = TextCellViewModel(title, false, boldText = true) fun cellWithExternalSoftwareSystem(title: String) = TextCellViewModel(title, false, greyText = true, boldText = true) fun cellWithLink(pageViewModel: PageViewModel, title: String, href: String) = LinkCellViewModel(LinkViewModel(pageViewModel, title, href), false) fun cellWithSoftwareSystemLink(pageViewModel: PageViewModel, title: String, href: String) = LinkCellViewModel(LinkViewModel(pageViewModel, title, href), false, boldText = true) fun cellWithExternalLink(title: String, href: String) = ExternalLinkCellViewModel(ExternalLinkViewModel(title, href), false) } override fun toString(): String { val builder = StringBuilder() builder.appendLine("header {") headerRows.forEach { builder.append(" ") builder.append(it) builder.appendLine() } builder.appendLine("}") builder.appendLine("body {") bodyRows.forEach { builder.append(" ") builder.append(it) builder.appendLine() } builder.appendLine("}") return builder.toString() } companion object { fun create(initializer: TableViewInitializerContext.() -> Unit): TableViewModel { val headerRows = mutableListOf() val bodyRows = mutableListOf() val ctx = TableViewInitializerContext(headerRows, bodyRows) ctx.initializer() return TableViewModel(headerRows, bodyRows) } } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/Theme.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model enum class Theme { LIGHT, DARK, AUTO } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ToHtml.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import com.structurizr.documentation.Format import com.vladsch.flexmark.ast.FencedCodeBlock import com.vladsch.flexmark.html.HtmlRenderer import com.vladsch.flexmark.html.HtmlWriter import com.vladsch.flexmark.html.LinkResolver import com.vladsch.flexmark.html.LinkResolverFactory import com.vladsch.flexmark.html.renderer.* import com.vladsch.flexmark.parser.Parser import com.vladsch.flexmark.util.ast.Node import kotlinx.html.div import kotlinx.html.stream.createHTML import net.sourceforge.plantuml.FileFormat import net.sourceforge.plantuml.FileFormatOption import net.sourceforge.plantuml.SourceStringReader import nl.avisi.structurizr.site.generatr.site.asUrlToDirectory import nl.avisi.structurizr.site.generatr.site.asUrlToFile import nl.avisi.structurizr.site.generatr.site.views.diagram import org.asciidoctor.Options import org.asciidoctor.SafeMode import org.jsoup.Jsoup import org.jsoup.nodes.Element import java.io.ByteArrayOutputStream const val embedPrefix = "embed:" fun toHtml( pageViewModel: PageViewModel, content: String, format: Format, svgFactory: (key: String, url: String) -> String? ): String = when (format) { Format.Markdown -> markdownToHtml(pageViewModel, content, svgFactory) Format.AsciiDoc -> asciidocToHtml(pageViewModel, content, svgFactory) } private fun markdownToHtml( pageViewModel: PageViewModel, markdown: String, svgFactory: (key: String, url: String) -> String? ): String { val flexmarkConfig = pageViewModel.flexmarkConfig val options = flexmarkConfig.flexmarkOptions val parser = Parser.builder(options).build() val renderer = HtmlRenderer.builder(options) .linkResolverFactory(CustomLinkResolver.Factory(pageViewModel)) .nodeRendererFactory { FencedCodeBlockRenderer() } .build() val markDownDocument = parser.parse(markdown) val html = renderer.render(markDownDocument) return Jsoup.parse(html) .apply { body().transformEmbeddedDiagramElements(pageViewModel, svgFactory) } .body() .html() } private class FencedCodeBlockRenderer : NodeRenderer { override fun getNodeRenderingHandlers(): MutableSet> = mutableSetOf(NodeRenderingHandler(FencedCodeBlock::class.java, this::render)) private fun render(fencedCodeBlock: FencedCodeBlock, nodeRendererContext: NodeRendererContext, htmlWriter: HtmlWriter) { if (fencedCodeBlock.info.toString() == "puml") { htmlWriter.tag("div") { val reader = SourceStringReader(fencedCodeBlock.contentChars.toString()) val stream = ByteArrayOutputStream() reader.outputImage(stream, FileFormatOption(FileFormat.SVG, false)) htmlWriter.raw(stream.toString()) } } else nodeRendererContext.delegateRender() } } private fun asciidocToHtml( pageViewModel: PageViewModel, asciidoc: String, svgFactory: (key: String, url: String) -> String? ): String { val options = Options.builder() .safe(SafeMode.SERVER) // Docs dir needs to be exposed from structurizr using `.baseDir(File("./docs/example/workspace-docs"))`, // which is not the case at the moment. // Needed for partial include `include::partial.adoc[]`, which structurizr also does not support. // see https://docs.asciidoctor.org/asciidoc/latest/directives/include/ // another option could be https://docs.asciidoctor.org/asciidoctorj/latest/locating-files/#globdirectorywalker-class .backend("html5") .build() val html = asciidoctorWithPUMLRenderer.convert(asciidoc, options) return Jsoup.parse(html) .apply { body().transformEmbeddedDiagramElements(pageViewModel, svgFactory) body().transformAsciiDocContent() body().transformAsciiDocAdmonition() body().transformAsciiDocImgSrc(pageViewModel) } .body() .html() } /** * Adds css class content to several asciidoc tags, as the default asciidoc structure is not compatible with bulma css. * [asciidoctor html5 template](https://github.com/asciidoctor/asciidoctor/blob/main/lib/asciidoctor/converter/html5.rb) */ private fun Element.transformAsciiDocContent() = this .select(".sect0, .sect1, .sect2, .sect3, .sect4, .sect5, .paragraph, .toc, .olist, .ulist, .dlist, .colist, .listingblock, .tableblock, .literalblock, .quoteblock, .stemblock, .verseblock, .imageblock, .videoblock, .audioblock, .admonitionblock") .addClass("content") private fun Element.transformAsciiDocAdmonition() = this .select(".admonitionblock").addClass("adm-block") .select("td.icon").addClass("adm-heading").parents() .select(".admonitionblock.note").addClass("adm-note").parents() .select(".admonitionblock.tip").addClass("adm-tip").parents() .select(".admonitionblock.important").addClass("adm-example").parents() .select(".admonitionblock.caution").addClass("adm-danger").parents() .select(".admonitionblock.warning").addClass("adm-warning") private fun Element.transformAsciiDocImgSrc(pageViewModel: PageViewModel) = this .select("img").map { it.attr("src", "/${it.attr("src").dropWhile { it == '/' }}".asUrlToFile(pageViewModel.url)) } private class CustomLinkResolver(private val pageViewModel: PageViewModel) : LinkResolver { override fun resolveLink(node: Node, context: LinkResolverBasicContext, link: ResolvedLink): ResolvedLink { if (link.url.startsWith(embedPrefix)) { return link .withStatus(LinkStatus.VALID) .withUrl(link.url) } if (link.url.matches("https?://.*".toRegex())) return link.withTarget("blank") return link.withStatus(LinkStatus.VALID) .withUrl("/${link.url.dropWhile { it == '/' }}" .let { if (it.endsWith('/')) it.asUrlToDirectory(pageViewModel.url) else it.asUrlToFile(pageViewModel.url) } ) } class Factory(private val viewModel: PageViewModel) : LinkResolverFactory { override fun apply(context: LinkResolverBasicContext): LinkResolver { return CustomLinkResolver(viewModel) } override fun getAfterDependents(): MutableSet>? = null override fun getBeforeDependents(): MutableSet>? = null override fun affectsGlobalScope() = false } } fun Element.transformEmbeddedDiagramElements( pageViewModel: PageViewModel, svgFactory: (key: String, url: String) -> String? ) = this.allElements .toList() .filter { it.tag().name == "img" && it.attr("src").startsWith(embedPrefix) } .forEach { val key = it.attr("src").substring(embedPrefix.length) val name = it.attr("alt").ifBlank { key } val html = createHTML().div { diagram(DiagramViewModel.forView(pageViewModel, key, name, null, null, svgFactory)) } it.parent()?.append(html) it.remove() } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/WorkspaceDecisionPageViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import com.structurizr.documentation.Decision import nl.avisi.structurizr.site.generatr.site.GeneratorContext class WorkspaceDecisionPageViewModel(generatorContext: GeneratorContext, decision: Decision) : PageViewModel(generatorContext) { override val url = url(decision) override val pageSubTitle: String = decision.title val content = toHtml( this, transformADRLinks(decision.content), decision.format, generatorContext.svgFactory ) private fun transformADRLinks(content: String) = content.replace("\\[(.*)]\\(#(\\d+)\\)".toRegex()) { "[${it.groupValues[1]}](decisions/${it.groupValues[2]}/)" } companion object { fun url(decision: Decision) = "/decisions/${decision.id}" } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/WorkspaceDecisionsPageViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import nl.avisi.structurizr.site.generatr.site.GeneratorContext class WorkspaceDecisionsPageViewModel(generatorContext: GeneratorContext) : PageViewModel(generatorContext) { override val url = url() override val pageSubTitle = "Decisions" val decisionsTable = createDecisionsTableViewModel(generatorContext.workspace.documentation.decisions) { WorkspaceDecisionPageViewModel.url(it) } companion object { fun url() = "/decisions" } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/WorkspaceDocumentationSectionPageViewModel.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import com.structurizr.documentation.Section import nl.avisi.structurizr.site.generatr.normalize import nl.avisi.structurizr.site.generatr.site.GeneratorContext class WorkspaceDocumentationSectionPageViewModel(generatorContext: GeneratorContext, section: Section) : PageViewModel(generatorContext) { override val url = url(section) override val pageSubTitle: String = section.contentTitle() val content = toHtml(this, section.content, section.format, generatorContext.svgFactory) companion object { fun url(section: Section): String { return "/${section.contentTitle().normalize()}" } } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/Document.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model.indexing import kotlinx.serialization.Serializable @Serializable data class Document(val href: String, val type: String, val title: String, val text: String) ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/Home.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model.indexing import com.structurizr.documentation.Documentation import nl.avisi.structurizr.site.generatr.site.asUrlToDirectory import nl.avisi.structurizr.site.generatr.site.model.HomePageViewModel import nl.avisi.structurizr.site.generatr.site.model.PageViewModel import nl.avisi.structurizr.site.generatr.site.model.contentText import nl.avisi.structurizr.site.generatr.site.model.contentTitle fun home(documentation: Documentation, viewModel: PageViewModel) = documentation.sections.firstOrNull() ?.let { section -> Document( HomePageViewModel.url().asUrlToDirectory(viewModel.url), "Home", section.contentTitle(), "${section.contentTitle()} ${section.contentText()}".trim() ) } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/SoftwareSystemComponents.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model.indexing import com.structurizr.model.SoftwareSystem import nl.avisi.structurizr.site.generatr.site.asUrlToDirectory import nl.avisi.structurizr.site.generatr.site.model.PageViewModel import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemPageViewModel fun softwareSystemComponents(softwareSystem: SoftwareSystem, viewModel: PageViewModel) = softwareSystem.containers .sortedBy { it.name } .flatMap { container -> container.components .sortedBy { it.name } .flatMap { component -> listOf(component.name, component.description, component.technology) } } .filter { it != null && it.isNotBlank() } .joinToString(" ") .ifBlank { null } ?.let { Document( SoftwareSystemPageViewModel.url(softwareSystem, SoftwareSystemPageViewModel.Tab.COMPONENT) .asUrlToDirectory(viewModel.url), "Component views", "${softwareSystem.name} | Component views", it ) } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/SoftwareSystemContainerDecisions.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model.indexing import com.structurizr.model.Container import nl.avisi.structurizr.site.generatr.site.asUrlToDirectory import nl.avisi.structurizr.site.generatr.site.model.PageViewModel import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerDecisionPageViewModel import nl.avisi.structurizr.site.generatr.site.model.contentText fun softwareSystemContainerDecisions(container: Container, viewModel: PageViewModel) = container .documentation .decisions .map { decision -> Document( SoftwareSystemContainerDecisionPageViewModel.url( container, decision ).asUrlToDirectory(viewModel.url), "Container Decision", "${container.name} | ${decision.title}", "${decision.title} ${decision.contentText()}".trim() ) } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/SoftwareSystemContainerSections.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model.indexing import com.structurizr.model.Container import nl.avisi.structurizr.site.generatr.site.asUrlToDirectory import nl.avisi.structurizr.site.generatr.site.model.* fun softwareSystemContainerSections(container: Container, viewModel: PageViewModel) = container .documentation .sections .map { section -> Document( SoftwareSystemContainerSectionPageViewModel.url( container, section ).asUrlToDirectory(viewModel.url), "Container Documentation", "${container.name} | ${section.contentTitle()}", "${section.contentTitle()} ${section.contentText()}".trim() ) } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/SoftwareSystemContainers.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model.indexing import com.structurizr.model.SoftwareSystem import nl.avisi.structurizr.site.generatr.site.asUrlToDirectory import nl.avisi.structurizr.site.generatr.site.model.PageViewModel import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemPageViewModel fun softwareSystemContainers(softwareSystem: SoftwareSystem, viewModel: PageViewModel) = softwareSystem.containers .sortedBy { it.name } .flatMap { container -> listOf(container.name, container.description, container.technology) } .filter { it != null && it.isNotBlank() } .joinToString(" ") .ifBlank { null } ?.let { Document( SoftwareSystemPageViewModel.url(softwareSystem, SoftwareSystemPageViewModel.Tab.CONTAINER) .asUrlToDirectory(viewModel.url), "Container views", "${softwareSystem.name} | Container views", it ) } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/SoftwareSystemContext.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model.indexing import com.structurizr.model.SoftwareSystem import nl.avisi.structurizr.site.generatr.site.asUrlToDirectory import nl.avisi.structurizr.site.generatr.site.model.PageViewModel import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemPageViewModel fun softwareSystemContext(softwareSystem: SoftwareSystem, viewModel: PageViewModel) = Document( SoftwareSystemPageViewModel.url(softwareSystem, SoftwareSystemPageViewModel.Tab.SYSTEM_CONTEXT) .asUrlToDirectory(viewModel.url), "Context views", "${softwareSystem.name} | Context views", "${softwareSystem.name} ${softwareSystem.description}".trim() ) ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/SoftwareSystemDecisions.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model.indexing import com.structurizr.model.SoftwareSystem import nl.avisi.structurizr.site.generatr.site.asUrlToDirectory import nl.avisi.structurizr.site.generatr.site.model.PageViewModel import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemDecisionPageViewModel import nl.avisi.structurizr.site.generatr.site.model.contentText fun softwareSystemDecisions(softwareSystem: SoftwareSystem, viewModel: PageViewModel) = softwareSystem.documentation .decisions .map { decision -> Document( SoftwareSystemDecisionPageViewModel.url( softwareSystem, decision ).asUrlToDirectory(viewModel.url), "Software System Decision", "${softwareSystem.name} | ${decision.title}", "${decision.title} ${decision.contentText()}".trim() ) } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/SoftwareSystemHome.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model.indexing import com.structurizr.model.SoftwareSystem import nl.avisi.structurizr.site.generatr.site.asUrlToDirectory import nl.avisi.structurizr.site.generatr.site.model.PageViewModel import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemPageViewModel import nl.avisi.structurizr.site.generatr.site.model.contentText import nl.avisi.structurizr.site.generatr.site.model.contentTitle fun softwareSystemHome(softwareSystem: SoftwareSystem, viewModel: PageViewModel): Document? { val (contentTitle, contentText) = softwareSystem.section() val propertyValues = softwareSystem.propertyValues() if (contentTitle.isBlank() && contentText.isBlank() && propertyValues.isBlank()) return null return Document( SoftwareSystemPageViewModel.url(softwareSystem, SoftwareSystemPageViewModel.Tab.HOME) .asUrlToDirectory(viewModel.url), "Software System Info", "${softwareSystem.name} | ${contentTitle.ifEmpty { "Info" }}", "$contentTitle $contentText $propertyValues".trim(), ) } private fun SoftwareSystem.section() = documentation.sections.firstOrNull()?.let { it.contentTitle() to it.contentText() } ?: ("" to "") private fun SoftwareSystem.propertyValues() = properties.values.joinToString(" ") ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/SoftwareSystemRelationships.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model.indexing import com.structurizr.model.SoftwareSystem import nl.avisi.structurizr.site.generatr.site.asUrlToDirectory import nl.avisi.structurizr.site.generatr.site.model.PageViewModel import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemPageViewModel fun softwareSystemRelationships(softwareSystem: SoftwareSystem, viewModel: PageViewModel) = softwareSystem.relationships .filter { r -> r.source == softwareSystem } .sortedBy { it.destination.name } .flatMap { relationship -> listOf(relationship.destination.name, relationship.description, relationship.technology) } .plus( softwareSystem.model.softwareSystems .sortedBy { it.name } .filterNot { system -> system == softwareSystem } .flatMap { other -> other.relationships .filter { r -> r.destination == softwareSystem } .sortedBy { it.source.name } .flatMap { relationship -> listOf(relationship.source.name, relationship.description, relationship.technology) } } ) .filter { it != null && it.isNotBlank() } .joinToString(" ") .ifBlank { null } ?.let { Document( SoftwareSystemPageViewModel.url(softwareSystem, SoftwareSystemPageViewModel.Tab.DEPENDENCIES) .asUrlToDirectory(viewModel.url), "Dependencies", "${softwareSystem.name} | Dependencies", it ) } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/SoftwareSystemSections.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model.indexing import com.structurizr.model.SoftwareSystem import nl.avisi.structurizr.site.generatr.site.asUrlToDirectory import nl.avisi.structurizr.site.generatr.site.model.* fun softwareSystemSections(softwareSystem: SoftwareSystem, viewModel: PageViewModel) = softwareSystem.documentation .sections .drop(1) // Drop software system home .map { section -> Document( SoftwareSystemSectionPageViewModel.url( softwareSystem, section ).asUrlToDirectory(viewModel.url), "Software System Documentation", "${softwareSystem.name} | ${section.contentTitle()}", "${section.contentTitle()} ${section.contentText()}".trim() ) } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/WorkspaceDecisions.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model.indexing import com.structurizr.documentation.Documentation import nl.avisi.structurizr.site.generatr.site.asUrlToDirectory import nl.avisi.structurizr.site.generatr.site.model.PageViewModel import nl.avisi.structurizr.site.generatr.site.model.WorkspaceDecisionPageViewModel import nl.avisi.structurizr.site.generatr.site.model.contentText fun workspaceDecisions(documentation: Documentation, viewModel: PageViewModel) = documentation.decisions .map { decision -> Document( WorkspaceDecisionPageViewModel.url(decision) .asUrlToDirectory(viewModel.url), "Workspace Decision", decision.title, "${decision.title} ${decision.contentText()}".trim() ) } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/WorkspaceSections.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model.indexing import com.structurizr.documentation.Documentation import nl.avisi.structurizr.site.generatr.normalize import nl.avisi.structurizr.site.generatr.site.asUrlToDirectory import nl.avisi.structurizr.site.generatr.site.model.PageViewModel import nl.avisi.structurizr.site.generatr.site.model.WorkspaceDocumentationSectionPageViewModel import nl.avisi.structurizr.site.generatr.site.model.contentText import nl.avisi.structurizr.site.generatr.site.model.contentTitle fun workspaceSections(documentation: Documentation, viewModel: PageViewModel) = documentation.sections .drop(1) // Drop home .map { section -> Document( WorkspaceDocumentationSectionPageViewModel.url(section) .asUrlToDirectory(viewModel.url), "Workspace Documentation", section.contentTitle(), "${section.contentTitle()} ${section.contentText()}".trim() ) } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/AutoReloading.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.* import nl.avisi.structurizr.site.generatr.site.asUrlToFile import nl.avisi.structurizr.site.generatr.site.model.PageViewModel fun HEAD.autoReloadScript(viewModel: PageViewModel) { script( type = ScriptType.textJavaScript, src = "../" + "/auto-reload.js".asUrlToFile(viewModel.url) ) { } } fun BODY.updatingSiteProgressBar() { div(classes = "is-overlay is-front-centered") { progress(classes = "progress is-small mt-1 is-hidden") { id = "updating-site" value = "0" } } } fun BODY.updateSiteErrorHero() { div(classes = "container is-hidden") { id = "update-site-error" div(classes = "hero is-danger mt-6") { div(classes = "hero-body") { p(classes = "title") { text("Error loading workspace file") } p(classes = "subtitle") { id = "update-site-error-message" } } } } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/CDN.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import com.structurizr.Workspace import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import java.lang.IllegalStateException class CDN(val workspace: Workspace) { private val cdnBaseURL = getCdnBaseUrl() private val json = Json { ignoreUnknownKeys = true } private val packageJson = object {}.javaClass.getResource("/package.json")?.readText() ?: throw IllegalStateException("package.json not found") private val dependencies = json.parseToJsonElement(packageJson).jsonObject["dependencies"] ?.jsonObject ?.map { Dependency(cdnBaseURL, it.key, it.value.jsonPrimitive.content) } ?: throw IllegalStateException("dependencies element not found in package.json") fun bulmaCss() = dependencies.single { it.name == "bulma" }.let { "${it.baseUrl()}/css/bulma.min.css" } fun katexJs() = dependencies.single { it.name == "katex" }.let { "${it.baseUrl()}/dist/katex.min.js" } fun katexCss() = dependencies.single { it.name == "katex" }.let { "${it.baseUrl()}/dist/katex.min.css" } fun lunrJs() = dependencies.single { it.name == "lunr" }.let { "${it.baseUrl()}/lunr.min.js" } fun lunrLanguagesStemmerJs() = dependencies.single { it.name == "lunr-languages" }.let { "${it.baseUrl()}/min/lunr.stemmer.support.min.js" } fun lunrLanguagesJs(language: String) = dependencies.single { it.name == "lunr-languages" }.let { "${it.baseUrl()}/min/lunr.$language.min.js" } fun mermaidJs() = dependencies.single { it.name == "mermaid" }.let { "${it.baseUrl()}/dist/mermaid.esm.min.mjs" } fun svgpanzoomJs() = dependencies.single { it.name == "svg-pan-zoom" }.let { "${it.baseUrl()}/dist/svg-pan-zoom.min.js" } fun webfontloaderJs() = dependencies.single { it.name == "webfontloader" }.let { "${it.baseUrl()}/webfontloader.js" } fun getCdnBaseUrl() = workspace.views.configuration.properties .getOrDefault("generatr.site.cdn", "https://cdn.jsdelivr.net/npm") .trimEnd('/') @Serializable data class Dependency(val cdnBaseURL: String, val name: String, val version: String) { fun baseUrl() = "$cdnBaseURL/$name@$version" } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/ContentDiv.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.* fun DIV.contentDiv(block: DIV.() -> Unit) { div(classes = "content p-3") { block() } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Diagram.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.* import nl.avisi.structurizr.site.generatr.site.model.DiagramViewModel fun FlowContent.diagram(viewModel: DiagramViewModel) { if (viewModel.svg != null) { val dialogId = "${viewModel.key}-modal" val svgId = "${viewModel.key}-svg" figure { style = "width: min(100%, ${viewModel.diagramWidthInPixels}px);" attributes["id"] = viewModel.key rawHtml(viewModel.svg) figcaption { a { onClick = "openSvgModal('$dialogId', '$svgId')" +viewModel.title if (!viewModel.description.isNullOrBlank()) { br +viewModel.description } } } } modal(dialogId) { // TODO: no links in this SVG rawHtml(viewModel.svg, svgId, "modal-box-content") div(classes = "has-text-centered") { +viewModel.title +" [" a(href = viewModel.svgLocation.relativeHref, target = "_blank") { +"svg" } +"|" a(href = viewModel.pngLocation.relativeHref, target = "_blank") { +"png" } +"|" a(href = viewModel.pumlLocation.relativeHref, target = "_blank") { +"puml" } +"]" } } } else div(classes = "notification is-danger") { +"No view with key" span(classes = "has-text-weight-bold") { +" ${viewModel.key} " } +"found!" } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/DiagramIndex.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.* import nl.avisi.structurizr.site.generatr.site.model.DiagramIndexViewModel fun FlowContent.diagramIndex(viewModel: DiagramIndexViewModel) { if(viewModel.visible) { p(classes = "subtitle is-6") { +"Index:" } ul { viewModel.entries.forEach { li { a(href = "#${it.key}") { +it.title } } } } } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/ExternalLink.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.FlowOrPhrasingContent import kotlinx.html.a import nl.avisi.structurizr.site.generatr.site.model.ExternalLinkViewModel fun FlowOrPhrasingContent.externalLink(viewModel: ExternalLinkViewModel) { a( href = viewModel.href, target = "_blank", ) { +viewModel.title } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Favicon.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.HEAD import kotlinx.html.link import nl.avisi.structurizr.site.generatr.site.model.FaviconViewModel fun HEAD.favicon(viewModel: FaviconViewModel) { link( rel = "icon", type = viewModel.type, href = viewModel.url ) } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/HomePage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.* import nl.avisi.structurizr.site.generatr.site.model.HomePageViewModel fun HTML.homePage(viewModel: HomePageViewModel) { page(viewModel) { contentDiv { rawHtml(viewModel.content) } } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Image.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.* import nl.avisi.structurizr.site.generatr.site.model.ImageViewViewModel fun FlowContent.image(viewModel: ImageViewViewModel) { val dialogId = "${viewModel.key}-modal" figure { style = "width: fit-content;" attributes["id"] = viewModel.key p(classes = "has-text-weight-bold") { +viewModel.title } img { src = viewModel.content } figcaption { a { onClick = "openModal('$dialogId')" +viewModel.title if (!viewModel.description.isNullOrBlank()) { br +viewModel.description } } } } modal(dialogId) { img(classes = "modal-box-content modal-image") { src = viewModel.content } div(classes = "has-text-centered") { +viewModel.title } } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Link.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.FlowOrPhrasingContent import kotlinx.html.a import nl.avisi.structurizr.site.generatr.site.model.LinkViewModel fun FlowOrPhrasingContent.link(viewModel: LinkViewModel, classes: String = "") { a( href = viewModel.relativeHref, classes = "$classes ${if (viewModel.active) "is-active" else ""}".trim() ) { +viewModel.title } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/MarkdownExtension.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.* import nl.avisi.structurizr.site.generatr.site.asUrlToFile import nl.avisi.structurizr.site.generatr.site.model.PageViewModel fun HEAD.markdownAdmonitionStylesheet(viewModel: PageViewModel) { link( rel = "stylesheet", href = "../" + "/admonition.css".asUrlToFile(viewModel.url) ) } fun BODY.markdownAdmonitionScript(viewModel: PageViewModel) { script( type = ScriptType.textJavaScript, src = "../" + "/admonition.js".asUrlToFile(viewModel.url) ) { } } fun HEAD.katexStylesheet(cdn: CDN) { // loading KaTeX as global on a webpage: https://katex.org/docs/browser.html#loading-as-global unsafe { raw(""" """) } } fun HEAD.katexScript(cdn: CDN) { // loading KaTeX as global on a webpage: https://katex.org/docs/browser.html#loading-as-global unsafe { raw(""" """) } } fun HEAD.katexFonts(cdn: CDN) { // loading KaTeX as global on a webpage: https://katex.org/docs/browser.html#loading-as-global unsafe { raw(""" """) } } fun BODY.katexRenderScript(viewModel: PageViewModel) { // Fix to support math rendering in markdown: script( type = ScriptType.textJavaScript, src = "../" + "/katex-render.js".asUrlToFile(viewModel.url) ) { } } fun BODY.mermaidScript(viewModel: PageViewModel) { // Fix to support mermaid diagrams in markdown: script( type = ScriptType.textJavaScript, src = "../" + "/reformat-mermaid.js".asUrlToFile(viewModel.url) ) { } // Simple full example, how to include Mermaid: https://mermaid.js.org/config/usage.html#simple-full-example script(type = "module") { unsafe { raw("import mermaid from '${viewModel.cdn.mermaidJs()}';") } } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Menu.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.* import nl.avisi.structurizr.site.generatr.site.model.LinkViewModel import nl.avisi.structurizr.site.generatr.site.model.MenuNodeViewModel import nl.avisi.structurizr.site.generatr.site.model.MenuViewModel fun DIV.menu(viewModel: MenuViewModel, nestGroups: Boolean) { aside(classes = "menu p-3") { generalSection(viewModel.generalItems) softwareSystemsSection(viewModel, nestGroups) } } private fun ASIDE.generalSection(items: List) { p(classes = "menu-label") { +"General" } menuItemLinks(items) } private fun ASIDE.softwareSystemsSection(viewModel: MenuViewModel, nestGroups: Boolean) { p(classes = "menu-label") { +"Software systems" } if (nestGroups) { ul(classes = "listree menu-list has-site-branding") { buildHtmlTree(viewModel.softwareSystemNodes(), viewModel).invoke(this) } } else { menuItemLinks(viewModel.softwareSystemItems) } } private fun ASIDE.menuItemLinks(items: List) { ul(classes = "menu-list has-site-branding") { li { items.forEach { link(it) } } } } private fun buildHtmlTree(node: MenuNodeViewModel, viewModel: MenuViewModel): UL.() -> Unit = { if (node.name.isNotEmpty() && node.children.isEmpty()) { val itemLink = viewModel.softwareSystemItems.find { it.title == node.name } li { if (itemLink != null) { link(itemLink) } } } if (node.name.isNotEmpty() && node.children.isNotEmpty()) { li { div(classes = "listree-submenu-heading") { +node.name } ul(classes = "listree-submenu-items") { for (child in node.children) { buildHtmlTree(child, viewModel).invoke(this) } } } } if (node.name.isEmpty() && node.children.isNotEmpty()) { ul(classes = "listree-submenu-items") { for (child in node.children) { buildHtmlTree(child, viewModel).invoke(this) } } } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Modal.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.FlowContent import kotlinx.html.button import kotlinx.html.div import kotlinx.html.id import kotlinx.html.onClick fun FlowContent.modal(dialogId: String, block: FlowContent.() -> Unit) { div(classes = "modal") { id = dialogId div(classes = "modal-background") { onClick = "closeModal('$dialogId')" } div(classes = "modal-content") { div(classes = "box") { block() } } button(classes = "modal-close is-large") { attributes["aria-label"] = "close" onClick = "closeModal('$dialogId')" } } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Page.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.* import nl.avisi.structurizr.site.generatr.site.asUrlToFile import nl.avisi.structurizr.site.generatr.site.model.PageViewModel import nl.avisi.structurizr.site.generatr.site.model.Theme fun HTML.page(viewModel: PageViewModel, block: DIV.() -> Unit) { attributes["lang"] = "en" when (viewModel.theme) { Theme.LIGHT -> { attributes["data-theme"] = "light" } Theme.DARK -> { attributes["data-theme"] = "dark" } Theme.AUTO -> { } } headFragment(viewModel) bodyFragment(viewModel, block) } private fun HTML.headFragment(viewModel: PageViewModel) { head { meta(charset = "utf-8") meta(name = "viewport", content = "width=device-width, initial-scale=1") title { +viewModel.pageTitle } link(rel = "stylesheet", href = viewModel.cdn.bulmaCss()) link(rel = "stylesheet", href = "../" + "/style.css".asUrlToFile(viewModel.url)) link(rel = "stylesheet", href = "./" + "/style-branding.css".asUrlToFile(viewModel.url)) script(type = ScriptType.textJavaScript, src = "../" + "/modal.js".asUrlToFile(viewModel.url)) { } script(type = ScriptType.textJavaScript, src = "../" + "/svg-modal.js".asUrlToFile(viewModel.url)) { } script(type = ScriptType.textJavaScript, src = viewModel.cdn.svgpanzoomJs()) { } if (viewModel.allowToggleTheme) script(type = ScriptType.textJavaScript, src = "../" + "/toggle-theme.js".asUrlToFile(viewModel.url)) { } if (viewModel.includeTreeview) link(rel = "stylesheet", href = "../" + "/treeview.css".asUrlToFile(viewModel.url)) if (viewModel.includeAdmonition) markdownAdmonitionStylesheet(viewModel) if (viewModel.includeKatex) katexStylesheet(viewModel.cdn) if (viewModel.includeKatex) katexScript(viewModel.cdn) if (viewModel.includeKatex) katexFonts(viewModel.cdn) if (viewModel.favicon.includeFavicon) favicon(viewModel.favicon) if (viewModel.includeAutoReloading) autoReloadScript(viewModel) if (viewModel.customStylesheet.includeCustomStylesheet) link(rel = "stylesheet", href = viewModel.customStylesheet.resourceURI) } } private fun HTML.bodyFragment(viewModel: PageViewModel, block: DIV.() -> Unit) { body { if (viewModel.includeAutoReloading) updatingSiteProgressBar() pageHeader(viewModel.headerBar) div(classes = "site-layout") { id = "site" menu(viewModel.menu, viewModel.includeTreeview) div(classes = "container is-fluid") { block() } } if (viewModel.includeAutoReloading) updateSiteErrorHero() if (viewModel.includeAdmonition) markdownAdmonitionScript(viewModel) mermaidScript(viewModel) if (viewModel.includeTreeview) { script(type = ScriptType.textJavaScript, src = "../" + "/treeview.js".asUrlToFile(viewModel.url)) { } script(type = ScriptType.textJavaScript) { unsafe { +"listree();" } } } if (viewModel.includeKatex) katexRenderScript(viewModel) } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/PageHeader.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.* import nl.avisi.structurizr.site.generatr.site.asUrlToFile import nl.avisi.structurizr.site.generatr.site.model.HeaderBarViewModel import nl.avisi.structurizr.site.generatr.site.model.ImageViewModel import nl.avisi.structurizr.site.generatr.site.model.LinkViewModel fun BODY.pageHeader(viewModel: HeaderBarViewModel) { script( type = ScriptType.textJavaScript, src = "../" + "/header.js".asUrlToFile(viewModel.url) ) { } nav(classes = "navbar") { role = "navigation" attributes["aria-label"] = "main navigation" div(classes = "navbar-brand has-site-branding") { if (viewModel.hasLogo) logo(viewModel.titleLink, viewModel.logo!!) navbarLink(viewModel.titleLink) } div(classes = "navbar-menu has-site-branding") { div(classes = "navbar-end") { div(classes = "navbar-item") { input(classes = "input is-small is-rounded") { id = "search" type = InputType.search size = "30" maxLength = "50" placeholder = "Search..." onKeyUp = "redirect(event, value, '${viewModel.searchLink.relativeHref}')" } } div(classes = "navbar-item has-dropdown is-hoverable") { a(classes = "navbar-link has-site-branding") { +viewModel.currentBranch } div(classes = "navbar-dropdown is-right") { viewModel.branches.forEach { branchLink -> a( classes = "navbar-item", href = branchLink.relativeHref ) { +branchLink.title } } if (viewModel.allowToggleTheme) { hr(classes = "navbar-divider") a( classes = "navbar-item", ) { onClick = "toggleTheme()" +"Toggle theme" } } hr(classes = "navbar-divider") div(classes = "navbar-item has-text-grey-light") { span { +"v" } span { +viewModel.version } } } } } } } } private fun DIV.logo(linkViewModel: LinkViewModel, imageViewModel: ImageViewModel) { a( classes = "navbar-item", href = linkViewModel.relativeHref ) { img { alt = linkViewModel.title src = imageViewModel.relativeHref } } } private fun DIV.navbarLink(viewModel: LinkViewModel) { a( classes = "navbar-item", href = viewModel.relativeHref ) { span(classes = "has-text-weight-semibold has-site-branding") { +viewModel.title } } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/RawHtml.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.* fun FlowContent.rawHtml(html: String, contentId: String? = null, contentClass: String? = null) { div { if (contentId != null) id = contentId if (contentClass != null) classes = setOf(contentClass) unsafe { +html } } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/RedirectRelative.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.HTML import kotlinx.html.body import kotlinx.html.head import kotlinx.html.meta import kotlinx.html.title fun HTML.redirectRelative(appendUrl: String) { attributes["lang"] = "en" head { meta { httpEquiv = "refresh" content = "0; url=./$appendUrl" } title { +"Structurizr site generatr" } } body() } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/RedirectUpPage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.HTML import kotlinx.html.body import kotlinx.html.head import kotlinx.html.meta import kotlinx.html.title fun HTML.redirectUpPage() { attributes["lang"] = "en" head { meta { httpEquiv = "refresh" content = "0; url=../" } title { +"Structurizr site generatr" } } body() } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SearchPage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.* import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import nl.avisi.structurizr.site.generatr.site.asUrlToFile import nl.avisi.structurizr.site.generatr.site.model.SearchViewModel fun HTML.searchPage(viewModel: SearchViewModel) { val language = if (viewModel.language == "en") "" else viewModel.language if (!supportedLanguages.contains(language)) throw IllegalArgumentException( "Indexing language $language is not supported, available languages are $supportedLanguages" ) page(viewModel) { contentDiv { h2 { +viewModel.pageSubTitle } script( type = ScriptType.textJavaScript, src = viewModel.cdn.lunrJs() ) { } script( type = ScriptType.textJavaScript, src = viewModel.cdn.lunrLanguagesStemmerJs() ) { } if (language.isNotBlank()) script( type = ScriptType.textJavaScript, src = viewModel.cdn.lunrLanguagesJs(language) ) { } script(type = ScriptType.textJavaScript) { unsafe { +"const documents = ${Json.encodeToString(viewModel.documents)};" +"const idx = lunr(function () {" if (viewModel.language.isNotBlank()) +"this.use(lunr.${viewModel.language});" +""" this.ref('href') this.field('text') documents.forEach(function (doc) { this.add(doc) }, this) }); """ } } script( type = ScriptType.textJavaScript, src = "../" + "/search.js".asUrlToFile(viewModel.url) ) { } div { id = "search-results" } } } } // From https://github.com/olivernn/lunr-languages private val supportedLanguages = listOf( "", "ar", "da", "de", "du", "es", "fi", "fr", "hi", "hu", "it", "ja", "jp", "ko", "nl", "no", "pt", "ro", "ru", "sv", "ta", "th", "tr", "vi", "zh", ) ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerComponentCodePage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.HTML import kotlinx.html.div import kotlinx.html.li import kotlinx.html.ul import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerComponentCodePageViewModel fun HTML.softwareSystemContainerComponentCodePage(viewModel: SoftwareSystemContainerComponentCodePageViewModel) { if (viewModel.visible) { softwareSystemPage(viewModel) { div(classes = "tabs") { ul(classes = "m-0 is-flex-wrap-wrap is-flex-shrink-1 is-flex-grow-0") { viewModel.containerTabs .forEach { li(classes = if (it.link.active) "is-active" else null) { link(it.link) } } } } div(classes = "tabs is-size-7") { ul(classes = "m-0 is-flex-wrap-wrap is-flex-shrink-1 is-flex-grow-0") { viewModel.componentTabs .forEach { li(classes = if (it.link.active) "is-active" else null) { link(it.link) } } } } diagramIndex(viewModel.diagramIndex) viewModel.images.forEach { image(it) } } } else redirectUpPage() } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerComponentDecisionPage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.HTML import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerComponentDecisionPageViewModel fun HTML.softwareSystemContainerComponentDecisionPage(viewModel: SoftwareSystemContainerComponentDecisionPageViewModel) { softwareSystemPage(viewModel) { rawHtml(viewModel.content) } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerComponentDecisionsPage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.HTML import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerComponentDecisionsPageViewModel fun HTML.softwareSystemContainerComponentDecisionsPage( viewModel: SoftwareSystemContainerComponentDecisionsPageViewModel ) { if (viewModel.visible) { softwareSystemPage(viewModel) { decisionTabs(viewModel) componentDecisionTabs(viewModel) table(viewModel.decisionsTable) } } else { redirectUpPage() } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerComponentSectionPage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.HTML import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerComponentSectionPageViewModel import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerSectionPageViewModel fun HTML.softwareSystemContainerComponentSectionPage(viewModel: SoftwareSystemContainerComponentSectionPageViewModel) { softwareSystemPage(viewModel) { rawHtml(viewModel.content) } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerComponentSectionsPage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.HTML import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerComponentSectionsPageViewModel fun HTML.softwareSystemContainerComponentSectionsPage(viewModel: SoftwareSystemContainerComponentSectionsPageViewModel) { if (viewModel.visible) softwareSystemPage(viewModel) { softwareSystemContainerSectionsBody(viewModel) table(viewModel.componentSectionTable) } else redirectUpPage() } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerComponentsPage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.* import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerComponentsPageViewModel fun HTML.softwareSystemContainerComponentsPage(viewModel: SoftwareSystemContainerComponentsPageViewModel) { if (viewModel.visible) { softwareSystemPage(viewModel) { div(classes = "tabs") { ul(classes = "m-0 is-flex-wrap-wrap is-flex-shrink-1 is-flex-grow-0") { viewModel.containerTabs .forEach { li(classes = if (it.link.active) "is-active" else null) { link(it.link) } } } } diagramIndex(viewModel.diagramIndex) viewModel.diagrams.forEach { diagram(it) } viewModel.images.forEach { image(it) } if(viewModel.hasProperties) { h3 { +"Properties" } table(viewModel.propertiesTable) } } } else redirectUpPage() } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerDecisionPage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.HTML import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerDecisionPageViewModel fun HTML.softwareSystemContainerDecisionPage(viewModel: SoftwareSystemContainerDecisionPageViewModel) { softwareSystemPage(viewModel) { rawHtml(viewModel.content) } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerDecisionsPage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.FlowContent import kotlinx.html.HTML import kotlinx.html.div import kotlinx.html.li import kotlinx.html.ul import nl.avisi.structurizr.site.generatr.site.model.BaseSoftwareSystemContainerDecisionsPageViewModel import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerDecisionsPageViewModel fun HTML.softwareSystemContainerDecisionsPage(viewModel: SoftwareSystemContainerDecisionsPageViewModel) { if (viewModel.onlyComponentDecisionsVisible) { redirectRelative( viewModel.componentDecisionsTabs.first().link.relativeHref ) } else if (viewModel.visible) { softwareSystemPage(viewModel) { decisionTabs(viewModel) componentDecisionTabs(viewModel) table(viewModel.decisionsTable) } } else { redirectUpPage() } } fun FlowContent.componentDecisionTabs(viewModel: BaseSoftwareSystemContainerDecisionsPageViewModel) { div(classes = "tabs is-size-7") { ul(classes = "m-0 is-flex-wrap-wrap is-flex-shrink-1 is-flex-grow-0") { viewModel.componentDecisionsTabs.forEach { tab -> li(classes = if (tab.link.active) "is-active" else null) { link(tab.link) } } } } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerPage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.HTML import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerPageViewModel fun HTML.softwareSystemContainerPage(viewModel: SoftwareSystemContainerPageViewModel) { if (viewModel.visible) softwareSystemPage(viewModel) { diagramIndex(viewModel.diagramIndex) viewModel.diagrams.forEach { diagram(it) } viewModel.images.forEach { image(it) } } else redirectUpPage() } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerSectionPage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.HTML import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerSectionPageViewModel fun HTML.softwareSystemContainerSectionPage(viewModel: SoftwareSystemContainerSectionPageViewModel) { softwareSystemPage(viewModel) { rawHtml(viewModel.content) } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerSectionsPage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.* import nl.avisi.structurizr.site.generatr.site.model.BaseSoftwareSystemContainerSectionsPageViewModel import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerSectionsPageViewModel fun HTML.softwareSystemContainerSectionsPage(viewModel: SoftwareSystemContainerSectionsPageViewModel) { if (viewModel.onlyComponentsDocumentationSectionsVisible) redirectRelative( viewModel.componentSectionsTabs.first().link.relativeHref ) else if (viewModel.visible) softwareSystemPage(viewModel) { softwareSystemContainerSectionsBody(viewModel) table(viewModel.sectionsTable) } else redirectUpPage() } fun FlowContent.softwareSystemContainerSectionsBody(viewModel: BaseSoftwareSystemContainerSectionsPageViewModel) { softwareSystemSectionsBody(viewModel) div(classes = "tabs is-size-7") { ul(classes = "m-0 is-flex-wrap-wrap is-flex-shrink-1 is-flex-grow-0") { viewModel.componentSectionsTabs.forEach { tab -> li(classes = if (tab.link.active) "is-active" else null) { link(tab.link) } } } } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContextPage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.HTML import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContextPageViewModel fun HTML.softwareSystemContextPage(viewModel: SoftwareSystemContextPageViewModel) { if (viewModel.visible) softwareSystemPage(viewModel) { diagramIndex(viewModel.diagramIndex) viewModel.diagrams.forEach { diagram(it) } } else redirectUpPage() } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemDecisionPage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.HTML import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemDecisionPageViewModel fun HTML.softwareSystemDecisionPage(viewModel: SoftwareSystemDecisionPageViewModel) { softwareSystemPage(viewModel) { rawHtml(viewModel.content) } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemDecisionsPage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.FlowContent import kotlinx.html.HTML import kotlinx.html.div import kotlinx.html.li import kotlinx.html.ul import nl.avisi.structurizr.site.generatr.site.model.BaseSoftwareSystemDecisionsPageViewModel import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemDecisionsPageViewModel import kotlin.collections.forEach fun HTML.softwareSystemDecisionsPage(viewModel: SoftwareSystemDecisionsPageViewModel) { if (viewModel.onlyContainersDecisionsVisible) { redirectRelative( viewModel.decisionTabs.first().link.relativeHref ) } else if (viewModel.visible) { softwareSystemPage(viewModel) { decisionTabs(viewModel) if (viewModel.softwareSystemDecisionsVisible) { table(viewModel.decisionsTable) } } } else { redirectUpPage() } } fun FlowContent.decisionTabs(viewModel: BaseSoftwareSystemDecisionsPageViewModel) { div(classes = "tabs") { ul(classes = "m-0 is-flex-wrap-wrap is-flex-shrink-1 is-flex-grow-0") { viewModel.decisionTabs .forEach { li(classes = if (it.link.active) "is-active" else null) { link(it.link) } } } } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemDependenciesPage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.HTML import kotlinx.html.h2 import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemDependenciesPageViewModel fun HTML.softwareSystemDependenciesPage(viewModel: SoftwareSystemDependenciesPageViewModel) { softwareSystemPage(viewModel) { h2 { +"Inbound" } table(viewModel.dependenciesInboundTable) h2 { +"Outbound" } table(viewModel.dependenciesOutboundTable) } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemDeploymentPage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.HTML import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemDeploymentPageViewModel fun HTML.softwareSystemDeploymentPage(viewModel: SoftwareSystemDeploymentPageViewModel) { if (viewModel.visible) softwareSystemPage(viewModel) { diagramIndex(viewModel.diagramIndex) viewModel.diagrams.forEach { diagram(it) } } else redirectUpPage() } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemDynamicPage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.HTML import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemDynamicPageViewModel fun HTML.softwareSystemDynamicPage(viewModel: SoftwareSystemDynamicPageViewModel) { if (viewModel.visible) softwareSystemPage(viewModel) { diagramIndex(viewModel.diagramIndex) viewModel.diagrams.forEach { diagram(it) } } else redirectUpPage() } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemHomePage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.HTML import kotlinx.html.h2 import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemHomePageViewModel fun HTML.softwareSystemHomePage(viewModel: SoftwareSystemHomePageViewModel) { softwareSystemPage(viewModel) { rawHtml(viewModel.content) if (viewModel.hasProperties) { h2 { +"Properties" } table(viewModel.propertiesTable) } } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemPage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.* import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemPageViewModel fun HTML.softwareSystemPage(viewModel: SoftwareSystemPageViewModel, block: FlowContent.() -> Unit) { page(viewModel) { h1(classes = "title mt-3") { +viewModel.pageSubTitle } h2(classes = "subtitle") { +viewModel.description } div(classes = "tabs mt-3") { ul(classes = "is-flex-wrap-wrap is-flex-shrink-1 is-flex-grow-0") { viewModel.tabs .filter { it.visible } .forEach { li(classes = if (it.link.active) "is-active" else null) { link(it.link) } } } } contentDiv { block() } } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemSectionPage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.HTML import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemSectionPageViewModel fun HTML.softwareSystemSectionPage(viewModel: SoftwareSystemSectionPageViewModel) { softwareSystemPage(viewModel) { rawHtml(viewModel.content) } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemSectionsPage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.* import nl.avisi.structurizr.site.generatr.site.model.BaseSoftwareSystemSectionsPageViewModel import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemSectionsPageViewModel fun HTML.softwareSystemSectionsPage(viewModel: SoftwareSystemSectionsPageViewModel) { if (viewModel.onlyContainersDocumentationSectionsVisible) { redirectRelative( viewModel.sectionsTabs.first().link.relativeHref ) } else if (viewModel.visible) softwareSystemPage(viewModel) { softwareSystemSectionsBody(viewModel) table(viewModel.sectionsTable) } else redirectUpPage() } fun FlowContent.softwareSystemSectionsBody(viewModel: BaseSoftwareSystemSectionsPageViewModel) { div(classes = "tabs") { ul(classes = "m-0 is-flex-wrap-wrap is-flex-shrink-1 is-flex-grow-0") { viewModel.sectionsTabs .forEach { li(classes = if (it.link.active) "is-active" else null) { link(it.link) } } } } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemsPage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.HTML import kotlinx.html.h2 import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemsPageViewModel fun HTML.softwareSystemsPage(viewModel: SoftwareSystemsPageViewModel) { page(viewModel = viewModel) { contentDiv { h2 { +viewModel.pageSubTitle } table(viewModel.softwareSystemsTable) } } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Table.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.* import nl.avisi.structurizr.site.generatr.site.model.TableViewModel import nl.avisi.structurizr.site.generatr.site.model.CellWidth fun FlowContent.table(viewModel: TableViewModel) { table (classes = "table is-fullwidth") { thead { viewModel.headerRows.forEach { row(it) } } tbody { viewModel.bodyRows.forEach { row(it) } } } } private fun THEAD.row(viewModel: TableViewModel.RowViewModel) { tr { viewModel.columns.forEach { cell(it) } } } private fun TBODY.row(viewModel: TableViewModel.RowViewModel) { tr { viewModel.columns.forEach { cell(it) } } } private fun TR.cell(viewModel: TableViewModel.CellViewModel) { when (viewModel) { is TableViewModel.TextCellViewModel -> { val classes = when (viewModel.width) { CellWidth.UNSPECIFIED -> null CellWidth.ONE_TENTH -> "is-one-tenth" CellWidth.ONE_FOURTH -> "is-one-fourth" CellWidth.TWO_FOURTH -> "is-two-fourth" } var spanClasses = "" if (viewModel.greyText) spanClasses += "has-text-grey " if (viewModel.boldText) spanClasses += "has-text-weight-bold" if (viewModel.isHeader) th(classes = classes) { span(classes = spanClasses) { +viewModel.title } } else td(classes = classes) { span(classes = spanClasses) { +viewModel.title } } } is TableViewModel.LinkCellViewModel -> { val classes = if (viewModel.boldText) "has-text-weight-bold" else null if (viewModel.isHeader) th(classes = classes) { link(viewModel.link) } else td(classes = classes) { link(viewModel.link) } } is TableViewModel.ExternalLinkCellViewModel -> if (viewModel.isHeader) th { externalLink(viewModel.link) } else td { externalLink(viewModel.link) } } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/WorkspaceDecisionPage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.HTML import nl.avisi.structurizr.site.generatr.site.model.WorkspaceDecisionPageViewModel fun HTML.workspaceDecisionPage(viewModel: WorkspaceDecisionPageViewModel) { page(viewModel) { contentDiv { rawHtml(viewModel.content) } } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/WorkspaceDecisionsPage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.HTML import kotlinx.html.h2 import nl.avisi.structurizr.site.generatr.site.model.WorkspaceDecisionsPageViewModel fun HTML.workspaceDecisionsPage(viewModel: WorkspaceDecisionsPageViewModel) { page(viewModel) { contentDiv { h2 { +viewModel.pageSubTitle } table(viewModel.decisionsTable) } } } ================================================ FILE: src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/WorkspaceDocumentationSectionPage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import kotlinx.html.HTML import nl.avisi.structurizr.site.generatr.site.model.WorkspaceDocumentationSectionPageViewModel fun HTML.workspaceDocumentationSectionPage(viewModel: WorkspaceDocumentationSectionPageViewModel) { page(viewModel) { contentDiv { rawHtml(viewModel.content) } } } ================================================ FILE: src/main/resources/assets/css/admonition.css ================================================ .adm-block { display: block; width: 99%; border-radius: 6px; padding-left: 10px; margin-bottom: 1em; border: 1px solid; border-left-width: 4px; box-shadow: 2px 2px 6px #cdcdcd; } .adm-heading { display: block; font-weight: bold; font-size: 0.9em; height: 1.8em; padding-top: 0.3em; padding-bottom: 2em; border-bottom: solid 1px; padding-left: 10px; margin-left: -10px; } .adm-body { display: block; padding-bottom: 0.5em; padding-top: 0.5em; margin-left: 1.5em; margin-right: 1.5em; } .adm-heading > span { color: initial; } .adm-icon { height: 1.6em; width: 1.6em; display: inline-block; vertical-align: middle; margin-right: 0.25em; margin-left: -0.25em; } .adm-hidden { display: none !important; } .adm-block.adm-collapsed > .adm-heading, .adm-block.adm-open > .adm-heading { position: relative; cursor: pointer; } .adm-block.adm-collapsed > .adm-heading { margin-bottom: 0; } .adm-block.adm-collapsed .adm-body { display: none !important; } .adm-block.adm-open > .adm-heading:after, .adm-block.adm-collapsed > .adm-heading:after { display: inline-block; position: absolute; top:calc(50% - .65em); right: 0.5em; font-size: 1.3em; content: '▼'; } .adm-block.adm-collapsed > .adm-heading:after { right: 0.50em; top:calc(50% - .75em); transform: rotate(90deg); } /* default scheme */ .adm-block { border-color: #ebebeb; border-bottom-color: #bfbfbf; } .adm-block.adm-abstract { border-left-color: #48C4FF; } .adm-block .adm-block.adm-abstract .adm-heading, .adm-block.adm-abstract .adm-heading { background: #E8F7FF; color: #48C4FF; border-bottom-color: #dbf3ff; } .adm-block.adm-abstract.adm-open > .adm-heading:after, .adm-block.adm-abstract.adm-collapsed > .adm-heading:after { color: #80d9ff; } .adm-block.adm-bug { border-left-color: #F50057; } .adm-block .adm-block.adm-bug .adm-heading, .adm-block.adm-bug .adm-heading { background: #FEE7EE; color: #F50057; border-bottom-color: #fcd9e4; } .adm-block.adm-bug.adm-open > .adm-heading:after, .adm-block.adm-bug.adm-collapsed > .adm-heading:after { color: #f57aab; } .adm-block.adm-danger { border-left-color: #FE1744; } .adm-block .adm-block.adm-danger .adm-heading, .adm-block.adm-danger .adm-heading { background: #FFE9ED; color: #FE1744; border-bottom-color: #ffd9e0; } .adm-block.adm-danger.adm-open > .adm-heading:after, .adm-block.adm-danger.adm-collapsed > .adm-heading:after { color: #fc7e97; } .adm-block.adm-example { border-left-color: #7940ff; } .adm-block .adm-block.adm-example .adm-heading, .adm-block.adm-example .adm-heading { background: #EFEBFF; color: #7940ff; border-bottom-color: #e0d9ff; } .adm-block.adm-example.adm-open > .adm-heading:after, .adm-block.adm-example.adm-collapsed > .adm-heading:after { color: #b199ff; } .adm-block.adm-fail { border-left-color: #FE5E5E; } .adm-block .adm-block.adm-fail .adm-heading, .adm-block.adm-fail .adm-heading { background: #FFEEEE; color: #Fe5e5e; border-bottom-color: #ffe3e3; } .adm-block.adm-fail.adm-open > .adm-heading:after, .adm-block.adm-fail.adm-collapsed > .adm-heading:after { color: #fcb1b1; } .adm-block.adm-faq { border-left-color: #5ED116; } .adm-block .adm-block.adm-faq .adm-heading, .adm-block.adm-faq .adm-heading { background: #EEFAE8; color: #5ED116; border-bottom-color: #e6fadc; } .adm-block.adm-faq.adm-open > .adm-heading:after, .adm-block.adm-faq.adm-collapsed > .adm-heading:after { color: #98cf72; } .adm-block.adm-info { border-left-color: #00B8D4; } .adm-block .adm-block.adm-info .adm-heading, .adm-block.adm-info .adm-heading { background: #E8F7FA; color: #00B8D4; border-bottom-color: #dcf5fa; } .adm-block.adm-info.adm-open > .adm-heading:after, .adm-block.adm-info.adm-collapsed > .adm-heading:after { color: #83ced6; } .adm-block.adm-note { border-left-color: #448AFF; } .adm-block.adm-block.adm-note .adm-heading, .adm-block.adm-note .adm-heading { background: #EDF4FF; color: #448AFF; border-bottom-color: #e0edff; } .adm-block.adm-note.adm-open > .adm-heading:after, .adm-block.adm-note.adm-collapsed > .adm-heading:after { color: #8cb8ff; } .adm-block.adm-quote { border-left-color: #9E9E9E; } .adm-block .adm-block.adm-quote .adm-heading, .adm-block.adm-quote .adm-heading { background: #F4F4F4; color: #9E9E9E; border-bottom-color: #e8e8e8; } .adm-block.adm-quote.adm-open > .adm-heading:after, .adm-block.adm-quote.adm-collapsed > .adm-heading:after { color: #b3b3b3; } .adm-block.adm-success { border-left-color: #1DCD63; } .adm-block .adm-block.adm-success .adm-heading, .adm-block.adm-success .adm-heading { background: #E9F8EE; color: #1DCD63; border-bottom-color: #dcf7e5; } .adm-block.adm-success.adm-open > .adm-heading:after, .adm-block.adm-success.adm-collapsed > .adm-heading:after { color: #7acc98; } .adm-block.adm-tip { border-left-color: #01BFA5; } .adm-block .adm-block.adm-tip .adm-heading, .adm-block.adm-tip .adm-heading { background: #E9F9F6; color: #01BFA5; border-bottom-color: #dcf7f2; } .adm-block.adm-tip.adm-open > .adm-heading:after, .adm-block.adm-tip.adm-collapsed > .adm-heading:after { color: #7dd1c0; } .adm-block.adm-warning { border-left-color: #FF9001; } .adm-block .adm-block.adm-warning .adm-heading, .adm-block.adm-warning .adm-heading { background: #FEF3E8; color: #FF9001; border-bottom-color: #Fef3e8; } .adm-block.adm-warning.adm-open > .adm-heading:after, .adm-block.adm-warning.adm-collapsed > .adm-heading:after { color: #fcbb6a; } /* Assciidoc adaptions */ .admonitionblock .icon > .title { word-break: normal; font-size: 1em; } .admonitionblock table, .admonitionblock thead, .admonitionblock tbody, .admonitionblock th, .admonitionblock td, .admonitionblock tr { display: block; } .admonitionblock td.icon { width: initial; height: initial; margin-left: -10px; } .admonitionblock .title { font-size: 1.5em; } ================================================ FILE: src/main/resources/assets/css/style.css ================================================ body { height: 100vh; display: flex; flex-direction: column; } .site-layout { display: grid; grid-template-columns: 300px 1fr; flex-grow: 1; } .menu { border-right: 1px solid #cccccc; } .menu-list a { background-color: transparent; } .is-front-centered { width: 40%; margin-left: 30%; height: fit-content; z-index: 50; } .is-one-tenth { width: 10%; } .is-one-fourth { width: 25%; } .is-two-fourth { width: 50%; } svg a:hover { opacity: 90%; } .modal-content { width: calc(100vw - 120px); } .modal-box-content { width: 100%; height: calc(100vh - 120px); } .modal-svg { display: inline; width: inherit; min-width: inherit; max-width: inherit; height: inherit; min-height: inherit; max-height: inherit; } a { color: #485fc7; /* Pre Bulma 1.x colour code for headers, tabs and links */ } .modal-image { object-fit: contain; } a.navbar-item:hover { background-color: transparent; } .tabs li.is-active a { color: #485fc7; border-bottom-color: #485fc7; } .tabs li { font-weight: bold; } .input { color: dimgrey!important; background-color: white!important; } .input::placeholder { color: darkgrey!important; } ================================================ FILE: src/main/resources/assets/css/treeview.css ================================================ .listree-submenu-heading { cursor: pointer; padding: .5em .75em; } ul.listree { list-style: none } ul.listree-submenu-items { list-style: none; white-space: nowrap; padding-left: 10px; } div.listree-submenu-heading.collapsed:before { content: "\25B6"; display: inline-block; min-width: 20px; } div.listree-submenu-heading.expanded:before { content: "\25BC"; display: inline-block; min-width: 20px; } .scrollable-menu { height: auto; max-width: 800px; overflow-y: hidden } .menu-list li ul { border-left: 0; margin: .25em; padding-left: .75em; } ================================================ FILE: src/main/resources/assets/js/admonition.js ================================================ (() => { let divs = document.getElementsByClassName("adm-block"); for (let i = 0; i < divs.length; i++) { let div = divs[i]; if (div.classList.contains("adm-collapsed") || div.classList.contains("adm-open")) { let headings = div.getElementsByClassName("adm-heading"); if (headings.length > 0) { headings[0].addEventListener("click", event => { let el = div; event.preventDefault(); event.stopImmediatePropagation(); if (el.classList.contains("adm-collapsed")) { console.debug("Admonition Open", event.srcElement); el.classList.remove("adm-collapsed"); el.classList.add("adm-open"); } else { console.debug("Admonition Collapse", event.srcElement); el.classList.add("adm-collapsed"); el.classList.remove("adm-open"); } }); } } } })(); ================================================ FILE: src/main/resources/assets/js/auto-reload.js ================================================ let hash = undefined; function connectToWs() { const ws = new WebSocket("ws://" + location.host + "/_events"); ws.onopen = () => { console.log("Connected to socket."); reloadIfNeeded(); } ws.onclose = (event) => { console.log('Socket has been closed. Attempting to reconnect ...', event.reason); setTimeout(() => connectToWs(), 1000); }; ws.onmessage = (event) => { if (event.data === "site-updating") { console.log("Site updating ...") showUpdatingSite() } else if (event.data === "site-updated") { console.log("Site update detected, detect page content change ...") hideUpdatingSite() hideUpdateSiteError() showSite() reloadIfNeeded(); } else { console.log(event.data) hideUpdatingSite() hideSite() showUpdateSiteError(event.data) } } } function reloadIfNeeded() { fetch(window.location + "index.html.md5") .then((response) => { if (!response.ok) { throw Error(response.statusText.toLowerCase()) } return response.text() }) .then((content) => { if (!hash) { hash = content; } else if (hash !== content) { console.log("Page content change detected, reloading ...") location.reload() } else { console.log("Page content has not been changed.") } }) .catch((error) => { if (error.message === "not found") { location.href = location.origin } else { console.log(error) } }); } function showUpdatingSite() { document.getElementById("updating-site").classList.remove("is-hidden") document.getElementById("updating-site").attributes.removeNamedItem("value") } function hideUpdatingSite() { document.getElementById("updating-site").classList.add("is-hidden") document.getElementById("updating-site").value = "0" } function showUpdateSiteError(content) { document.getElementById("update-site-error-message").innerText = content document.getElementById("update-site-error").classList.remove("is-hidden") } function hideUpdateSiteError() { document.getElementById("update-site-error-message").innerText = "" document.getElementById("update-site-error").classList.add("is-hidden") } function showSite() { document.getElementById("site").classList.remove("is-hidden"); } function hideSite() { document.getElementById("site").classList.add("is-hidden"); } connectToWs(); ================================================ FILE: src/main/resources/assets/js/header.js ================================================ function redirect(event, value, href) { if (event.key === 'Enter' && value.length >= 1) window.location.href = href + '?q=' + value; } ================================================ FILE: src/main/resources/assets/js/katex-render.js ================================================ // Script taken from flexmark wiki: https://github.com/vsch/flexmark-java/wiki/Extensions#gitlab-flavoured-markdown (function () { document.addEventListener("DOMContentLoaded", function () { var mathElems = document.getElementsByClassName("katex"); var elems = []; for (const i in mathElems) { if (mathElems.hasOwnProperty(i)) elems.push(mathElems[i]); } elems.forEach(elem => { katex.render(elem.textContent, elem, { throwOnError: false, displayMode: elem.nodeName !== 'SPAN', }); }); }); })(); ================================================ FILE: src/main/resources/assets/js/modal.js ================================================ function openModal(id) { document.getElementById(id).classList.add('is-active'); } function closeModal(id) { window.removeEventListener('resize', resetPz); document.getElementById(id).classList.remove('is-active'); } // Add a keyboard event to close all modals document.addEventListener('keydown', (event) => { if (event.code === 'Escape') { (document.querySelectorAll('.modal') || []).forEach((modal) => { if (modal.classList.contains('is-active')) { closeModal(modal.id); } }); } }); ================================================ FILE: src/main/resources/assets/js/reformat-mermaid.js ================================================ // solution from https://css-tricks.com/making-mermaid-diagrams-in-markdown/ // select
 _and_ 

document.querySelectorAll("pre.mermaid, pre>code.language-mermaid, div.mermaid").forEach($el => {
    // if the second selector got a hit, reference the parent 
    if ($el.tagName === "CODE")
      $el = $el.parentElement
    // put the Mermaid contents in the expected 
// plus keep the original contents in a nice
$el.outerHTML = `
${$el.textContent}
Diagram source
${$el.textContent}
` }) ================================================ FILE: src/main/resources/assets/js/search.js ================================================ window.onpageshow = function() { const el = document.getElementById('search'); el.focus(); const params = new URLSearchParams(window.location.search); const terms = params.get('q'); if (terms) { el.value = terms; search(terms); } }; function search(terms) { const results = idx.search(terms).map(item => { const document = documents.find(post => item.ref === post.href); return { score: item.score, href: document.href, type: document.type, title: document.title }; }); const div = document.getElementById('search-results'); clearResultElements(div); if (results.length === 0) { div.appendChild(createNoResultsElement()); } else { results.forEach(result => { div.appendChild(createResultElement(result)); }); } } function clearResultElements(div) { while (div.firstChild) { div.removeChild(div.firstChild); } } function createResultElement(result) { const p = document.createElement('p'); const link = document.createElement('a'); link.href = result.href; const title = document.createElement('p'); title.innerText = result.title; title.className = 'mb-0'; const type = document.createElement('small'); type.innerText = result.type; link.appendChild(title); p.appendChild(link); p.appendChild(type); return p; } function createNoResultsElement() { const p = document.createElement('p'); const small = document.createElement('small'); small.className = 'is-italic'; small.innerText = 'Your search yielded no results'; p.appendChild(small); return p; } ================================================ FILE: src/main/resources/assets/js/svg-modal.js ================================================ let pz = undefined; function resetPz() { if (pz) { pz.resize(); pz.center(); pz.reset(); } } function openSvgModal(id, svgId) { openModal(id); const svgElement = document.getElementById(svgId).firstElementChild; svgElement.classList.add('modal-svg'); pz = svgPanZoom(svgElement, { zoomEnabled: true, controlIconsEnabled: true, fit: true, center: true, minZoom: 1, maxZoom: 5 }); resetPz(); // Reset position on window resize window.addEventListener('resize', resetPz); } function closeSvgModal(id) { if (pz) { pz.destroy(); } closeModal(id); } ================================================ FILE: src/main/resources/assets/js/toggle-theme.js ================================================ if (!localStorage.getItem("data-theme")) { const prefersDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; localStorage.setItem("data-theme", prefersDarkMode ? "dark" : "light"); } document.documentElement.setAttribute("data-theme", localStorage.getItem("data-theme")); function toggleTheme() { if (localStorage.getItem("data-theme") === "light") { document.documentElement.setAttribute("data-theme", "dark"); localStorage.setItem("data-theme", "dark"); } else { document.documentElement.setAttribute("data-theme", "light"); localStorage.setItem("data-theme", "light"); } } ================================================ FILE: src/main/resources/assets/js/treeview.js ================================================ function listree() { const subMenuHeadingClass = "listree-submenu-heading"; const expandedClass = "expanded"; const collapsedClass = "collapsed"; const activeClass = "is-active"; const subMenuHeadings = document.getElementsByClassName(subMenuHeadingClass); Array .from(subMenuHeadings) .forEach(function(subMenuHeading) { // Collapse all the subMenuHeadings while searching for is-active class let foundActive = false; subMenuHeading.classList.add(collapsedClass); subMenuHeading.nextElementSibling.style.display = "none"; // Check if this sub-menu heading is active const liElements = subMenuHeading.nextElementSibling.children; for (let i = 0; i < liElements.length; i++) { if (liElements[i].hasChildNodes()) { if (liElements[i].children[0].tagName === "A") { if (liElements[i].children[0].classList.contains(activeClass)) { foundActive = true; break; } } } } // Expand all parent sub-menus until root menu is reached if (foundActive) { subMenuHeading.classList.remove(collapsedClass); subMenuHeading.classList.add(expandedClass); subMenuHeading.nextElementSibling.style.display = "block"; let currentSubMenu = subMenuHeading.parentElement; while (currentSubMenu !== null && !currentSubMenu.classList.contains("listree")) { if(currentSubMenu.tagName === "UL"){ if(currentSubMenu.previousElementSibling != null) { currentSubMenu.previousElementSibling.classList.remove(collapsedClass); currentSubMenu.previousElementSibling.classList.add(expandedClass); currentSubMenu.style.display = "block"; } } currentSubMenu = currentSubMenu.parentElement } } // Add the eventlistener to all subMenuHeadings subMenuHeading.addEventListener("click", function(event) { event.preventDefault(); const subMenuList = event.target.nextElementSibling; if (subMenuList.style.display === "none") { subMenuHeading.classList.remove(collapsedClass); subMenuHeading.classList.add(expandedClass); subMenuHeading.nextElementSibling.style.display = "block"; } else { subMenuHeading.classList.remove(expandedClass); subMenuHeading.classList.add(collapsedClass); subMenuHeading.nextElementSibling.style.display = "none"; } event.stopPropagation(); }); }); } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/BranchComparatorTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site import assertk.assertThat import assertk.assertions.containsExactly import nl.avisi.structurizr.site.generatr.branchComparator import org.junit.jupiter.api.DynamicTest import org.junit.jupiter.api.TestFactory class BranchComparatorTest { @TestFactory fun `default branch first then by string`() = listOf( listOf("a", "B", "main"), listOf("a", "main", "B"), listOf("B", "a", "main"), listOf("B", "main", "a"), listOf("main", "a", "B"), listOf("main", "B", "a"), ).map { branches -> DynamicTest.dynamicTest(branches.toString()) { assertThat(branches.sortedWith(branchComparator("main"))) .containsExactly("main", "a", "B") } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/PlantUmlExporterTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site import assertk.assertThat import assertk.assertions.contains import assertk.assertions.isEqualTo import com.structurizr.Workspace import org.junit.jupiter.api.DynamicTest import org.junit.jupiter.api.TestFactory import kotlin.test.Test class PlantUmlExporterTest { @TestFactory fun `adds skinparam to remove explicit size from generated svg`() = exporters().map { (type, exporterFn) -> DynamicTest.dynamicTest("($type)") { val workspace = createWorkspaceWithOneSystem() val diagram = exporterFn(workspace, "/landscape/").export(workspace.views.systemContextViews.first()) assertThat(diagram.definition) .contains("skinparam svgDimensionStyle false") } } @TestFactory fun `adds skinparam to preserve the aspect ratio of the generated svg`() = exporters().map { (type, exporterFn) -> DynamicTest.dynamicTest("($type)") { val workspace = createWorkspaceWithOneSystem() val diagram = exporterFn(workspace, "/landscape/") .export(workspace.views.systemContextViews.first()) assertThat(diagram.definition) .contains("skinparam preserveAspectRatio meet") } } private fun exporters() = listOf( ExporterType.C4.name.lowercase() to { workspace: Workspace, url: String -> C4PlantUmlExporterWithElementLinks(workspace, url) }, ExporterType.STRUCTURIZR.name.lowercase() to { workspace: Workspace, url: String -> StructurizrPlantUmlExporterWithElementLinks(workspace, url) } ) @Test fun `renders diagram (c4)`() { val workspace = createWorkspaceWithOneSystem() val diagram = C4PlantUmlExporterWithElementLinks(workspace, "/landscape/") .export(workspace.views.systemContextViews.first()) assertThat(diagram.definition.withoutC4HeaderAndFooter()).isEqualTo( """ System(System1, "System 1", ${'$'}descr="", ${'$'}tags="", ${'$'}link="") """.trimIndent() ) } @Test fun `renders diagram (structurizr)`() { val workspace = createWorkspaceWithOneSystem() val diagram = StructurizrPlantUmlExporterWithElementLinks(workspace, "/landscape/") .export(workspace.views.systemContextViews.first()) assertThat(diagram.definition.withoutStructurizrHeaderAndFooter()).isEqualTo( """ rectangle "==System 1\n[Software System]" <> as System1 """.trimIndent() ) } @Test fun `renders System Diagram with link to container (c4)`() { val workspace = createWorkspaceWithOneSystemWithContainers() val diagram = C4PlantUmlExporterWithElementLinks(workspace, "/container/") .export(workspace.views.systemContextViews.first()) assertThat(diagram.definition.withoutC4HeaderAndFooter()).isEqualTo( """ System(System1, "System 1", ${'$'}descr="", ${'$'}tags="", ${'$'}link="../system-1/container/") """.trimIndent() ) } @Test fun `renders System Diagram with link to container (structurizr)`() { val workspace = createWorkspaceWithOneSystemWithContainers() val diagram = StructurizrPlantUmlExporterWithElementLinks(workspace, "/container/") .export(workspace.views.systemContextViews.first()) assertThat(diagram.definition.withoutStructurizrHeaderAndFooter()).isEqualTo( """ rectangle "==System 1\n[Software System]" <> as System1 [[../system-1/container/]] """.trimIndent() ) } @Test fun `renders System Diagram with link to other system and link to container (c4)`() { val workspace = createSystemContextViewForWorkspaceWithTwoSystemWithContainers() val diagram = C4PlantUmlExporterWithElementLinks(workspace, "/container/") .export(workspace.views.systemContextViews.first()) assertThat(diagram.definition.withoutC4HeaderAndFooter()).isEqualTo( """ System(System1, "System 1", ${'$'}descr="", ${'$'}tags="", ${'$'}link="../system-1/container/") System(System2, "System 2", ${'$'}descr="", ${'$'}tags="", ${'$'}link="../system-2/context/") Rel(System2, System1, "uses", ${'$'}techn="", ${'$'}tags="", ${'$'}link="") """.trimIndent() ) } @Test fun `renders System Diagram with link to other system and link to container (structurizr)`() { val workspace = createSystemContextViewForWorkspaceWithTwoSystemWithContainers() val diagram = StructurizrPlantUmlExporterWithElementLinks(workspace, "/container/") .export(workspace.views.systemContextViews.first()) assertThat(diagram.definition.withoutStructurizrHeaderAndFooter()).isEqualTo( """ rectangle "==System 1\n[Software System]" <> as System1 [[../system-1/container/]] rectangle "==System 2\n[Software System]" <> as System2 [[../system-2/context/]] System2 --> System1 <> : "uses" """.trimIndent() ) } @Test fun `renders Container Diagram with link to component diagram (c4)`() { val workspace = createWorkspaceWithOneSystemWithContainersAndComponents() val diagram = C4PlantUmlExporterWithElementLinks(workspace, "/container/") .export(workspace.views.containerViews.first()) assertThat(diagram.definition.withoutC4HeaderAndFooter()).isEqualTo( """ System_Boundary("System1_boundary", "System 1", ${'$'}tags="") { Container(System1.Container1, "Container 1", ${'$'}techn="", ${'$'}descr="", ${'$'}tags="", ${'$'}link="../system-1/component/container-1/") } """.trimIndent() ) } @Test fun `renders Container Diagram with link to component diagram (structurizr)`() { val workspace = createWorkspaceWithOneSystemWithContainersAndComponents() val diagram = StructurizrPlantUmlExporterWithElementLinks(workspace, "/container/") .export(workspace.views.containerViews.first()) assertThat(diagram.definition.withoutStructurizrHeaderAndFooter()).isEqualTo( """ rectangle "System 1\n[Software System]" <> { rectangle "==Container 1\n[Container]" <> as System1.Container1 [[../system-1/component/container-1/]] } """.trimIndent() ) } @Test fun `renders Container Diagram with link to code view (c4)`() { val workspace = createWorkspaceWithOneSystemWithContainersComponentAndImage() val diagram = C4PlantUmlExporterWithElementLinks(workspace, "/container/") .export(workspace.views.componentViews.first()) assertThat(diagram.definition.withoutC4HeaderAndFooter()).isEqualTo( """ System_Boundary("System1_boundary", "System 1", ${'$'}tags="") { Container_Boundary("System1.Container1_boundary", "Container 1", ${'$'}tags="") { Component(System1.Container1.Component1, "Component 1", ${'$'}techn="", ${'$'}descr="", ${'$'}tags="", ${'$'}link="../system-1/code/container-1/component-1/") Component(System1.Container1.Component2, "Component 2", ${'$'}techn="", ${'$'}descr="", ${'$'}tags="", ${'$'}link="") Component(System1.Container1.Component3, "Component 3", ${'$'}techn="", ${'$'}descr="", ${'$'}tags="", ${'$'}link="") } } """.trimIndent() ) } @Test fun `renders Container Diagram with link to code view (structurizr)`() { val workspace = createWorkspaceWithOneSystemWithContainersComponentAndImage() val diagram = StructurizrPlantUmlExporterWithElementLinks(workspace, "/container/") .export(workspace.views.componentViews.first()) assertThat(diagram.definition.withoutStructurizrHeaderAndFooter()).isEqualTo( """ rectangle "System 1\n[Software System]" <> { rectangle "Container 1\n[Container]" <> { rectangle "==Component 1\n[Component]" <> as System1.Container1.Component1 [[../system-1/code/container-1/component-1/]] rectangle "==Component 2\n[Component]" <> as System1.Container1.Component2 rectangle "==Component 3\n[Component]" <> as System1.Container1.Component3 } } """.trimIndent() ) } @Test fun `renders Components Diagram without link to other Component Diagram (c4)`() { val workspace = Workspace("workspace name", "") val system = workspace.model.addSoftwareSystem("System 1") val container1 = system.addContainer("Container 1") val container2 = system.addContainer("Container 2") container1.addComponent("Component 1").apply { uses(container2, "uses") } container2.addComponent("Component 2") val view = workspace.views.createComponentView(container1, "Component2", "") .apply { addAllElements() } val diagram = C4PlantUmlExporterWithElementLinks(workspace, "/system-1/component/") .export(view) assertThat(diagram.definition.withoutC4HeaderAndFooter()).isEqualTo( """ System_Boundary("System1_boundary", "System 1", ${'$'}tags="") { Container_Boundary("System1.Container1_boundary", "Container 1", ${'$'}tags="") { Component(System1.Container1.Component1, "Component 1", ${'$'}techn="", ${'$'}descr="", ${'$'}tags="", ${'$'}link="") } Container(System1.Container2, "Container 2", ${'$'}techn="", ${'$'}descr="", ${'$'}tags="", ${'$'}link="") } Rel(System1.Container1.Component1, System1.Container2, "uses", ${'$'}techn="", ${'$'}tags="", ${'$'}link="") """.trimIndent() ) } @Test fun `renders Components Diagram without link to other Component Diagram (structurizr)`() { val workspace = Workspace("workspace name", "") val system = workspace.model.addSoftwareSystem("System 1") val container1 = system.addContainer("Container 1") val container2 = system.addContainer("Container 2") container1.addComponent("Component 1").apply { uses(container2, "uses") } container2.addComponent("Component 2") val view = workspace.views.createComponentView(container1, "Component2", "") .apply { addAllElements() } val diagram = StructurizrPlantUmlExporterWithElementLinks(workspace, "/system-1/component/") .export(view) assertThat(diagram.definition.withoutStructurizrHeaderAndFooter()).isEqualTo( """ rectangle "System 1\n[Software System]" <> { rectangle "Container 1\n[Container]" <> { rectangle "==Component 1\n[Component]" <> as System1.Container1.Component1 } rectangle "==Container 2\n[Container]" <> as System1.Container2 } System1.Container1.Component1 --> System1.Container2 <> : "uses" """.trimIndent() ) } @Test fun `link to other software system (c4)`() { val workspace = createWorkspaceWithTwoSystems() val diagram = C4PlantUmlExporterWithElementLinks(workspace, "/landscape/") .export(workspace.views.systemContextViews.first()) assertThat(diagram.definition.withoutC4HeaderAndFooter()).isEqualTo( """ System(System1, "System 1", ${'$'}descr="", ${'$'}tags="", ${'$'}link="") System(System2, "System 2", ${'$'}descr="", ${'$'}tags="", ${'$'}link="../system-2/context/") Rel(System2, System1, "uses", ${'$'}techn="", ${'$'}tags="", ${'$'}link="") """.trimIndent() ) } @Test fun `link to other software system (structurizr)`() { val workspace = createWorkspaceWithTwoSystems() val diagram = StructurizrPlantUmlExporterWithElementLinks(workspace, "/landscape/") .export(workspace.views.systemContextViews.first()) assertThat(diagram.definition.withoutStructurizrHeaderAndFooter()).isEqualTo( """ rectangle "==System 1\n[Software System]" <> as System1 rectangle "==System 2\n[Software System]" <> as System2 [[../system-2/context/]] System2 --> System1 <> : "uses" """.trimIndent() ) } @Test fun `external software system (declared external by tag) (c4)`() { val workspace = createWorkspaceWithTwoSystems() workspace.views.configuration.addProperty("generatr.site.externalTag", "External System") workspace.model.softwareSystems.single { it.name == "System 2" }.addTags("External System") val view = workspace.views.systemContextViews.first() val diagram = C4PlantUmlExporterWithElementLinks(workspace, "/landscape/") .export(view) assertThat(diagram.definition.withoutC4HeaderAndFooter()).isEqualTo( """ System(System1, "System 1", ${'$'}descr="", ${'$'}tags="", ${'$'}link="") System(System2, "System 2", ${'$'}descr="", ${'$'}tags="", ${'$'}link="") Rel(System2, System1, "uses", ${'$'}techn="", ${'$'}tags="", ${'$'}link="") """.trimIndent() ) } @Test fun `external software system (declared external by tag) (structurizr)`() { val workspace = createWorkspaceWithTwoSystems() workspace.views.configuration.addProperty("generatr.site.externalTag", "External System") workspace.model.softwareSystems.single { it.name == "System 2" }.addTags("External System") val view = workspace.views.systemContextViews.first() val diagram = StructurizrPlantUmlExporterWithElementLinks(workspace, "/landscape/") .export(view) assertThat(diagram.definition.withoutStructurizrHeaderAndFooter()).isEqualTo( """ rectangle "==System 1\n[Software System]" <> as System1 rectangle "==System 2\n[Software System]" <> as System2 System2 --> System1 <> : "uses" """.trimIndent() ) } @Test fun `link to other software system from two path segments deep (c4)`() { val workspace = createWorkspaceWithTwoSystems() val diagram = C4PlantUmlExporterWithElementLinks(workspace, "/system-1/context/") .export(workspace.views.systemContextViews.first()) assertThat(diagram.definition.withoutC4HeaderAndFooter()).isEqualTo( """ System(System1, "System 1", ${'$'}descr="", ${'$'}tags="", ${'$'}link="") System(System2, "System 2", ${'$'}descr="", ${'$'}tags="", ${'$'}link="../../system-2/context/") Rel(System2, System1, "uses", ${'$'}techn="", ${'$'}tags="", ${'$'}link="") """.trimIndent() ) } @Test fun `link to other software system from two path segments deep (structurizr)`() { val workspace = createWorkspaceWithTwoSystems() val diagram = StructurizrPlantUmlExporterWithElementLinks(workspace, "/system-1/context/") .export(workspace.views.systemContextViews.first()) assertThat(diagram.definition.withoutStructurizrHeaderAndFooter()).isEqualTo( """ rectangle "==System 1\n[Software System]" <> as System1 rectangle "==System 2\n[Software System]" <> as System2 [[../../system-2/context/]] System2 --> System1 <> : "uses" """.trimIndent() ) } private fun createWorkspaceWithOneSystem(): Workspace { val workspace = Workspace("workspace name", "").apply { views.configuration.addProperty("generatr.site.excludedTag", "External System") } val system = workspace.model.addSoftwareSystem("System 1") workspace.views.createSystemContextView(system, "Context1", "") .apply { addAllElements() } return workspace } private fun createWorkspaceWithOneSystemWithContainers(): Workspace { val workspace = Workspace("workspace name", "") val system = workspace.model.addSoftwareSystem("System 1") system.addContainer("Container 1") system.addContainer("Container 2") workspace.views.createSystemContextView(system, "Context 1", "") .apply { addAllElements() } workspace.views.createContainerView(system, "Container1", "") .apply { addAllElements() } return workspace } private fun createSystemContextViewForWorkspaceWithTwoSystemWithContainers(): Workspace { val workspace = Workspace("workspace name", "") val system = workspace.model.addSoftwareSystem("System 1") workspace.model.addSoftwareSystem("System 2").apply { uses(system, "uses") } system.addContainer("Container 1") system.addContainer("Container 2") workspace.views.createSystemContextView(system, "Context 1", "") .apply { addAllElements() } workspace.views.createContainerView(system, "Container1", "") .apply { addAllElements() } return workspace } private fun createWorkspaceWithOneSystemWithContainersAndComponents(): Workspace { val workspace = Workspace("workspace name", "") val system = workspace.model.addSoftwareSystem("System 1") val container = system.addContainer("Container 1") container.addComponent("Component 1") container.addComponent("Component 2") container.addComponent("Component 3") workspace.views.createSystemContextView(system, "Context 1", "") .apply { addAllElements() } workspace.views.createContainerView(system, "Container1", "") .apply { addAllElements() } workspace.views.createComponentView(container, "Component1", "") .apply { addAllElements() } return workspace } private fun createWorkspaceWithOneSystemWithContainersComponentAndImage() = createWorkspaceWithOneSystemWithContainersAndComponents().apply { views.createImageView(model.softwareSystems.single().containers.single().components.single { it.name == "Component 1" }, "imageview-001").also { it.description = "Image View Description" it.title = "Image View Title" it.contentType = "image/png" it.content = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" } } private fun createWorkspaceWithTwoSystems(): Workspace { val workspace = Workspace("workspace name", "") val system = workspace.model.addSoftwareSystem("System 1") workspace.model.addSoftwareSystem("System 2").apply { uses(system, "uses") } workspace.views.createSystemContextView(system, "Context 1", "") .apply { addAllElements() } return workspace } private fun String.withoutC4HeaderAndFooter() = this .split(System.lineSeparator()) .dropWhile { !it.startsWith("System") && !it.startsWith("Container") } .dropLast(3) .joinToString(System.lineSeparator()) .trimEnd() private fun String.withoutStructurizrHeaderAndFooter() = this .split(System.lineSeparator()) .dropWhile { !it.startsWith("rectangle") } .dropLast(1) .joinToString(System.lineSeparator()) .trimEnd() } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/StringUtilitiesTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site import assertk.assertThat import assertk.assertions.isEqualTo import nl.avisi.structurizr.site.generatr.normalize import org.junit.jupiter.api.DynamicTest import org.junit.jupiter.api.TestFactory class StringUtilitiesTest { @TestFactory fun `normalize strips invalid chars`() = listOf( listOf("doc", "doc"), listOf("d c", "d-c"), listOf("doc:", "doc"), listOf(" doc ", "-doc-"), listOf("aux", "aux-"), ).map { (actual, expected) -> DynamicTest.dynamicTest("normalize replaces $actual with $expected") { assertThat(actual.normalize()).isEqualTo(expected) } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/StructurizrUtilitiesTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site import assertk.assertThat import assertk.assertions.isEqualTo import com.structurizr.Workspace import nl.avisi.structurizr.site.generatr.includedProperties import kotlin.test.Test class StructurizrUtilitiesTest { private val svgFactory = { _: String, _: String -> "" } private fun generatorContext( workspaceName: String = "Workspace name", branches: List = listOf("main"), currentBranch: String = "main", version: String = "1.0.0" ) = GeneratorContext(version, Workspace(workspaceName, ""), branches, currentBranch, false, svgFactory) @Test fun `includedProperties filters out structurizr and generatr properties`() { val element = generatorContext().workspace.model.addSoftwareSystem("System 1") element.addProperty("structurizr.key", "value") element.addProperty("generatr.key", "value") element.addProperty("structurizrnotquite.key", "value") element.addProperty("generatrbut.not.key", "value") element.addProperty("other.key", "value") val includedProperties = element.includedProperties assertThat(includedProperties.size).isEqualTo(3) assertThat(includedProperties.keys).isEqualTo(setOf("structurizrnotquite.key", "generatrbut.not.key", "other.key")) } @Test fun `includedProperties returns all properties when none are filtered out`() { val element = generatorContext().workspace.model.addSoftwareSystem("System 1") element.addProperty("key1", "value1") element.addProperty("key2", "value2") val includedProperties = element.includedProperties assertThat(includedProperties.size).isEqualTo(2) assertThat(includedProperties.keys.first()).isEqualTo("key1") assertThat(includedProperties.keys.last()).isEqualTo("key2") } @Test fun `includedProperties returns empty map when no properties are set`() { val element = generatorContext().workspace.model.addSoftwareSystem("System 1") val includedProperties = element.includedProperties assertThat(includedProperties.size).isEqualTo(0) } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/E2ETestFixture.kt ================================================ package nl.avisi.structurizr.site.generatr.site.e2e import assertk.assertThat import assertk.assertions.isTrue import com.microsoft.playwright.Browser import com.microsoft.playwright.BrowserContext import com.microsoft.playwright.Page import com.microsoft.playwright.Playwright import nl.avisi.structurizr.site.generatr.main import org.junit.jupiter.api.* import org.junit.jupiter.api.extension.ExtendWith import java.io.IOException import java.net.BindException import java.net.URI import java.net.http.HttpClient import java.net.http.HttpRequest import java.net.http.HttpResponse import java.time.Duration @ExtendWith(RetryExtension::class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) abstract class E2ETestFixture { companion object { @JvmStatic protected val PORT = 9999 @JvmStatic protected val SITE_URL = "http://127.0.0.1:$PORT" } private lateinit var serverThread: Thread private lateinit var playwright: Playwright private lateinit var browser: Browser private lateinit var context: BrowserContext protected lateinit var page: Page @BeforeAll @Order(1) fun serveExampleSite() { serverThread = Thread(::serve).apply { isDaemon = true start() } var reachable = false var count = 0 while (!reachable && count++ < 1000) { reachable = siteIsReachable() Thread.sleep(Duration.ofMillis(100)) } assertThat(reachable, name = "site reachable").isTrue() } @BeforeAll @Order(2) fun launchBrowser() { playwright = Playwright.create() browser = playwright.chromium().launch() } @BeforeEach @Order(1) fun createContextAndPage() { context = browser.newContext() page = context.newPage() } @AfterEach fun closeContext() { context.close() } @AfterAll fun closeBrowser() { playwright.close() serverThread.interrupt() } private fun serve() { try { main(arrayOf("serve", "-w", "docs/example/workspace.dsl", "-p", PORT.toString())) } catch (_: InterruptedException) { // ignore, server shutdown } catch (exception: IOException) { if (exception.cause !is BindException) { throw exception } } } private fun siteIsReachable(): Boolean { try { val response = HttpClient .newBuilder() .connectTimeout(Duration.ofMillis(250)) .build() .send(HttpRequest.newBuilder(URI.create("http://127.0.0.1:$PORT")).GET().build(), HttpResponse.BodyHandlers.ofString()) return response.statusCode() == 200 && response.body().contains("Structurizr site generatr") } catch (_: IOException) { return false } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/PageTestHelper.kt ================================================ package nl.avisi.structurizr.site.generatr.site.e2e import com.microsoft.playwright.Locator import com.microsoft.playwright.Page import com.microsoft.playwright.assertions.LocatorAssertions import com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat class PageTestHelper { fun testMenu(page: Page, additionalActions: (Locator) -> Unit) { val menu = page.locator(".menu > *") val general = menu.nth(0) val softwareSystems = menu.nth(2) assertThat(general).containsText("GENERAL", LocatorAssertions.ContainsTextOptions().setUseInnerText(true)) assertThat(softwareSystems).containsText("SOFTWARE SYSTEMS", LocatorAssertions.ContainsTextOptions().setUseInnerText(true)) val generalMenu = menu.nth(1).locator("a") val softwareSystemsMenu = menu.nth(3).locator("a") val generalMenuTexts = listOf( "Home", "Decisions", "Software Systems", "Embedding diagrams and images", "Extended Markdown features", "AsciiDoc features" ) generalMenuTexts.forEachIndexed { index, text -> assertThat(generalMenu.nth(index)).containsText(text) } val softwareSystemsMenuTexts = listOf( "ATM", "E-mail System", "Internet Banking System", "Mainframe Banking System" ) softwareSystemsMenuTexts.forEachIndexed { index, text -> assertThat(softwareSystemsMenu.nth(index)).containsText(text) } additionalActions(menu) } fun testTitleAndSubtitle(page: Page, title: String, subtitle: String) { val titleLocator = page.locator(".container > .title") val subtitleLocator = page.locator(".container > .subtitle") assertThat(titleLocator).containsText(title) assertThat(subtitleLocator).containsText(subtitle) } fun testTabs(page: Page, additionalActions: (Locator) -> Unit) { val tabs = page.locator(".container > .tabs > ul > li") val tabTexts = listOf( "Info", "Context views", "Container views", "Component views", "Code views", "Dynamic views", "Deployment views", "Dependencies", "Decisions", "Documentation" ) tabTexts.forEachIndexed { index, text -> assertThat(tabs.nth(index)).containsText(text) } additionalActions(tabs) } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/RetryExtension.kt ================================================ package nl.avisi.structurizr.site.generatr.site.e2e import org.junit.jupiter.api.extension.ExtensionContext import org.junit.jupiter.api.extension.TestExecutionExceptionHandler class RetryExtension : TestExecutionExceptionHandler { companion object { private const val MAX_RETRIES = 3 private val namespace: ExtensionContext.Namespace = ExtensionContext.Namespace.create("RetryExtension") } override fun handleTestExecutionException( context: ExtensionContext, throwable: Throwable ) { val store = context.getStore(namespace) val retries = store.getOrDefault("retries", Int::class.java, 0) if (retries < MAX_RETRIES) { store.put("retries", retries + 1) println("Retrying test " + context.displayName + ", attempt " + store.get("retries")) } else { throw throwable } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/SearchPageTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.e2e import com.microsoft.playwright.Locator import com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Order import kotlin.test.Test class SearchPageTest : E2ETestFixture() { private val pageTestHelper = PageTestHelper() @BeforeEach @Order(2) fun `navigate to page`() { page.navigate(SITE_URL) val search = page.locator(".navbar input").and(page.getByPlaceholder("Search...")) search.fill("internet banking system") search.press("Enter") } @Test fun `title is correct`() { assertThat(page).hasTitle("Search results | Big Bank plc") } @Test fun `menu renders correctly`() { pageTestHelper.testMenu(page) { menu -> assertThat(menu.and(page.locator(".is-active"))).hasCount(0) } } @Test fun `content renders correctly`() { val content = page.locator(".content > *").filter(Locator.FilterOptions().setVisible(true)) assertThat(content.nth(0)).containsText("Search results") val expectedSearchResults = listOf( listOf("Internet Banking System | Context views", "Context views"), listOf("E-mail System | Dependencies", "Dependencies"), listOf("Internet Banking System | Description", "Software System Info"), listOf("Mainframe Banking System | Dependencies", "Dependencies"), listOf("Internet Banking System | Component views", "Component views"), listOf("Internet Banking System | Container views", "Container views"), listOf("Internet Banking System | Record Internet Banking System architecture decisions", "Software System Decision"), listOf("ATM | Dependencies", "Dependencies"), listOf("Mainframe Banking System | Context views", "Context views"), listOf("Internet Banking System | Dependencies", "Dependencies"), listOf("E-mail System | Context views", "Context views"), listOf("Big Bank plc architecture", "Home"), listOf("Embedding diagrams and images", "Workspace Documentation"), ) expectedSearchResults.forEachIndexed { rowIndex, row -> row.forEachIndexed { colIndex, text -> assertThat(content.nth(1).locator("p:not(.mb-0)").also { it.nth(0).waitFor() }.nth(rowIndex).locator("> *").nth(colIndex)).containsText(text) } } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/SoftwareSystemDependenciesPageTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.e2e import com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Order import kotlin.test.Test class SoftwareSystemDependenciesPageTest : E2ETestFixture() { private val pageTestHelper = PageTestHelper() @BeforeEach @Order(2) fun `navigate to page`() { page.navigate(SITE_URL) page.locator(".menu a").and(page.getByText("Internet Banking System")).click() page.locator(".container > .tabs a").and(page.getByText("Dependencies")).click() } @Test fun `title is correct`() { assertThat(page).hasTitle("Internet Banking System | Big Bank plc") } @Test fun `menu renders correctly`() { pageTestHelper.testMenu(page) { menu -> assertThat(menu.locator(".is-active")).containsText("Internet Banking System") } } @Test fun `title and subtitle render correctly`() { pageTestHelper.testTitleAndSubtitle( page, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments." ) } @Test fun `tabs render correctly`() { pageTestHelper.testTabs(page) { tabs -> assertThat(tabs.and(page.locator(".is-active"))).containsText("Dependencies") } } @Test fun `content renders correctly`() { val content = page.locator(".content > *") assertThat(content.nth(0)).containsText("Inbound") assertThat(content.nth(2)).containsText("Outbound") val expectedTableHeadings = listOf("System", "Description", "Technology") expectedTableHeadings.forEachIndexed { index, text -> assertThat(content.nth(1).locator("thead th").nth(index)).containsText(text) } expectedTableHeadings.forEachIndexed { index, text -> assertThat(content.nth(3).locator("thead th").nth(index)).containsText(text) } val outboundData = listOf( listOf( "E-mail System", "Sends e-mail using", "" ), listOf( "Mainframe Banking System", "Gets account information from, and makes payments using", "" ) ) outboundData.forEachIndexed { rowIndex, data -> data.forEachIndexed { cellIndex, text -> assertThat(content.nth(3).locator("tbody tr").nth(rowIndex).locator("td").nth(cellIndex)).containsText(text) } } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/SoftwareSystemHomePageTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.e2e import com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Order import kotlin.test.Test class SoftwareSystemHomePageTest : E2ETestFixture() { private val pageTestHelper = PageTestHelper() @BeforeEach @Order(2) fun `navigate to page`() { page.navigate(SITE_URL) page.locator(".menu a").and(page.getByText("Internet Banking System")).click() } @Test fun `title is correct`() { assertThat(page).hasTitle("Internet Banking System | Big Bank plc") } @Test fun `menu renders correctly`() { pageTestHelper.testMenu(page) { menu -> assertThat(menu.locator(".is-active")).containsText("Internet Banking System") } } @Test fun `title and subtitle render correctly`() { pageTestHelper.testTitleAndSubtitle( page, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments." ) } @Test fun `tabs render correctly`() { pageTestHelper.testTabs(page) { tabs -> assertThat(tabs.and(page.locator(".is-active"))).containsText("Info") } } @Test fun `content renders correctly`() { val content = page.locator(".content > div > *") assertThat(content.nth(0)).containsText("Description") assertThat(content.nth(2)).containsText("Vision") assertThat(content.nth(4)).containsText("References") assertThat(content.nth(1)).containsText("This is our fancy Internet Banking System.") assertThat(content.nth(3)).containsText("One system to rule them all!") assertThat(content.nth(5)).containsText("Source Code on GitHub: [https://github.com/avisi-cloud/structurizr-site-generatr]") } @Test fun `properties render correctly`() { val properties = page.locator(".content > :not(div)") assertThat(properties.nth(0)).containsText("Properties") val table = properties.and(page.locator(".table")) val tableHeadings = table.locator("thead th") assertThat(tableHeadings.nth(0)).containsText("Name") assertThat(tableHeadings.nth(1)).containsText("Value") val expectedPropertyValues = listOf( listOf( "Development Team", "Dev/Internet Services" ), listOf( "Owner", "Customer Services" ), listOf( "Url", "https://en.wikipedia.org/wiki/Online_banking" ), ) expectedPropertyValues.forEachIndexed { rowIndex, row -> row.forEachIndexed { colIndex, text -> assertThat(table.locator("tbody tr").nth(rowIndex).locator("td").nth(colIndex)).containsText(text) } } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/SoftwareSystemsPageTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.e2e import com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Order import kotlin.test.Test class SoftwareSystemsPageTest : E2ETestFixture() { private val pageTestHelper = PageTestHelper() @BeforeEach @Order(2) fun `navigate to page`() { page.navigate(SITE_URL) page.locator(".menu a").and(page.getByText("Software Systems")).click() } @Test fun `title is correct`() { assertThat(page).hasTitle("Software Systems | Big Bank plc") } @Test fun `menu renders correctly`() { pageTestHelper.testMenu(page) { menu -> assertThat(menu.locator(".is-active")).containsText("Software Systems") } } @Test fun `content renders correctly`() { val content = page.locator(".content > *") val title = content.nth(0) assertThat(title).containsText("Software Systems") val table = content.and(page.locator(".table")) val tableHeadings = table.locator("thead th") assertThat(tableHeadings.nth(0)).containsText("Name") assertThat(tableHeadings.nth(1)).containsText("Description") val expectedData = listOf( listOf( "Acquirer (External)", "Facilitates PIN transactions for merchants." ), listOf( "ATM", "Allows customers to withdraw cash." ), listOf( "E-mail System", "The internal Microsoft Exchange e-mail system." ), listOf( "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments." ), listOf( "Mainframe Banking System", "Stores all of the core banking information about customers, accounts, transactions, etc." ) ) expectedData.forEachIndexed { rowIndex, row -> row.forEachIndexed { colIndex, text -> assertThat(table.locator("tbody tr").nth(rowIndex).locator("td").nth(colIndex)).containsText(text) } } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/WorkspaceHomePageTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.e2e import com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Order import kotlin.test.Test class WorkspaceHomePageTest : E2ETestFixture() { private val pageTestHelper = PageTestHelper() @BeforeEach @Order(2) fun `navigate to page`() { page.navigate(SITE_URL) } @Test fun `title is correct`() { assertThat(page).hasTitle("Big Bank plc") } @Test fun `menu renders correctly`() { pageTestHelper.testMenu(page) { menu -> assertThat(menu.locator(".is-active")).hasText("Home") } } @Test fun `content renders correctly`() { val content = page.locator(".content > div > *") val title = content.nth(0) val paragraph1 = content.nth(1) val paragraph2 = content.nth(2) assertThat(title).containsText("Big Bank plc architecture") assertThat(paragraph1).containsText("This site contains the C4 architecture model for Big Bank plc.") assertThat(paragraph2).containsText("This page is the home page, because it is the first file in the workspace-docs directory, when the files are sorted alphabetically.") } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/decisions/DecisionPageTestHelper.kt ================================================ package nl.avisi.structurizr.site.generatr.site.e2e.decisions import assertk.assertThat import assertk.assertions.isEqualTo import com.microsoft.playwright.Locator import com.microsoft.playwright.Page class DecisionPageTestHelper { fun testDecisionContent(page: Page, additionalActions: (List) -> Unit) { val content = page.locator(".content > div > *") val title = content.nth(0) val date = content.nth(1) val status = content.nth(3) val context = content.nth(5) val decision = content.nth(7) val consequences = content.nth(9) assertThat(content.nth(2).innerText()).isEqualTo("Status") assertThat(content.nth(4).innerText()).isEqualTo("Context") assertThat(content.nth(6).innerText()).isEqualTo("Decision") assertThat(content.nth(8).innerText()).isEqualTo("Consequences") additionalActions(listOf( title, date, status, context, decision, consequences )) } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/decisions/DecisionsPageTestHelper.kt ================================================ package nl.avisi.structurizr.site.generatr.site.e2e.decisions import assertk.assertThat import assertk.assertions.isEqualTo import com.microsoft.playwright.Locator import com.microsoft.playwright.Page class DecisionsPageTestHelper { fun testDecisionTabs(page: Page, additionalActions: (Locator) -> Unit) { val tabs = page.locator(".content .tabs:not(.is-size-7) a") additionalActions(tabs) } fun testComponentDecisionTabs(page: Page, additionalActions: (Locator) -> Unit) { val tabs = page.locator(".content .tabs.is-size-7 a") additionalActions(tabs) } fun testDecisionsTable(page: Page, additionalActions: (List) -> Unit) { val tableHeadings = page.locator(".table thead th") val tableRows = page.locator(".table tbody tr") val locators = tableRows.all().map { row -> row.locator("td") } assertThat(tableHeadings.count()).isEqualTo(4) assertThat(tableHeadings.nth(0).innerText()).isEqualTo("ID") assertThat(tableHeadings.nth(1).innerText()).isEqualTo("Date") assertThat(tableHeadings.nth(2).innerText()).isEqualTo("Status") assertThat(tableHeadings.nth(3).innerText()).isEqualTo("Title") additionalActions(locators) } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/decisions/SoftwareSystemContainerComponentDecisionPageTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.e2e.decisions import com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat import nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture import nl.avisi.structurizr.site.generatr.site.e2e.PageTestHelper import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Order import kotlin.test.Test class SoftwareSystemContainerComponentDecisionPageTest : E2ETestFixture() { private val pageTestHelper = PageTestHelper() private val decisionPageTestHelper = DecisionPageTestHelper() @BeforeEach @Order(2) fun `navigate to page`() { page.navigate(SITE_URL) page.locator(".menu a").and(page.getByText("Internet Banking System")).click() page.locator(".container > .tabs a").and(page.getByText("Decisions")).click() page.locator(".content > .tabs a").and(page.getByText("API Application")).click() page.locator(".content > .tabs a").and(page.getByText("E-mail Component")).click() page.locator(".table a").and(page.getByText("Record Email Component architecture decision")).click() } @Test fun `title is correct`() { assertThat(page).hasTitle("Internet Banking System | Big Bank plc") } @Test fun `menu renders correctly`() { pageTestHelper.testMenu(page) { menu -> assertThat(menu.locator(".is-active")).containsText("Internet Banking System") } } @Test fun `title and subtitle render correctly`() { pageTestHelper.testTitleAndSubtitle( page, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments." ) } @Test fun `tabs render correctly`() { pageTestHelper.testTabs(page) { tabs -> assertThat(tabs.and(page.locator(".is-active"))).containsText("Decisions") } } @Test fun `content renders correctly`() { decisionPageTestHelper.testDecisionContent(page) { locators -> val expectedData = listOf( "1. Record Email Component architecture decision", "Date: 2022-06-21", "Accepted", "We need to record the architectural decisions made on this project.", "We will use Architecture Decision Records, as described by Michael Nygard.", "See Michael Nygard’s article, linked above. For a lightweight ADR toolset, see Nat Pryce’s adr-tools." ) expectedData.forEachIndexed { index, text -> assertThat(locators[index]).containsText(text) } } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/decisions/SoftwareSystemContainerComponentDecisionsPageTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.e2e.decisions import com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat import nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Order import kotlin.test.Test class SoftwareSystemContainerComponentDecisionsPageTest : E2ETestFixture() { private val decisionPageTestHelper = DecisionsPageTestHelper() @BeforeEach @Order(2) fun `navigate to page`() { page.navigate(SITE_URL) page.locator(".menu a").and(page.getByText("Internet Banking System")).click() page.locator(".container > .tabs a").and(page.getByText("Decisions")).click() page.locator(".content > .tabs a").and(page.getByText("API Application")).click() page.locator(".content > .tabs a").and(page.getByText("E-mail Component")).click() } @Test fun `title is correct`() { assertThat(page).hasTitle("Internet Banking System | Big Bank plc") } @Test fun `decision tabs render correctly`() { decisionPageTestHelper.testDecisionTabs(page) { tabs -> assertThat(tabs).hasCount(3) assertThat(tabs.nth(0)).containsText("System") assertThat(tabs.nth(1)).containsText("API Application") assertThat(tabs.nth(2)).containsText("Database") assertThat(tabs.nth(1)).hasClass("is-active") } } @Test fun `component decision tabs render correctly`() { decisionPageTestHelper.testComponentDecisionTabs(page) { tabs -> assertThat(tabs).hasCount(2) assertThat(tabs.nth(0)).containsText("Mainframe Banking System Facade") assertThat(tabs.nth(1)).containsText("E-mail Component") assertThat(tabs.nth(1)).hasClass("is-active") } } @Test fun `decisions table renders correctly`() { decisionPageTestHelper.testDecisionsTable(page) { locators -> val expectedData = listOf( listOf("1", "21-06-2022", "Accepted", "Record Email Component architecture decision"), listOf("2", "17-12-2024", "Superseded", "Implement Feature 1"), listOf("3", "17-12-2024", "Accepted", "Another Realisation of Feature 1"), ) expectedData.forEachIndexed { rowIndex, row -> row.forEachIndexed { colIndex, text -> assertThat(locators[rowIndex].nth(colIndex)).containsText(text) } } } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/decisions/SoftwareSystemContainerDecisionPage.kt ================================================ package nl.avisi.structurizr.site.generatr.site.e2e.decisions import com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat import nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture import nl.avisi.structurizr.site.generatr.site.e2e.PageTestHelper import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Order import kotlin.test.Test class SoftwareSystemContainerDecisionPage : E2ETestFixture() { private val pageTestHelper = PageTestHelper() private val decisionPageTestHelper = DecisionPageTestHelper() @BeforeEach @Order(2) fun `navigate to page`() { page.navigate(SITE_URL) page.locator(".menu a").and(page.getByText("Internet Banking System")).click() page.locator(".container > .tabs a").and(page.getByText("Decisions")).click() page.locator(".content > .tabs a").and(page.getByText("Database")).click() page.locator(".table a").and(page.getByText("Using Oracle database schema")).click() } @Test fun `title is correct`() { assertThat(page).hasTitle("Internet Banking System | Big Bank plc") } @Test fun `menu renders correctly`() { pageTestHelper.testMenu(page) { menu -> assertThat(menu.locator(".is-active")).containsText("Internet Banking System") } } @Test fun `title and subtitle render correctly`() { pageTestHelper.testTitleAndSubtitle( page, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments." ) } @Test fun `tabs render correctly`() { pageTestHelper.testTabs(page) { tabs -> assertThat(tabs.and(page.locator(".is-active"))).containsText("Decisions") } } @Test fun `content renders correctly`() { decisionPageTestHelper.testDecisionContent(page) { locators -> val expected = listOf( "4. Using Oracle database schema", "Date: 2025-11-13", "Accepted", "The issue motivating this decision, and any context that influences or constrains the decision.", "The change that we’re proposing or have agreed to implement.", "What becomes easier or more difficult to do and any risks introduced by the change that will need to be mitigated." ) expected.forEachIndexed { index, text -> assertThat(locators[index]).containsText(text) } } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/decisions/SoftwareSystemContainerDecisionsPageTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.e2e.decisions import com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat import nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture import nl.avisi.structurizr.site.generatr.site.e2e.PageTestHelper import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Order import kotlin.test.Test class SoftwareSystemContainerDecisionsPageTest : E2ETestFixture() { private val pageTestHelper = PageTestHelper() private val decisionPageTestHelper = DecisionsPageTestHelper() @BeforeEach @Order(2) fun `navigate to page`() { page.navigate(SITE_URL) page.locator(".menu a").and(page.getByText("Internet Banking System")).click() page.locator(".container > .tabs a").and(page.getByText("Decisions")).click() page.locator(".content > .tabs a").and(page.getByText("Database")).click() } @Test fun `title is correct`() { assertThat(page).hasTitle("Internet Banking System | Big Bank plc") } @Test fun `menu renders correctly`() { pageTestHelper.testMenu(page) { menu -> assertThat(menu.locator(".is-active")).containsText("Internet Banking System") } } @Test fun `title and subtitle render correctly`() { pageTestHelper.testTitleAndSubtitle( page, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments." ) } @Test fun `tabs render correctly`() { pageTestHelper.testTabs(page) { tabs -> assertThat(tabs.and(page.locator(".is-active"))).containsText("Decisions") } } @Test fun `decision tabs render correctly`() { decisionPageTestHelper.testDecisionTabs(page) { tabs -> assertThat(tabs).hasCount(3) assertThat(tabs.nth(0)).containsText("System") assertThat(tabs.nth(1)).containsText("API Application") assertThat(tabs.nth(2)).containsText("Database") assertThat(tabs.nth(2)).hasClass("is-active") } } @Test fun `component decision tabs render correctly`() { decisionPageTestHelper.testComponentDecisionTabs(page) { tabs -> assertThat(tabs).hasCount(1) assertThat(tabs.nth(0)).containsText("Container") assertThat(tabs.nth(0)).hasClass("is-active") } } @Test fun `decisions table renders correctly`() { decisionPageTestHelper.testDecisionsTable(page) { locators -> val expectedData = listOf( listOf("4", "13-11-2025", "Accepted", "Using Oracle database schema") ) expectedData.forEachIndexed { rowIndex, row -> row.forEachIndexed { colIndex, text -> assertThat(locators[rowIndex].nth(colIndex)).containsText(text) } } } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/decisions/SoftwareSystemDecisionPageTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.e2e.decisions import com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat import nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture import nl.avisi.structurizr.site.generatr.site.e2e.PageTestHelper import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Order import kotlin.test.Test class SoftwareSystemDecisionPageTest : E2ETestFixture() { private val pageTestHelper = PageTestHelper() private val decisionPageTestHelper = DecisionPageTestHelper() @BeforeEach @Order(2) fun `navigate to page`() { page.navigate(SITE_URL) page.locator(".menu a").and(page.getByText("Internet Banking System")).click() page.locator(".container > .tabs a").and(page.getByText("Decisions")).click() page.locator(".table a").and(page.getByText("Record Internet Banking System architecture decisions")).click() } @Test fun `title is correct`() { assertThat(page).hasTitle("Internet Banking System | Big Bank plc") } @Test fun `menu renders correctly`() { pageTestHelper.testMenu(page) { menu -> assertThat(menu.locator(".is-active")).containsText("Internet Banking System") } } @Test fun `title and subtitle render correctly`() { pageTestHelper.testTitleAndSubtitle( page, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments." ) } @Test fun `tabs render correctly`() { pageTestHelper.testTabs(page) { tabs -> assertThat(tabs.and(page.locator(".is-active"))).containsText("Decisions") } } @Test fun `content renders correctly`() { decisionPageTestHelper.testDecisionContent(page) { locators -> val expectedData = listOf( "1. Record Internet Banking System architecture decisions", "Date: 2022-06-21", "Accepted", "We need to record the architectural decisions made on this project.", "We will use Architecture Decision Records, as described by Michael Nygard.", "See Michael Nygard’s article, linked above. For a lightweight ADR toolset, see Nat Pryce’s adr-tools." ) expectedData.forEachIndexed { index, text -> assertThat(locators[index]).containsText(text) } } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/decisions/SoftwareSystemDecisionsPageTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.e2e.decisions import com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat import nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture import nl.avisi.structurizr.site.generatr.site.e2e.PageTestHelper import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Order import kotlin.test.Test class SoftwareSystemDecisionsPageTest : E2ETestFixture() { private val pageTestHelper = PageTestHelper() private val decisionPageTestHelper = DecisionsPageTestHelper() @BeforeEach @Order(2) fun `navigate to page`() { page.navigate(SITE_URL) page.locator(".menu a").and(page.getByText("Internet Banking System")).click() page.locator(".container > .tabs a").and(page.getByText("Decisions")).click() } @Test fun `title is correct`() { assertThat(page).hasTitle("Internet Banking System | Big Bank plc") } @Test fun `menu renders correctly`() { pageTestHelper.testMenu(page) { menu -> assertThat(menu.locator(".is-active")).containsText("Internet Banking System") } } @Test fun `title and subtitle render correctly`() { pageTestHelper.testTitleAndSubtitle( page, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments." ) } @Test fun `tabs render correctly`() { pageTestHelper.testTabs(page) { tabs -> assertThat(tabs.and(page.locator(".is-active"))).containsText("Decisions") } } @Test fun `decision tabs render correctly`() { decisionPageTestHelper.testDecisionTabs(page) { tabs -> assertThat(tabs).hasCount(3) assertThat(tabs.nth(0)).containsText("System") assertThat(tabs.nth(1)).containsText("API Application") assertThat(tabs.nth(2)).containsText("Database") assertThat(tabs.nth(0)).containsClass("is-active") } } @Test fun `component decision tabs render correctly`() { decisionPageTestHelper.testComponentDecisionTabs(page) { locators -> assertThat(locators).hasCount(0) } } @Test fun `decisions table renders correctly`() { decisionPageTestHelper.testDecisionsTable(page) { locators -> val expectedData = listOf( listOf("1", "21-06-2022", "Accepted", "Record Internet Banking System architecture decisions") ) expectedData.forEachIndexed { rowIndex, row -> row.forEachIndexed { colIndex, text -> assertThat(locators[rowIndex].nth(colIndex)).containsText(text) } } } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/decisions/WorkspaceDecisionPageTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.e2e.decisions import com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat import nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture import nl.avisi.structurizr.site.generatr.site.e2e.PageTestHelper import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Order import kotlin.test.Test class WorkspaceDecisionPageTest : E2ETestFixture() { private val pageTestHelper = PageTestHelper() private val decisionPageTestHelper = DecisionPageTestHelper() @BeforeEach @Order(2) fun `navigate to page`() { page.navigate(SITE_URL) page.locator(".menu a").and(page.getByText("Decisions")).click() page.locator(".content a").and(page.getByText("Record architecture decisions")).click() } @Test fun `title is correct`() { page.onDOMContentLoaded { assertThat(page).hasTitle("Record architecture decisions | Big Bank plc") } } @Test fun `menu renders correctly`() { pageTestHelper.testMenu(page) { menu -> assertThat(menu.locator(".is-active")).containsText("Decisions") } } @Test fun `content renders correctly`() { decisionPageTestHelper.testDecisionContent(page) { locators -> val expectedData = listOf( "1. Record architecture decisions", "Date: 2022-06-21", "Accepted", "We need to record the architectural decisions made on this project.", "We will use Architecture Decision Records, as described by Michael Nygard.", "See Michael Nygard’s article, linked above. For a lightweight ADR toolset, see Nat Pryce’s adr-tools." ) expectedData.forEachIndexed { index, text -> assertThat(locators[index]).containsText(text) } } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/decisions/WorkspaceDecisionsPageTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.e2e.decisions import com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat import nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture import nl.avisi.structurizr.site.generatr.site.e2e.PageTestHelper import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Order import kotlin.test.Test class WorkspaceDecisionsPageTest : E2ETestFixture() { private val pageTestHelper = PageTestHelper() private val decisionsPageTestHelper = DecisionsPageTestHelper() @BeforeEach @Order(2) fun `navigate to page`() { page.navigate(SITE_URL) page.locator(".menu a").and(page.getByText("Decisions")).click() } @Test fun `title is correct`() { assertThat(page).hasTitle("Decisions | Big Bank plc") } @Test fun `menu renders correctly`() { pageTestHelper.testMenu(page) { menu -> assertThat(menu.locator(".is-active")).containsText("Decisions") } } @Test fun `decisions table renders correctly`() { decisionsPageTestHelper.testDecisionsTable(page) { locators -> val expectedData = listOf( listOf("1", "21-06-2022", "Accepted", "Record architecture decisions"), listOf("2", "17-12-2024", "Superseded", "Implement Feature 1"), listOf("3", "17-12-2024", "Accepted", "Another Realisation of Feature 1"), listOf("4", "13-11-2025", "Accepted", "Using Oracle database schema"), ) expectedData.forEachIndexed { rowIndex, row -> row.forEachIndexed { colIndex, text -> assertThat(locators[rowIndex].nth(colIndex)).containsText(text) } } } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/sections/SectionPageTestHelper.kt ================================================ package nl.avisi.structurizr.site.generatr.site.e2e.sections import com.microsoft.playwright.Locator import com.microsoft.playwright.Page class SectionPageTestHelper { fun testSectionContent(page: Page, additionalActions: (List) -> Unit) { val content = page.locator(".content > div > *") val title = content.nth(0) val notes = content.nth(1) additionalActions(listOf( title, notes )) } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/sections/SectionsPageTestHelper.kt ================================================ package nl.avisi.structurizr.site.generatr.site.e2e.sections import com.microsoft.playwright.Locator import com.microsoft.playwright.Page import com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat class SectionsPageTestHelper { fun testSectionTabs(page: Page, additionalActions: (Locator) -> Unit) { val tabs = page.locator(".content .tabs:not(.is-size-7) a") additionalActions(tabs) } fun testComponentSectionTabs(page: Page, additionalActions: (Locator) -> Unit) { val tabs = page.locator(".content .tabs.is-size-7 a") additionalActions(tabs) } fun testSectionsTable(page: Page, additionalActions: (List) -> Unit) { val tableHeadings = page.locator(".table thead th") val tableRows = page.locator(".table tbody tr") val locators = tableRows.all().map { row -> row.locator("td") } assertThat(tableHeadings).hasCount(2) assertThat(tableHeadings.nth(0)).containsText("#") assertThat(tableHeadings.nth(1)).containsText("Title") additionalActions(locators) } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/sections/SoftwareSystemContainerComponentSectionPageTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.e2e.sections import com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat import nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture import nl.avisi.structurizr.site.generatr.site.e2e.PageTestHelper import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Order import kotlin.test.Test class SoftwareSystemContainerComponentSectionPageTest : E2ETestFixture() { private val pageTestHelper = PageTestHelper() private val sectionPageTestHelper = SectionPageTestHelper() @BeforeEach @Order(2) fun `navigate to page`() { page.navigate(SITE_URL) page.locator(".menu a").and(page.getByText("Internet Banking System")).click() page.locator(".container > .tabs a").and(page.getByText("Documentation")).click() page.locator(".content > .tabs a").and(page.getByText("API Application")).click() page.locator(".content > .tabs a").and(page.getByText("E-mail Component")).click() page.locator(".table a").and(page.getByText("Email Component inner workings")).click() } @Test fun `title is correct`() { assertThat(page).hasTitle("Internet Banking System | Big Bank plc") } @Test fun `menu renders correctly`() { pageTestHelper.testMenu(page) { menu -> assertThat(menu.locator(".is-active")).containsText("Internet Banking System") } } @Test fun `title and subtitle render correctly`() { pageTestHelper.testTitleAndSubtitle( page, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments." ) } @Test fun `tabs render correctly`() { pageTestHelper.testTabs(page) { tabs -> assertThat(tabs.and(page.locator(".is-active"))).containsText("Documentation") } } @Test fun `sections table renders correctly`() { sectionPageTestHelper.testSectionContent(page) { locators -> assertThat(locators[0]).containsText("Email Component inner workings") assertThat(locators[1]).containsText("This is how the e-mail component works.") } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/sections/SoftwareSystemContainerComponentSectionsPageTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.e2e.sections import com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat import nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture import nl.avisi.structurizr.site.generatr.site.e2e.PageTestHelper import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Order import kotlin.test.Test class SoftwareSystemContainerComponentSectionsPageTest : E2ETestFixture() { private val pageTestHelper = PageTestHelper() private val sectionsPageTestHelper = SectionsPageTestHelper() @BeforeEach @Order(2) fun `navigate to page`() { page.navigate(SITE_URL) page.locator(".menu a").and(page.getByText("Internet Banking System")).click() page.locator(".container > .tabs a").and(page.getByText("Documentation")).click() page.locator(".content > .tabs a").and(page.getByText("API Application")).click() page.locator(".content > .tabs a").and(page.getByText("E-mail Component")).click() } @Test fun `title is correct`() { assertThat(page).hasTitle("Internet Banking System | Big Bank plc") } @Test fun `menu renders correctly`() { pageTestHelper.testMenu(page) { menu -> assertThat(menu.locator(".is-active")).containsText("Internet Banking System") } } @Test fun `title and subtitle render correctly`() { pageTestHelper.testTitleAndSubtitle( page, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments." ) } @Test fun `tabs render correctly`() { pageTestHelper.testTabs(page) { tabs -> assertThat(tabs.and(page.locator(".is-active"))).containsText("Documentation") } } @Test fun `section tabs render correctly`() { sectionsPageTestHelper.testSectionTabs(page) { tabs -> assertThat(tabs).hasCount(3) assertThat(tabs.nth(0)).containsText("System") assertThat(tabs.nth(1)).containsText("API Application") assertThat(tabs.nth(2)).containsText("Database") assertThat(tabs.nth(1)).hasClass("is-active") } } @Test fun `component section tabs render correctly`() { sectionsPageTestHelper.testComponentSectionTabs(page) { tabs -> assertThat(tabs).hasCount(2) assertThat(tabs.nth(0)).containsText("Mainframe Banking System Facade") assertThat(tabs.nth(1)).containsText("E-mail Component") assertThat(tabs.nth(1)).hasClass("is-active") } } @Test fun `sections table renders correctly`() { sectionsPageTestHelper.testSectionsTable(page) { locators -> val expectedData = listOf( listOf("1", "Email Component inner workings") ) expectedData.forEachIndexed { rowIndex, row -> row.forEachIndexed { colIndex, text -> assertThat(locators[rowIndex].nth(colIndex)).containsText(text) } } } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/sections/SoftwareSystemContainerSectionPageTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.e2e.sections import com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat import nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture import nl.avisi.structurizr.site.generatr.site.e2e.PageTestHelper import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Order import kotlin.test.Test class SoftwareSystemContainerSectionPageTest : E2ETestFixture() { private val pageTestHelper = PageTestHelper() private val sectionPageTestHelper = SectionPageTestHelper() @BeforeEach @Order(2) fun `navigate to page`() { page.navigate(SITE_URL) page.locator(".menu a").and(page.getByText("Internet Banking System")).click() page.locator(".container > .tabs a").and(page.getByText("Documentation")).click() page.locator(".content > .tabs a").and(page.getByText("Database")).click() page.locator(".table a").and(page.getByText("Usage")).click() } @Test fun `title is correct`() { assertThat(page).hasTitle("Internet Banking System | Big Bank plc") } @Test fun `menu renders correctly`() { pageTestHelper.testMenu(page) { menu -> assertThat(menu.locator(".is-active")).containsText("Internet Banking System") } } @Test fun `title and subtitle render correctly`() { pageTestHelper.testTitleAndSubtitle( page, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments." ) } @Test fun `tabs render correctly`() { pageTestHelper.testTabs(page) { tabs -> assertThat(tabs.and(page.locator(".is-active"))).containsText("Documentation") } } @Test fun `sections table renders correctly`() { sectionPageTestHelper.testSectionContent(page) { locators -> assertThat(locators[0]).containsText("Usage") assertThat(locators[1]).containsText("This is how we use this thing.") } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/sections/SoftwareSystemContainerSectionsPageTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.e2e.sections import com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat import nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture import nl.avisi.structurizr.site.generatr.site.e2e.PageTestHelper import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Order import kotlin.test.Test class SoftwareSystemContainerSectionsPageTest : E2ETestFixture() { private val pageTestHelper = PageTestHelper() private val sectionsPageTestHelper = SectionsPageTestHelper() @BeforeEach @Order(2) fun `navigate to page`() { page.navigate(SITE_URL) page.locator(".menu a").and(page.getByText("Internet Banking System")).click() page.locator(".container > .tabs a").and(page.getByText("Documentation")).click() page.locator(".content > .tabs a").and(page.getByText("Database")).click() } @Test fun `title is correct`() { assertThat(page).hasTitle("Internet Banking System | Big Bank plc") } @Test fun `menu renders correctly`() { pageTestHelper.testMenu(page) { menu -> assertThat(menu.locator(".is-active")).containsText("Internet Banking System") } } @Test fun `title and subtitle render correctly`() { pageTestHelper.testTitleAndSubtitle( page, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments." ) } @Test fun `tabs render correctly`() { pageTestHelper.testTabs(page) { tabs -> assertThat(tabs.and(page.locator(".is-active"))).containsText("Documentation") } } @Test fun `section tabs render correctly`() { sectionsPageTestHelper.testSectionTabs(page) { tabs -> assertThat(tabs).hasCount(3) assertThat(tabs.nth(0)).containsText("System") assertThat(tabs.nth(1)).containsText("API Application") assertThat(tabs.nth(2)).containsText("Database") assertThat(tabs.nth(2)).hasClass("is-active") } } @Test fun `component section tabs render correctly`() { sectionsPageTestHelper.testComponentSectionTabs(page) { tabs -> assertThat(tabs).hasCount(1) assertThat(tabs.nth(0)).containsText("Container") assertThat(tabs.nth(0)).hasClass("is-active") } } @Test fun `sections table renders correctly`() { sectionsPageTestHelper.testSectionsTable(page) { locators -> val expectedData = listOf( listOf("1", "Usage") ) expectedData.forEachIndexed { rowIndex, row -> row.forEachIndexed { colIndex, text -> assertThat(locators[rowIndex].nth(colIndex)).containsText(text) } } } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/sections/SoftwareSystemSectionPageTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.e2e.sections import com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat import nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture import nl.avisi.structurizr.site.generatr.site.e2e.PageTestHelper import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Order import kotlin.test.Test class SoftwareSystemSectionPageTest : E2ETestFixture() { private val pageTestHelper = PageTestHelper() private val sectionPageTestHelper = SectionPageTestHelper() @BeforeEach @Order(2) fun `navigate to page`() { page.navigate(SITE_URL) page.locator(".menu a").and(page.getByText("Internet Banking System")).click() page.locator(".container > .tabs a").and(page.getByText("Documentation")).click() page.locator(".table a").and(page.getByText("History")).click() } @Test fun `title is correct`() { assertThat(page).hasTitle("Internet Banking System | Big Bank plc") } @Test fun `menu renders correctly`() { pageTestHelper.testMenu(page) { menu -> assertThat(menu.locator(".is-active")).containsText("Internet Banking System") } } @Test fun `title and subtitle render correctly`() { pageTestHelper.testTitleAndSubtitle( page, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments." ) } @Test fun `tabs render correctly`() { pageTestHelper.testTabs(page) { tabs -> assertThat(tabs.and(page.locator(".is-active"))).containsText("Documentation") } } @Test fun `content renders correctly`() { sectionPageTestHelper.testSectionContent(page) { locators -> assertThat(locators[0]).containsText("History") assertThat(locators[1]).containsText("Some notes how we got to the current state.") } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/sections/SoftwareSystemSectionsPageTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.e2e.sections import com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat import nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture import nl.avisi.structurizr.site.generatr.site.e2e.PageTestHelper import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Order import kotlin.test.Test class SoftwareSystemSectionsPageTest : E2ETestFixture() { private val pageTestHelper = PageTestHelper() private val sectionsPageTestHelper = SectionsPageTestHelper() @BeforeEach @Order(2) fun `navigate to page`() { page.navigate(SITE_URL) page.locator(".menu a").and(page.getByText("Internet Banking System")).click() page.locator(".container > .tabs a").and(page.getByText("Documentation")).click() } @Test fun `title is correct`() { assertThat(page).hasTitle("Internet Banking System | Big Bank plc") } @Test fun `menu renders correctly`() { pageTestHelper.testMenu(page) { menu -> assertThat(menu.locator(".is-active")).containsText("Internet Banking System") } } @Test fun `title and subtitle render correctly`() { pageTestHelper.testTitleAndSubtitle( page, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments." ) } @Test fun `tabs render correctly`() { pageTestHelper.testTabs(page) { tabs -> assertThat(tabs.and(page.locator(".is-active"))).containsText("Documentation") } } @Test fun `section tabs render correctly`() { sectionsPageTestHelper.testSectionTabs(page) { tabs -> assertThat(tabs).hasCount(3) assertThat(tabs.nth(0)).containsText("System") assertThat(tabs.nth(1)).containsText("API Application") assertThat(tabs.nth(2)).containsText("Database") assertThat(tabs.nth(0)).hasClass("is-active") } } @Test fun `component section tabs render correctly`() { sectionsPageTestHelper.testComponentSectionTabs(page) { tabs -> assertThat(tabs).hasCount(0) } } @Test fun `sections table renders correctly`() { sectionsPageTestHelper.testSectionsTable(page) { locators -> val expectedData = listOf( listOf("1", "History"), listOf("2", "Usage"), ) expectedData.forEachIndexed { rowIndex, row -> row.forEachIndexed { colIndex, text -> assertThat(locators[rowIndex].nth(colIndex)).containsText(text) } } } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/AsciidocToHtmlTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.matches import com.structurizr.documentation.Format import org.junit.jupiter.api.Test class AsciidocToHtmlTest : ViewModelTest() { @Test fun `translates asciidoc`() { val generatorContext = generatorContext() val viewModel = HomePageViewModel(generatorContext) val html = toHtml( viewModel, """ == header content """.trimIndent(), Format.AsciiDoc, svgFactory ) assertThat(html).isEqualTo( """

header

content

""".trimIndent() ) } @Test fun `translates asciidoc table`() { val generatorContext = generatorContext() val viewModel = HomePageViewModel(generatorContext) val html = toHtml( viewModel, """ == header |=== |header1 |header2 |content |content |=== """.trimIndent(), Format.AsciiDoc, svgFactory ) assertThat(html).isEqualTo( """

header

header1 header2

content

content

""".trimIndent() ) } @Test fun `translates asciidoc admonition block`() { val generatorContext = generatorContext() val viewModel = HomePageViewModel(generatorContext) val html = toHtml( viewModel, """ == header NOTE: This is a note. """.trimIndent(), Format.AsciiDoc, svgFactory ) assertThat(html).isEqualTo( """

header

Note
This is a note.
""".trimIndent() ) } @Test fun `renders PlantUML code to SVG`() { val generatorContext = generatorContext() val viewModel = HomePageViewModel(generatorContext) val html = toHtml( viewModel, """ [plantuml] ---- @startuml class Foo @enduml ---- """.trimIndent(), Format.AsciiDoc, svgFactory ) assertThat(html).matches("
.*.*
".toRegex(RegexOption.DOT_MATCHES_ALL)) } @Test fun `translates mermaid graphings in asciidoc`() { val generatorContext = generatorContext() val viewModel = HomePageViewModel(generatorContext) val html = toHtml( viewModel, """ == Mermaid [source, mermaid] ---- graph TD; A-->B; A-->C; B-->D; C-->D; ---- """.trimIndent(), Format.AsciiDoc, svgFactory ) assertThat(html).isEqualTo( """

Mermaid

graph TD;
            A-->B;
            A-->C;
            B-->D;
            C-->D;
""".trimIndent() ) } @Test fun `embedded diagram`() { val generatorContext = generatorContext() val viewModel = HomePageViewModel(generatorContext) val html = toHtml(viewModel, "image::embed:SystemLandscape[System Landscape Diagram]", Format.AsciiDoc, svgFactory) assertThat(html).isEqualTo( """
System Landscape Diagram
""".trimIndent() ) } @Test fun `embedded diagram key not found`() { val generatorContext = generatorContext() val viewModel = HomePageViewModel(generatorContext) val html = toHtml(viewModel, "image::embed:non-existing[Diagram]", Format.AsciiDoc) { _, _ -> null } assertThat(html).isEqualTo( """
No view with key non-existing found!
""".trimIndent() ) } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/BranchHomeLinkViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.isEqualTo import kotlin.test.Test class BranchHomeLinkViewModelTest : ViewModelTest() { @Test fun `title is branch name`() { val viewModel = BranchHomeLinkViewModel(pageViewModel(), "branch-1") assertThat(viewModel.title) .isEqualTo("branch-1") } @Test fun `relative href to`() { val viewModel = BranchHomeLinkViewModel(pageViewModel("/master/decisions/1"), "branch-1") assertThat(viewModel.relativeHref) .isEqualTo("../../../../branch-1/") } @Test fun `relative href from home`() { val viewModel = BranchHomeLinkViewModel(pageViewModel("/"), "master") assertThat(viewModel.relativeHref) .isEqualTo("./../master/") } @Test fun `title is branch name when branch name contains slash`() { val viewModel = BranchHomeLinkViewModel(pageViewModel(), "feat/branch-1") assertThat(viewModel.title) .isEqualTo("feat/branch-1") } @Test fun `relative href from home when branch name contains slash`() { val viewModel = BranchHomeLinkViewModel(pageViewModel("/"), "feat/branch-1") assertThat(viewModel.relativeHref) .isEqualTo("./../feat%2Fbranch-1/") } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/ContentTextTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.isEqualTo import com.structurizr.documentation.Format import com.structurizr.documentation.Section import org.junit.jupiter.api.DynamicTest import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestFactory import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource class ContentTextTest { @TestFactory fun `no content`() = listOf(Format.Markdown, Format.AsciiDoc) .map { format -> DynamicTest.dynamicTest(format.name) { val section = Section(format, "") assertThat(section.contentText()).isEqualTo("") } } @TestFactory fun `only whitespaces`() = listOf(Format.Markdown, Format.AsciiDoc) .map { format -> DynamicTest.dynamicTest(format.name) { val section = Section(format, " \n ") assertThat(section.contentText()).isEqualTo("") } } @ParameterizedTest @ValueSource(strings = ["# header", "## header", "### header"]) fun `ignores markdown title`(content: String) { val section = Section(Format.Markdown, content) assertThat(section.contentText()).isEqualTo("") } @ParameterizedTest @ValueSource(strings = ["= header"]) fun `ignores asciidoc title`(content: String) { val section = Section(Format.AsciiDoc, content) assertThat(section.contentText()).isEqualTo("") } @TestFactory fun `simple paragraph`() = listOf(Format.Markdown, Format.AsciiDoc) .map { format -> DynamicTest.dynamicTest(format.name) { val section = Section(format, "some content") assertThat(section.contentText()).isEqualTo("some content") } } @Test fun `markdown headers and paragraphs`() { val section = Section(Format.Markdown, "# header\n\nsome content\n\n## subheader\n\nmore content") assertThat(section.contentText()).isEqualTo("subheader some content more content") } @Test fun `asciidoc headers and paragraphs`() { val section = Section(Format.AsciiDoc, "= header\n\nsome content\n\n== subheader\n\nmore content") assertThat(section.contentText()).isEqualTo("some content subheader more content") } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/ContentTitleTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.isEqualTo import com.structurizr.documentation.Format import com.structurizr.documentation.Section import org.junit.jupiter.api.DynamicTest import org.junit.jupiter.api.TestFactory import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource import kotlin.test.Test class ContentTitleTest { @TestFactory fun `no content`() = listOf(Format.Markdown, Format.AsciiDoc) .map { format -> DynamicTest.dynamicTest(format.name) { val section = Section(format, "") assertThat(section.contentTitle()).isEqualTo("untitled document") } } @TestFactory fun `only whitespaces`() = listOf(Format.Markdown, Format.AsciiDoc) .map { format -> DynamicTest.dynamicTest(format.name) { val section = Section(format, " \n ") assertThat(section.contentTitle()).isEqualTo("untitled document") } } @TestFactory fun `simple paragraph`() = listOf(Format.Markdown, Format.AsciiDoc) .map { format -> DynamicTest.dynamicTest(format.name) { val section = Section(format, "some content") assertThat(section.contentTitle()).isEqualTo("untitled document") } } @ParameterizedTest @ValueSource(strings = ["# header", "## header", "### header"]) fun `with markdown heading`(content: String) { val section = Section(Format.Markdown, content) assertThat(section.contentTitle()).isEqualTo("header") } @ParameterizedTest @ValueSource(strings = ["= header", "== header", "=== header"]) fun `with asciidoc heading`(content: String) { val section = Section(Format.AsciiDoc, content) assertThat(section.contentTitle()).isEqualTo("header") } @Test fun `with asciidoc heading including logo`() { val section = Section(Format.AsciiDoc, "= image:logo.png[logo] header") assertThat(section.contentTitle()).isEqualTo("header") } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/CustomStylesheetViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.isEqualTo import org.junit.jupiter.api.Test class CustomStylesheetViewModelTest : ViewModelTest() { private val generatorContext = generatorContext() private val viewModel = pageViewModel() @Test fun `no custom stylesheet`() { val customStylesheetViewModel = CustomStylesheetViewModel(generatorContext, viewModel) assertThat(customStylesheetViewModel.resourceURI).isEqualTo(null) } @Test fun `custom stylesheet set to URL`() { generatorContext.workspace.views.configuration.addProperty("generatr.style.customStylesheet","http://example.uri/custom.css") val customStylesheetViewModel = CustomStylesheetViewModel(generatorContext, viewModel) assertThat(customStylesheetViewModel.resourceURI).isEqualTo("http://example.uri/custom.css") } @Test fun `custom stylesheet set to local asset`() { generatorContext.workspace.views.configuration.addProperty("generatr.style.customStylesheet","site/custom.css") val customStylesheetViewModel = CustomStylesheetViewModel(generatorContext, viewModel) assertThat(customStylesheetViewModel.resourceURI).isEqualTo("../site/custom.css") } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/DecisionTabViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.isEqualTo import kotlin.test.Test class DecisionTabViewModelTest : ViewModelTest() { @Test fun `no decision tabs`() { val context = generatorContext() val softwareSystem = context.workspace.model.addSoftwareSystem("Software system") val softwareSystemPageViewModel = SoftwareSystemPageViewModel(context, softwareSystem, SoftwareSystemPageViewModel.Tab.DECISIONS) val decisionTabViewModel = softwareSystemPageViewModel.createDecisionsTabViewModel(softwareSystem, SoftwareSystemPageViewModel.Tab.DECISIONS) assertThat(decisionTabViewModel.size).isEqualTo(0) } @Test fun `software system decision tab available`() { val context = generatorContext() val softwareSystem = context.workspace.model.addSoftwareSystem("Software system") softwareSystem.documentation.addDecision(createDecision("1", "Accepted")) val softwareSystemPageViewModel = SoftwareSystemPageViewModel(context, softwareSystem, SoftwareSystemPageViewModel.Tab.DECISIONS) val decisionTabViewModel = softwareSystemPageViewModel.createDecisionsTabViewModel(softwareSystem, SoftwareSystemPageViewModel.Tab.DECISIONS) assertThat(decisionTabViewModel.size).isEqualTo(1) assertThat(decisionTabViewModel.first().title).isEqualTo("System") } @Test fun `container decision tab available `() { val context = generatorContext() val softwareSystem = context.workspace.model.addSoftwareSystem("Software system") softwareSystem.addContainer("Some Container").documentation.addDecision(createDecision("2", "Proposed")) val softwareSystemPageViewModel = SoftwareSystemPageViewModel(context, softwareSystem, SoftwareSystemPageViewModel.Tab.DECISIONS) val decisionTabViewModel = softwareSystemPageViewModel.createDecisionsTabViewModel(softwareSystem, SoftwareSystemPageViewModel.Tab.DECISIONS) assertThat(decisionTabViewModel.size).isEqualTo(1) assertThat(decisionTabViewModel.first().title).isEqualTo("Some Container") } @Test fun `container & system decision tabs available`() { val context = generatorContext() val softwareSystem = context.workspace.model.addSoftwareSystem("Software system") softwareSystem.documentation.addDecision(createDecision("1", "Accepted")) softwareSystem.addContainer("Some Container").documentation.addDecision(createDecision("2", "Proposed")) val softwareSystemPageViewModel = SoftwareSystemPageViewModel(context, softwareSystem, SoftwareSystemPageViewModel.Tab.DECISIONS) val decisionTabViewModel = softwareSystemPageViewModel.createDecisionsTabViewModel(softwareSystem, SoftwareSystemPageViewModel.Tab.DECISIONS) assertThat(decisionTabViewModel.size).isEqualTo(2) assertThat(decisionTabViewModel.first().title).isEqualTo("System") assertThat(decisionTabViewModel.last().title).isEqualTo("Some Container") } @Test fun `check container decisions tabs are filtered`() { val context = generatorContext() val softwareSystem = context.workspace.model.addSoftwareSystem("Software system") softwareSystem.addContainer("Some Container").documentation.addDecision(createDecision("2", "Proposed")) softwareSystem.addContainer("Another Container") val softwareSystemPageViewModel = SoftwareSystemPageViewModel(context, softwareSystem, SoftwareSystemPageViewModel.Tab.DECISIONS) val decisionTabViewModel = softwareSystemPageViewModel.createDecisionsTabViewModel(softwareSystem, SoftwareSystemPageViewModel.Tab.DECISIONS) assertThat(decisionTabViewModel.size).isEqualTo(1) assertThat(decisionTabViewModel.last().title).isEqualTo("Some Container") } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/DecisionsTableViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.isEqualTo import java.time.LocalDate import java.time.Month import kotlin.test.Test class DecisionsTableViewModelTest : ViewModelTest() { @Test fun `no decisions available`() { assertThat(pageViewModel().createDecisionsTableViewModel(emptySet()) { "href" }) .isEqualTo( TableViewModel.create { decisionsTableHeaderRow() } ) } @Test fun `many decisions sorted by integer value of id`() { val decision1 = createDecision("10", "Accepted", LocalDate.of(2022, Month.JANUARY, 1)) val decision2 = createDecision("2", "Proposed", LocalDate.of(2022, Month.JANUARY, 2)) val pageViewModel = pageViewModel() assertThat(pageViewModel.createDecisionsTableViewModel(setOf(decision1, decision2)) { it.id }).isEqualTo( TableViewModel.create { decisionsTableHeaderRow() bodyRow( cellWithIndex("2"), cell("02-01-2022"), cell("Proposed"), cellWithLink(pageViewModel, "Decision 2", decision2.id) ) bodyRow( cellWithIndex("10"), cell("01-01-2022"), cell("Accepted"), cellWithLink(pageViewModel, "Decision 10", decision1.id) ) } ) } private fun TableViewModel.TableViewInitializerContext.decisionsTableHeaderRow() { headerRow(headerCellSmall("ID"), headerCell("Date"), headerCell("Status"), headerCellLarge("Title")) } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/DiagramIndexViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isTrue import kotlin.test.Test class DiagramIndexViewModelTest : ViewModelTest() { private val generatorContext = generatorContext() @Test fun `index without diagrams` () { val viewModel = DiagramIndexViewModel(diagramViewModels()) assertThat(viewModel.entries).isEqualTo(listOf()) } @Test fun `index with diagram` () { val softwareSystem = generatorContext.workspace.model.addSoftwareSystem("Software system") generatorContext.workspace.views.createSystemContextView(softwareSystem, "system-1", "") val viewModel = DiagramIndexViewModel(diagramViewModels()) assertThat(viewModel.entries).isEqualTo(listOf(IndexEntry("system-1", "System Context View: Software system"))) } @Test fun `index with diagram with description` () { val softwareSystem = generatorContext.workspace.model.addSoftwareSystem("Software system") generatorContext.workspace.views.createSystemContextView(softwareSystem, "system-1", "Some description") val viewModel = DiagramIndexViewModel(diagramViewModels()) assertThat(viewModel.entries).isEqualTo(listOf(IndexEntry("system-1", "System Context View: Software system (Some description)"))) } @Test fun `index with exactly one diagram` () { val softwareSystem = generatorContext.workspace.model.addSoftwareSystem("Software system") generatorContext.workspace.views.createSystemContextView(softwareSystem, "system-1", "") val viewModel = DiagramIndexViewModel(diagramViewModels()) assertThat(viewModel.visible).isFalse() } @Test fun `index with diagram and image with description` () { val softwareSystem = generatorContext.workspace.model.addSoftwareSystem("Software system") generatorContext.workspace.views.createSystemContextView(softwareSystem, "system-1", "Some description") generatorContext.workspace.views.createImageView(softwareSystem, "image-1") val viewModel = DiagramIndexViewModel(diagramViewModels(), imageViewViewModels()) assertThat(viewModel.visible).isTrue() assertThat(viewModel.entries).isEqualTo(listOf( IndexEntry("system-1", "System Context View: Software system (Some description)"), IndexEntry("imageview-2", "Image View Title (Image View Description)") )) } private fun diagramViewModels() = generatorContext.workspace.views.systemContextViews .map { DiagramViewModel.forView(this.pageViewModel(), it, generatorContext.svgFactory) } private fun imageViewViewModels() = listOf( ImageViewViewModel(createImageView(generatorContext.workspace, generatorContext.workspace.model.addSoftwareSystem("Software system2"))) ) } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/FaviconViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertFailure import assertk.assertThat import assertk.assertions.* import kotlin.test.Test class FaviconViewModelTest : ViewModelTest() { private val generatorContext = generatorContext("some workspace") @Test fun favicon() { generatorContext.workspace.views.configuration.addProperty( "generatr.style.faviconPath", "site/favicon.png" ) val faviconViewModel = faviconViewModel() assertThat(faviconViewModel.includeFavicon).isTrue() assertThat(faviconViewModel.type).isEqualTo("image/png") assertThat(faviconViewModel.url).isEqualTo("../../site/favicon.png") } @Test fun `invalid favicon`() { generatorContext.workspace.views.configuration.addProperty( "generatr.style.faviconPath", "favicon" ) assertFailure { faviconViewModel() }.hasMessage("Favicon must be a valid *.ico, *.png of *.gif file") } @Test fun `no favicon`() { val faviconViewModel = faviconViewModel() assertThat(faviconViewModel.includeFavicon).isFalse() } private fun faviconViewModel() = object : PageViewModel(generatorContext) { override val url: String = "/master/system" override val pageSubTitle: String = "subtitle" }.favicon } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/HeaderBarViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isTrue import org.junit.jupiter.api.DynamicTest import org.junit.jupiter.api.TestFactory import kotlin.test.Test class HeaderBarViewModelTest : ViewModelTest() { private val generatorContext = generatorContext( "some workspace", branches = listOf("main", "branch-2"), version = "0.42.1337" ) private val pageViewModel = object : PageViewModel(generatorContext) { override val url: String = "/master/system" override val pageSubTitle: String = "subtitle" } @Test fun `workspace title link`() { val viewModel = HeaderBarViewModel(pageViewModel, generatorContext) assertThat(viewModel.titleLink).isEqualTo( LinkViewModel(pageViewModel, "some workspace", HomePageViewModel.url()) ) } @Test fun `current branch`() { val viewModel = HeaderBarViewModel(pageViewModel, generatorContext) assertThat(viewModel.currentBranch).isEqualTo(generatorContext.currentBranch) } @Test fun `branch pulldown menu`() { val viewModel = HeaderBarViewModel(pageViewModel, generatorContext) assertThat(viewModel.branches).containsExactly( BranchHomeLinkViewModel(pageViewModel, "main"), BranchHomeLinkViewModel(pageViewModel, "branch-2") ) } @Test fun `version number`() { val viewModel = HeaderBarViewModel(pageViewModel, generatorContext) assertThat(viewModel.version).isEqualTo("0.42.1337") } @Test fun logo() { generatorContext.workspace.views.configuration.addProperty( "generatr.style.logoPath", "site/logo.png" ) val viewModel = HeaderBarViewModel(pageViewModel, generatorContext) assertThat(viewModel.hasLogo).isTrue() assertThat(viewModel.logo).isEqualTo(ImageViewModel(pageViewModel, "/site/logo.png")) } @Test fun `no logo`() { val viewModel = HeaderBarViewModel(pageViewModel, generatorContext) assertThat(viewModel.hasLogo).isFalse() } @TestFactory fun `no dark mode`() = listOf( "light" to false, "dark" to false, "auto" to true, ).map { (theme, allowToggle) -> DynamicTest.dynamicTest(theme) { generatorContext.workspace.views.configuration.addProperty( "generatr.site.theme", theme ) val viewModel = HeaderBarViewModel(object : PageViewModel(generatorContext) { override val url: String = "/master/system" override val pageSubTitle: String = "subtitle" }, generatorContext) assertThat(viewModel.allowToggleTheme).isEqualTo(allowToggle) } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/HomePageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isTrue import com.structurizr.documentation.Format import nl.avisi.structurizr.site.generatr.site.model.HomePageViewModel.Companion.DEFAULT_HOMEPAGE_CONTENT import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource import kotlin.test.Test class HomePageViewModelTest : ViewModelTest() { @Test fun url() { assertThat(HomePageViewModel.url()) .isEqualTo("/") } @Test fun `default homepage`() { val generatorContext = generatorContext() val viewModel = HomePageViewModel(generatorContext) assertThat(viewModel.content) .isEqualTo(toHtml(viewModel, DEFAULT_HOMEPAGE_CONTENT, Format.Markdown, svgFactory)) } @Test fun `homepage with workspace docs`() { val generatorContext = generatorContext() generatorContext.workspace.documentation.addSection( createSection("Section content") ) val viewModel = HomePageViewModel(generatorContext) assertThat(viewModel.content) .isEqualTo(toHtml(viewModel, "Section content", Format.Markdown, svgFactory)) } @ParameterizedTest @ValueSource(strings = ["", " "]) fun `page title with blank workspace name`(workspaceName: String) { val generatorContext = generatorContext(workspaceName) val viewModel = HomePageViewModel(generatorContext) assertThat(viewModel.pageTitle).isEqualTo("Home") } @Test fun `page title with workspace name`() { val generatorContext = generatorContext("Workspace name") val viewModel = HomePageViewModel(generatorContext) assertThat(viewModel.pageTitle).isEqualTo("Workspace name") } @Test fun `header bar`() { val generatorContext = generatorContext("Workspace name") val viewModel = HomePageViewModel(generatorContext) assertThat(viewModel.headerBar.titleLink.title).isEqualTo("Workspace name") } @Test fun menu() { val generatorContext = generatorContext("Workspace name") val viewModel = HomePageViewModel(generatorContext) assertThat(viewModel.menu.generalItems[0].active).isTrue() } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/ImageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.isEqualTo import kotlin.test.Test class ImageViewModelTest : ViewModelTest() { @Test fun `relative href`() { val pageViewModel = pageViewModel("/some-page/some-subpage/") val viewModel = ImageViewModel(pageViewModel, "/svg/image.svg") assertThat(viewModel.relativeHref).isEqualTo("../../svg/image.svg") } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/ImageViewViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.isEqualTo import com.structurizr.model.SoftwareSystem import kotlin.test.Test class ImageViewViewModelTest : ViewModelTest() { private val generatorContext = generatorContext() private val softwareSystem: SoftwareSystem = generatorContext.workspace.model.addSoftwareSystem("Software system 1") private val container = softwareSystem.addContainer("Container 1") private val component = container.addComponent("Component 1") @Test fun `image name for software system`() { val imageView = ImageViewViewModel(createImageView(generatorContext.workspace, softwareSystem, title = null)) assertThat(imageView.title).isEqualTo("Software system 1 - Containers") } @Test fun `image name for container`() { val imageView = ImageViewViewModel(createImageView(generatorContext.workspace, container, title = null)) assertThat(imageView.title).isEqualTo("Software system 1 - Container 1 - Components") } @Test fun `image name for component`() { val imageView = ImageViewViewModel(createImageView(generatorContext.workspace, component, title = null)) assertThat(imageView.title).isEqualTo("Software system 1 - Container 1 - Component 1 - Code") } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/IndexingTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.containsAll import assertk.assertions.containsAtLeast import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isNull import com.structurizr.documentation.Format import com.structurizr.documentation.Section import nl.avisi.structurizr.site.generatr.site.model.indexing.* import kotlin.test.Test class IndexingTest : ViewModelTest() { val workspace = this.generatorContext().workspace @Test fun `no home`() { val document = home(workspace.documentation, this.pageViewModel()) assertThat(document).isNull() } @Test fun `indexes home`() { workspace.documentation.addSection(createSection("# Home\nSome info")) val document = home(workspace.documentation, this.pageViewModel()) assertThat(document).isEqualTo(Document("../", "Home", "Home", "Home Some info")) } @Test fun `no workspace sections`() { val documents = workspaceSections(workspace.documentation, this.pageViewModel()) assertThat(documents).isEmpty() } @Test fun `indexes non home workspace sections`() { workspace.documentation.addSection(createSection("# Home\nSome info")) workspace.documentation.addSection(createSection("# Landscape\nMore info")) val documents = workspaceSections(workspace.documentation, this.pageViewModel()) assertThat(documents).containsAtLeast( Document( "../landscape/", "Workspace Documentation", "Landscape", "Landscape More info" ) ) } @Test fun `no workspace decisions`() { val documents = workspaceDecisions(workspace.documentation, this.pageViewModel()) assertThat(documents).isEmpty() } @Test fun `indexes workspace decisions`() { workspace.documentation.addDecision(createDecision()) val documents = workspaceDecisions(workspace.documentation, this.pageViewModel()) assertThat(documents).containsAtLeast( Document( "../decisions/1/", "Workspace Decision", "Decision 1", "Decision 1 Decision 1 content" ) ) } @Test fun `no software system home or properties`() { workspace.model.addSoftwareSystem("Software System 1", "One system to rule them all") val document = softwareSystemHome(workspace.model.softwareSystems.single(), this.pageViewModel()) assertThat(document).isNull() } @Test fun `indexes software system with home section`() { workspace.model.addSoftwareSystem("Software System 1", "One system to rule them all").apply { documentation.addSection(Section(Format.Markdown, "# Introduction\nSome info")) } val document = softwareSystemHome(workspace.model.softwareSystems.single(), this.pageViewModel()) assertThat(document).isEqualTo( Document( "../software-system-1/", "Software System Info", "Software System 1 | Introduction", "Introduction Some info" ) ) } @Test fun `indexes software system with properties`() { workspace.model.addSoftwareSystem("Software System 1", "One system to rule them all").apply { addProperty("Prop1", "Value 1") } val document = softwareSystemHome(workspace.model.softwareSystems.single(), this.pageViewModel()) assertThat(document).isEqualTo( Document( "../software-system-1/", "Software System Info", "Software System 1 | Info", "Value 1" ) ) } @Test fun `indexes software system with home section and properties`() { workspace.model.addSoftwareSystem("Software System 1", "One system to rule them all").apply { documentation.addSection(Section(Format.Markdown, "# Introduction\nSome info")) addProperty("Prop 1", "Value 1") addProperty("Prop 2", "Value 2") } val document = softwareSystemHome(workspace.model.softwareSystems.single(), this.pageViewModel()) assertThat(document).isEqualTo( Document( "../software-system-1/", "Software System Info", "Software System 1 | Introduction", "Introduction Some info Value 1 Value 2" ) ) } @Test fun `indexes software system context`() { workspace.model.addSoftwareSystem("Software System 1", "One system to rule them all") val document = softwareSystemContext(workspace.model.softwareSystems.single(), this.pageViewModel()) assertThat(document).isEqualTo( Document( "../software-system-1/context/", "Context views", "Software System 1 | Context views", "Software System 1 One system to rule them all" ) ) } @Test fun `no software system containers`() { workspace.model.addSoftwareSystem("Software System 1", "One system to rule them all") val document = softwareSystemContainers(workspace.model.softwareSystems.single(), this.pageViewModel()) assertThat(document).isNull() } @Test fun `indexes software system containers`() { workspace.model.addSoftwareSystem("Software System 1", "One system to rule them all").apply { addContainer("Container 1", "a container") addContainer("Container 2", "a second container", "tech 1") } val document = softwareSystemContainers(workspace.model.softwareSystems.single(), this.pageViewModel()) assertThat(document).isEqualTo( Document( "../software-system-1/container/", "Container views", "Software System 1 | Container views", "Container 1 a container Container 2 a second container tech 1" ) ) } @Test fun `no software system components`() { workspace.model.addSoftwareSystem("Software System 1", "One system to rule them all") val document = softwareSystemComponents(workspace.model.softwareSystems.single(), this.pageViewModel()) assertThat(document).isNull() } @Test fun `indexes software system components`() { workspace.model.addSoftwareSystem("Software System 1", "One system to rule them all").apply { addContainer("Container 1", "a container").apply { addComponent("Component 1", "a component") addComponent("Component 2", "a second component") } addContainer("Container 2", "a second container", "tech 1").apply { addComponent("Component 3", "a third component", "tech 1") } } val document = softwareSystemComponents(workspace.model.softwareSystems.single(), this.pageViewModel()) assertThat(document).isEqualTo( Document( "../software-system-1/component/", "Component views", "Software System 1 | Component views", "Component 1 a component Component 2 a second component Component 3 a third component tech 1" ) ) } @Test fun `no software system relationships`() { workspace.model.addSoftwareSystem("Software System 1", "One system to rule them all") val document = softwareSystemRelationships(workspace.model.softwareSystems.single(), this.pageViewModel()) assertThat(document).isNull() } @Test fun `indexes software system relationships`() { val system1 = workspace.model.addSoftwareSystem("Software System 1", "One system to rule them all") val system2 = workspace.model.addSoftwareSystem("Software System 2", "One system to rule the others") system1.uses(system2, "reads") system2.uses(system1, "writes", "tech 1") val document = softwareSystemRelationships( workspace.model.softwareSystems.single { it.name == "Software System 1" }, this.pageViewModel() ) assertThat(document).isEqualTo( Document( "../software-system-1/dependencies/", "Dependencies", "Software System 1 | Dependencies", "Software System 2 reads Software System 2 writes tech 1" ) ) } @Test fun `no software system decisions`() { workspace.model.addSoftwareSystem("Software System 1", "One system to rule them all") val documents = softwareSystemDecisions(workspace.model.softwareSystems.single(), this.pageViewModel()) assertThat(documents).isEmpty() } @Test fun `indexes software system decisions`() { workspace.model.addSoftwareSystem("Software System 1", "One system to rule them all").apply { documentation.addDecision(createDecision("1")) documentation.addDecision(createDecision("2")) } val documents = softwareSystemDecisions(workspace.model.softwareSystems.single(), this.pageViewModel()) assertThat(documents).containsAtLeast( Document( "../software-system-1/decisions/1/", "Software System Decision", "Software System 1 | Decision 1", "Decision 1 Decision 1 content" ), Document( "../software-system-1/decisions/2/", "Software System Decision", "Software System 1 | Decision 2", "Decision 2 Decision 2 content" ) ) } @Test fun `no software system sections`() { workspace.model.addSoftwareSystem("Software System 1", "One system to rule them all") val documents = softwareSystemSections(workspace.model.softwareSystems.single(), this.pageViewModel()) assertThat(documents).isEmpty() } @Test fun `only software system section for software system home`() { workspace.model.addSoftwareSystem("Software System 1", "One system to rule them all").apply { documentation.addSection(Section(Format.Markdown, "# Introduction\nSome info")) } val documents = softwareSystemSections(workspace.model.softwareSystems.single(), this.pageViewModel()) assertThat(documents).isEmpty() } @Test fun `indexes software system sections`() { workspace.model.addSoftwareSystem("Software System 1", "One system to rule them all").apply { documentation.addSection(Section(Format.Markdown, "# Introduction\nSome info")) documentation.addSection(Section(Format.Markdown, "# Usage\nThat's how it works")) documentation.addSection(Section(Format.Markdown, "# History\nThat's how we got here")) } val documents = softwareSystemSections(workspace.model.softwareSystems.single(), this.pageViewModel()) assertThat(documents).containsAtLeast( Document( "../software-system-1/sections/usage/", "Software System Documentation", "Software System 1 | Usage", "Usage That's how it works" ), Document( "../software-system-1/sections/history/", "Software System Documentation", "Software System 1 | History", "History That's how we got here" ) ) } @Test fun `no container sections`() { val softwareSystem = workspace.model.addSoftwareSystem("Software System 1", "One system to rule them all") softwareSystem.addContainer("Container 1", "a container") val documents = softwareSystemContainerSections(softwareSystem.containers.single(), this.pageViewModel()) assertThat(documents).isEmpty() } @Test fun `no container decisions`() { val softwareSystem = workspace.model.addSoftwareSystem("Software System 1", "One system to rule them all") softwareSystem.addContainer("Container 1", "a container") val documents = softwareSystemContainerDecisions(softwareSystem.containers.single(), this.pageViewModel()) assertThat(documents).isEmpty() } @Test fun `indexes container decisions`() { val softwareSystem = workspace.model.addSoftwareSystem("Software System 1", "One system to rule them all") val container = softwareSystem.addContainer("Container 1", "a container").apply { documentation.addDecision(createDecision("1")) documentation.addDecision(createDecision("2")) } val documents = softwareSystemContainerDecisions(container, this.pageViewModel()) assertThat(documents).containsAtLeast( Document( "../software-system-1/decisions/container-1/1/", "Container Decision", "Container 1 | Decision 1", "Decision 1 Decision 1 content" ), Document( "../software-system-1/decisions/container-1/2/", "Container Decision", "Container 1 | Decision 2", "Decision 2 Decision 2 content" ) ) } @Test fun `indexes container sections`() { val softwareSystem = workspace.model.addSoftwareSystem("Software System 1", "One system to rule them all") val container = softwareSystem.addContainer("Container 1", "a container").apply { documentation.addSection(Section(Format.Markdown, "# Introduction\nSome info")) documentation.addSection(Section(Format.Markdown, "# Usage\nThat's how it works")) documentation.addSection(Section(Format.Markdown, "# History\nThat's how we got here")) } val documents = softwareSystemContainerSections(container, this.pageViewModel()) assertThat(documents).containsAtLeast( Document( "../software-system-1/sections/container-1/usage/", "Container Documentation", "Container 1 | Usage", "Usage That's how it works" ), Document( "../software-system-1/sections/container-1/history/", "Container Documentation", "Container 1 | History", "History That's how we got here" ) ) } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/LinkViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isTrue import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource import kotlin.test.Test class LinkViewModelTest : ViewModelTest() { @Test fun `exact links are active when the page url matches exactly`() { val pageViewModel = pageViewModel("/some-page") val viewModel = LinkViewModel(pageViewModel, "Some page", "/some-page") assertThat(viewModel.active).isTrue() } @ParameterizedTest @ValueSource(strings = ["/some-page/1", "/", "/some-other-page"]) fun `exact links are not active when the page url matches exactly`(href: String) { val pageViewModel = pageViewModel("/some-page") val viewModel = LinkViewModel(pageViewModel, "Some page", href) assertThat(viewModel.active).isFalse() } @ParameterizedTest @ValueSource(strings = ["/some-page", "/some-page/1", "/some-page/subpage/subsubpage"]) fun `non-exact links are active when the page url matches partially`(pageHref: String) { val pageViewModel = pageViewModel(pageHref) val viewModel = LinkViewModel(pageViewModel, "Some page", "/some-page", Match.CHILD) assertThat(viewModel.active).isTrue() } @ParameterizedTest @ValueSource(strings = ["/", "/some-other-page/1", "/some-other-page/subpage/subsubpage"]) fun `non-exact links are not active when the page url doesn't match partially`(pageHref: String) { val pageViewModel = pageViewModel(pageHref) val viewModel = LinkViewModel(pageViewModel, "Some page", "/some-page", Match.CHILD) assertThat(viewModel.active).isFalse() } @Test fun `non-exact links are only active when page url is a subdirectory of the link`() { val expectInactivePartialMatch = LinkViewModel(pageViewModel("/page-two"), "Some page", "/page", Match.CHILD) val expectActivePartialMatch = LinkViewModel(pageViewModel("/page/two"), "Some page", "/page", Match.CHILD) assertThat(expectInactivePartialMatch.active).isFalse() assertThat(expectActivePartialMatch.active).isTrue() } @Test fun `relative href`() { val pageViewModel = pageViewModel("/some-page/some-subpage") val viewModel = LinkViewModel(pageViewModel, "Some other page", "/some-other-page") assertThat(viewModel.relativeHref).isEqualTo("../../some-other-page/") } @ParameterizedTest @ValueSource(strings = ["/some-page/sibling-page-1/", "/some-page/sibling-page-2/"]) fun `sibling links are active when the previous url path matches`(pageHref: String) { val pageViewModel = pageViewModel(pageHref) val viewModel = LinkViewModel(pageViewModel, "Some page", "/some-page/sibling-page-3/", Match.SIBLING) assertThat(viewModel.active).isTrue() } @ParameterizedTest @ValueSource(strings = ["/some-other-page/sibling-page-1/", "/some-other-page/sibling-page-2/"]) fun `sibling links are not active when the previous url path doesn't match`(pageHref: String) { val pageViewModel = pageViewModel(pageHref) val viewModel = LinkViewModel(pageViewModel, "Some page", "/some-page/sibling-page-1", Match.SIBLING) assertThat(viewModel.active).isFalse() } @Test fun `sibling links are only active when previous page url path matches previous href path url`() { val expectInactiveSiblingMatch = LinkViewModel(pageViewModel("/page/two/"), "Some page", "/some-page/", Match.SIBLING) val expectActiveSiblingMatch = LinkViewModel(pageViewModel("/page/two"), "Some page", "/page/one", Match.SIBLING) assertThat(expectInactiveSiblingMatch.active).isFalse() assertThat(expectActiveSiblingMatch.active).isTrue() } @ParameterizedTest @ValueSource(strings = ["/some-page/sibling/child/", "/some-page/other-sibling/other-child/"]) fun `sibling child links are active when two url paths back matches`(pageHref: String) { val pageViewModel = pageViewModel(pageHref) val viewModel = LinkViewModel(pageViewModel, "Some page", "/some-page/another-sibling/another-child", Match.SIBLING_CHILD) assertThat(viewModel.active).isTrue() } @ParameterizedTest @ValueSource(strings = ["/some-page/sibling/child/", "/some-page/other-sibling/other-child/"]) fun `sibling child links are not active when two url paths back doesnt match`(pageHref: String) { val pageViewModel = pageViewModel(pageHref) val viewModel = LinkViewModel(pageViewModel, "Some other page", "/some-other-page/not-a-sibling/child/", Match.SIBLING_CHILD) assertThat(viewModel.active).isFalse() } @Test fun `sibling child links are only active when two url paths back matches with two url paths back from href`() { val expectInactiveSiblingChildMatch = LinkViewModel(pageViewModel("/page/one/description"), "Some page", "/some-page/", Match.SIBLING_CHILD) val expectActiveSiblingChildMatch = LinkViewModel(pageViewModel("/page/one/description"), "Some page", "/page/two/title-screen", Match.SIBLING_CHILD) assertThat(expectInactiveSiblingChildMatch.active).isFalse() assertThat(expectActiveSiblingChildMatch.active).isTrue() } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/MarkdownToHtmlTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.matches import com.structurizr.documentation.Format import org.junit.jupiter.api.Test class MarkdownToHtmlTest : ViewModelTest() { @Test fun `translates markdown`() { val generatorContext = generatorContext() val viewModel = HomePageViewModel(generatorContext) val html = toHtml( viewModel, """ ## header content """.trimIndent(), Format.Markdown, svgFactory ) assertThat(html).isEqualTo( """

header

content

""".trimIndent() ) } @Test fun `translates markdown table as default`() { val generatorContext = generatorContext() val viewModel = HomePageViewModel(generatorContext) val html = toHtml( viewModel, """ ## header | header1 | header2 | | ------- | ------- | | content | content | """.trimIndent(), Format.Markdown, svgFactory ) assertThat(html).isEqualTo( """

header

header1 header2
content content
""".trimIndent() ) } @Test fun `doesnt translate markdown table with extension property without Tables`() { val generatorContext = generatorContext().apply { workspace.views.configuration.addProperty("generatr.markdown.flexmark.extensions", "Admonition") } val viewModel = HomePageViewModel(generatorContext) val html = toHtml( viewModel, """ ## header | header1 | header2 | | ------- | ------- | | content | content | """.trimIndent(), Format.Markdown, svgFactory ) assertThat(html).isEqualTo( """

header

| header1 | header2 | | ------- | ------- | | content | content |

""".trimIndent() ) } @Test fun `translates markdown table with extension property containing Tables`() { val generatorContext = generatorContext().apply { workspace.views.configuration.addProperty("generatr.markdown.flexmark.extensions", "Admonition, Tables") } val viewModel = HomePageViewModel(generatorContext) val html = toHtml( viewModel, """ ## header | header1 | header2 | | ------- | ------- | | content | content | """.trimIndent(), Format.Markdown, svgFactory ) assertThat(html).isEqualTo( """

header

header1 header2
content content
""".trimIndent() ) } @Test fun `translates markdown admonition block with extension property containing Admonition`() { val generatorContext = generatorContext().apply { workspace.views.configuration.addProperty("generatr.markdown.flexmark.extensions", "Admonition, Tables") } val viewModel = HomePageViewModel(generatorContext) val html = toHtml( viewModel, """ ## header !!! faq "FAQ" This is a FAQ. """.trimIndent(), Format.Markdown, svgFactory ) assertThat(html).isEqualTo( """

header

FAQ

This is a FAQ.

""".trimIndent() ) } @Test fun `renders PlantUML code to SVG`() { val generatorContext = generatorContext() val viewModel = HomePageViewModel(generatorContext) val html = toHtml( viewModel, """ ```puml @startuml class Foo @enduml ``` """.trimIndent(), Format.Markdown, svgFactory ) assertThat(html).matches("
.*.*
".toRegex(RegexOption.DOT_MATCHES_ALL)) } @Test fun `translates mermaid graphings in markdown with extension property containing GitLab`() { val generatorContext = generatorContext().apply { workspace.views.configuration.addProperty("generatr.markdown.flexmark.extensions", "Admonition, GitLab, Tables") } val viewModel = HomePageViewModel(generatorContext) val html = toHtml( viewModel, """ ## Mermaid ```mermaid graph TD; A-->B; A-->C; B-->D; C-->D; ``` """.trimIndent(), Format.Markdown, svgFactory ) assertThat(html).isEqualTo( """

Mermaid

graph TD;
            A-->B;
            A-->C;
            B-->D;
            C-->D;
            
""".trimIndent() ) } @Test fun `translates mermaid graphings in markdown without extension property containing GitLab`() { val generatorContext = generatorContext().apply { workspace.views.configuration.addProperty("generatr.markdown.flexmark.extensions", "Admonition, Tables") } val viewModel = HomePageViewModel(generatorContext) val html = toHtml( viewModel, """ ## Mermaid ```mermaid graph TD; A-->B; A-->C; B-->D; C-->D; ``` """.trimIndent(), Format.Markdown, svgFactory ) assertThat(html).isEqualTo( """

Mermaid

graph TD;
            A-->B;
            A-->C;
            B-->D;
            C-->D;
            
""".trimIndent() ) } @Test fun `embedded diagram`() { val generatorContext = generatorContext() val viewModel = HomePageViewModel(generatorContext) val html = toHtml(viewModel, "![System Landscape Diagram](embed:SystemLandscape)", Format.Markdown, svgFactory) assertThat(html).isEqualTo( """

System Landscape Diagram

""".trimIndent() ) } @Test fun `embedded diagram key not found`() { val generatorContext = generatorContext() val viewModel = HomePageViewModel(generatorContext) val html = toHtml(viewModel, "![Diagram](embed:non-existing)", Format.Markdown) { _, _ -> null } assertThat(html).isEqualTo( """

No view with key non-existing found!

""".trimIndent() ) } @Test fun `links to pages`() { val generatorContext = generatorContext() val viewModel = HomePageViewModel(generatorContext) val html = toHtml( viewModel, """ ## header Link: [Some decision](/decisions/1/), """.trimIndent(), Format.Markdown, svgFactory ) assertThat(html).isEqualTo( """

header

Link: Some decision,

""".trimIndent() ) } @Test fun `links to files`() { val generatorContext = generatorContext() val viewModel = HomePageViewModel(generatorContext) val html = toHtml( viewModel, """ ## header Link: [Some document](/document.md), """.trimIndent(), Format.Markdown, svgFactory ) assertThat(html).isEqualTo( """

header

Link: Some document,

""".trimIndent() ) } @Test fun `links to external sites`() { val generatorContext = generatorContext() val viewModel = HomePageViewModel(generatorContext) val html = toHtml( viewModel, """ ## header Link: [Wikipedia](https://www.wikipedia.org/), """.trimIndent(), Format.Markdown, svgFactory ) assertThat(html).isEqualTo( """

header

Link: Wikipedia,

""".trimIndent() ) } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.hasSize import assertk.assertions.isEqualTo import assertk.assertions.isTrue import com.structurizr.documentation.Decision import com.structurizr.documentation.Format import nl.avisi.structurizr.site.generatr.site.GeneratorContext import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource class MenuViewModelTest : ViewModelTest() { @ParameterizedTest @ValueSource(strings = ["main", "branch-2"]) fun `fixed general items`(currentBranch: String) { val generatorContext = generatorContext(branches = listOf("main", "branch-2"), currentBranch = currentBranch) val pageViewModel = createPageViewModel(generatorContext) val viewModel = MenuViewModel(generatorContext, pageViewModel) assertThat(viewModel.generalItems).containsExactly( LinkViewModel(pageViewModel, "Home", HomePageViewModel.url()) ) } @ParameterizedTest @ValueSource(strings = ["main", "branch-2"]) fun `software systems menu item if available`(currentBranch: String) { val generatorContext = generatorContext(branches = listOf("main", "branch-2"), currentBranch = currentBranch) .apply { workspace.model.addSoftwareSystem("Software system 1") } val pageViewModel = createPageViewModel(generatorContext) val viewModel = MenuViewModel(generatorContext, pageViewModel) assertThat(viewModel.generalItems[1]).isEqualTo( LinkViewModel(pageViewModel, "Software Systems", SoftwareSystemsPageViewModel.url()) ) } @ParameterizedTest @ValueSource(strings = ["main", "branch-2"]) fun `decisions menu item if available`(currentBranch: String) { val generatorContext = generatorContext(branches = listOf("main", "branch-2"), currentBranch = currentBranch).apply { workspace.documentation.addDecision(Decision("1").apply { title = "Decision 1" status = "Proposed" format = Format.Markdown content = "Content" }) } val pageViewModel = createPageViewModel(generatorContext) val viewModel = MenuViewModel(generatorContext, pageViewModel) assertThat(viewModel.generalItems[1]).isEqualTo( LinkViewModel(pageViewModel, "Decisions", WorkspaceDecisionsPageViewModel.url(), Match.CHILD) ) } @ParameterizedTest @ValueSource(strings = ["main", "branch-2"]) fun `workspace-level documentation in general section`(currentBranch: String) { val generatorContext = generatorContext(branches = listOf("main", "branch-2"), currentBranch = currentBranch) generatorContext.workspace.documentation.addSection(createSection("# Home")) val section1 = createSection("# Doc 1") .also { generatorContext.workspace.documentation.addSection(it) } val section2 = createSection("# Doc Title 2") .also { generatorContext.workspace.documentation.addSection(it) } val pageViewModel = createPageViewModel(generatorContext) val viewModel = MenuViewModel(generatorContext, pageViewModel) assertThat(viewModel.generalItems.drop(1)).containsExactly( LinkViewModel(pageViewModel, "Doc 1", WorkspaceDocumentationSectionPageViewModel.url(section1)), LinkViewModel(pageViewModel, "Doc Title 2", WorkspaceDocumentationSectionPageViewModel.url(section2)) ) } @ParameterizedTest @ValueSource(strings = ["main", "branch-2"]) fun `links to software system pages sorted alphabetically case insensitive`(currentBranch: String) { val generatorContext = generatorContext(branches = listOf("main", "branch-2"), currentBranch = currentBranch) val system2 = generatorContext.workspace.model.addSoftwareSystem("System 2", "") val system1 = generatorContext.workspace.model.addSoftwareSystem("system 1", "") val pageViewModel = createPageViewModel(generatorContext) val viewModel = MenuViewModel(generatorContext, pageViewModel) assertThat(viewModel.softwareSystemItems).containsExactly( LinkViewModel( pageViewModel, "system 1", SoftwareSystemPageViewModel.url(system1, SoftwareSystemPageViewModel.Tab.HOME), Match.CHILD ), LinkViewModel( pageViewModel, "System 2", SoftwareSystemPageViewModel.url(system2, SoftwareSystemPageViewModel.Tab.HOME), Match.CHILD ) ) } @ParameterizedTest @ValueSource(strings = ["main", "branch-2"]) fun `active links`(currentBranch: String) { val generatorContext = generatorContext(branches = listOf("main", "branch-2"), currentBranch = currentBranch) val system = generatorContext.workspace.model.addSoftwareSystem("System 1", "") MenuViewModel(generatorContext, createPageViewModel(generatorContext, url = HomePageViewModel.url())) .let { assertThat(it.generalItems[0].active).isTrue() } MenuViewModel( generatorContext, createPageViewModel( generatorContext, url = SoftwareSystemPageViewModel.url( system, SoftwareSystemPageViewModel.Tab.HOME ) ) ) .let { assertThat(it.softwareSystemItems[0].active).isTrue() } } @Test fun `do not show menu entries for software systems with an external location (declared external by tag)`() { val generatorContext = generatorContext(branches = listOf("main", "branch-2"), currentBranch = "main") generatorContext.workspace.views.configuration.addProperty("generatr.site.externalTag", "External System") generatorContext.workspace.model.addSoftwareSystem("System 1").apply { group = "Group 1" } generatorContext.workspace.model.addSoftwareSystem("External system").apply { addTags("External System") } MenuViewModel(generatorContext, createPageViewModel(generatorContext, url = HomePageViewModel.url())) .let { assertThat(it.softwareSystemNodes().children).hasSize(1) assertThat(it.softwareSystemNodes().children[0].name).isEqualTo("Group 1") assertThat(it.softwareSystemNodes().children[0].children).hasSize(1) assertThat(it.softwareSystemNodes().children[0].children[0].name).isEqualTo("System 1") } } @Test fun `show software systems in nested groups from single group in software systems list`() { val generatorContext = generatorContext(branches = listOf("main", "branch-2"), currentBranch = "main") generatorContext.workspace.views.configuration.addProperty("generatr.site.nestGroups", "true") generatorContext.workspace.model.addSoftwareSystem("System 1").apply { group = "Group 1" } MenuViewModel(generatorContext, createPageViewModel(generatorContext, url = HomePageViewModel.url())) .let { assertThat(it.softwareSystemNodes().children).hasSize(1) assertThat(it.softwareSystemNodes().children[0].name).isEqualTo("Group 1") assertThat(it.softwareSystemNodes().children[0].children).hasSize(1) assertThat(it.softwareSystemNodes().children[0].children[0].name).isEqualTo("System 1") } } @Test fun `show nested groups in software systems list`() { val generatorContext = generatorContext(branches = listOf("main", "branch-2"), currentBranch = "main") generatorContext.workspace.views.configuration.addProperty("generatr.site.nestGroups", "true") generatorContext.workspace.model.addProperty("structurizr.groupSeparator", "/") generatorContext.workspace.model.addSoftwareSystem("System 1").apply { group = "Group 1" } generatorContext.workspace.model.addSoftwareSystem("System 2").apply { group = "Group 1" } generatorContext.workspace.model.addSoftwareSystem("System 3").apply { group = "Group 2" } generatorContext.workspace.model.addSoftwareSystem("System 4").apply { group = "Group 1/Group 3" } MenuViewModel(generatorContext, createPageViewModel(generatorContext, url = HomePageViewModel.url())) .let { assertThat(it.softwareSystemNodes().children).hasSize(2) assertThat(it.softwareSystemNodes().children[0].name).isEqualTo("Group 1") assertThat(it.softwareSystemNodes().children[0].children).hasSize(3) assertThat(it.softwareSystemNodes().children[0].children[0].name).isEqualTo("Group 3") assertThat(it.softwareSystemNodes().children[0].children[0].children[0].name).isEqualTo("System 4") } } private fun createPageViewModel(generatorContext: GeneratorContext, url: String = "/master/page"): PageViewModel { return object : PageViewModel(generatorContext) { override val url = url override val pageSubTitle = "subtitle" } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/PageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.isEqualTo import org.junit.jupiter.api.Test class PageViewModelTest : ViewModelTest() { @Test fun `page title`() { assertThat(pageViewModel().pageTitle).isEqualTo("Some page | Workspace name") } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/PropertiesTableViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.isEqualTo import kotlin.test.Test class PropertiesTableViewModelTest : ViewModelTest() { @Test fun `no properties available`() { assertThat(createPropertiesTableViewModel(emptyMap())) .isEqualTo( TableViewModel.create { propertiesTableHeaderRow() } ) } @Test fun `many properties sorted by key`() { val properties = mapOf( "b name" to "value", "a name" to "value" ) assertThat(createPropertiesTableViewModel(properties)).isEqualTo( TableViewModel.create { propertiesTableHeaderRow() bodyRow( cell("a name"), cell("value"), ) bodyRow( cell("b name"), cell("value"), ) } ) } @Test fun `properties with link`() { val properties = mapOf( "url" to "https://tempuri.org/", ) assertThat(createPropertiesTableViewModel(properties)).isEqualTo( TableViewModel.create { propertiesTableHeaderRow() bodyRow( cell("url"), cellWithExternalLink("https://tempuri.org/", "https://tempuri.org/"), ) } ) } private fun TableViewModel.TableViewInitializerContext.propertiesTableHeaderRow() { headerRow(headerCellMedium("Name"), headerCell("Value")) } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SearchViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.hasSize import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import com.structurizr.documentation.Format import com.structurizr.documentation.Section import org.junit.jupiter.api.Test class SearchViewModelTest : ViewModelTest() { @Test fun `no index language configured`() { val viewModel = SearchViewModel(generatorContext()) assertThat(viewModel.language).isEmpty() } @Test fun `index language configured`() { val generatorContext = generatorContext().apply { workspace.views.configuration.addProperty("generatr.search.language", "nl") } val viewModel = SearchViewModel(generatorContext) assertThat(viewModel.language).isEqualTo("nl") } @Test fun `nothing to index`() { val viewModel = SearchViewModel(generatorContext()) assertThat(viewModel.documents).hasSize(0) } @Test fun `indexes global pages`() { val generatorContext = generatorContext() generatorContext.workspace.apply { documentation.addSection(createSection("# Home\nSome info")) documentation.addSection(createSection("# Landscape\nMore info")) documentation.addDecision(createDecision()) } val viewModel = SearchViewModel(generatorContext) assertThat(viewModel.documents.map { it.type }) .containsExactly("Home", "Workspace Decision", "Workspace Documentation") } @Test fun `indexes software system without additional information`() { val generatorContext = generatorContext() generatorContext.workspace.apply { model.addSoftwareSystem("Software system") } val viewModel = SearchViewModel(generatorContext) assertThat(viewModel.documents.map { it.type }) .containsExactly("Context views") } @Test fun `indexes no external software system (declared external by tag)`() { val generatorContext = generatorContext() generatorContext.workspace.apply { views.configuration.addProperty("generatr.site.externalTag", "External System") model.addSoftwareSystem("Software system").apply { addTags("External System") } } val viewModel = SearchViewModel(generatorContext) assertThat(viewModel.documents.map { it.type }) .isEmpty() } @Test fun `indexes all software system information`() { val generatorContext = generatorContext() generatorContext.workspace.apply { model.addSoftwareSystem("Software system").apply { documentation.addSection(Section(Format.Markdown, "# Introduction\nSome info")) addContainer("Container 1", "a container").apply { addComponent("Component 1", "a component") documentation.addDecision(createDecision("1")) documentation.addSection(Section(Format.Markdown, "# Component Usage\nThat's how it works")) } documentation.addDecision(createDecision("1")) documentation.addSection(Section(Format.Markdown, "# Usage\nThat's how it works")) } } val viewModel = SearchViewModel(generatorContext) assertThat(viewModel.documents.map { it.type }) .containsExactly( "Software System Info", "Context views", "Container views", "Component views", "Software System Decision", "Software System Documentation", "Container Documentation", "Container Decision" ) } @Test fun `indexes software systems with relations`() { val generatorContext = generatorContext() generatorContext.workspace.apply { val system1 = model.addSoftwareSystem("Software System 1", "One system to rule them all") val system2 = model.addSoftwareSystem("Software System 2", "One system to rule the others") system1.uses(system2, "reads") system2.uses(system1, "writes", "tech 1") } val viewModel = SearchViewModel(generatorContext) assertThat(viewModel.documents.map { it.type }) .containsExactly("Context views", "Dependencies", "Context views", "Dependencies") } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentCodePageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.hasSize import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isTrue import com.structurizr.model.Container import com.structurizr.model.SoftwareSystem import nl.avisi.structurizr.site.generatr.normalize import org.junit.jupiter.api.Test class SoftwareSystemContainerComponentCodePageViewModelTest : ViewModelTest() { private val generatorContext = generatorContext() private val softwareSystem: SoftwareSystem = generatorContext.workspace.model .addSoftwareSystem("Software system").also { val backend = it.addContainer("Backend") val frontend = it.addContainer("Frontend") generatorContext.workspace.views.createComponentView(backend, "component-1-backend", "Component view 1 - Backend") generatorContext.workspace.views.createComponentView(backend, "component-2-backend", "Component view 2 - Backend") generatorContext.workspace.views.createComponentView(frontend, "component-1-frontend", "Component view 1 - Frontend") generatorContext.workspace.views.createComponentView(frontend, "component-2-frontend", "Component view 2 - Frontend") } private val backendContainer: Container = softwareSystem.containers.elementAt(0) private val frontendContainer: Container = softwareSystem.containers.elementAt(1) private val backendComponent = backendContainer.addComponent("Backend Component") private val backendComponent2 = backendContainer.addComponent("Java Application") private val frontendComponent = frontendContainer.addComponent("Frontend Component") private val backendImageView = createImageView(generatorContext.workspace, backendComponent) private val backendImageView2 = createImageView(generatorContext.workspace, backendComponent2) private val frontendImageView = createImageView(generatorContext.workspace, frontendComponent) @Test fun `active tab`() { val viewModel = SoftwareSystemContainerComponentCodePageViewModel(generatorContext, backendContainer, backendComponent) assertThat(viewModel.tabs.single { it.link.active }.tab) .isEqualTo(SoftwareSystemPageViewModel.Tab.CODE) } @Test fun `container tabs`() { val viewModel = SoftwareSystemContainerComponentCodePageViewModel(generatorContext, backendContainer, backendComponent) val containerTabList = listOf( ContainerTabViewModel(viewModel, "Backend", "/software-system/code/backend/backend-component", Match.SIBLING), ContainerTabViewModel(viewModel, "Frontend", "/software-system/code/frontend/frontend-component", Match.SIBLING) ) assertThat(viewModel.containerTabs.elementAtOrNull(0)).isEqualTo(containerTabList.elementAt(0)) assertThat(viewModel.containerTabs.elementAtOrNull(1)).isEqualTo(containerTabList.elementAt(1)) } @Test fun `component tabs`() { val viewModel = SoftwareSystemContainerComponentCodePageViewModel(generatorContext, backendContainer, backendComponent) val componentTabList = listOf( ComponentTabViewModel(viewModel, "Backend Component", "/software-system/code/backend/backend-component"), ComponentTabViewModel(viewModel, "Java Application", "/software-system/code/backend/java-application") ) assertThat(viewModel.componentTabs.elementAtOrNull(0)).isEqualTo(componentTabList.elementAt(0)) assertThat(viewModel.componentTabs.elementAtOrNull(1)).isEqualTo(componentTabList.elementAt(1)) } @Test fun url() { var viewModel = SoftwareSystemContainerComponentCodePageViewModel(generatorContext, backendContainer, backendComponent) assertThat(SoftwareSystemContainerComponentCodePageViewModel.url(backendContainer, backendComponent)) .isEqualTo("/${softwareSystem.name.normalize()}/code/${backendContainer.name.normalize()}/${backendComponent.name.normalize()}") assertThat(viewModel.url) .isEqualTo(SoftwareSystemContainerComponentCodePageViewModel.url(backendContainer, backendComponent)) viewModel = SoftwareSystemContainerComponentCodePageViewModel(generatorContext, frontendContainer, frontendComponent) assertThat(SoftwareSystemContainerComponentCodePageViewModel.url(frontendContainer, frontendComponent)) .isEqualTo("/${softwareSystem.name.normalize()}/code/${frontendContainer.name.normalize()}/${frontendComponent.name.normalize()}") assertThat(viewModel.url) .isEqualTo(SoftwareSystemContainerComponentCodePageViewModel.url(frontendContainer, frontendComponent)) } @Test fun `has image`() { var viewModel = SoftwareSystemContainerComponentCodePageViewModel(generatorContext, backendContainer, backendComponent) assertThat(viewModel.visible).isTrue() assertThat(viewModel.images).hasSize(1) assertThat(viewModel.images.single().imageView).isEqualTo(backendImageView) viewModel = SoftwareSystemContainerComponentCodePageViewModel(generatorContext, backendContainer, backendComponent2) assertThat(viewModel.visible).isTrue() assertThat(viewModel.images).hasSize(1) assertThat(viewModel.images.single().imageView).isEqualTo(backendImageView2) viewModel = SoftwareSystemContainerComponentCodePageViewModel(generatorContext, frontendContainer, frontendComponent) assertThat(viewModel.visible).isTrue() assertThat(viewModel.images).hasSize(1) assertThat(viewModel.images.single().imageView).isEqualTo(frontendImageView) } @Test fun `hidden view`() { val viewModel = SoftwareSystemContainerComponentCodePageViewModel( generatorContext, softwareSystem.addContainer("Container"), backendContainer.addComponent("Component") ) assertThat(viewModel.visible).isFalse() } @Test fun `no index`() { val viewModel = SoftwareSystemContainerComponentCodePageViewModel(generatorContext, backendContainer, backendComponent) assertThat(viewModel.diagramIndex.visible).isFalse() } @Test fun `index visible`() { createImageView(generatorContext.workspace, backendComponent, "other-imageview") val viewModel = SoftwareSystemContainerComponentCodePageViewModel(generatorContext, backendContainer, backendComponent) assertThat(viewModel.diagramIndex.visible).isTrue() } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentDecisionPageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.isEqualTo import com.structurizr.documentation.Format import kotlin.test.Test class SoftwareSystemContainerComponentDecisionPageViewModelTest : ViewModelTest() { private val generatorContext = generatorContext() private val softwareSystem = generatorContext.workspace.model.addSoftwareSystem("Software System") private val container = softwareSystem.addContainer("API Application") private val component = container.addComponent("Email Component") @Test fun url() { val decision = createDecision() val viewModel = SoftwareSystemContainerComponentDecisionPageViewModel(generatorContext, component, decision) assertThat(viewModel.url) .isEqualTo("/software-system/decisions/api-application/email-component/1") } @Test fun `active tab`() { val viewModel = SoftwareSystemContainerComponentDecisionPageViewModel(generatorContext, component, createDecision()) assertThat(viewModel.tabs.single { it.link.active }.tab) .isEqualTo(SoftwareSystemPageViewModel.Tab.DECISIONS) } @Test fun content() { val decision = createDecision() val viewModel = SoftwareSystemContainerComponentDecisionPageViewModel(generatorContext, component, decision) assertThat(viewModel.content).isEqualTo(toHtml(viewModel, decision.content, Format.Markdown, svgFactory)) } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentDecisionsPageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isTrue import com.structurizr.model.Component import com.structurizr.model.Container import com.structurizr.model.SoftwareSystem import kotlin.test.Test class SoftwareSystemContainerComponentDecisionsPageViewModelTest : ViewModelTest() { private val generatorContext = generatorContext() private val softwareSystem: SoftwareSystem = generatorContext.workspace.model.addSoftwareSystem("Software system") private val container: Container = softwareSystem.addContainer("API Application") private val component: Component = container.addComponent("Email Component") @Test fun `active tab`() { val viewModel = SoftwareSystemContainerComponentDecisionsPageViewModel(generatorContext, component) assertThat(viewModel.tabs.single { it.link.active }.tab) .isEqualTo(SoftwareSystemPageViewModel.Tab.DECISIONS) } @Test fun `decisions tabs`() { softwareSystem.documentation.addDecision(createDecision("1", "Accepted")) container.documentation.addDecision(createDecision("1", "Accepted")) val viewModel = SoftwareSystemContainerComponentDecisionsPageViewModel(generatorContext, component) assertThat(viewModel.decisionTabs) .isEqualTo(listOf( DecisionTabViewModel( viewModel, "System", "/software-system/decisions" ), DecisionTabViewModel( viewModel, "API Application", "/software-system/decisions/api-application", Match.CHILD ) )) } @Test fun `decisions tabs without container decisions`() { softwareSystem.documentation.addDecision(createDecision("1", "Accepted")) val viewModel = SoftwareSystemContainerComponentDecisionsPageViewModel(generatorContext, component) assertThat(viewModel.decisionTabs) .isEqualTo(listOf( DecisionTabViewModel( viewModel, "System", "/software-system/decisions" ) )) } @Test fun `decisions tabs without software system decisions`() { container.documentation.addDecision(createDecision("1", "Accepted")) val viewModel = SoftwareSystemContainerComponentDecisionsPageViewModel(generatorContext, component) assertThat(viewModel.decisionTabs) .isEqualTo(listOf( DecisionTabViewModel( viewModel, "API Application", "/software-system/decisions/api-application", Match.CHILD ) )) } @Test fun `component decisions tabs`() { container.documentation.addDecision(createDecision("1", "Accepted")) createComponentDecisions() val viewModel = SoftwareSystemContainerComponentDecisionsPageViewModel(generatorContext, component) assertThat(viewModel.componentDecisionsTabs) .isEqualTo(listOf( DecisionTabViewModel( viewModel, "Container", "/software-system/decisions/api-application" ), DecisionTabViewModel( viewModel, "Email Component", "/software-system/decisions/api-application/email-component" ) )) } @Test fun `component decisions tabs without container decisions`() { createComponentDecisions() val viewModel = SoftwareSystemContainerComponentDecisionsPageViewModel(generatorContext, component) assertThat(viewModel.componentDecisionsTabs) .isEqualTo(listOf( DecisionTabViewModel( viewModel, "Email Component", "/software-system/decisions/api-application/email-component" ) )) } @Test fun `component decisions tabs without component decisions`() { container.documentation.addDecision(createDecision("1", "Accepted")) val viewModel = SoftwareSystemContainerComponentDecisionsPageViewModel(generatorContext, component) assertThat(viewModel.componentDecisionsTabs) .isEqualTo(listOf( DecisionTabViewModel( viewModel, "Container", "/software-system/decisions/api-application" ) )) } @Test fun `decisions table`() { createComponentDecisions() val viewModel = SoftwareSystemContainerComponentDecisionsPageViewModel(generatorContext, component) var counter = 1 assertThat(viewModel.decisionsTable) .isEqualTo( viewModel.createDecisionsTableViewModel(component.documentation.decisions) { "/software-system/decisions/api-application/email-component/${counter++}" } ) } @Test fun `is visible`() { createComponentDecisions() val viewModel = SoftwareSystemContainerComponentDecisionsPageViewModel(generatorContext, component) assertThat(viewModel.visible).isTrue() } @Test fun `hidden view`() { val viewModel = SoftwareSystemContainerComponentDecisionsPageViewModel(generatorContext, component) assertThat(viewModel.visible).isFalse() } private fun createComponentDecisions() { component.documentation.addDecision(createDecision( "1", "Accepted" )) component.documentation.addDecision(createDecision( "2", "Superseded by [3. Another Realisation of Feature 1](0003-another-realisation-of-feature-1.md)" )) component.documentation.addDecision(createDecision( "3", "Accepted\n\nSupersedes [2. Implement Feature 1](0002-implement-feature-1.md)" )) } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentSectionPageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.isEqualTo import com.structurizr.documentation.Format import nl.avisi.structurizr.site.generatr.normalize import kotlin.test.Test class SoftwareSystemContainerComponentSectionPageViewModelTest : ViewModelTest() { private val generatorContext = generatorContext() private val softwareSystem = generatorContext.workspace.model.addSoftwareSystem("Software system").also { it.addContainer("API Application").apply { documentation.addSection(createSection()) addComponent("Some Component").apply { documentation.addSection(createSection()) } } } private val component = softwareSystem.containers.first().components.first() private val section = component.documentation.sections.first() @Test fun url() { val viewModel = SoftwareSystemContainerComponentSectionPageViewModel(generatorContext, component, section) assertThat(SoftwareSystemContainerComponentSectionPageViewModel.url(component, section)) .isEqualTo("/software-system/sections/api-application/some-component/content") assertThat(viewModel.url) .isEqualTo(SoftwareSystemContainerComponentSectionPageViewModel.url(component, section)) } @Test fun `active tab`() { val viewModel = SoftwareSystemContainerComponentSectionPageViewModel(generatorContext, component, section) assertThat(viewModel.tabs.single { it.link.active }.tab) .isEqualTo(SoftwareSystemPageViewModel.Tab.SECTIONS) } @Test fun content() { val viewModel = SoftwareSystemContainerComponentSectionPageViewModel(generatorContext, component, section) assertThat(viewModel.content).isEqualTo(toHtml(viewModel, section.content, Format.Markdown, svgFactory)) } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentSectionsPageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.all import assertk.assertThat import assertk.assertions.* import kotlin.test.BeforeTest import kotlin.test.Test class SoftwareSystemContainerComponentSectionsPageViewModelTest : ViewModelTest() { private val generatorContext = generatorContext() private val softwareSystem = generatorContext.workspace.model.addSoftwareSystem("Software system").also { it.documentation.addSection(createSection()) it.documentation.addSection(createSection()) it.addContainer("API Application").apply { documentation.addSection(createSection()) addComponent("Some Component").apply { documentation.addSection(createSection()) } } } private val component = softwareSystem.containers.first().components.first() @Test fun `active tab documentation`() { val viewModel = SoftwareSystemContainerComponentSectionsPageViewModel(generatorContext, component) assertThat(viewModel.tabs.single { it.link.active }.tab).isEqualTo(SoftwareSystemPageViewModel.Tab.SECTIONS) } @Test fun `active tab component`() { val viewModel = SoftwareSystemContainerComponentSectionsPageViewModel(generatorContext, component) assertThat(viewModel.sectionsTabs.single { it.link.active }.title).isEqualTo("API Application") assertThat(viewModel.sectionsTabs.single { it.title == "System" }.link.active).isFalse() } @Test fun `active tab component section`() { val viewModel = SoftwareSystemContainerComponentSectionsPageViewModel(generatorContext, component) assertThat(viewModel.componentSectionsTabs.single { it.link.active }.title).isEqualTo("Some Component") } @Test fun `component section table`() { val viewModel = SoftwareSystemContainerComponentSectionsPageViewModel(generatorContext, component) assertThat(viewModel.componentSectionTable.bodyRows).all { hasSize(1) index(0).transform { (it.columns[1] as TableViewModel.LinkCellViewModel).link } .isEqualTo( LinkViewModel( viewModel, "Content", "/software-system/sections/api-application/some-component/content" ) ) } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentsPageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.* import com.structurizr.model.Container import com.structurizr.model.SoftwareSystem import nl.avisi.structurizr.site.generatr.normalize import kotlin.test.Test import kotlin.test.assertTrue class SoftwareSystemContainerComponentsPageViewModelTest : ViewModelTest() { private val generatorContext = generatorContext() private val softwareSystem: SoftwareSystem = generatorContext.workspace.model .addSoftwareSystem("Software system").also { val backend = it.addContainer("Backend") val frontend = it.addContainer("Frontend") it.addContainer("Api").also { c -> c.addProperty("test", "value") } generatorContext.workspace.views.createComponentView(backend, "component-1-backend", "Component view 1 - Backend") generatorContext.workspace.views.createComponentView(backend, "component-2-backend", "Component view 2 - Backend") generatorContext.workspace.views.createComponentView(frontend, "component-1-frontend", "Component view 1 - Frontend") generatorContext.workspace.views.createComponentView(frontend, "component-2-frontend", "Component view 2 - Frontend") } private val backendContainer: Container = softwareSystem.containers.elementAt(0) private val frontendContainer: Container = softwareSystem.containers.elementAt(1) private val apiContainer: Container = softwareSystem.containers.elementAt(2) private val backendImageView = createImageView(generatorContext.workspace, backendContainer) private val frontendImageView = createImageView(generatorContext.workspace, frontendContainer) @Test fun `active tab`() { val viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, backendContainer) assertThat(viewModel.tabs.single { it.link.active }.tab) .isEqualTo(SoftwareSystemPageViewModel.Tab.COMPONENT) } @Test fun `container tabs`() { val viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, backendContainer) val componentTabList = listOf( ContainerTabViewModel(viewModel, "Api", "/software-system/component/api"), ContainerTabViewModel(viewModel, "Backend", "/software-system/component/backend"), ContainerTabViewModel(viewModel, "Frontend", "/software-system/component/frontend")) assertThat(viewModel.containerTabs.elementAtOrNull(0)).isEqualTo(componentTabList.elementAt(0)) assertThat(viewModel.containerTabs.elementAtOrNull(1)).isEqualTo(componentTabList.elementAt(1)) assertThat(viewModel.containerTabs.elementAtOrNull(2)).isEqualTo(componentTabList.elementAt(2)) } @Test fun url() { var viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, backendContainer) assertThat(SoftwareSystemContainerComponentsPageViewModel.url(backendContainer)) .isEqualTo("/${softwareSystem.name.normalize()}/component/${backendContainer.name.normalize()}") assertThat(viewModel.url) .isEqualTo(SoftwareSystemContainerComponentsPageViewModel.url(backendContainer)) viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, frontendContainer) assertThat(SoftwareSystemContainerComponentsPageViewModel.url(frontendContainer)) .isEqualTo("/${softwareSystem.name.normalize()}/component/${frontendContainer.name.normalize()}") assertThat(viewModel.url) .isEqualTo(SoftwareSystemContainerComponentsPageViewModel.url(frontendContainer)) } @Test fun `diagrams correctly mapped to containers`() { var viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, backendContainer) assertThat(viewModel.visible).isTrue() assertThat(viewModel.diagrams).containsExactly( DiagramViewModel( "component-1-backend", "Component View: Software system - Backend", "Component view 1 - Backend", """""", 800, ImageViewModel(viewModel, "/svg/component-1-backend.svg"), ImageViewModel(viewModel, "/png/component-1-backend.png"), ImageViewModel(viewModel, "/puml/component-1-backend.puml") ), DiagramViewModel( "component-2-backend", "Component View: Software system - Backend", "Component view 2 - Backend", """""", 800, ImageViewModel(viewModel, "/svg/component-2-backend.svg"), ImageViewModel(viewModel, "/png/component-2-backend.png"), ImageViewModel(viewModel, "/puml/component-2-backend.puml") ) ) viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, frontendContainer) assertThat(viewModel.visible).isTrue() assertThat(viewModel.diagrams).containsExactly( DiagramViewModel( "component-1-frontend", "Component View: Software system - Frontend", "Component view 1 - Frontend", """""", 800, ImageViewModel(viewModel, "/svg/component-1-frontend.svg"), ImageViewModel(viewModel, "/png/component-1-frontend.png"), ImageViewModel(viewModel, "/puml/component-1-frontend.puml") ), DiagramViewModel( "component-2-frontend", "Component View: Software system - Frontend", "Component view 2 - Frontend", """""", 800, ImageViewModel(viewModel, "/svg/component-2-frontend.svg"), ImageViewModel(viewModel, "/png/component-2-frontend.png"), ImageViewModel(viewModel, "/puml/component-2-frontend.puml") ) ) } @Test fun `has image`() { var viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, backendContainer) assertThat(viewModel.visible).isTrue() assertThat(viewModel.images).hasSize(1) assertThat(viewModel.images.single().imageView).isEqualTo(backendImageView) viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, frontendContainer) assertThat(viewModel.visible).isTrue() assertThat(viewModel.images).hasSize(1) assertThat(viewModel.images.single().imageView).isEqualTo(frontendImageView) } @Test fun `has only properties`() { val viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, apiContainer) assertTrue(viewModel.visible) assertThat(viewModel.propertiesTable.bodyRows).isNotEmpty() assertThat(viewModel.images).isEmpty() assertThat(viewModel.diagrams).isEmpty() } @Test fun `hidden view`() { val viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, softwareSystem.addContainer("Container")) assertThat(viewModel.visible).isFalse() } @Test fun `no index`() { val viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, apiContainer) assertThat(viewModel.diagramIndex.visible).isFalse() } @Test fun `has index`() { val viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, backendContainer) assertThat(viewModel.diagramIndex.visible).isTrue() } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerDecisionPageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.isEqualTo import com.structurizr.documentation.Format import nl.avisi.structurizr.site.generatr.normalize import kotlin.test.Test class SoftwareSystemContainerDecisionPageViewModelTest : ViewModelTest() { private val generatorContext = generatorContext() private val softwareSystem = generatorContext.workspace.model.addSoftwareSystem("Software System").also { it.addContainer("API Application") } private val container = softwareSystem.containers.first() @Test fun url() { val decision = createDecision() val viewModel = SoftwareSystemContainerDecisionPageViewModel(generatorContext, container, decision) assertThat(SoftwareSystemContainerDecisionPageViewModel.url(container, decision)) .isEqualTo("/${softwareSystem.name.normalize()}/decisions/${container.name.normalize()}/${decision.id}") assertThat(viewModel.url) .isEqualTo(SoftwareSystemContainerDecisionPageViewModel.url(container, decision)) } @Test fun `active tab`() { val viewModel = SoftwareSystemContainerDecisionPageViewModel(generatorContext, container, createDecision()) assertThat(viewModel.tabs.single { it.link.active }.tab) .isEqualTo(SoftwareSystemPageViewModel.Tab.DECISIONS) } @Test fun content() { val decision = createDecision() val viewModel = SoftwareSystemContainerDecisionPageViewModel(generatorContext, container, decision) assertThat(viewModel.content).isEqualTo(toHtml(viewModel, decision.content, Format.Markdown, svgFactory)) } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerDecisionsPageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isTrue import com.structurizr.model.Component import com.structurizr.model.Container import com.structurizr.model.SoftwareSystem import kotlin.test.Test class SoftwareSystemContainerDecisionsPageViewModelTest : ViewModelTest() { private val generatorContext = generatorContext() private val softwareSystem: SoftwareSystem = generatorContext.workspace.model.addSoftwareSystem("Software system") private val container: Container = softwareSystem.addContainer("API Application") private val component: Component = container.addComponent("Email Component") @Test fun `active tab`() { val viewModel = SoftwareSystemContainerDecisionsPageViewModel(generatorContext, container) assertThat(viewModel.tabs.single { it.link.active }.tab) .isEqualTo(SoftwareSystemPageViewModel.Tab.DECISIONS) } @Test fun `decisions tabs`() { softwareSystem.documentation.addDecision(createDecision("1", "Accepted")) container.documentation.addDecision(createDecision("1", "Accepted")) val viewModel = SoftwareSystemContainerDecisionsPageViewModel(generatorContext, container) assertThat(viewModel.decisionTabs) .isEqualTo(listOf( DecisionTabViewModel( viewModel, "System", "/software-system/decisions" ), DecisionTabViewModel( viewModel, "API Application", "/software-system/decisions/api-application", Match.CHILD ) )) } @Test fun `decisions tabs without container decisions`() { softwareSystem.documentation.addDecision(createDecision("1", "Accepted")) val viewModel = SoftwareSystemContainerDecisionsPageViewModel(generatorContext, container) assertThat(viewModel.decisionTabs) .isEqualTo(listOf( DecisionTabViewModel( viewModel, "System", "/software-system/decisions" ) )) } @Test fun `decisions tabs without software system decisions`() { container.documentation.addDecision(createDecision("1", "Accepted")) val viewModel = SoftwareSystemContainerDecisionsPageViewModel(generatorContext, container) assertThat(viewModel.decisionTabs) .isEqualTo(listOf( DecisionTabViewModel( viewModel, "API Application", "/software-system/decisions/api-application", Match.CHILD ) )) } @Test fun `component decisions tabs`() { container.documentation.addDecision(createDecision("1", "Accepted")) createComponentDecisions() val viewModel = SoftwareSystemContainerDecisionsPageViewModel(generatorContext, container) assertThat(viewModel.componentDecisionsTabs) .isEqualTo(listOf( DecisionTabViewModel( viewModel, "Container", "/software-system/decisions/api-application" ), DecisionTabViewModel( viewModel, "Email Component", "/software-system/decisions/api-application/email-component" ) )) } @Test fun `component decisions tabs without container decisions`() { createComponentDecisions() val viewModel = SoftwareSystemContainerDecisionsPageViewModel(generatorContext, container) assertThat(viewModel.componentDecisionsTabs) .isEqualTo(listOf( DecisionTabViewModel( viewModel, "Email Component", "/software-system/decisions/api-application/email-component" ) )) } @Test fun `component decisions tabs without component decisions`() { container.documentation.addDecision(createDecision("1", "Accepted")) val viewModel = SoftwareSystemContainerDecisionsPageViewModel(generatorContext, container) assertThat(viewModel.componentDecisionsTabs) .isEqualTo(listOf( DecisionTabViewModel( viewModel, "Container", "/software-system/decisions/api-application" ) )) } @Test fun `decisions table`() { container.documentation.addDecision(createDecision("1", "Accepted")) val viewModel = SoftwareSystemContainerDecisionsPageViewModel(generatorContext, container) assertThat(viewModel.decisionsTable) .isEqualTo( viewModel.createDecisionsTableViewModel(container.documentation.decisions) { "/software-system/decisions/api-application/1" } ) } @Test fun `is visible`() { container.documentation.addDecision(createDecision("1", "Accepted")) val viewModel = SoftwareSystemContainerDecisionsPageViewModel(generatorContext, container) assertThat(viewModel.visible).isTrue() assertThat(viewModel.onlyComponentDecisionsVisible).isFalse() } @Test fun `only component decisions visible`() { createComponentDecisions() val viewModel = SoftwareSystemContainerDecisionsPageViewModel(generatorContext, container) assertThat(viewModel.visible).isTrue() assertThat(viewModel.onlyComponentDecisionsVisible).isTrue() } @Test fun `hidden view`() { val viewModel = SoftwareSystemContainerDecisionsPageViewModel(generatorContext, container) assertThat(viewModel.visible).isFalse() assertThat(viewModel.onlyComponentDecisionsVisible).isFalse() } private fun createComponentDecisions() { component.documentation.addDecision(createDecision( "1", "Accepted" )) component.documentation.addDecision(createDecision( "2", "Superseded by [3. Another Realisation of Feature 1](0003-another-realisation-of-feature-1.md)" )) component.documentation.addDecision(createDecision( "3", "Accepted\n\nSupersedes [2. Implement Feature 1](0002-implement-feature-1.md)" )) } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerPageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.hasSize import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isTrue import com.structurizr.model.SoftwareSystem import kotlin.test.Test class SoftwareSystemContainerPageViewModelTest : ViewModelTest() { private val generatorContext = generatorContext() private val softwareSystem: SoftwareSystem = generatorContext.workspace.model .addSoftwareSystem("Software system").also { generatorContext.workspace.views.createContainerView(it, "container-1", "Container view 1") generatorContext.workspace.views.createContainerView(it, "container-2", "Container view 2") } private val imageView = createImageView(generatorContext.workspace, softwareSystem) @Test fun `active tab`() { val viewModel = SoftwareSystemContainerPageViewModel(generatorContext, softwareSystem) assertThat(viewModel.tabs.single { it.link.active }.tab) .isEqualTo(SoftwareSystemPageViewModel.Tab.CONTAINER) } @Test fun `diagrams sorted by key`() { val viewModel = SoftwareSystemContainerPageViewModel(generatorContext, softwareSystem) assertThat(viewModel.visible).isTrue() assertThat(viewModel.diagrams).containsExactly( DiagramViewModel( "container-1", "Container View: Software system", "Container view 1", """""", 800, ImageViewModel(viewModel, "/svg/container-1.svg"), ImageViewModel(viewModel, "/png/container-1.png"), ImageViewModel(viewModel, "/puml/container-1.puml") ), DiagramViewModel( "container-2", "Container View: Software system", "Container view 2", """""", 800, ImageViewModel(viewModel, "/svg/container-2.svg"), ImageViewModel(viewModel, "/png/container-2.png"), ImageViewModel(viewModel, "/puml/container-2.puml") ) ) } @Test fun `has image`() { val viewModel = SoftwareSystemContainerPageViewModel(generatorContext, softwareSystem) assertThat(viewModel.visible).isTrue() assertThat(viewModel.images).hasSize(1) assertThat(viewModel.images.single().imageView).isEqualTo(imageView) } @Test fun `hidden view`() { val viewModel = SoftwareSystemContainerPageViewModel( generatorContext, generatorContext.workspace.model.addSoftwareSystem("Software system 2") ) assertThat(viewModel.visible).isFalse() } @Test fun `no index`() { val viewModel = SoftwareSystemContainerPageViewModel( generatorContext, generatorContext.workspace.model.addSoftwareSystem("Software system 2").also { generatorContext.workspace.views.createContainerView(it, "container-3", "Container view 3") } ) assertThat(viewModel.diagramIndex.visible).isFalse() } @Test fun `has index`() { val viewModel = SoftwareSystemContainerPageViewModel(generatorContext, softwareSystem) assertThat(viewModel.diagramIndex.visible).isTrue() } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerSectionPageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.isEqualTo import com.structurizr.documentation.Format import nl.avisi.structurizr.site.generatr.normalize import kotlin.test.Test class SoftwareSystemContainerSectionPageViewModelTest : ViewModelTest() { private val generatorContext = generatorContext() private val softwareSystem = generatorContext.workspace.model.addSoftwareSystem("Software System").also { it.addContainer("API Application") } private val container = softwareSystem.containers.first() @Test fun url() { val section = createSection() val viewModel = SoftwareSystemContainerSectionPageViewModel(generatorContext, container, section) assertThat(SoftwareSystemContainerSectionPageViewModel.url(container, section)) .isEqualTo("/${softwareSystem.name.normalize()}/sections/${container.name.normalize()}/${section.contentTitle().normalize()}") assertThat(viewModel.url) .isEqualTo(SoftwareSystemContainerSectionPageViewModel.url(container, section)) } @Test fun `active tab`() { val viewModel = SoftwareSystemContainerSectionPageViewModel(generatorContext, container, createSection()) assertThat(viewModel.tabs.single { it.link.active }.tab) .isEqualTo(SoftwareSystemPageViewModel.Tab.SECTIONS) } @Test fun content() { val section = createSection() val viewModel = SoftwareSystemContainerSectionPageViewModel(generatorContext, container, section) assertThat(viewModel.content).isEqualTo(toHtml(viewModel, section.content, Format.Markdown, svgFactory)) } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerSectionsPageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.all import assertk.assertThat import assertk.assertions.* import kotlin.test.BeforeTest import kotlin.test.Test class SoftwareSystemContainerSectionsPageViewModelTest : ViewModelTest() { private val generatorContext = generatorContext() private val softwareSystem = generatorContext.workspace.model.addSoftwareSystem("Software System").apply { documentation.addSection(createSection()) documentation.addSection(createSection()) } private val container = softwareSystem.addContainer("API Application") @Test fun `active tab`() { val viewModel = SoftwareSystemContainerSectionsPageViewModel(generatorContext, container) assertThat(viewModel.tabs.single { it.link.active }.tab) .isEqualTo(SoftwareSystemPageViewModel.Tab.SECTIONS) } @Test fun `sections table`() { container.documentation.addSection(createSection()) val viewModel = SoftwareSystemContainerSectionsPageViewModel(generatorContext, container) assertThat(viewModel.sectionsTable.bodyRows).all { hasSize(1) index(0).transform { (it.columns[1] as TableViewModel.LinkCellViewModel).link } .isEqualTo( LinkViewModel( viewModel, "Content", "/software-system/sections/api-application/content" ) ) } } @Test fun `sections tabs`() { container.documentation.addSection(createSection()) val viewModel = SoftwareSystemContainerSectionsPageViewModel(generatorContext, container) assertThat(viewModel.sectionsTabs).all { hasSize(2) index(0).all { transform { it.link.active }.isFalse() transform { it.link.title }.isEqualTo("System") } index(1).all { transform { it.link.active }.isTrue() transform { it.link }.isEqualTo( LinkViewModel( viewModel, "API Application", "/software-system/sections/api-application", Match.CHILD ) ) } } } @Test fun `component sections tabs`() { container.documentation.addSection(createSection()) container.addComponent("Some Component").apply { documentation.addSection(createSection()) } val viewModel = SoftwareSystemContainerSectionsPageViewModel(generatorContext, container) assertThat(viewModel.componentSectionsTabs).all { hasSize(2) index(0).all { transform { it.link.active }.isTrue() transform { it.title }.isEqualTo("Container") } index(1).all { transform { it.link }.isEqualTo( LinkViewModel( viewModel, "Some Component", "/software-system/sections/api-application/some-component" ) ) } } } @Test fun `has sections`() { container.documentation.addSection(createSection()) val viewModel = SoftwareSystemContainerSectionsPageViewModel(generatorContext, container) assertThat(viewModel.visible).isTrue() assertThat(viewModel.onlyComponentsDocumentationSectionsVisible).isFalse() } @Test fun `no sections`() { val viewModel = SoftwareSystemContainerSectionsPageViewModel(generatorContext, container) assertThat(viewModel.visible).isFalse() assertThat(viewModel.onlyComponentsDocumentationSectionsVisible).isFalse() } @Test fun `child has section`() { container.addComponent("Email Component").documentation.addSection(createSection()) val viewModel = SoftwareSystemContainerSectionsPageViewModel(generatorContext, container) assertThat(viewModel.visible).isTrue() assertThat(viewModel.onlyComponentsDocumentationSectionsVisible).isTrue() } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContextPageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isFalse import assertk.assertions.isTrue import com.structurizr.model.SoftwareSystem import kotlin.test.Test class SoftwareSystemContextPageViewModelTest : ViewModelTest() { private val generatorContext = generatorContext() private val softwareSystem: SoftwareSystem = generatorContext.workspace.model .addSoftwareSystem("Software system").also { generatorContext.workspace.views.createSystemContextView(it, "context-1", "System context view 1") generatorContext.workspace.views.createSystemContextView(it, "context-2", "System context view 2") } @Test fun `diagrams sorted by key`() { val viewModel = SoftwareSystemContextPageViewModel(generatorContext, softwareSystem) assertThat(viewModel.diagrams).containsExactly( DiagramViewModel( "context-1", "System Context View: Software system", "System context view 1", """""", 800, ImageViewModel(viewModel, "/svg/context-1.svg"), ImageViewModel(viewModel, "/png/context-1.png"), ImageViewModel(viewModel, "/puml/context-1.puml") ), DiagramViewModel( "context-2", "System Context View: Software system", "System context view 2", """""", 800, ImageViewModel(viewModel, "/svg/context-2.svg"), ImageViewModel(viewModel, "/png/context-2.png"), ImageViewModel(viewModel, "/puml/context-2.puml") ) ) } @Test fun `hidden view`() { val viewModel = SoftwareSystemContextPageViewModel( generatorContext, generatorContext.workspace.model.addSoftwareSystem("Software system 2") ) assertThat(viewModel.visible).isFalse() } @Test fun `no index`() { val viewModel = SoftwareSystemContextPageViewModel(generatorContext, generatorContext.workspace.model .addSoftwareSystem("Software system 2").also { generatorContext.workspace.views.createSystemContextView(it, "context-3", "System context view 3") }) assertThat(viewModel.diagramIndex.visible).isFalse() } @Test fun `has index`() { val viewModel = SoftwareSystemContextPageViewModel(generatorContext, softwareSystem) assertThat(viewModel.diagramIndex.visible).isTrue() } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDecisionPageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.isEqualTo import com.structurizr.documentation.Format import nl.avisi.structurizr.site.generatr.normalize import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemPageViewModel.Companion.url import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemPageViewModel.Tab import kotlin.test.Test class SoftwareSystemDecisionPageViewModelTest : ViewModelTest() { private val generatorContext = generatorContext() private val softwareSystem = generatorContext.workspace.model.addSoftwareSystem("Software System") @Test fun url() { val decision = createDecision() val viewModel = SoftwareSystemDecisionPageViewModel(generatorContext, softwareSystem, decision) assertThat(SoftwareSystemDecisionPageViewModel.url(softwareSystem, decision)) .isEqualTo("/${softwareSystem.name.normalize()}/decisions/${decision.id}") assertThat(viewModel.url) .isEqualTo(SoftwareSystemDecisionPageViewModel.url(softwareSystem, decision)) } @Test fun `active tab`() { val viewModel = SoftwareSystemDecisionPageViewModel(generatorContext, softwareSystem, createDecision()) assertThat(viewModel.tabs.single { it.link.active }.tab) .isEqualTo(Tab.DECISIONS) } @Test fun content() { val decision = createDecision() val viewModel = SoftwareSystemDecisionPageViewModel(generatorContext, softwareSystem, decision) assertThat(viewModel.content).isEqualTo(toHtml(viewModel, decision.content, Format.Markdown, svgFactory)) } @Test fun `link to other ADR`() { val decision = createDecision().apply { content = """ Decision with [link to other ADR](#2). [Web link](https://google.com) [Internal link](#other-section) """.trimIndent() } val viewModel = SoftwareSystemDecisionPageViewModel(generatorContext, softwareSystem, decision) assertThat(viewModel.content).isEqualTo( toHtml( viewModel, """ Decision with [link to other ADR](${url(softwareSystem, Tab.DECISIONS)}/2). [Web link](https://google.com) [Internal link](#other-section) """.trimIndent(), Format.Markdown, svgFactory ) ) } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDecisionsPageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isTrue import com.structurizr.model.Container import com.structurizr.model.SoftwareSystem import kotlin.test.Test class SoftwareSystemDecisionsPageViewModelTest : ViewModelTest() { private val generatorContext = generatorContext() private val softwareSystem: SoftwareSystem = generatorContext.workspace.model.addSoftwareSystem("Software system") private val container: Container = softwareSystem.addContainer("API Application") @Test fun `active tab`() { val viewModel = SoftwareSystemDecisionsPageViewModel(generatorContext, softwareSystem) assertThat(viewModel.tabs.single { it.link.active }.tab) .isEqualTo(SoftwareSystemPageViewModel.Tab.DECISIONS) } @Test fun `decisions tabs`() { softwareSystem.documentation.addDecision(createDecision("1", "Accepted")) container.documentation.addDecision(createDecision("1", "Accepted")) val viewModel = SoftwareSystemDecisionsPageViewModel(generatorContext, softwareSystem) assertThat(viewModel.decisionTabs) .isEqualTo(listOf( DecisionTabViewModel( viewModel, "System", "/software-system/decisions" ), DecisionTabViewModel( viewModel, "API Application", "/software-system/decisions/api-application", Match.CHILD ) )) } @Test fun `decisions tabs without container decisions`() { softwareSystem.documentation.addDecision(createDecision("1", "Accepted")) val viewModel = SoftwareSystemDecisionsPageViewModel(generatorContext, softwareSystem) assertThat(viewModel.decisionTabs) .isEqualTo(listOf( DecisionTabViewModel( viewModel, "System", "/software-system/decisions" ) )) } @Test fun `decisions tabs without software system decisions`() { container.documentation.addDecision(createDecision("1", "Accepted")) val viewModel = SoftwareSystemDecisionsPageViewModel(generatorContext, softwareSystem) assertThat(viewModel.decisionTabs) .isEqualTo(listOf( DecisionTabViewModel( viewModel, "API Application", "/software-system/decisions/api-application", Match.CHILD ) )) } @Test fun `decisions table`() { softwareSystem.documentation.addDecision(createDecision("1", "Accepted")) val viewModel = SoftwareSystemDecisionsPageViewModel(generatorContext, softwareSystem) assertThat(viewModel.decisionsTable) .isEqualTo( viewModel.createDecisionsTableViewModel(softwareSystem.documentation.decisions) { "/software-system/decisions/1" } ) } @Test fun `is visible`() { softwareSystem.documentation.addDecision(createDecision("1", "Accepted")) val viewModel = SoftwareSystemDecisionsPageViewModel(generatorContext, softwareSystem) assertThat(viewModel.visible).isTrue() assertThat(viewModel.onlyContainersDecisionsVisible).isFalse() } @Test fun `only container decisions visible`() { container.documentation.addDecision(createDecision("1", "Accepted")) val viewModel = SoftwareSystemDecisionsPageViewModel(generatorContext, softwareSystem) assertThat(viewModel.visible).isTrue() assertThat(viewModel.onlyContainersDecisionsVisible).isTrue() } @Test fun `hidden view`() { val viewModel = SoftwareSystemDecisionsPageViewModel(generatorContext, softwareSystem) assertThat(viewModel.visible).isFalse() assertThat(viewModel.onlyContainersDecisionsVisible).isFalse() } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDependenciesPageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.* import kotlin.test.Test class SoftwareSystemDependenciesPageViewModelTest : ViewModelTest() { private val generatorContext = generatorContext() private val softwareSystem1 = generatorContext.workspace.model.addSoftwareSystem("Software system 1") private val softwareSystem2 = generatorContext.workspace.model.addSoftwareSystem("Software system 2") @Test fun `active tab`() { val viewModel = SoftwareSystemDependenciesPageViewModel(generatorContext, softwareSystem1) assertThat(viewModel.tabs.single { it.link.active }.tab) .isEqualTo(SoftwareSystemPageViewModel.Tab.DEPENDENCIES) } @Test fun `empty table when no dependencies are present`() { val viewModel = SoftwareSystemDependenciesPageViewModel(generatorContext, softwareSystem1) assertThat(viewModel.dependenciesInboundTable).isEqualTo( TableViewModel.create { dependenciesTableHeader() } ) } @Test fun `show outbound dependencies in table when present`() { softwareSystem1.uses(softwareSystem2, "Uses SOAP", "SOAP") val viewModel = SoftwareSystemDependenciesPageViewModel(generatorContext, softwareSystem1) assertThat(viewModel.dependenciesOutboundTable).isEqualTo( TableViewModel.create { dependenciesTableHeader() bodyRow( cellWithSoftwareSystemLink( viewModel, softwareSystem2.name, SoftwareSystemPageViewModel.url(softwareSystem2, SoftwareSystemPageViewModel.Tab.HOME) ), cell("Uses SOAP"), cell("SOAP"), ) } ) } @Test fun `show inbound dependencies in table when present`() { softwareSystem2.uses(softwareSystem1, "Uses REST", "REST") val viewModel = SoftwareSystemDependenciesPageViewModel(generatorContext, softwareSystem1) assertThat(viewModel.dependenciesInboundTable).isEqualTo( TableViewModel.create { dependenciesTableHeader() bodyRow( cellWithSoftwareSystemLink( viewModel, softwareSystem2.name, SoftwareSystemPageViewModel.url(softwareSystem2, SoftwareSystemPageViewModel.Tab.HOME) ), cell("Uses REST"), cell("REST"), ) } ) } @Test fun `only relationships from and to software systems`() { val backend1 = softwareSystem1.addContainer("Backend 1") val backend2 = softwareSystem2.addContainer("Backend 2") softwareSystem1.uses(softwareSystem2, "Uses REST", "REST") softwareSystem2.uses(softwareSystem1, "Uses SOAP", "SOAP") backend1.uses(softwareSystem2, "Uses from container 1 to system 2") backend2.uses(softwareSystem1, "Uses from container 2 to system 1", "REST") softwareSystem1.uses(backend2, "Uses from system 1 to container 2", "REST") val viewModel = SoftwareSystemDependenciesPageViewModel(generatorContext, softwareSystem1) // Inbound Table assertThat(viewModel.dependenciesInboundTable.bodyRows.extractTitle()) .containsExactly("Software system 2") // Outbound Table assertThat(viewModel.dependenciesOutboundTable.bodyRows.extractTitle()) .containsExactly("Software system 2") } @Test fun `dependencies from and to external systems (declared external by tag)`() { generatorContext.workspace.views.configuration.addProperty("generatr.site.externalTag", "External System") val externalSystem = generatorContext.workspace.model.addSoftwareSystem("External system").apply { addTags("External System") } externalSystem.uses(softwareSystem1, "Uses", "REST") softwareSystem1.uses(externalSystem, "Uses", "REST") val viewModel = SoftwareSystemDependenciesPageViewModel(generatorContext, softwareSystem1) assertThat(viewModel.dependenciesInboundTable.bodyRows[0].columns[0]) .isEqualTo(TableViewModel.TextCellViewModel("External system (External)", isHeader = false, greyText = true, boldText = true)) assertThat(viewModel.dependenciesOutboundTable.bodyRows[0].columns[0]) .isEqualTo(TableViewModel.TextCellViewModel("External system (External)", isHeader = false, greyText = true, boldText = true)) } @Test fun `sort by source system name case insensitive`() { val system = generatorContext.workspace.model.addSoftwareSystem("Software system 3") system.uses(softwareSystem1, "Uses", "REST") softwareSystem1.uses(softwareSystem2, "Uses REST", "REST") softwareSystem2.uses(softwareSystem1, "Uses SOAP", "SOAP") val viewModel = SoftwareSystemDependenciesPageViewModel(generatorContext, softwareSystem1) // Inbound Table assertThat(viewModel.dependenciesInboundTable.bodyRows.extractTitle()) .containsExactly("Software system 2", "Software system 3") // Outbound Table assertThat(viewModel.dependenciesInboundTable.bodyRows.extractTitle()) .containsExactly("Software system 2", "Software system 3") } private fun TableViewModel.TableViewInitializerContext.dependenciesTableHeader() { headerRow( headerCellMedium("System"), headerCellLarge("Description"), headerCell("Technology"), ) } private fun List.extractTitle() = map { when (val source = it.columns[0]) { is TableViewModel.TextCellViewModel -> source.title is TableViewModel.LinkCellViewModel -> source.link.title is TableViewModel.ExternalLinkCellViewModel -> source.link.title } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDeploymentPageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.* import com.structurizr.model.SoftwareSystem import kotlin.test.Test class SoftwareSystemDeploymentPageViewModelTest : ViewModelTest() { private val generatorContext = generatorContext() private val softwareSystem: SoftwareSystem = generatorContext.workspace.model .addSoftwareSystem("Software system").also { generatorContext.workspace.views.createDeploymentView(it, "deployment-1", "Deployment view 1") generatorContext.workspace.views.createDeploymentView(it, "deployment-2", "Deployment view 2") } @Test fun `active tab`() { val viewModel = SoftwareSystemDeploymentPageViewModel(generatorContext, softwareSystem) assertThat(viewModel.tabs.single { it.link.active }.tab) .isEqualTo(SoftwareSystemPageViewModel.Tab.DEPLOYMENT) } @Test fun `diagrams sorted by key`() { val viewModel = SoftwareSystemDeploymentPageViewModel(generatorContext, softwareSystem) assertThat(viewModel.diagrams).containsExactly( DiagramViewModel( "deployment-1", "Deployment View: Software system - Default", "Deployment view 1", """""", 800, ImageViewModel(viewModel, "/svg/deployment-1.svg"), ImageViewModel(viewModel, "/png/deployment-1.png"), ImageViewModel(viewModel, "/puml/deployment-1.puml") ), DiagramViewModel( "deployment-2", "Deployment View: Software system - Default", "Deployment view 2", """""", 800, ImageViewModel(viewModel, "/svg/deployment-2.svg"), ImageViewModel(viewModel, "/png/deployment-2.png"), ImageViewModel(viewModel, "/puml/deployment-2.puml") ) ) } @Test fun `hidden view`() { val viewModel = SoftwareSystemDeploymentPageViewModel( generatorContext, generatorContext.workspace.model.addSoftwareSystem("Software system 2") ) assertThat(viewModel.visible).isFalse() } @Test fun `no index`() { val viewModel = SoftwareSystemDeploymentPageViewModel(generatorContext, generatorContext.workspace.model .addSoftwareSystem("Software system 2").also { generatorContext.workspace.views.createDeploymentView(it, "deployment-3", "Deployment view 3") }) assertThat(viewModel.diagramIndex.visible).isFalse() } @Test fun `has index`() { val viewModel = SoftwareSystemDeploymentPageViewModel(generatorContext, softwareSystem) assertThat(viewModel.diagramIndex.visible).isTrue() } @Test fun `includes star-scoped deployment view when belongsTo in properties`() { val viewModel = SoftwareSystemDeploymentPageViewModel( generatorContext, generatorContext.workspace.model.addSoftwareSystem("Software system 3").also { generatorContext.workspace.views.createDeploymentView("star-scoped", "Star Scoped Deployment View").apply { addProperty("generatr.view.deployment.belongsTo", it.name) } }) assertThat(viewModel.visible, "is visible").isTrue() assertThat(viewModel.diagrams).exactly(1) { assert -> assert.transform { it.key }.isEqualTo("star-scoped") } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDynamicPageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isTrue import com.structurizr.model.SoftwareSystem import kotlin.test.Test class SoftwareSystemDynamicPageViewModelTest : ViewModelTest() { private val generatorContext = generatorContext() private val softwareSystem: SoftwareSystem = generatorContext.workspace.model .addSoftwareSystem("Software system").also { val backend = it.addContainer("Backend") val frontend = it.addContainer("Frontend") generatorContext.workspace.views.createDynamicView(backend, "backend-dynamic", "Dynamic view 1") generatorContext.workspace.views.createDynamicView(frontend, "frontend-dynamic", "Dynamic view 2") } @Test fun `active tab`() { val viewModel = SoftwareSystemDynamicPageViewModel(generatorContext, softwareSystem) assertThat(viewModel.tabs.single { it.link.active }.tab) .isEqualTo(SoftwareSystemPageViewModel.Tab.DYNAMIC) } @Test fun `diagrams sorted by key`() { val viewModel = SoftwareSystemDynamicPageViewModel(generatorContext, softwareSystem) assertThat(viewModel.diagrams).containsExactly( DiagramViewModel( "backend-dynamic", "Dynamic View: Software system - Backend", "Dynamic view 1", """""", 800, ImageViewModel(viewModel, "/svg/backend-dynamic.svg"), ImageViewModel(viewModel, "/png/backend-dynamic.png"), ImageViewModel(viewModel, "/puml/backend-dynamic.puml") ), DiagramViewModel( "frontend-dynamic", "Dynamic View: Software system - Frontend", "Dynamic view 2", """""", 800, ImageViewModel(viewModel, "/svg/frontend-dynamic.svg"), ImageViewModel(viewModel, "/png/frontend-dynamic.png"), ImageViewModel(viewModel, "/puml/frontend-dynamic.puml") ) ) } @Test fun `hidden view`() { val viewModel = SoftwareSystemDynamicPageViewModel( generatorContext, generatorContext.workspace.model.addSoftwareSystem("Software system 2") ) assertThat(viewModel.visible).isFalse() } @Test fun `no index`() { val viewModel = SoftwareSystemDynamicPageViewModel(generatorContext, generatorContext.workspace.model .addSoftwareSystem("Software system 2").also { val backend = it.addContainer("Api") generatorContext.workspace.views.createDynamicView(backend, "api-dynamic", "Dynamic view 3") }) assertThat(viewModel.diagramIndex.visible).isFalse() } @Test fun `has index`() { val viewModel = SoftwareSystemDynamicPageViewModel(generatorContext, softwareSystem) assertThat(viewModel.diagramIndex.visible).isTrue() } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemHomePageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.all import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.prop import com.structurizr.documentation.Format import com.structurizr.model.SoftwareSystem import kotlin.test.Test class SoftwareSystemHomePageViewModelTest : ViewModelTest() { @Test fun `active tab`() { val generatorContext = generatorContext() val softwareSystem: SoftwareSystem = generatorContext.workspace.model.addSoftwareSystem("Software system") val viewModel = SoftwareSystemHomePageViewModel(generatorContext, softwareSystem) assertThat(viewModel.tabs.single { it.link.active }.tab) .isEqualTo(SoftwareSystemPageViewModel.Tab.HOME) } @Test fun `no section present`() { val generatorContext = generatorContext() val softwareSystem: SoftwareSystem = generatorContext.workspace.model.addSoftwareSystem("Software system") .apply { description = "It's a system." } val viewModel = SoftwareSystemHomePageViewModel(generatorContext, softwareSystem) assertThat(viewModel.content) .isEqualTo( toHtml( viewModel, "# Description${System.lineSeparator()}${softwareSystem.description}", Format.Markdown, svgFactory ) ) } @Test fun `section present`() { val generatorContext = generatorContext() val softwareSystem: SoftwareSystem = generatorContext.workspace.model.addSoftwareSystem("Software system") .apply { documentation.addSection(createSection("# Title")) } val viewModel = SoftwareSystemHomePageViewModel(generatorContext, softwareSystem) assertThat(viewModel.content) .isEqualTo( toHtml(viewModel, softwareSystem.documentation.sections.single().content, Format.Markdown, svgFactory) ) } @Test fun `no properties present`() { val generatorContext = generatorContext() val softwareSystem: SoftwareSystem = generatorContext.workspace.model.addSoftwareSystem("Software system") .apply { description = "It's a system." } val viewModel = SoftwareSystemHomePageViewModel(generatorContext, softwareSystem) assertThat(viewModel) .all { prop(SoftwareSystemHomePageViewModel::hasProperties).isEqualTo(false) prop(SoftwareSystemHomePageViewModel::propertiesTable) .isEqualTo(createPropertiesTableViewModel(mapOf())) } } @Test fun `properties present`() { val generatorContext = generatorContext() val softwareSystem: SoftwareSystem = generatorContext.workspace.model.addSoftwareSystem("Software system") .apply { description = "It's a system." addProperty("Url", "https://tempuri.org/") } val viewModel = SoftwareSystemHomePageViewModel(generatorContext, softwareSystem) assertThat(viewModel) .all { prop(SoftwareSystemHomePageViewModel::hasProperties).isEqualTo(true) prop(SoftwareSystemHomePageViewModel::propertiesTable) .isEqualTo(createPropertiesTableViewModel(softwareSystem.properties)) } } @Test fun `internal properties present`() { val generatorContext = generatorContext() val softwareSystem: SoftwareSystem = generatorContext.workspace.model.addSoftwareSystem("Software system") .apply { description = "It's a system." addProperty("structurizr.dsl.identifier", "id") } val viewModel = SoftwareSystemHomePageViewModel(generatorContext, softwareSystem) assertThat(viewModel) .all { prop(SoftwareSystemHomePageViewModel::hasProperties).isEqualTo(false) prop(SoftwareSystemHomePageViewModel::propertiesTable) .isEqualTo(createPropertiesTableViewModel(mapOf())) } } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemPageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.* import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemPageViewModel.Tab import org.junit.jupiter.api.DynamicTest import org.junit.jupiter.api.TestFactory import kotlin.test.Test class SoftwareSystemPageViewModelTest : ViewModelTest() { @TestFactory fun url() = listOf( Tab.HOME to "/software-system-with-1-name", Tab.SYSTEM_CONTEXT to "/software-system-with-1-name/context", Tab.CONTAINER to "/software-system-with-1-name/container", Tab.COMPONENT to "/software-system-with-1-name/component", Tab.DEPLOYMENT to "/software-system-with-1-name/deployment", Tab.DEPENDENCIES to "/software-system-with-1-name/dependencies", Tab.DECISIONS to "/software-system-with-1-name/decisions", ).map { (tab, expectedUrl) -> DynamicTest.dynamicTest("url - $tab") { val generatorContext = generatorContext() val softwareSystem = generatorContext.workspace.model.addSoftwareSystem("Software system With 1 name") assertThat(SoftwareSystemPageViewModel.url(softwareSystem, tab)) .isEqualTo(expectedUrl) assertThat(SoftwareSystemPageViewModel(generatorContext, softwareSystem, tab).url) .isEqualTo(SoftwareSystemPageViewModel.url(softwareSystem, tab)) } } @TestFactory fun subtitle() = Tab.entries.map { tab -> DynamicTest.dynamicTest("subtitle - $tab") { val generatorContext = generatorContext() val softwareSystem = generatorContext.workspace.model.addSoftwareSystem("Some software system") assertThat(SoftwareSystemPageViewModel(generatorContext, softwareSystem, tab).pageSubTitle) .isEqualTo("Some software system") } } @TestFactory fun `active tab`() = Tab.entries .filter { it != Tab.COMPONENT && it != Tab.CODE } // Component & code links are dynamic .map { tab -> DynamicTest.dynamicTest("active tab - $tab") { val generatorContext = generatorContext() val softwareSystem = generatorContext.workspace.model.addSoftwareSystem("Some software system") val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, tab) assertThat( viewModel.tabs .single { it.link.active } .tab ).isEqualTo(tab) } } private val generatorContext = generatorContext() private val softwareSystem = generatorContext.workspace.model.addSoftwareSystem("Some Software system", "Description") @Test fun `software system description`() { val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME) assertThat(viewModel.description).isEqualTo("Description") } @Test fun tabs() { val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME) assertThat(viewModel.tabs.map { it.tab }) .containsExactly( Tab.HOME, Tab.SYSTEM_CONTEXT, Tab.CONTAINER, Tab.COMPONENT, Tab.CODE, Tab.DYNAMIC, Tab.DEPLOYMENT, Tab.DEPENDENCIES, Tab.DECISIONS, Tab.SECTIONS ) assertThat(viewModel.tabs.map { it.link.title }) .containsExactly( "Info", "Context views", "Container views", "Component views", "Code views", "Dynamic views", "Deployment views", "Dependencies", "Decisions", "Documentation" ) } @Test fun `home tab is visible`() { val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME) assertThat(getTab(viewModel, Tab.HOME).visible).isTrue() } @Test fun `dependencies tab is visible`() { val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME) assertThat(getTab(viewModel, Tab.DEPENDENCIES).visible).isTrue() } @Test fun `context views tab only visible when context diagrams available`() { val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME) assertThat(getTab(viewModel, Tab.SYSTEM_CONTEXT).visible).isFalse() generatorContext.workspace.views.createSystemContextView(softwareSystem, "context", "description") assertThat(getTab(viewModel, Tab.SYSTEM_CONTEXT).visible).isTrue() } @Test fun `container views tab only visible when container diagrams available`() { val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME) assertThat(getTab(viewModel, Tab.CONTAINER).visible).isFalse() generatorContext.workspace.views.createContainerView(softwareSystem, "container", "description") assertThat(getTab(viewModel, Tab.CONTAINER).visible).isTrue() } @Test fun `component views tab only visible when component diagrams available`() { val container = softwareSystem.addContainer("Backend") val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME) assertThat(getTab(viewModel, Tab.COMPONENT).visible).isFalse() generatorContext.workspace.views.createComponentView(container, "component", "description") assertThat(getTab(viewModel, Tab.COMPONENT).visible).isTrue() } @Test fun `code views tab only visible when component diagrams available and component diagram has image view`() { val container = softwareSystem.addContainer("Backend") val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME) assertThat(getTab(viewModel, Tab.CODE).visible).isFalse() val component = container.addComponent("Backend Component") generatorContext.workspace.views.createComponentView(container, "component", "description") createImageView(generatorContext.workspace, component) assertThat(getTab(viewModel, Tab.CODE).visible).isTrue() } @Test fun `dynamic views tab only visible when dynamic diagrams available`() { val container = softwareSystem.addContainer("Backend") val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME) assertThat(getTab(viewModel, Tab.DYNAMIC).visible).isFalse() generatorContext.workspace.views.createDynamicView(container, "component", "description") assertThat(getTab(viewModel, Tab.DYNAMIC).visible).isTrue() } @Test fun `deployment views tab only visible when deployment diagrams available`() { val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME) assertThat(getTab(viewModel, Tab.DEPLOYMENT).visible).isFalse() generatorContext.workspace.views.createDeploymentView(softwareSystem, "deployment", "description") assertThat(getTab(viewModel, Tab.DEPLOYMENT).visible).isTrue() } @Test fun `decisions views tab visible when software system decisions available`() { val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME) assertThat(getTab(viewModel, Tab.DECISIONS).visible).isFalse() softwareSystem.documentation.addDecision(createDecision("1", "Proposed")) assertThat(getTab(viewModel, Tab.DECISIONS).visible).isTrue() } @Test fun `decisions views tab visible when component decisions available in software system`() { val container = softwareSystem.addContainer("Some Container") container.addComponent("Some component").documentation.addDecision(createDecision("2", "Proposed")) val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME) assertThat(getTab(viewModel, Tab.DECISIONS).visible).isTrue() } @Test fun `decisions views tab visible when container decisions available in software system`() { val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME) assertThat(getTab(viewModel, Tab.DECISIONS).visible).isFalse() softwareSystem.addContainer("Some Container").documentation.addDecision(createDecision("2", "Proposed")) assertThat(getTab(viewModel, Tab.DECISIONS).visible).isTrue() } @Test fun `decisions views tab visible when container & software system decisions are available`() { val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME) assertThat(getTab(viewModel, Tab.DECISIONS).visible).isFalse() softwareSystem.addContainer("Some Container").documentation.addDecision(createDecision("2", "Proposed")) softwareSystem.documentation.addDecision(createDecision("1", "Proposed")) assertThat(getTab(viewModel, Tab.DECISIONS).visible).isTrue() } @Test fun `sections views tab only visible when two or more sections available`() { val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME) assertThat(getTab(viewModel, Tab.SECTIONS).visible).isFalse() softwareSystem.documentation.addSection(createSection("# Section 0000")) assertThat(getTab(viewModel, Tab.SECTIONS).visible).isFalse() softwareSystem.documentation.addSection(createSection("# Section 0001")) assertThat(getTab(viewModel, Tab.SECTIONS).visible).isTrue() } @Test fun `sections views tab visible when container sections available in software system`() { val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME) assertThat(getTab(viewModel, Tab.SECTIONS).visible).isFalse() softwareSystem.addContainer("Some Container").documentation.addSection(createSection()) assertThat(getTab(viewModel, Tab.SECTIONS).visible).isTrue() } @Test fun `sections views tab visible when container & software system sections are available`() { val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME) assertThat(getTab(viewModel, Tab.SECTIONS).visible).isFalse() softwareSystem.addContainer("Some Container").documentation.addSection(createSection()) repeat(2) { // the first section is shown in the software system home softwareSystem.documentation.addSection(createSection()) } assertThat(getTab(viewModel, Tab.SECTIONS).visible).isTrue() } private fun getTab(viewModel: SoftwareSystemPageViewModel, tab: Tab) = viewModel.tabs.single { it.tab == tab } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemSectionPageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.isEqualTo import com.structurizr.documentation.Format import nl.avisi.structurizr.site.generatr.normalize import kotlin.test.Test class SoftwareSystemSectionPageViewModelTest : ViewModelTest() { private val generatorContext = generatorContext() private val softwareSystem = generatorContext.workspace.model.addSoftwareSystem("Software System") @Test fun url() { val section = createSection() val viewModel = SoftwareSystemSectionPageViewModel(generatorContext, softwareSystem, section) assertThat(SoftwareSystemSectionPageViewModel.url(softwareSystem, section)) .isEqualTo("/${softwareSystem.name.normalize()}/sections/${section.contentTitle().normalize()}") assertThat(viewModel.url) .isEqualTo(SoftwareSystemSectionPageViewModel.url(softwareSystem, section)) } @Test fun `active tab`() { val viewModel = SoftwareSystemSectionPageViewModel(generatorContext, softwareSystem, createSection()) assertThat(viewModel.tabs.single { it.link.active }.tab) .isEqualTo(SoftwareSystemPageViewModel.Tab.SECTIONS) } @Test fun content() { val section = createSection() val viewModel = SoftwareSystemSectionPageViewModel(generatorContext, softwareSystem, section) assertThat(viewModel.content).isEqualTo(toHtml(viewModel, section.content, Format.Markdown, svgFactory)) } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemSectionsPageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.all import assertk.assertThat import assertk.assertions.hasSize import assertk.assertions.index import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isTrue import nl.avisi.structurizr.site.generatr.normalize import kotlin.test.Test class SoftwareSystemSectionsPageViewModelTest : ViewModelTest() { private val generatorContext = generatorContext() private val softwareSystem = generatorContext.workspace.model.addSoftwareSystem("Software system") @Test fun `active tab`() { val viewModel = SoftwareSystemSectionsPageViewModel(generatorContext, softwareSystem) assertThat(viewModel.tabs.single { it.link.active }.tab) .isEqualTo(SoftwareSystemPageViewModel.Tab.SECTIONS) } @Test fun `sections table`() { listOf("# Section 0000", "# Section 0001") .forEach { softwareSystem.documentation.addSection(createSection(it)) } val viewModel = SoftwareSystemSectionsPageViewModel(generatorContext, softwareSystem) assertThat(viewModel.sectionsTable.bodyRows).all { hasSize(1) index(0).transform { (it.columns[1] as TableViewModel.LinkCellViewModel).link } .isEqualTo( LinkViewModel( viewModel, "Section 0001", "/software-system/sections/section-0001" ) ) } } @Test fun `has sections`() { listOf("# Section 0000", "# Section 0001") .forEach { softwareSystem.documentation.addSection(createSection(it)) } val viewModel = SoftwareSystemSectionsPageViewModel(generatorContext, softwareSystem) assertThat(viewModel.visible).isTrue() assertThat(viewModel.onlyContainersDocumentationSectionsVisible).isFalse() } @Test fun `no sections`() { val viewModel = SoftwareSystemSectionsPageViewModel( generatorContext, generatorContext.workspace.model.addSoftwareSystem("Software system 2") ) assertThat(viewModel.visible).isFalse() assertThat(viewModel.onlyContainersDocumentationSectionsVisible).isFalse() } @Test fun `child has section`() { val container = softwareSystem.addContainer("API Application") container.documentation.addSection(createSection()) val viewModel = SoftwareSystemSectionsPageViewModel(generatorContext, softwareSystem) assertThat(viewModel.visible).isTrue() assertThat(viewModel.onlyContainersDocumentationSectionsVisible).isTrue() } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemsPageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isEqualTo import kotlin.test.Test class SoftwareSystemsPageViewModelTest : ViewModelTest() { @Test fun url() { assertThat(SoftwareSystemsPageViewModel.url()) .isEqualTo("/software-systems") assertThat(SoftwareSystemsPageViewModel(generatorContext()).url) .isEqualTo(SoftwareSystemsPageViewModel.url()) } @Test fun subtitle() { assertThat(SoftwareSystemsPageViewModel(generatorContext()).pageSubTitle) .isEqualTo("Software Systems") } @Test fun `software systems table`() { val generatorContext = generatorContext() val system1 = generatorContext.workspace.model.addSoftwareSystem("System 1", "System 1 description") val system2 = generatorContext.workspace.model.addSoftwareSystem("System 2", "System 2 description") val viewModel = SoftwareSystemsPageViewModel(generatorContext) assertThat(viewModel.softwareSystemsTable).isEqualTo( TableViewModel.create { headerRow(headerCellMedium("Name"), headerCell("Description")) bodyRow( cellWithSoftwareSystemLink( viewModel, system1.name, SoftwareSystemPageViewModel.url( system1, SoftwareSystemPageViewModel.Tab.HOME ) ), cell(system1.description) ) bodyRow( cellWithSoftwareSystemLink( viewModel, system2.name, SoftwareSystemPageViewModel.url( system2, SoftwareSystemPageViewModel.Tab.HOME ) ), cell(system2.description) ) } ) } @Test fun `software systems table sorted by name - case insensitive`() { val generatorContext = generatorContext() val system1 = generatorContext.workspace.model.addSoftwareSystem("system 1", "System 1 description") val system2 = generatorContext.workspace.model.addSoftwareSystem("System 2", "System 2 description") val viewModel = SoftwareSystemsPageViewModel(generatorContext) val names = viewModel.softwareSystemsTable.bodyRows .map { it.columns[0] } .filterIsInstance() .map { it.link.title } assertThat(names).containsExactly(system1.name, system2.name) } @Test fun `external systems have grey text (declared external by tag)`() { val generatorContext = generatorContext() generatorContext.workspace.views.configuration.addProperty("generatr.site.externalTag", "External System") generatorContext.workspace.model.addSoftwareSystem("system 1", "System 1 description") generatorContext.workspace.model.addSoftwareSystem("system 2", "System 2 description").apply { addTags("External System") } val viewModel = SoftwareSystemsPageViewModel(generatorContext) assertThat(viewModel.softwareSystemsTable.bodyRows[1].columns[0]) .isEqualTo(TableViewModel.TextCellViewModel("system 2 (External)", isHeader = false, greyText = true, boldText = true)) } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/TableViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.containsAtLeast import kotlin.test.Test class TableViewModelTest : ViewModelTest() { private val pageViewModel = object : PageViewModel(generatorContext()) { override val url = "/test-page" override val pageSubTitle = "Test page" } @Test fun `header rows`() { val viewModel = TableViewModel.create { headerRow(headerCell("1"), headerCell("2"), headerCell("3")) } assertThat(viewModel.headerRows).containsAtLeast( TableViewModel.RowViewModel( listOf( TableViewModel.TextCellViewModel("1", isHeader = true), TableViewModel.TextCellViewModel("2", isHeader = true), TableViewModel.TextCellViewModel("3", isHeader = true), ) ) ) } @Test fun `header row with index`() { val viewModel = TableViewModel.create { headerRow(headerCellSmall("1")) } assertThat(viewModel.headerRows).containsAtLeast( TableViewModel.RowViewModel( listOf( TableViewModel.TextCellViewModel("1", isHeader = true, width = CellWidth.ONE_TENTH) ) ) ) } @Test fun `header row with name`() { val viewModel = TableViewModel.create { headerRow(headerCellMedium("Name")) } assertThat(viewModel.headerRows).containsAtLeast( TableViewModel.RowViewModel( listOf( TableViewModel.TextCellViewModel("Name", isHeader = true, width = CellWidth.ONE_FOURTH) ) ) ) } @Test fun `header row with description`() { val viewModel = TableViewModel.create { headerRow(headerCellLarge("Description")) } assertThat(viewModel.headerRows).containsAtLeast( TableViewModel.RowViewModel( listOf( TableViewModel.TextCellViewModel("Description", isHeader = true, width = CellWidth.TWO_FOURTH) ) ) ) } @Test fun `body rows`() { val viewModel = TableViewModel.create { bodyRow(cell("1"), cell("2"), cell("3")) } assertThat(viewModel.bodyRows).containsAtLeast( TableViewModel.RowViewModel( listOf( TableViewModel.TextCellViewModel("1", isHeader = false), TableViewModel.TextCellViewModel("2", isHeader = false), TableViewModel.TextCellViewModel("3", isHeader = false), ) ) ) } @Test fun `cell with link`() { val viewModel = TableViewModel.create { bodyRow(cellWithLink(pageViewModel, "click me", "/decisions")) } assertThat(viewModel.bodyRows).containsAtLeast( TableViewModel.RowViewModel( listOf( TableViewModel.LinkCellViewModel( LinkViewModel(pageViewModel, "click me", "/decisions"), isHeader = false ) ) ) ) } @Test fun `cell with external link`() { val viewModel = TableViewModel.create { bodyRow(cellWithExternalLink("Temporary URI", "https://tempuri.org/")) } assertThat(viewModel.bodyRows).containsAtLeast( TableViewModel.RowViewModel( listOf( TableViewModel.ExternalLinkCellViewModel( ExternalLinkViewModel("Temporary URI", "https://tempuri.org/"), isHeader = false ) ) ) ) } @Test fun `cell with index`() { val viewModel = TableViewModel.create { bodyRow(cellWithIndex("1")) } assertThat(viewModel.bodyRows).containsAtLeast( TableViewModel.RowViewModel( listOf( TableViewModel.TextCellViewModel( "1", isHeader = false, greyText = false, boldText = true ) ) ) ) } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/ViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import com.structurizr.Workspace import com.structurizr.documentation.Decision import com.structurizr.documentation.Format import com.structurizr.documentation.Section import com.structurizr.model.Element import com.structurizr.view.ImageView import nl.avisi.structurizr.site.generatr.site.GeneratorContext import java.time.LocalDate import java.time.ZoneId import java.util.* abstract class ViewModelTest { protected val svgFactory = { _: String, _: String -> """""" } protected fun generatorContext( workspaceName: String = "Workspace name", branches: List = listOf("main"), currentBranch: String = "main", version: String = "1.0.0" ) = GeneratorContext(version, Workspace(workspaceName, ""), branches, currentBranch, false, svgFactory) protected fun pageViewModel(pageHref: String = "/some-page") = object : PageViewModel(generatorContext()) { override val url = pageHref override val pageSubTitle = "Some page" } protected fun createDecision( id: String = "1", decisionStatus: String = "Accepted", decisionDate: LocalDate = LocalDate.now() ) = Decision(id).apply { title = "Decision $id" status = decisionStatus date = Date.from(decisionDate.atStartOfDay(ZoneId.systemDefault()).toInstant()) format = Format.Markdown content = "Decision $id content" } protected fun createSection(content: String = "# Content") = Section(Format.Markdown, content) protected fun createImageView(workspace: Workspace, element: Element, key: String = "imageview-${element.id}", title: String? = "Image View Title"): ImageView = workspace.views.createImageView(element, key).also { it.description = "Image View Description" it.title = title it.contentType = "image/png" it.content = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/WorkspaceDecisionPageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.isEqualTo import com.structurizr.documentation.Format import kotlin.test.Test class WorkspaceDecisionPageViewModelTest : ViewModelTest() { @Test fun url() { val decision = createDecision() assertThat(WorkspaceDecisionPageViewModel.url(decision)) .isEqualTo("/decisions/${decision.id}") assertThat(WorkspaceDecisionPageViewModel(generatorContext(), decision).url) .isEqualTo(WorkspaceDecisionPageViewModel.url(decision)) } @Test fun subtitle() { val decision = createDecision() val viewModel = WorkspaceDecisionPageViewModel(generatorContext(), decision) assertThat(viewModel.pageSubTitle).isEqualTo(decision.title) } @Test fun content() { val decision = createDecision() val viewModel = WorkspaceDecisionPageViewModel(generatorContext(), decision) assertThat(viewModel.content).isEqualTo(toHtml(viewModel, decision.content, Format.Markdown, svgFactory)) } @Test fun `link to other ADR`() { val decision = createDecision().apply { content = """ Decision with [link to other ADR](#2). [Web link](https://google.com) [Section link](#other-section) [Internal link](../embedding-diagrams-and-images/) """.trimIndent() } val viewModel = WorkspaceDecisionPageViewModel(generatorContext(), decision) assertThat(viewModel.content).isEqualTo( toHtml( viewModel, """ Decision with [link to other ADR](decisions/2/). [Web link](https://google.com) [Section link](#other-section) [Internal link](../embedding-diagrams-and-images/) """.trimIndent(), Format.Markdown, svgFactory ) ) } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/WorkspaceDecisionsPageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.isEqualTo import java.time.LocalDate import java.time.Month import kotlin.test.Test class WorkspaceDecisionsPageViewModelTest : ViewModelTest() { @Test fun url() { assertThat(WorkspaceDecisionsPageViewModel.url()) .isEqualTo("/decisions") } @Test fun `decisions table`() { val generatorContext = generatorContext() val decision1 = createDecision("1", "Accepted", LocalDate.of(2022, Month.JANUARY, 1)) val decision2 = createDecision("2", "Proposed", LocalDate.of(2022, Month.JANUARY, 2)) generatorContext.workspace.documentation.addDecision(decision1) generatorContext.workspace.documentation.addDecision(decision2) val viewModel = WorkspaceDecisionsPageViewModel(generatorContext) assertThat(viewModel.decisionsTable).isEqualTo( viewModel.createDecisionsTableViewModel(generatorContext.workspace.documentation.decisions) { WorkspaceDecisionPageViewModel.url(it) } ) } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/WorkspaceDocumentationSectionPageViewModelTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.model import assertk.assertThat import assertk.assertions.isEqualTo import com.structurizr.documentation.Format import kotlin.test.Test class WorkspaceDocumentationSectionPageViewModelTest : ViewModelTest() { @Test fun url() { val section = createSection("# Some section With words and 1 number") assertThat(WorkspaceDocumentationSectionPageViewModel.url(section)) .isEqualTo("/some-section-with-words-and-1-number") } @Test fun `normalized url`() { val generatorContext = generatorContext() val viewModel = WorkspaceDocumentationSectionPageViewModel( generatorContext, createSection("# Some section With words and 1 number") ) assertThat(viewModel.url).isEqualTo("/some-section-with-words-and-1-number") } @Test fun subtitle() { val generatorContext = generatorContext() val section = createSection() val viewModel = WorkspaceDocumentationSectionPageViewModel(generatorContext, section) assertThat(viewModel.pageSubTitle).isEqualTo(section.contentTitle()) } @Test fun content() { val generatorContext = generatorContext() val section = createSection() val viewModel = WorkspaceDocumentationSectionPageViewModel(generatorContext, section) assertThat(viewModel.content).isEqualTo(toHtml(viewModel, section.content, Format.Markdown, svgFactory)) } } ================================================ FILE: src/test/kotlin/nl/avisi/structurizr/site/generatr/site/views/CDNTest.kt ================================================ package nl.avisi.structurizr.site.generatr.site.views import assertk.all import assertk.assertThat import assertk.assertions.* import com.structurizr.Workspace import org.junit.jupiter.api.DynamicTest import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestFactory class CDNTest { @TestFactory fun `cdn locations`() : List { val workspace = Workspace("workspace name", "") val cdn = CDN(workspace) return listOf( cdn.bulmaCss() to "/css/bulma.min.css", cdn.katexJs() to "/dist/katex.min.js", cdn.katexCss() to "/dist/katex.min.css", cdn.lunrJs() to "/lunr.min.js", cdn.lunrLanguagesStemmerJs() to "/min/lunr.stemmer.support.min.js", cdn.lunrLanguagesJs("en") to "/min/lunr.en.min.js", cdn.mermaidJs() to "/dist/mermaid.esm.min.mjs", cdn.svgpanzoomJs() to "/dist/svg-pan-zoom.min.js", cdn.webfontloaderJs() to "/webfontloader.js" ).map { (url, suffix) -> DynamicTest.dynamicTest(url) { assertThat(url).all { startsWith("https://") endsWith(suffix) doesNotContain("~", "^", ">", "<", "=", ":=") } } } } @Test fun `usage of default cdn url`() { val workspace = Workspace("workspace", "") val cdn = CDN(workspace) assertThat(cdn.workspace.views.configuration.properties["generatr.site.cdn"]).isNull() assertThat(cdn.getCdnBaseUrl()).isEqualTo("https://cdn.jsdelivr.net/npm") } @Test fun `usage of view configuration with ending slash`() { val workspace = Workspace("workspace name", "").apply { views.configuration.addProperty("generatr.site.cdn", "https://cdn.companyname.net/npm/") } val cdn = CDN(workspace) assertThat(cdn.getCdnBaseUrl()).isEqualTo("https://cdn.companyname.net/npm") } @Test fun `usage of view configuration without ending slash`() { val workspace = Workspace("workspace name", "").apply { views.configuration.addProperty("generatr.site.cdn", "https://cdn.companyname.net/npm") } val cdn = CDN(workspace) assertThat(cdn.getCdnBaseUrl()).isEqualTo("https://cdn.companyname.net/npm") } }