[
  {
    "path": ".dockerignore",
    "content": "*\n!build/install\n"
  },
  {
    "path": ".editorconfig",
    "content": "# EditorConfig is awesome:\n# - https://EditorConfig.org\n# - https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties\n# - https://blog.logrocket.com/using-prettier-eslint-automate-formatting-fixing-javascript/\n# - https://prettier.io/docs/en/options.html\n\n# top-most EditorConfig file\nroot = true\n\n[*]\ncharset = utf-8\n# end_of_line = lf | crlf\n# indent_size = 2\n# indent_style = tab\ninsert_final_newline = true\n# supported in only some editors; instead use prettier.printWidth\nmax_line_length = 120\ntrim_trailing_whitespace = true\n\n[*.{markdown,md,mdx}]\nindent_style = space\ntrim_trailing_whitespace = false\n\n\n[*.{json,yaml,yml,toml}]\n# indent_size = 2\nindent_style = space\n\n[Dockerfile]\n# indent_size = 4\nindent_style = space\n\n[*.{kt,kts}]\nmax_line_length = 160\nij_kotlin_allow_trailing_comma_on_call_site = false\nij_kotlin_allow_trailing_comma = false\n\nktlint_standard_no-wildcard-imports = disabled\nktlint_standard_argument-list-wrapping = disabled\nktlint_standard_if-else-wrapping = disabled\nktlint_standard_if-else-bracing = disabled\nktlint_standard_multiline-if-else = disabled\nktlint_standard_function-signature = disabled\nktlint_standard_blank-line-before-declaration = disabled\nktlint_standard_multiline-expression-wrapping = disabled\nktlint_standard_string-template-indent = disabled\n"
  },
  {
    "path": ".gitattributes",
    "content": "#\n# https://help.github.com/articles/dealing-with-line-endings/\n#\n# These are explicitly windows files and should use crlf\n*.bat           text eol=crlf\n\n"
  },
  {
    "path": ".github/workflows/pr.yml",
    "content": "name: pr\non:\n  pull_request:\n    branches:\n      - main\n  push:\n    branches:\n      - main\n\nenv:\n  IMAGE_NAME: ${{ github.event.repository.name }}\n  VERSION: '0.0.1'\n\njobs:\n  build-gradle:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-java@v5\n        with:\n          distribution: temurin\n          java-version: 21\n      - name: Setup Gradle\n        uses: gradle/gradle-build-action@v3.5.0\n      - name: Ensure browsers are installed\n        run: ./gradlew playwright -Pargs=\"install --with-deps\"\n      - name: Execute Gradle build\n        run: ./gradlew test installDist\n      - name: Archive production artifacts\n        uses: actions/upload-artifact@v7\n        with:\n          name: binaries\n          path: build/install\n          retention-days: 1\n  build-docker:\n    runs-on: ubuntu-latest\n    needs: build-gradle\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n      - name: Set up Docker Buildx\n        id: buildx\n        uses: docker/setup-buildx-action@v4\n\n      - name: Download binaries\n        uses: actions/download-artifact@v8\n        with:\n          name: binaries\n          path: build/install\n\n      - name: Build container image\n        uses: docker/build-push-action@v7\n        with:\n          push: false\n          builder: ${{ steps.buildx.outputs.name }}\n          context: .\n          file: ./Dockerfile\n          platforms: linux/amd64,linux/arm64/v8\n          tags: |\n            ghcr.io/avisi-cloud/${{ env.IMAGE_NAME }}:${{ env.VERSION }}\n            ghcr.io/avisi-cloud/${{ env.IMAGE_NAME }}:latest\n          labels: |\n            org.opencontainers.image.title=${{ github.event.repository.name }}\n            org.opencontainers.image.description=${{ github.event.repository.description }}\n            org.opencontainers.image.url=${{ github.event.repository.html_url }}\n            org.opencontainers.image.revision=${{ github.sha }}\n            org.opencontainers.image.version=${{ env.VERSION }}\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: release\non:\n  push:\n    tags:\n      - '*'\n\nenv:\n  IMAGE_NAME: ${{ github.event.repository.name }}\n\njobs:\n  build-gradle:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-java@v5\n        with:\n          distribution: temurin\n          java-version: 21\n      - name: Setup Gradle\n        uses: gradle/gradle-build-action@v3.5.0\n      - name: Ensure browsers are installed\n        run: ./gradlew playwright -Pargs=\"install --with-deps\"\n      - name: Execute Gradle build\n        run: ./gradlew -PprojectVersion=${{ github.ref_name }} test assemble installDist\n      - name: Archive binaries\n        uses: actions/upload-artifact@v7\n        with:\n          name: binaries\n          path: build/install\n          retention-days: 1\n      - name: Archive distribution\n        uses: actions/upload-artifact@v7\n        with:\n          name: distribution\n          path: build/distributions\n          retention-days: 1\n  build-push-docker:\n    runs-on: ubuntu-latest\n    needs: build-gradle\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@v4\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n      - name: Set up Docker Buildx\n        id: buildx\n        uses: docker/setup-buildx-action@v4\n      - name: Set up cosign\n        uses: sigstore/cosign-installer@main\n\n      - name: Download binaries\n        uses: actions/download-artifact@v8\n        with:\n          name: binaries\n          path: build/install\n      - name: Download distribution\n        uses: actions/download-artifact@v8\n        with:\n          name: distribution\n          path: build/distributions\n\n      - name: Build and publish container image\n        uses: docker/build-push-action@v7\n        id: build_push\n        with:\n          push: true\n          builder: ${{ steps.buildx.outputs.name }}\n          context: .\n          file: ./Dockerfile\n          platforms: linux/amd64,linux/arm64/v8\n          tags: |\n            ghcr.io/avisi-cloud/${{ env.IMAGE_NAME }}:${{ github.ref_name }}\n            ghcr.io/avisi-cloud/${{ env.IMAGE_NAME }}:latest\n          labels: |\n            org.opencontainers.image.title=${{ github.event.repository.name }}\n            org.opencontainers.image.description=${{ github.event.repository.description }}\n            org.opencontainers.image.url=${{ github.event.repository.html_url }}\n            org.opencontainers.image.source=${{ github.event.repository.html_url }}\n            org.opencontainers.image.revision=${{ github.sha }}\n            org.opencontainers.image.version=${{ github.ref_name }}\n      - name: sign container image\n        run: |\n          cosign sign --yes --key env://COSIGN_KEY ghcr.io/avisi-cloud/${{ env.IMAGE_NAME }}:${{ github.ref_name }}@${{ steps.build_push.outputs.digest }}\n        shell: bash\n        env:\n          COSIGN_KEY: ${{secrets.COSIGN_KEY}}\n          COSIGN_PASSWORD: ${{secrets.COSIGN_PASSWORD}}\n\n      - name: Check images\n        run: |\n          docker buildx imagetools inspect ghcr.io/avisi-cloud/${IMAGE_NAME}:${{ github.ref_name }}\n          docker pull ghcr.io/avisi-cloud/${IMAGE_NAME}:${{ github.ref_name }}\n          cosign verify --key cosign.pub ghcr.io/avisi-cloud/${IMAGE_NAME}:${{ github.ref_name }}\n      - uses: anchore/sbom-action@v0\n        with:\n          image: ghcr.io/avisi-cloud/${{ env.IMAGE_NAME }}:${{ github.ref_name }}\n\n  create-draft-release:\n    runs-on: ubuntu-latest\n    needs: build-gradle\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n\n      - name: Download distribution\n        uses: actions/download-artifact@v8\n        with:\n          name: distribution\n          path: build/distributions\n\n      - name: Create Release Draft\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          gh release create ${{ github.ref_name }} \\\n            --draft \\\n            --title \"${{ github.ref_name }}\"\n      - name: Upload Release Assets\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: gh release upload ${{ github.ref_name }} ./build/distributions/*\n\n  generate-example-site:\n    runs-on: ubuntu-latest\n    needs: build-gradle\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n      - uses: actions/setup-java@v5\n        with:\n          distribution: temurin\n          java-version: 21\n\n      - name: Download binaries\n        uses: actions/download-artifact@v8\n        with:\n          name: binaries\n          path: build/install\n\n      - name: Set up environment\n        run: |\n          sudo apt-get install -y graphviz\n          chmod +x build/install/structurizr-site-generatr/bin/structurizr-site-generatr\n\n      - name: Generate example site\n        run: >\n          build/install/structurizr-site-generatr/bin/structurizr-site-generatr generate-site\n          --git-url https://github.com/avisi-cloud/structurizr-site-generatr.git\n          --workspace-file docs/example/workspace.dsl\n          --assets-dir docs/example/assets\n          --branches main\n          --default-branch main\n          --version ${{ github.ref_name }}\n\n      - name: Upload example site as GitHub Pages artifact\n        uses: actions/upload-pages-artifact@v5\n        with:\n          path: ./build/site\n\n  publish-example-site:\n    runs-on: ubuntu-latest\n    needs: generate-example-site\n\n    permissions:\n      pages: write\n      id-token: write\n\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n\n    steps:\n      - name: Deploy example site to GitHub Pages\n        id: deployment\n        uses: actions/deploy-pages@v5\n"
  },
  {
    "path": ".gitignore",
    "content": ".gradle\n.idea\nbuild\nout\n*.md_\n.vscode\nbin\n"
  },
  {
    "path": ".markdownlint.json",
    "content": "{\n    \"default\": true,\n    \"MD013\": false,\n    \"MD041\": false\n}"
  },
  {
    "path": ".prettierrc",
    "content": "{ \"printWidth\": 120 }\n"
  },
  {
    "path": ".run/all unit tests.run.xml",
    "content": "<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"all unit tests\" type=\"JUnit\" factoryName=\"JUnit\">\n    <module name=\"structurizr-site-generatr.test\" />\n    <option name=\"PACKAGE_NAME\" value=\"\" />\n    <option name=\"MAIN_CLASS_NAME\" value=\"\" />\n    <option name=\"METHOD_NAME\" value=\"\" />\n    <option name=\"TEST_OBJECT\" value=\"package\" />\n    <method v=\"2\">\n      <option name=\"Make\" enabled=\"true\" />\n    </method>\n  </configuration>\n</component>"
  },
  {
    "path": ".run/generate site for example model (from git repo all branches) .run.xml",
    "content": "<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"generate site for example model (from git repo all branches) \" type=\"JetRunConfigurationType\">\n    <option name=\"MAIN_CLASS_NAME\" value=\"nl.avisi.structurizr.site.generatr.AppKt\" />\n    <module name=\"structurizr-site-generatr.main\" />\n    <option name=\"PROGRAM_PARAMETERS\" value=\"generate-site --git-url https://github.com/avisi-cloud/structurizr-site-generatr.git --workspace-file docs/example/workspace.dsl --all-branches --default-branch main --exclude-branches gh-pages\" />\n    <shortenClasspath name=\"NONE\" />\n    <method v=\"2\">\n      <option name=\"Make\" enabled=\"true\" />\n    </method>\n  </configuration>\n</component>"
  },
  {
    "path": ".run/generate site for example model (from git repo).run.xml",
    "content": "<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"generate site for example model (from git repo)\" type=\"JetRunConfigurationType\">\n    <option name=\"MAIN_CLASS_NAME\" value=\"nl.avisi.structurizr.site.generatr.AppKt\" />\n    <module name=\"structurizr-site-generatr.main\" />\n    <option name=\"PROGRAM_PARAMETERS\" value=\"generate-site --git-url https://github.com/avisi-cloud/structurizr-site-generatr.git --workspace-file docs/example/workspace.dsl --branches main,fix-diagram-size-on-pages --default-branch main\" />\n    <shortenClasspath name=\"NONE\" />\n    <method v=\"2\">\n      <option name=\"Make\" enabled=\"true\" />\n    </method>\n  </configuration>\n</component>"
  },
  {
    "path": ".run/generate site for example model (local).run.xml",
    "content": "<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"generate site for example model (local)\" type=\"JetRunConfigurationType\">\n    <option name=\"MAIN_CLASS_NAME\" value=\"nl.avisi.structurizr.site.generatr.AppKt\" />\n    <module name=\"structurizr-site-generatr.main\" />\n    <option name=\"PROGRAM_PARAMETERS\" value=\"generate-site --workspace-file docs/example/workspace.dsl --assets-dir docs/example/assets\" />\n    <shortenClasspath name=\"NONE\" />\n    <method v=\"2\">\n      <option name=\"Make\" enabled=\"true\" />\n    </method>\n  </configuration>\n</component>"
  },
  {
    "path": ".run/serve example model.run.xml",
    "content": "<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"serve example model\" type=\"JetRunConfigurationType\">\n    <option name=\"MAIN_CLASS_NAME\" value=\"nl.avisi.structurizr.site.generatr.AppKt\" />\n    <module name=\"structurizr-site-generatr.main\" />\n    <option name=\"PROGRAM_PARAMETERS\" value=\"serve --workspace-file docs/example/workspace.dsl --assets-dir docs/example/assets\" />\n    <shortenClasspath name=\"NONE\" />\n    <method v=\"2\">\n      <option name=\"Make\" enabled=\"true\" />\n    </method>\n  </configuration>\n</component>"
  },
  {
    "path": ".tool-versions",
    "content": "java temurin-21.0.10+7.0.LTS\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# How to contribute\n\nWe welcome contributions! You can contribute\nby [filing an issue](https://github.com/avisi-cloud/structurizr-site-generatr/issues), or by filing a pull request.\n\nFor those who'd like to help out by filing a pull request, here's some information to help you get started quickly.\n\n## Recommended development tooling\n\nWe recommend using IntelliJ Community or Ultimate edition as your IDE. Additionally, you will need the following tools:\n\n- Git for version control\n- Java Development Kit (JDK) version 18 for building the source code\n- (optional) [asdf-vm](https://asdf-vm.com/). We have provided a `.tool-versions` file in this repository, which is used\n  by `asdf` to help you to install the correct JDK version for building this tool.\n\n## Working from the command line\n\nIf you prefer working with a terminal, here's a few commands you can use for some common tasks. For all these commands,\nwe assume that the current working directory is the root of this repository. For all these common tasks, we use the\nGradle CLI. Please refer to Gradle's [user manual](https://docs.gradle.org/current/userguide/userguide.html) if you're\nnot familiar with Gradle.\n\n### Running tests\n\n```shell\n./gradlew test\n```\n\n### Running the application from source\n\n```shell\n./gradlew run --args \"<program arguments>\"\n```\n\n#### Example: Start a development server from source\n\n```shell\n./gradlew run --args \"serve --workspace-file docs/example/workspace.dsl --assets-dir docs/example/assets\"\n```\n\n## Working with IntelliJ\n\nFor those working with IntelliJ, we've provided some run configurations for running the program from source:\n\n| Name                                              | Description                                                                                         |\n|---------------------------------------------------|-----------------------------------------------------------------------------------------------------|\n| `all unit tests`                                  | Runs all unit tests                                                                                 |\n| `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   |\n| `generate site for example model (local)`         | Generates a site for the example model in `docs/example` from the local clone of the Git repository |\n| `serve example model`                             | Starts a development server for the example model in `docs/example`                                 |\n\n## Updating documentation\n\nAt the time of writing this document, the documentation of this tool is sparse. Please provide updated documentation\nwith your PR's, where applicable.\n\nThis is what's currently available:\n\n- There's some basic documentation available in [README.md](README.md), which describes the basic usage of this tool.\n- The default homepage (located in the `HomePageViewModel` class) provides some basic documentation on customizing the\n  homepage.\n- Finally, there's the example model, which contains some documentation on embedding diagrams in documentation Markdown\n  files.\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM eclipse-temurin:21.0.10_7-jre-jammy\n\nUSER root\nRUN apt update && apt install graphviz --yes && rm -rf /var/lib/apt/lists/*\nRUN mkdir -p /var/model \\\n    && chown 65532:65532 /var/model\nRUN useradd -d /home/generatr -u 65532 --create-home generatr\n\nENTRYPOINT [\"/opt/structurizr-site-generatr/bin/structurizr-site-generatr\"]\n\nWORKDIR /opt/structurizr-site-generatr\nCOPY build/install/structurizr-site-generatr ./\nRUN chmod +x /opt/structurizr-site-generatr/bin/structurizr-site-generatr\n\nUSER generatr\nVOLUME [\"/var/model\"]\nWORKDIR /var/model\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "<!-- TOC -->\n* [Structurizr Site Generatr](#structurizr-site-generatr)\n  * [Features](#features)\n  * [Getting Started](#getting-started)\n    * [Installation using Homebrew - Recommended](#installation-using-homebrew---recommended)\n    * [Manual installation](#manual-installation)\n    * [Docker](#docker)\n      * [[Optional] Verify the Structurizr Site Generatr image with CoSign](#optional-verify-the-structurizr-site-generatr-image-with-cosign)\n  * [Usage](#usage)\n    * [Help](#help)\n    * [Version](#version)\n    * [Generate a website](#generate-a-website)\n      * [From a C4 Workspace](#from-a-c4-workspace)\n      * [For those taking the Docker approach](#for-those-taking-the-docker-approach)\n      * [Generate a website from a Git repository](#generate-a-website-from-a-git-repository)\n    * [Start a development web server around the generated website](#start-a-development-web-server-around-the-generated-website)\n      * [For those taking the Docker approach](#for-those-taking-the-docker-approach-1)\n  * [Customizing the generated website](#customizing-the-generated-website)\n  * [Contributing](#contributing)\n  * [Background](#background)\n<!-- TOC -->\n\n# Structurizr Site Generatr\n\nA static site generator for [C4 architecture models](https://c4model.com/) created with [Structrizr DSL](https://docs.structurizr.com/dsl).\nSee [Background](#background) for the story behind this tool.\n\n[Click here to see an example of a generated site](https://avisi-cloud.github.io/structurizr-site-generatr) based on\nthe [Big Bank plc example](https://structurizr.com/dsl?example=big-bank-plc) from <https://structurizr.com>. This site\nis generated from the example workspace in this repository.\n\n## Features\n\n- Generate a static HTML site, based on a Structurizr DSL workspace.\n- Generates diagrams in SVG, PNG and PlantUML format, which can be viewed and downloaded from the generated site.\n- Easy browsing through the site by clicking on software system and container elements in the diagrams. Note that\n  external software systems are excluded from the menu. A software system is considered external when it lives outside\n  the (deprecated) enterprise boundary or when it contains a specific tag, see [Customizing the generated website](#customizing-the-generated-website). \n- Start a development server which generates a site, serves it and updates the site automatically whenever a file that's\n  part of the Structurizr workspace changes.\n- Include documentation (in Markdown or AsciiDoc format) in the generated site. Both workspace level documentation and software\n  system level documentation are included in the site.\n- Include ADR's in the generated site. Again, both workspace level ADR's and software system level ADR's are included in\n  the site.\n- Include static assets in the generated site, which can be used in ADR's and documentation.\n- Generate a site from a Structurizr DSL model in a Git repository. Supports multiple branches, which makes it possible\n  to for example maintain an actual state in `master` and one or more future states in feature branches. The generated\n  site includes diagrams for all valid configured or detected branches.\n- Include a version number in the generated site.\n\n## Getting Started\n\nTo get started with the Structurizr Site Generatr, you can either:\n\n- Install it to your local machine (recommended for the best experience), or\n- Execute it on your local machine via a container (requires Docker)\n\n**Please note**: The intended use of the Docker image is to generate a site from a CI pipeline. Using it for model\ndevelopment is possible, but not a usage scenario that's actively supported.\n\n### Installation using Homebrew - Recommended\n\nAs this approach relies on [Homebrew](https://brew.sh/), ensure this is already installed. For Windows and other\noperating systems not supported by Homebrew, please use the [Docker approach](#docker) instead.\n\nTo install Structurizr Site Generatr execute the following commands in your terminal:\n\n```shell\nbrew tap avisi-cloud/tools\nbrew install structurizr-site-generatr\n\nstructurizr-site-generatr --help\n```\n\nPeriodically, you would have to update your local installation to take advantage of any new\n[Structurizr Site Generatr releases](https://github.com/avisi-cloud/structurizr-site-generatr/releases).\n\n### Manual installation\n\nIf using Homebrew is not an option for you, it's also possible to install Structurizr Site Generatr manually. This can\nbe done as follows:\n\n- Consult the\n  [Structurizr Site Generatr releases](https://github.com/avisi-cloud/structurizr-site-generatr/releases) and choose\n  the version you wish to use\n- Download the `.tar.gz` or `.zip` distribution\n- Extract the archive using your favourite tool\n- For ease of use, it's recommended to add Structurizr Site Generatr's `bin` directory to your `PATH`\n\n### Docker\n\nThough local installation is recommended for development where possible, Structurizr Site Generatr is also a packaged\nas a Docker image. Therefore, to use this approach, ensure [Docker](https://www.docker.com/) is already installed.\nAdditionally, for Windows 10+ users, you may want to take advantage of\n[WSL2](https://docs.microsoft.com/en-us/windows/wsl/install) (Windows Subsystem for Linux). Both Docker and WSL2\nare topics too vast to repeat here, so you are invited to study these as prerequisite learning for this approach.\n\nThen to download our packaged image, consider the\n[Structurizr Site Generatr releases](https://github.com/avisi-cloud/structurizr-site-generatr/releases) and choose\nthe version you wish to use. Then, in your terminal, execute the following:\n\n```shell\ndocker pull ghcr.io/avisi-cloud/structurizr-site-generatr\n```\n\nOnce downloaded, you can execute Structurizr Site Generatr via a temporary Docker container by executing the\nfollowing in your terminal:\n\n```shell\ndocker run -it --rm ghcr.io/avisi-cloud/structurizr-site-generatr --help\n```\n\n#### [Optional] Verify the Structurizr Site Generatr image with [CoSign](https://github.com/sigstore/cosign)\n\n```shell\ncat cosign.pub\n-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzezKl0vAWSHosQ0JLEsDzNBd2nGm\n08KqX+imYqq2avlbH+ehprJFMqKK0/I/bY0q5W9hQC8SLzTRJ9Q5dB9UiQ==\n-----END PUBLIC KEY-----\ncosign verify --key cosign.pub ghcr.io/avisi-cloud/structurizr-site-generatr\n```\n\nOr by using the Github repo url:\n\n```shell\n cosign verify --key https://github.com/avisi-cloud/structurizr-site-generatr ghcr.io/avisi-cloud/structurizr-site-generatr\n ```\n\n## Usage\n\nThese examples use the [example workspace](docs/example) in this repository.\n\nOnce installed, Structurizr Site Generatr is operated via your terminal by issuing commands. Each command is\nexplained here:\n\n### Help\n\nTo learn about available commands, or parameters for individual commands, call Structurizr Site Generatr with the\n`--help` argument.\n\n```shell\ninstalled> structurizr-site-generatr --help\n\n   docker> docker run -it --rm ghcr.io/avisi-cloud/structurizr-site-generatr --help\n\nUsage: structurizr-site-generatr options_list\nSubcommands:\n    serve - Start a development server\n    generate-site - Generate a site for the selected workspace.\n    version - Print version information\n\nOptions:\n    --help, -h -> Usage info\n```\n\n### Version\n\nTo query the version of Structurizr Site Generatr installed / used.\n\n```shell\ninstalled> structurizr-site-generatr version\n\n   docker> docker run -it --rm ghcr.io/avisi-cloud/structurizr-site-generatr version\n\nStructurizr Site Generatr v1.1.3\n```\n\n### Generate a website\n\n#### From a [C4 Workspace](https://docs.structurizr.com/dsl)\n\nThis is the primary use case of Structurizr Site Generatr -- to generate a website from a\n[C4 Workspace](https://docs.structurizr.com/dsl).\n\n```shell\ninstalled> structurizr-site-generatr generate-site --workspace-file workspace.dsl --assets-dir assets\n\n   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\n```\n\nHere, the `--workspace-file` or `-w` parameter specifies the input\n[C4 Workspace DSL file](https://docs.structurizr.com/dsl) to the `generate-site` command. The `--assets-dir` or `-a` parameter is not\nrequired, but usually needed as well. Additional parameters that affect website generation can be reviewed\nusing the `--help` operator.\n\nBy default, the generated website will be placed in `./build`, which is overwritten if it already exisits.\n\n#### For those taking the Docker approach\n\nWhen using the Docker approach, the local file system must be made available to the temporary Structurizr Site\nGeneratr container via a\n[Docker file system volume mount](https://docs.docker.com/engine/reference/commandline/run/#mount-volume--v---read-only).\nThis is achieved by specifying the `-v` parameter with a linux-like **absolute** path to the folder containing\nthe .dsl file specified via `-w`. See how `C:\\Projects\\C4` has become `-v c:/projects/c4:/var/model` in the above\nexample?\n\n#### Generate a website from a Git repository\n\nInstead of relying on local .dsl files only, the `generate-site` command can also retrieve input files from a\nGit repository as follows. This is particularly advantageous for demos, documentation, or CI/CD pipelines.\n\nTo explicitly name the branches that you want to build sites from you can use the --branches option.\n\n```shell\nstructurizr-site-generatr generate-site\n    --git-url https://github.com/avisi-cloud/structurizr-site-generatr.git\n    --workspace-file docs/example/workspace.dsl\n    --assets-dir docs/example/assets\n    --branches main,future,old\n    --default-branch main\n```\n\nor 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.\n\n```shell\nstructurizr-site-generatr generate-site\n    --git-url https://github.com/avisi-cloud/structurizr-site-generatr.git\n    --workspace-file docs/example/workspace.dsl\n    --assets-dir docs/example/assets\n    --all-branches\n    --exclude-branches gh-pages\n    --default-branch main\n```\n\nBoth the --branches and --exclude-branches options are comma separated lists and can contain multiple branch names.\n\n### Start a development web server around the generated website\n\nTo aid composition of [C4 Workspace DSL files](https://docs.structurizr.com/dsl), the `serve` command will\ngenerate 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**.\nA different port for the web server can be specified with `-p PORT`. Additional parameters that affect website generation and the development\nweb server can be reviewed using the `--help` operator.\n\n```shell\ninstalled> structurizr-site-generatr serve -w workspace.dsl\n\n   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\n```\n\nBy default, a development web server will be started and accessible at http://localhost:8080/ (if available).\n\n#### For those taking the Docker approach\n\nHowever, when using the Docker approach, this development web server is within the temporary Structurizr Site\nGeneratr container. So\n[Docker port mapping](https://docs.docker.com/engine/reference/commandline/run/#publish-or-expose-port--p---expose)\nis needed to expose the container's port 8080 to the host (web browser). In the example above, the\n`-p 8080:8080` argument tells Docker to bind the local machine / host's port 8080 to the container's port 8080.\n\n## Customizing the generated website\n\nBy default, the site generator uses the\n[C4PlantUmlExporter](https://docs.structurizr.com/export/plantuml#c4plantumlexporter)\nto generate the diagrams. When using this exporter, all properties available for the C4PlantUMLExporter, e.g. `c4plantuml.tags`, can be applied\nand affect the diagrams in the generate site. See also [Diagram notation](https://docs.structurizr.com/export/comparison) for an overview of supported features\nand limitations for this exporter.\n\nThe look and feel of the generated site can be customized with several additional view properties in the C4\narchitecture model:\n\n| Property name                           | Description                                                                                                                                                                                                                                                                                                                                      | Default                        | Example                                              |\n|-----------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------|------------------------------------------------------|\n| `generatr.style.colors.primary`         | Primary site color, used for header bar background and active menu background.                                                                                                                                                                                                                                                                   | `#333333`                      | `#485fc7`                                            |\n| `generatr.style.colors.secondary`       | Secondary site color, used for font color in header bar and for active menu.                                                                                                                                                                                                                                                                     | `#cccccc`                      | `#ffffff`                                            |\n| `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`                                   |\n| `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`                                      |\n| `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 |\n| `generatr.search.language`              | Indexing/stemming language for the search index. See [Lunr language support](https://github.com/olivernn/lunr-languages)                                                                                                                                                                                                                         | `en`                           | `nl`                                                 |\n| `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`                                  |\n| `generatr.svglink.target`               | Specifies the link target for element links in the exported svg                                                                                                                                                                                                                                                                                  | `_top`                         | `_self`                                              |\n| `generatr.site.exporter`                | Specifies the UML exporter, can be `c4` (uses the `C4PlantUMLExporter`) or `structurizr` (uses the `StructurizrPlantUMLExporter`)                                                                                                                                                                                                                | `c4`                           | `structurizr`                                        |\n| `generatr.site.externalTag`             | Software systems containing this tag will be considered external                                                                                                                                                                                                                                                                                 |                                |                                                      |\n| `generatr.site.nestGroups`              | Will show software systems in the left side navigator in collapsable groups                                                                                                                                                                                                                                                                      | `false`                        | `true`                                               |\n| `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`                         |\n| `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`                                               |\n\nTo control the behavior of views, apply the following properties:\n\n| Property name                        | Description                                                                  | Value                | Scope           |\n|--------------------------------------|------------------------------------------------------------------------------|----------------------|-----------------|\n| `generatr.view.deployment.belongsTo` | Associate the diagram into the Deployment tab of a specific software system. | software system name | deployment view |\n\nSee the included example for usage of some those properties in the\n[C4 architecture model example](https://github.com/avisi-cloud/structurizr-site-generatr/blob/main/docs/example/workspace.dsl#L163).\n\n## Contributing\n\nWe welcome contributions! Please refer to [CONTRIBUTING.md](CONTRIBUTING.md) for more information on how you can help.\n\n## Background\n\nAt Avisi, we're big fans of the [C4 model](https://c4model.com). We use it in many projects, big and small, to document\nthe architecture of the systems and system landscapes we're working on.\n\nWe started out by using PlantUML, combined with the [C4 extension](https://github.com/plantuml-stdlib/C4-PlantUML). This\nworks well for small systems. However, for larger application landscapes, maintained by multiple development teams, this\nlead to duplication and inconsistency across diagrams.\n\nTo solve this problem, we needed a model based approach. Rather than maintaining separate diagrams and trying to keep\nthem consistent, we needed to have a single model, from which diagrams could be generated. This is why we started using\nthe [Structurizr for Java](https://github.com/structurizr/java) library. About a year later, we migrated our models to\nStructurizr DSL.\n\nWe created custom tooling to generate diagrams and check them in to our Git repository. All diagrams could be seen by\nnavigating to our GitLab repository. This is less than ideal, because we like to make these diagrams easily accessible\nto everyone, including our customers. This is why we decided that we needed a way of publishing our models to an easily\naccessible website.\n\nThis tool is the result. It's still a work in progress, but it has already proven to be very useful to us.\n"
  },
  {
    "path": "build.gradle.kts",
    "content": "import org.gradle.api.tasks.testing.logging.TestExceptionFormat\nimport org.gradle.api.tasks.testing.logging.TestLogEvent\nimport org.jetbrains.kotlin.gradle.dsl.JvmTarget\n\nplugins {\n    kotlin(\"jvm\") version \"2.3.20\"\n    kotlin(\"plugin.serialization\") version \"2.3.20\"\n    application\n}\n\ndescription = \"Structurizr Site Generatr\"\ngroup = \"cloud.avisi\"\n\nversion = project.properties[\"projectVersion\"] ?: \"0.0.0-SNAPSHOT\"\n\nrepositories {\n    mavenCentral()\n}\n\ndependencies {\n    implementation(platform(\"org.jetbrains.kotlin:kotlin-bom\"))\n    implementation(\"org.jetbrains.kotlin:kotlin-stdlib\")\n    implementation(\"org.jetbrains.kotlinx:kotlinx-cli:0.3.6\")\n\n    implementation(\"org.eclipse.jgit:org.eclipse.jgit:7.6.0.202603022253-r\")\n\n    implementation(\"com.structurizr:structurizr-core:6.1.0\")\n    implementation(\"com.structurizr:structurizr-dsl:6.1.0\")\n    implementation(\"com.structurizr:structurizr-export:6.1.0\")\n\n    implementation(\"net.sourceforge.plantuml:plantuml:1.2026.2\")\n\n    implementation(\"com.vladsch.flexmark:flexmark-all:0.64.8\")\n    implementation(\"org.asciidoctor:asciidoctorj:3.0.1\")\n    implementation(\"org.jsoup:jsoup:1.22.1\")\n\n    implementation(\"org.jetbrains.kotlinx:kotlinx-html-jvm:0.12.0\")\n    implementation(\"org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0\")\n\n    implementation(\"org.eclipse.jetty:jetty-server:12.1.8\")\n    implementation(\"org.eclipse.jetty.websocket:jetty-websocket-jetty-server:12.1.8\")\n\n    runtimeOnly(\"org.slf4j:slf4j-simple:2.0.17\")\n\n    // Support for Structurizr scripting languages\n    runtimeOnly(\"org.jetbrains.kotlin:kotlin-scripting-jsr223:2.3.20\")\n    runtimeOnly(\"org.codehaus.groovy:groovy-jsr223:3.0.25\")\n    runtimeOnly(\"org.jruby:jruby-core:9.4.14.0\")\n\n    testImplementation(kotlin(\"test\"))\n    testImplementation(\"org.junit.jupiter:junit-jupiter-params\")\n    testImplementation(\"com.willowtreeapps.assertk:assertk-jvm:0.28.1\")\n\n    testImplementation(\"com.microsoft.playwright:playwright:1.59.0\")\n}\n\napplication {\n    mainClass.set(\"nl.avisi.structurizr.site.generatr.AppKt\")\n}\n\nkotlin {\n    jvmToolchain(21)\n    compilerOptions {\n        jvmTarget.set(JvmTarget.JVM_17)\n    }\n}\n\njava {\n    targetCompatibility = JavaVersion.VERSION_17\n}\n\ntasks {\n    processResources {\n        from(\"package.json\")\n    }\n\n    register<JavaExec>(\"playwright\") {\n        classpath(sourceSets[\"test\"].runtimeClasspath)\n        mainClass = \"com.microsoft.playwright.CLI\"\n        args = (project.properties[\"args\"] as String?)?.split(\" \") ?: emptyList()\n    }\n\n    test {\n        useJUnitPlatform()\n        systemProperty(\"file.encoding\", \"UTF-8\")\n        testLogging.events = setOf(TestLogEvent.PASSED, TestLogEvent.FAILED, TestLogEvent.STANDARD_OUT, TestLogEvent.STANDARD_ERROR)\n        testLogging.exceptionFormat = TestExceptionFormat.FULL\n    }\n\n    jar {\n        manifest {\n            attributes[\"Implementation-Title\"] = project.description\n            attributes[\"Implementation-Version\"] = project.version\n        }\n    }\n\n    startScripts {\n        // This is a workaround for an issue with the generated .bat file,\n        // reported in https://github.com/avisi-cloud/structurizr-site-generatr/issues/463.\n        // Instead of listing each and every .jar file in the lib directory, we just use a\n        // wildcard to include everything in the lib directory in the classpath.\n        classpath = files(\"lib/*\")\n    }\n\n    withType<Tar> {\n        archiveExtension.set(\"tar.gz\")\n        compression = Compression.GZIP\n    }\n}\n"
  },
  {
    "path": "cosign.pub",
    "content": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzezKl0vAWSHosQ0JLEsDzNBd2nGm\n08KqX+imYqq2avlbH+ehprJFMqKK0/I/bY0q5W9hQC8SLzTRJ9Q5dB9UiQ==\n-----END PUBLIC KEY-----\n"
  },
  {
    "path": "docs/example/.adr-dir",
    "content": "workspace-adrs\n"
  },
  {
    "path": "docs/example/assets/site/custom.css",
    "content": "svg g g[id^=\"link\"]:hover path {\n    stroke:#FF0000 !important;\n    stroke-width: 2.0;\n}\n\nsvg g g[id^=\"link\"]:hover polygon {\n    stroke:#FF0000 !important;\n}\n\nsvg g g[id^=\"link\"]:hover text {\n    fill:#FF0000 !important;\n}\n"
  },
  {
    "path": "docs/example/internet-banking-system/adr/0001-record-architecture-decisions.md",
    "content": "# 1. Record Internet Banking System architecture decisions\n\nDate: 2022-06-21\n\n## Status\n\nAccepted\n\n## Context\n\nWe need to record the architectural decisions made on this project.\n\n## Decision\n\nWe will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions).\n\n## Consequences\n\nSee Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools).\n"
  },
  {
    "path": "docs/example/internet-banking-system/api-application/email-component/adr/0001-record-architecture-decisions.md",
    "content": "# 1. Record Email Component architecture decision\n\nDate: 2022-06-21\n\n## Status\n\nAccepted\n\n## Context\n\nWe need to record the architectural decisions made on this project.\n\n## Decision\n\nWe will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions).\n\n## Consequences\n\nSee Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools).\n"
  },
  {
    "path": "docs/example/internet-banking-system/api-application/email-component/adr/0002-implement-feature-1.md",
    "content": "# 2. Implement Feature 1\n\nDate: 2024-12-17\n\n## Status\n\nSuperseded by [3. Another Realisation of Feature 1](0003-another-realisation-of-feature-1.md)\n\n## Context\n\nThe issue motivating this decision, and any context that influences or constrains the decision.\n\n## Decision\n\nThe change that we're proposing or have agreed to implement.\n\n## Consequences\n\nWhat becomes easier or more difficult to do and any risks introduced by the change that will need to be mitigated.\n"
  },
  {
    "path": "docs/example/internet-banking-system/api-application/email-component/adr/0003-another-realisation-of-feature-1.md",
    "content": "# 3. Another Realisation of Feature 1\n\nDate: 2024-12-17\n\n## Status\n\nAccepted\n\nSupersedes [2. Implement Feature 1](0002-implement-feature-1.md)\n\n## Context\n\nThe issue motivating this decision, and any context that influences or constrains the decision.\n\n## Decision\n\nThe change that we're proposing or have agreed to implement.\n\n## Consequences\n\nWhat becomes easier or more difficult to do and any risks introduced by the change that will need to be mitigated.\n"
  },
  {
    "path": "docs/example/internet-banking-system/api-application/email-component/docs/0001-inner-workings.md",
    "content": "# Email Component inner workings\n\nThis is how the e-mail component works.\n"
  },
  {
    "path": "docs/example/internet-banking-system/api-application/mainframe-banking-system-facade/adr/0001-record-architecture-decisions.md",
    "content": "# 1. Record Mainframe Banking System Facade architecture decision\n\nDate: 2022-06-21\n\n## Status\n\nAccepted\n\n## Context\n\nWe need to record the architectural decisions made on this project.\n\n## Decision\n\nWe will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions).\n\n## Consequences\n\nSee Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools).\n"
  },
  {
    "path": "docs/example/internet-banking-system/api-application/mainframe-banking-system-facade/docs/0000-introduction.md",
    "content": "# Mainframe Banking System Facade\n\nThis is the Mainframe Banking System Facade.\n"
  },
  {
    "path": "docs/example/internet-banking-system/api-application/mainframe-banking-system-facade/docs/0001-inner-workings.md",
    "content": "# Inner workings of Mainframe Banking System Facade\n\nThis is how the facade works.\n"
  },
  {
    "path": "docs/example/internet-banking-system/database/adr/0004-using-oracle-database-schema.md",
    "content": "# 4. Using Oracle database schema\n\nDate: 2025-11-13\n\n## Status\n\nAccepted\n\n## Context\n\nThe issue motivating this decision, and any context that influences or constrains the decision.\n\n## Decision\n\nThe change that we're proposing or have agreed to implement.\n\n## Consequences\n\nWhat becomes easier or more difficult to do and any risks introduced by the change that will need to be mitigated.\n"
  },
  {
    "path": "docs/example/internet-banking-system/database/docs/0002-guide.md",
    "content": "# Usage\n\nThis is how we use this thing.\n"
  },
  {
    "path": "docs/example/internet-banking-system/docs/0000-introduction.md",
    "content": "# Description\n\nThis is our fancy Internet Banking System.\n\n## Vision\n\nOne system to rule them all!\n\n## References\n\n- Source Code on GitHub: [https://github.com/avisi-cloud/structurizr-site-generatr]\n"
  },
  {
    "path": "docs/example/internet-banking-system/docs/0001-history.md",
    "content": "# History\n\nSome notes how we got to the current state.\n"
  },
  {
    "path": "docs/example/internet-banking-system/docs/0002-guide.md",
    "content": "# Usage\n\nThis is how we use this thing.\n"
  },
  {
    "path": "docs/example/workspace-adrs/0001-record-architecture-decisions.md",
    "content": "# 1. Record architecture decisions\n\nDate: 2022-06-21\n\n## Status\n\nAccepted\n\n## Context\n\nWe need to record the architectural decisions made on this project.\n\n## Decision\n\nWe will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions).\n\n## Consequences\n\nSee Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools).\n"
  },
  {
    "path": "docs/example/workspace-adrs/0002-implement-feature-1.md",
    "content": "# 2. Implement Feature 1\n\nDate: 2024-12-17\n\n## Status\n\nSuperseded by [3. Another Realisation of Feature 1](0003-another-realisation-of-feature-1.md)\n\n## Context\n\nThe issue motivating this decision, and any context that influences or constrains the decision.\n\n## Decision\n\nThe change that we're proposing or have agreed to implement.\n\n## Consequences\n\nWhat becomes easier or more difficult to do and any risks introduced by the change that will need to be mitigated.\n"
  },
  {
    "path": "docs/example/workspace-adrs/0003-another-realisation-of-feature-1.md",
    "content": "# 3. Another Realisation of Feature 1\n\nDate: 2024-12-17\n\n## Status\n\nAccepted\n\nSupersedes [2. Implement Feature 1](0002-implement-feature-1.md)\n\n## Context\n\nThe issue motivating this decision, and any context that influences or constrains the decision.\n\n## Decision\n\nThe change that we're proposing or have agreed to implement.\n\n## Consequences\n\nWhat becomes easier or more difficult to do and any risks introduced by the change that will need to be mitigated.\n"
  },
  {
    "path": "docs/example/workspace-adrs/0004-using-oracle-database-schema.md",
    "content": "# 4. Using Oracle database schema\n\nDate: 2025-11-13\n\n## Status\n\nAccepted\n\n## Context\n\nThe issue motivating this decision, and any context that influences or constrains the decision.\n\n## Decision\n\nThe change that we're proposing or have agreed to implement.\n\n## Consequences\n\nWhat becomes easier or more difficult to do and any risks introduced by the change that will need to be mitigated.\n"
  },
  {
    "path": "docs/example/workspace-docs/00-index.md",
    "content": "## Big Bank plc architecture\n\nThis site contains the C4 architecture model for Big Bank plc.\n\nThis page is the home page, because it is the first file in the `workspace-docs` directory, when the files are sorted\nalphabetically.\n"
  },
  {
    "path": "docs/example/workspace-docs/01-embedding-diagrams-and-images.md",
    "content": "## Embedding diagrams and images\n\nThis page showcases the ability to embed diagrams and static images in documentation.\n\n### Embedding diagrams\n\nDiagrams can be embedded using the `embed:` syntax:\n\n```markdown\n![System Landscape Diagram](embed:SystemLandscape)\n```\n\nSee also: <https://www.structurizr.com/help/documentation/diagrams>\n\n#### Example: Embedded diagram\n\n![System Landscape Diagram](embed:SystemLandscape)\n\n### Embedding static images\n\nStatic assets can be included in the generated site, using the `--assets-dir` command-line flag. This flag can be used\nwith the `serve` command and the `generate-site` command.\n\nWhen, for example, you would like to embed a nice picture which is located in the `pictures` directory under the assets\ndirectory, you can do that as follows:\n\n```markdown\n![A nice picture](/pictures/nice-picture.png)\n```\n\n#### Example: Embedded picture\n\n[Sun](https://www.flickr.com/photos/schmollmolch/4937297813/), by Christian Scheja\n\n![A nice picture](/pictures/nice-picture.png)\n\n### Embedding PlantUML diagrams\n\nStructurizr Site Generatr supports rendering PlantUML embedded in Markdown files.\n\n#### Sequence diagram example\n\n````markdown\n```puml\n@startuml\nFoo -> Bar: doSomething()\n@enduml\n```\n````\n\n```puml\n@startuml\nFoo -> Bar: doSomething()\n@enduml\n```\n\n### Class diagram example\n\n````markdown\n```puml\n@startuml\nclass Foo {\n    +property: String\n    +foo()\n}\n\nclass Bar {\n    -privateProperty: String\n    +bar()\n}\n\nFoo ..> Bar: Uses\n@enduml\n```\n````\n\n```puml\n@startuml\nclass Foo {\n    +property: String\n    +foo()\n}\n\nclass Bar {\n    -privateProperty: String\n    +bar()\n}\n\nFoo ..> Bar: Uses\n@enduml\n```\n\n### Embedding mermaid diagrams\n\nStructurizr Site Generatr is supporting mermaid diagrams in markdown pages using the actual mermaid.js version.\nTherefore every diagram type, supported by mermaid may be used in markdown documentation files.\n\n* flowchart\n* sequence diagram\n* class diagram\n* state diagram\n* entity-relationship diagram\n* user journey\n* gantt chart\n* pie chart\n* requirement diagram\n* and some more\n\nPlease find the full list of supported chart types\non [mermaid.js.org/intro](https://mermaid.js.org/intro/#diagram-types)\n\n#### Flowchart Diagram Example\n\n````markdown\n```mermaid\ngraph TD;\n  A-->B;\n  A-->C;\n  B-->D;\n  C-->D;\n```\n````\n\n```mermaid\ngraph TD;\n  A-->B;\n  A-->C;\n  B-->D;\n  C-->D;\n```\n\n#### Sequence Diagram Example\n\n````markdown\n```mermaid\nsequenceDiagram\n    participant Alice\n    participant Bob\n    Alice->>John: Hello John, how are you?\n    loop Healthcheck\n        John->>John: Fight against hypochondria\n    end\n    Note right of John: Rational thoughts <br/>prevail!\n    John-->>Alice: Great!\n    John->>Bob: How about you?\n    Bob-->>John: Jolly good!\n```\n````\n\n```mermaid\nsequenceDiagram\n    participant Alice\n    participant Bob\n    Alice->>John: Hello John, how are you?\n    loop Healthcheck\n        John->>John: Fight against hypochondria\n    end\n    Note right of John: Rational thoughts <br/>prevail!\n    John-->>Alice: Great!\n    John->>Bob: How about you?\n    Bob-->>John: Jolly good!\n```\n"
  },
  {
    "path": "docs/example/workspace-docs/02-markdown-features.md",
    "content": "## Extended Markdown features\n\nThis 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).\n\nMost of these extended features have to be activated in your architecture model as a property in workspace views.\n\n```DSL\nworkspace {\n    ...\n    views {\n        ...\n        properties {\n            ...\n            // full list of available \"generatr.markdown.flexmark.extensions\":\n            // - Abbreviation\n            // - Admonition\n            // - AnchorLink\n            // - Aside\n            // - Attributes\n            // - Autolink\n            // - Definition\n            // - Emoji\n            // - EnumeratedReference\n            // - Footnotes\n            // - GfmIssues\n            // - GfmStrikethroughSubscript\n            // - GfmTaskList\n            // - GfmUsers\n            // - GitLab\n            // - Ins\n            // - Macros\n            // - MediaTags\n            // - ResizableImage\n            // - Superscript\n            // - Tables\n            // - TableOfContents\n            // - SimulatedTableOfContents\n            // - Typographic\n            // - WikiLinks\n            // - XWikiMacro\n            // - YAMLFrontMatter\n            // - YouTubeLink\n            // see https://github.com/vsch/flexmark-java/wiki/Extensions\n            // ATTENTION:\n            // * \"generatr.markdown.flexmark.extensions\" values must be separated by comma\n            // * it's not possible to use \"GitLab\" and \"ResizableImage\" extensions together\n            // default behaviour, if no generatr.markdown.flexmark.extensions property is specified, is to load the Tables extension only\n            \"generatr.markdown.flexmark.extensions\" \"Abbreviation,Admonition,AnchorLink,Attributes,Autolink,Definition,Emoji,Footnotes,GfmTaskList,GitLab,MediaTags,Tables,TableOfContents,Typographic\"\n            ...\n        }\n        ...\n    }\n    ...\n}\n```\n\n### Table of Contents\n\n`[TOC]` element which renders a table of contents\n\n```markdown\n[TOC]\n```\n\nwill render into\n\n[TOC]\n\n#### Usage info\n\n\"TableOfContents\" is an optional feature, that has to be activated in workspace views properties\n\n```DSL\n    \"generatr.markdown.flexmark.extensions\" \"TableOfContents\"\n```\n\n### Render Tables\n\nStandard tables can be embedded in markdown text syntax:\n\n```markdown\n| header1 | header2 |\n| ------- | ------- |\n| content | content |\n```\n\nThis will be rendered as\n\n| header1 | header2 |\n| ------- | ------- |\n| content | content |\n\n#### Usage info\n\n\"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.\n\n```DSL\n    \"generatr.markdown.flexmark.extensions\" \"Tables\"\n```\n\n### Admonition Blocks\n\nAdmonitions create block-styled side content.\n\n```markdown\n!!! faq \"FAQ\"\n    This is a FAQ.\n!!! attention \"Warning\"\n    This is a warning message\n\n!!! info \"information\"\n    this is an additional information\n```\n\nThis will be rendered as\n\n!!! faq \"FAQ\"\n    This is a FAQ.\n!!! attention \"Warning\"\n    This is a warning message\n\n!!! info \"information\"\n    this is an additional information\n\n#### Usage info\n\n\"Admonition Blocks\" is an optional feature, that has to be activated in workspace views properties\n\n```DSL\n    \"generatr.markdown.flexmark.extensions\" \"Admonition\"\n```\n\n### GitLab flavored markdown extensions\n\nPlease see [GitLab flavored markdown features](https://docs.gitlab.com/ee/user/markdown.html?tab=Rendered+Markdown) for a detailed description.\nUnfortunately only the following features are supported by Flexmark markdown renderer, that is used here.\n\n#### Multiline Block quote delimiters\n\n```markdown\n>>>\nIf you paste a message from somewhere else\n\nthat spans multiple lines,\n\nyou can quote that without having to manually prepend `>` to every line!\n>>>\n```\n\n>>>\nIf you paste a message from somewhere else\n\nthat spans multiple lines,\n\nyou can quote that without having to manually prepend `>` to every line!\n>>>\n\n#### Inline diff (deletions and additions)\n\nWith inline diff tags, you can display `{+ additions +}` or `[- deletions -]`.\n\nThe wrapping tags can be either curly braces or square brackets:\n\n```markdown\n- {+ addition 1 +}\n- [+ addition 2 +]\n- {- deletion 3 -}\n- [- deletion 4 -]\n```\n\n- {+ addition 1 +}\n- [+ addition 2 +]\n- {- deletion 3 -}\n- [- deletion 4 -]\n\n#### Math\n\nMath written in LaTeX syntax is rendered with [KaTeX](https://github.com/KaTeX/KaTeX).\n_KaTeX only supports a [subset](https://katex.org/docs/supported.html) of LaTeX._\n\nMath written between dollar signs with backticks (``$`...`$``) or single dollar signs (`$...$`)\nis rendered inline with the text.\n\nMath written between double dollar signs (`$$...$$`) or in a code block with\nthe language declared as `math` is rendered on a separate line:\n\n````markdown\nThis math is inline: $`a^2+b^2=c^2`$.\n\nThis math is on a separate line using a ```` ```math ```` block:\n\n```math\na^2+b^2=c^2\n```\n````\n\nThis math is inline: $`a^2+b^2=c^2`$.\n\nThis math is on a separate line using a ```` ```math ```` block:\n\n```math\na^2+b^2=c^2\n```\n\n#### Usage info\n\n\"GitLab Flavored Markdown\" is an optional feature, that has to be activated in workspace views properties\n\n```DSL\n    \"generatr.markdown.flexmark.extensions\" \"GitLab\"\n```\n\n### AnchorLink\n\nAutomatically adds anchor links to headings, using GitHub id generation algorithm\n\n#### Usage info\n\n\"AnchorLink\" is an optional feature, that has to be activated in workspace views properties\n\n```DSL\n    \"generatr.markdown.flexmark.extensions\" \"AnchorLink\"\n```\n\n### Definition Lists\n\nConverts definition syntax of Php Markdown Extra Definition List to `<dl></dl>` HTML and corresponding AST nodes.\n\n```markdown\nDefinition Term\n: Definition of above term\n: Another definition of above term\n```\n\nDefinition Term\n: Definition of above term\n: Another definition of above term\n\n#### Usage info\n\n\"Definition Lists\" is an optional feature, that has to be activated in workspace views properties\n\n```DSL\n    \"generatr.markdown.flexmark.extensions\" \"Definition\"\n```\n\n### Emoji\n\nAllows 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)\n\n```markdown\nthumbsup :thumbsup:  \ncalendar :calendar:  \nwarning :warning:  \n```\n\nthumbsup :thumbsup:  \ncalendar :calendar:  \nwarning :warning:  \n\n#### Usage info\n\n\"Emoji\" is an optional feature, that has to be activated in workspace views properties\n\n```DSL\n    \"generatr.markdown.flexmark.extensions\" \"Emoji\"\n```\n\n### GfmTaskList\n\nEnables list items based task lists whose text begins with: `[ ]`, `[x]` or `[X]`\n\n```markdown\n- [x] Completed task\n- [ ] Incomplete task\n  - [x] Sub-task 1\n  - [ ] Sub-task 3\n\n1. [x] Completed task\n1. [ ] Incomplete task\n   1. [x] Sub-task 1\n   1. [ ] Sub-task 3\n```\n\nwill be rendered as\n\n- [x] Completed task\n- [ ] Incomplete task\n  - [x] Sub-task 1\n  - [ ] Sub-task 3\n\n1. [x] Completed task\n1. [ ] Incomplete task\n   1. [x] Sub-task 1\n   1. [ ] Sub-task 3\n\n#### Usage info\n\n\"GfmTaskList\" is an optional feature, that has to be activated in workspace views properties\n\n```DSL\n    \"generatr.markdown.flexmark.extensions\" \"GfmTaskList\"\n```\n\n### Links to Markdown documents\n\nPlacing links on markdown pages is easy. You can link to:\n\n* an overview page like [Workspace decisions](/decisions/),\n* an individual decision [ADR-0001. record architecture decisions](/decisions/1/)\n* or some system documentation like [Internet Banking](/internet-banking-system/).\n"
  },
  {
    "path": "docs/example/workspace-docs/03-asciidoc-features.adoc",
    "content": "= AsciiDoc features\n:toc: macro\n:imagesdir: ../assets\n:tip-caption: 💡Tip\n\n== AsciiDoc features 📌\n\nThis 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].\n\ntoc::[]\n\n=== Embedding diagrams\n\nDiagrams can be embedded using the `embed:` syntax:\n\n[source, asciidoc]\n----\nimage::embed:SystemLandscape[System Landscape Diagram]\n----\n\nSee also: https://www.structurizr.com/help/documentation/diagrams\n\n==== Example: Embedded diagram\n\nimage::embed:SystemLandscape[System Landscape Diagram]\n\n=== Embedding static images\n\n==== Example Embedded picture\n\nWhen, 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:\n\n[source, asciidoc]\n----\nimage::/pictures/nice-picture.png[A nice picture]\n----\n\nhttps://www.flickr.com/photos/schmollmolch/4937297813/[Sun], by Christian Scheja\n\nimage::/pictures/nice-picture.png[A nice picture]\n\n=== Embedding PlantUML diagrams\n\n==== Sequence Diagram Example\n\n[source, asciidoc]\n-----\n[plantuml]\n----\n@startuml\nFoo -> Bar: doSomething()\n@enduml\n----\n-----\n\n[plantuml]\n----\n@startuml\nFoo -> Bar: doSomething()\n@enduml\n----\n\n==== Class Diagram Example\n\n[source, asciidoc]\n-----\n[plantuml]\n----\n@startuml\nclass Foo {\n    +property: String\n    +foo()\n}\n\nclass Bar {\n    -privateProperty: String\n    +bar()\n}\n\nFoo ..> Bar: Uses\n@enduml\n----\n-----\n\n[plantuml]\n----\n@startuml\nclass Foo {\n    +property: String\n    +foo()\n}\n\nclass Bar {\n    -privateProperty: String\n    +bar()\n}\n\nFoo ..> Bar: Uses\n@enduml\n----\n\n=== Embedding mermaid diagrams\n\n==== Flowchart Diagram Example\n\n[source, asciidoc]\n-----\n[source, mermaid]\n----\ngraph TD;\nA-->B;\nA-->C;\nB-->D;\nC-->D;\n----\n-----\n\n[source, mermaid]\n----\ngraph TD;\n  A-->B;\n  A-->C;\n  B-->D;\n  C-->D;\n----\n\n==== Sequence Diagram Example\n\n[source, asciidoc]\n-----\n[source, mermaid]\n----\nsequenceDiagram\n    participant Alice\n    participant Bob\n    Alice->>John: Hello John, how are you?\n    loop Healthcheck\n        John->>John: Fight against hypochondria\n    end\n    Note right of John: Rational thoughts <br/>prevail!\n    John-->>Alice: Great!\n    John->>Bob: How about you?\n    Bob-->>John: Jolly good!\n----\n-----\n\n[source, mermaid]\n----\nsequenceDiagram\n    participant Alice\n    participant Bob\n    Alice->>John: Hello John, how are you?\n    loop Healthcheck\n        John->>John: Fight against hypochondria\n    end\n    Note right of John: Rational thoughts <br/>prevail!\n    John-->>Alice: Great!\n    John->>Bob: How about you?\n    Bob-->>John: Jolly good!\n----\n\n=== Tables\n\n[source, asciidoc]\n----\n|===\n|Column 1, Header Row |Column 2, Header Row\n\n|Cell in column 1, row 1\n|Cell in column 2, row 1\n\n|Cell in column 1, row 2\n|Cell in column 2, row 2\n|===\n----\n\nThis will be rendered as\n\n|===\n|Column 1, Header Row |Column 2, Header Row\n\n|Cell in column 1, row 1\n|Cell in column 2, row 1\n\n|Cell in column 1, row 2\n|Cell in column 2, row 2\n|===\n\n=== Admonition Blocks\n\nAdmonitions create block-styled side content.\n\nNOTE: This is a note.\n\n[TIP]\n.Info\n=====\nGo to this URL to learn more about it:\n\n* https://docs.asciidoctor.org/asciidoc/latest/blocks/admonitions/\n\nCAUTION: This is Caution message!\n\nWARNING: This is a Warning message!\n=====\n\n[IMPORTANT]\nOne more thing. Happy documenting!\n\n=== Block quotes\n\n[quote,attribution,citation title and information]\nQuote or excerpt text\n\n=== Checklist\n\n[source, asciidoc]\n----\n* [*] checked\n* [x] also checked\n* [ ] not checked\n* normal list item\n----\n\nwill be rendered as:\n\n* [*] checked\n* [x] also checked\n* [ ] not checked\n* normal list item\n"
  },
  {
    "path": "docs/example/workspace.dsl",
    "content": "/*\n * This is a combined version of the following workspaces:\n *\n * - \"Big Bank plc - System Landscape\" (https://structurizr.com/share/28201/)\n * - \"Big Bank plc - Internet Banking System\" (https://structurizr.com/share/36141/)\n*/\nworkspace \"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.\" {\n    !docs workspace-docs\n    !adrs workspace-adrs\n\n    model {\n        properties {\n            \"structurizr.groupSeparator\" \"/\"\n        }\n\n        customer = person \"Personal Banking Customer\" \"A customer of the bank, with personal bank accounts.\" \"Customer\"\n\n        acquirer = softwaresystem \"Acquirer\" \"Facilitates PIN transactions for merchants.\" \"External System\"\n\n        group \"Big Bank plc\" {\n            supportStaff = person \"Customer Service Staff\" \"Customer service staff within the bank.\" \"Bank Staff\" {\n                properties {\n                    \"Location\" \"Customer Services\"\n                }\n            }\n            backoffice = person \"Back Office Staff\" \"Administration and support staff within the bank.\" \"Bank Staff\" {\n                properties {\n                    \"Location\" \"Internal Services\"\n                }\n            }\n\n            mainframe = softwaresystem \"Mainframe Banking System\" \"Stores all of the core banking information about customers, accounts, transactions, etc.\" \"Existing System\"\n            email = softwaresystem \"E-mail System\" \"The internal Microsoft Exchange e-mail system.\" \"Existing System\"\n            atm = softwaresystem \"ATM\" \"Allows customers to withdraw cash.\" \"Existing System\"\n\n            internetBankingSystem = softwaresystem \"Internet Banking System\" \"Allows customers to view information about their bank accounts, and make payments.\" {\n                !adrs internet-banking-system/adr\n                !docs internet-banking-system/docs\n                properties {\n                    \"Owner\" \"Customer Services\"\n                    \"Development Team\" \"Dev/Internet Services\"\n                }\n                url https://en.wikipedia.org/wiki/Online_banking\n\n                singlePageApplication = container \"Single-Page Application\" \"Provides all of the Internet banking functionality to customers via their web browser.\" \"JavaScript and Angular\" \"Web Browser\"\n                mobileApp = container \"Mobile App\" \"Provides a limited subset of the Internet banking functionality to customers via their mobile device.\" \"Xamarin\" \"Mobile App\"\n                webApplication = container \"Web Application\" \"Delivers the static content and the Internet banking single page application.\" \"Java and Spring MVC\"\n                apiApplication = container \"API Application\" \"Provides Internet banking functionality via a JSON/HTTPS API.\" \"Java and Spring MVC\" {\n                    properties {\n                        Owner \"Team 1\"\n                    }\n                    signinController = component \"Sign In Controller\" \"Allows users to sign in to the Internet Banking System.\" \"Spring MVC Rest Controller\"\n                    accountsSummaryController = component \"Accounts Summary Controller\" \"Provides customers with a summary of their bank accounts.\" \"Spring MVC Rest Controller\"\n                    resetPasswordController = component \"Reset Password Controller\" \"Allows users to reset their passwords with a single use URL.\" \"Spring MVC Rest Controller\"\n                    securityComponent = component \"Security Component\" \"Provides functionality related to signing in, changing passwords, etc.\" \"Spring Bean\"\n                    mainframeBankingSystemFacade = component \"Mainframe Banking System Facade\" \"A facade onto the mainframe banking system.\" \"Spring Bean\" {\n                        !adrs internet-banking-system/api-application/mainframe-banking-system-facade/adr\n                        !docs internet-banking-system/api-application/mainframe-banking-system-facade/docs\n                    }\n                    emailComponent = component \"E-mail Component\" \"Sends e-mails to users.\" \"Spring Bean\" {\n                        !adrs internet-banking-system/api-application/email-component/adr\n                        !docs internet-banking-system/api-application/email-component/docs\n                    }\n                }\n                database = container \"Database\" \"Stores user registration information, hashed authentication credentials, access logs, etc.\" \"Oracle Database Schema\" \"Database\" {\n                    !adrs internet-banking-system/database/adr\n                    !docs internet-banking-system/database/docs\n                }\n            }\n        }\n\n        # relationships between people and software systems\n        customer -> internetBankingSystem \"Views account balances, and makes payments using\"\n        internetBankingSystem -> mainframe \"Gets account information from, and makes payments using\"\n        internetBankingSystem -> email \"Sends e-mail using\"\n        email -> customer \"Sends e-mails to\"\n        customer -> supportStaff \"Asks questions to\" \"Telephone\"\n        supportStaff -> mainframe \"Uses\"\n        customer -> atm \"Withdraws cash using\"\n        atm -> mainframe \"Uses\"\n        backoffice -> mainframe \"Uses\"\n\n        acquirer -> mainframe \"Peforms clearing and settlement\"\n\n        # relationships to/from containers\n        customer -> webApplication \"Visits bigbank.com/ib using\" \"HTTPS\"\n        customer -> singlePageApplication \"Views account balances, and makes payments using\"\n        customer -> mobileApp \"Views account balances, and makes payments using\"\n        webApplication -> singlePageApplication \"Delivers to the customer's web browser\"\n\n        # relationships to/from components\n        singlePageApplication -> signinController \"Makes API calls to\" \"JSON/HTTPS\"\n        singlePageApplication -> accountsSummaryController \"Makes API calls to\" \"JSON/HTTPS\"\n        singlePageApplication -> resetPasswordController \"Makes API calls to\" \"JSON/HTTPS\"\n        mobileApp -> signinController \"Makes API calls to\" \"JSON/HTTPS\"\n        mobileApp -> accountsSummaryController \"Makes API calls to\" \"JSON/HTTPS\"\n        mobileApp -> resetPasswordController \"Makes API calls to\" \"JSON/HTTPS\"\n        signinController -> securityComponent \"Uses\"\n        accountsSummaryController -> mainframeBankingSystemFacade \"Uses\"\n        resetPasswordController -> securityComponent \"Uses\"\n        resetPasswordController -> emailComponent \"Uses\"\n        securityComponent -> database \"Reads from and writes to\" \"JDBC\"\n        mainframeBankingSystemFacade -> mainframe \"Makes API calls to\" \"XML/HTTPS\"\n        emailComponent -> email \"Sends e-mail using\"\n\n        deploymentEnvironment \"Development\" {\n            deploymentNode \"Developer Laptop\" \"\" \"Microsoft Windows 10 or Apple macOS\" {\n                deploymentNode \"Web Browser\" \"\" \"Chrome, Firefox, Safari, or Edge\" {\n                    developerSinglePageApplicationInstance = containerInstance singlePageApplication\n                }\n                deploymentNode \"Docker Container - Web Server\" \"\" \"Docker\" {\n                    deploymentNode \"Apache Tomcat\" \"\" \"Apache Tomcat 8.x\" {\n                        developerWebApplicationInstance = containerInstance webApplication\n                        developerApiApplicationInstance = containerInstance apiApplication\n                    }\n                }\n                deploymentNode \"Docker Container - Database Server\" \"\" \"Docker\" {\n                    deploymentNode \"Database Server\" \"\" \"Oracle 12c\" {\n                        developerDatabaseInstance = containerInstance database\n                    }\n                }\n            }\n            deploymentNode \"Big Bank plc\" \"\" \"Big Bank plc data center\" \"\" {\n                deploymentNode \"bigbank-dev001\" \"\" \"\" \"\" {\n                    softwareSystemInstance mainframe\n                }\n            }\n\n        }\n\n        deploymentEnvironment \"Live\" {\n            deploymentNode \"Customer's mobile device\" \"\" \"Apple iOS or Android\" {\n                liveMobileAppInstance = containerInstance mobileApp\n            }\n            deploymentNode \"Customer's computer\" \"\" \"Microsoft Windows or Apple macOS\" {\n                deploymentNode \"Web Browser\" \"\" \"Chrome, Firefox, Safari, or Edge\" {\n                    liveSinglePageApplicationInstance = containerInstance singlePageApplication\n                }\n            }\n\n            deploymentNode \"Big Bank plc\" \"\" \"Big Bank plc data center\" {\n                deploymentNode \"bigbank-web***\" \"\" \"Ubuntu 16.04 LTS\" \"\" 4 {\n                    deploymentNode \"Apache Tomcat\" \"\" \"Apache Tomcat 8.x\" {\n                        liveWebApplicationInstance = containerInstance webApplication\n                    }\n                }\n                deploymentNode \"bigbank-api***\" \"\" \"Ubuntu 16.04 LTS\" \"\" 8 {\n                    deploymentNode \"Apache Tomcat\" \"\" \"Apache Tomcat 8.x\" {\n                        liveApiApplicationInstance = containerInstance apiApplication\n                    }\n                }\n\n                deploymentNode \"bigbank-db01\" \"\" \"Ubuntu 16.04 LTS\" {\n                    primaryDatabaseServer = deploymentNode \"Oracle - Primary\" \"\" \"Oracle 12c\" {\n                        livePrimaryDatabaseInstance = containerInstance database\n                    }\n                }\n                deploymentNode \"bigbank-db02\" \"\" \"Ubuntu 16.04 LTS\" \"Failover\" {\n                    secondaryDatabaseServer = deploymentNode \"Oracle - Secondary\" \"\" \"Oracle 12c\" \"Failover\" {\n                        liveSecondaryDatabaseInstance = containerInstance database \"Failover\"\n                    }\n                }\n                deploymentNode \"bigbank-prod001\" \"\" \"\" \"\" {\n                    softwareSystemInstance mainframe\n                }\n            }\n\n            primaryDatabaseServer -> secondaryDatabaseServer \"Replicates data to\"\n        }\n\n        deploymentEnvironment \"Environment Landscape\" {\n            deploymentNode \"bigbank-prod001\" {\n                softwareSystemInstance mainframe\n            }\n            deploymentNode \"bigbank-preprod001\" {\n                softwareSystemInstance mainframe\n            }\n            deploymentNode \"bigbank-test001\" {\n                softwareSystemInstance mainframe\n            }\n            deploymentNode \"bigbank-staging1\" {\n                softwareSystemInstance email\n            }\n            deploymentNode \"bigbank-prod1\" {\n                softwareSystemInstance email\n            }\n        }\n    }\n\n    views {\n        properties {\n            \"c4plantuml.elementProperties\" \"true\"\n            \"c4plantuml.tags\" \"true\"\n            \"generatr.style.colors.primary\" \"#485fc7\"\n            \"generatr.style.colors.secondary\" \"#ffffff\"\n            \"generatr.style.faviconPath\" \"site/favicon.ico\"\n            \"generatr.style.logoPath\" \"site/logo.png\"\n\n            // Absolute URL's like \"https://example.com/custom.css\" are also supported\n            \"generatr.style.customStylesheet\" \"site/custom.css\"\n\n            \"generatr.svglink.target\" \"_self\"\n\n            // Full list of available \"generatr.markdown.flexmark.extensions\"\n            // \"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\"\n            // see https://github.com/vsch/flexmark-java/wiki/Extensions\n            // ATTENTION:\n            // * \"generatr.markdown.flexmark.extensions\" values must be separated by comma\n            // * it's not possible to use \"GitLab\" and \"ResizableImage\" extensions together\n            // default behaviour, if no generatr.markdown.flexmark.extensions property is specified, is to load the Tables extension only\n            \"generatr.markdown.flexmark.extensions\" \"Abbreviation,Admonition,AnchorLink,Attributes,Autolink,Definition,Emoji,Footnotes,GfmTaskList,GitLab,MediaTags,Tables,TableOfContents,Typographic\"\n\n            \"generatr.site.exporter\" \"structurizr\"\n            \"generatr.site.externalTag\" \"External System\"\n            \"generatr.site.nestGroups\" \"false\"\n            \"generatr.site.cdn\" \"https://cdn.jsdelivr.net/npm\"\n            \"generatr.site.theme\" \"auto\"\n        }\n\n        systemlandscape \"SystemLandscape\" {\n            include *\n            autoLayout\n        }\n\n        image atm {\n            image atm/atm-example.png\n            title \"ATM System\"\n            description \"Image View to show how the ATM system works internally\"\n        }\n\n        systemcontext internetBankingSystem \"SystemContext\" {\n            include *\n            animation {\n                internetBankingSystem\n                customer\n                mainframe\n                email\n            }\n            autoLayout\n            title \"System Context of Internet Banking System\"\n            description \"Describes the overall context\"\n        }\n\n        container internetBankingSystem \"Containers\" {\n            include *\n            animation {\n                customer mainframe email\n                webApplication\n                singlePageApplication\n                mobileApp\n                apiApplication\n                database\n            }\n            autoLayout\n        }\n\n        component apiApplication \"Components\" {\n            include *\n            animation {\n                singlePageApplication mobileApp database email mainframe\n                signinController securityComponent\n                accountsSummaryController mainframeBankingSystemFacade\n                resetPasswordController emailComponent\n            }\n            autoLayout\n        }\n\n        image database {\n            image internet-banking-system/database-erd-example.jpg\n            title \"Entity Relationship Diagram\"\n            description \"Image View to show the ERD diagram for the database container\"\n        }\n\n        image accountsSummaryController {\n            image internet-banking-system/uml-class-diagram.png\n            title \"AccountsSummaryController Zoom-In\"\n            description \"This is a sample imageView for code of a component\"\n        }\n\n        dynamic apiApplication \"SignIn\" \"Summarises how the sign in feature works in the single-page application.\" {\n            singlePageApplication -> signinController \"Submits credentials to\"\n            signinController -> securityComponent \"Validates credentials using\"\n            securityComponent -> database \"select * from users where username = ?\"\n            database -> securityComponent \"Returns user data to\"\n            securityComponent -> signinController \"Returns true if the hashed password matches\"\n            signinController -> singlePageApplication \"Sends back an authentication token to\"\n            autoLayout\n        }\n\n        deployment internetBankingSystem \"Development\" \"DevelopmentDeployment\" {\n            include *\n            animation {\n                developerSinglePageApplicationInstance\n                developerWebApplicationInstance developerApiApplicationInstance\n                developerDatabaseInstance\n            }\n            autoLayout\n        }\n\n        deployment internetBankingSystem \"Live\" \"LiveDeployment\" {\n            include *\n            animation {\n                liveSinglePageApplicationInstance\n                liveMobileAppInstance\n                liveWebApplicationInstance liveApiApplicationInstance\n                livePrimaryDatabaseInstance\n                liveSecondaryDatabaseInstance\n            }\n            autoLayout\n        }\n\n        deployment * \"Environment Landscape\" \"EnvLandscapeMainframe\" {\n            include mainframe\n            autoLayout\n            properties {\n                \"generatr.view.deployment.belongsTo\" \"Mainframe Banking System\"\n            }\n        }\n\n        styles {\n            element \"Person\" {\n                color #ffffff\n                shape Person\n            }\n            element \"Customer\" {\n                background #686868\n            }\n            element \"Bank Staff\" {\n                background #08427B\n            }\n            element \"Software System\" {\n                background #1168bd\n                color #ffffff\n            }\n            element \"External System\" {\n                background #686868\n            }\n            element \"Existing System\" {\n                background #999999\n                color #ffffff\n            }\n            element \"Container\" {\n                background #438dd5\n                color #ffffff\n            }\n            element \"Web Browser\" {\n                shape WebBrowser\n            }\n            element \"Mobile App\" {\n                shape MobileDeviceLandscape\n            }\n            element \"Database\" {\n                shape Cylinder\n            }\n            element \"Component\" {\n                background #85bbf0\n                color #000000\n            }\n            element \"Failover\" {\n                opacity 25\n            }\n\n            element \"Group\" {\n                background #f1f1f1\n                color #444444\n            }\n            element \"Boundary\" {\n                background #f1f1f1\n                color #444444\n            }\n\n            // default style\n            element \"Element\" {\n                fontSize 24\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-9.4.1-bin.zip\nnetworkTimeout=10000\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# SPDX-License-Identifier: Apache-2.0\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\n# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)\nAPP_HOME=$( cd -P \"${APP_HOME:-./}\" > /dev/null && printf '%s\\n' \"$PWD\" ) || exit\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    if ! command -v java >/dev/null 2>&1\n    then\n        die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Collect all arguments for the java command:\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,\n#     and any embedded shellness will be escaped.\n#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be\n#     treated as '${Hostname}' itself on the command line.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -jar \"$APP_HOME/gradle/wrapper/gradle-wrapper.jar\" \\\n        \"$@\"\n\n# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n    die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n@rem SPDX-License-Identifier: Apache-2.0\r\n@rem\r\n\r\n@if \"%DEBUG%\"==\"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\r\n@rem This is normally unused\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif %ERRORLEVEL% equ 0 goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\n\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -jar \"%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\" %*\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif %ERRORLEVEL% equ 0 goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nset EXIT_CODE=%ERRORLEVEL%\r\nif %EXIT_CODE% equ 0 set EXIT_CODE=1\r\nif not \"\"==\"%GRADLE_EXIT_CONSOLE%\" exit %EXIT_CODE%\r\nexit /b %EXIT_CODE%\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "package.json",
    "content": "{\n    \"name\": \"structurizr-site-generatr\",\n    \"dependencies\": {\n        \"bulma\": \"1.0.4\",\n        \"katex\": \"0.16.33\",\n        \"lunr\": \"2.3.9\",\n        \"lunr-languages\": \"1.14.0\",\n        \"mermaid\": \"11.12.3\",\n        \"svg-pan-zoom\": \"3.6.2\",\n        \"webfontloader\": \"1.6.28\"\n    }\n}\n"
  },
  {
    "path": "renovate.json",
    "content": "{\n  \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n  \"extends\": [\"config:recommended\"],\n  \"prHourlyLimit\": 2,\n  \"prConcurrentLimit\": 6,\n  \"packageRules\": [\n    {\n      \"groupName\": \"kotlin\",\n      \"matchPackageNames\": [\n        \"org.jetbrains.kotlin.jvm{/,}**\",\n        \"org.jetbrains.kotlin.plugin.serialization{/,}**\",\n        \"org.jetbrains.kotlin:{/,}**\"\n      ]\n    },\n    {\n      \"groupName\": \"kotlinx\",\n      \"matchPackageNames\": [\"org.jetbrains.kotlinx:{/,}**\"]\n    },\n    {\n      \"groupName\": \"structurizr\",\n      \"matchPackageNames\": [\"com.structurizr:{/,}**\"]\n    },\n    {\n      \"groupName\": \"jetty\",\n      \"matchPackageNames\": [\"org.eclipse.jetty:{/,}**\", \"org.eclipse.jetty.websocket:{/,}**\"]\n    },\n    {\n      \"matchDepNames\": \"net.sourceforge.plantuml:plantuml\",\n      \"allowedVersions\": \"/^[0-9]+\\\\.[0-9]+\\\\.[0-9]+$/\"\n    },\n    {\n      \"matchDepNames\": \"org.jruby:jruby-core\",\n      \"allowedVersions\": \"^9\"\n    },\n    {\n      \"groupName\": \"actions\",\n      \"matchDepTypes\": [\"action\"]\n    }\n  ]\n}\n"
  },
  {
    "path": "settings.gradle.kts",
    "content": "rootProject.name = \"structurizr-site-generatr\"\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/App.kt",
    "content": "@file:Suppress(\"EXPERIMENTAL_IS_NOT_ENABLED\")\n@file:OptIn(ExperimentalCli::class)\n\npackage nl.avisi.structurizr.site.generatr\n\nimport kotlinx.cli.ArgParser\nimport kotlinx.cli.ExperimentalCli\n\nfun main(args: Array<String>) {\n    val parser = ArgParser(\"structurizr-site-generatr\")\n\n    parser.subcommands(ServeCommand(), GenerateSiteCommand(), VersionCommand())\n    parser.parse(args)\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/ClonedRepository.kt",
    "content": "package nl.avisi.structurizr.site.generatr\n\nimport org.eclipse.jgit.api.CreateBranchCommand\nimport org.eclipse.jgit.api.Git\nimport org.eclipse.jgit.api.ListBranchCommand\nimport org.eclipse.jgit.storage.file.FileRepositoryBuilder\nimport org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider\nimport java.io.File\n\nclass ClonedRepository(\n    val cloneDir: File,\n    private val url: String,\n    private val username: String?,\n    private val password: String?\n) {\n    private val repo = FileRepositoryBuilder.create(File(cloneDir, \".git\"))\n\n    fun refreshLocalClone() {\n        val credentialsProvider = if (username != null)\n            UsernamePasswordCredentialsProvider(username, password)\n        else\n            null\n\n        if (cloneDir.isDirectory) {\n            Git(repo).pull()\n                .setCredentialsProvider(credentialsProvider)\n                .call()\n        } else {\n            cloneDir.deleteRecursively()\n            val cloneCommand = Git.cloneRepository()\n                .setURI(url)\n                .setDirectory(cloneDir)\n            if (credentialsProvider != null)\n                cloneCommand.setCredentialsProvider(credentialsProvider)\n            cloneCommand.call()\n        }\n    }\n\n    fun checkoutBranch(branch: String) {\n        val git = Git(repo)\n\n        git.branchCreate()\n            .setName(branch)\n            .setUpstreamMode(CreateBranchCommand.SetupUpstreamMode.SET_UPSTREAM)\n            .setStartPoint(\"origin/$branch\")\n            .setForce(true)\n            .call()\n        git.checkout()\n            .setName(branch)\n            .call()\n    }\n\n    fun getBranchNames(excludeBranches: List<String>) =\n        Git(repo).branchList().setListMode(ListBranchCommand.ListMode.REMOTE).call()\n            .map { it.name.toString().substringAfter(\"/remotes/origin/\") }\n            .onEach { println(\"Found the following branch: $it\") }\n            .filter { it !in excludeBranches }\n\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/CreateStructurizrWorkspace.kt",
    "content": "package nl.avisi.structurizr.site.generatr\n\nimport com.structurizr.dsl.StructurizrDslParser\nimport com.structurizr.model.Element\nimport com.structurizr.view.ThemeUtils\nimport java.io.File\n\nfun createStructurizrWorkspace(workspaceFile: File) =\n    StructurizrDslParser()\n        .apply { parse(workspaceFile) }\n        .workspace\n        .apply {\n            ThemeUtils.loadThemes(this)\n            model.elements.forEach {\n                moveUrlToProperty(it) // We need the URL later for our own links, preserve the original in a property\n            }\n        }\n        ?: throw IllegalStateException(\"Workspace could not be parsed\")\n\nprivate fun moveUrlToProperty(element: Element) {\n    if (element.url != null) {\n        element.addProperty(\"Url\", element.url)\n        element.url = null\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/GenerateSiteCommand.kt",
    "content": "@file:OptIn(ExperimentalCli::class)\n\npackage nl.avisi.structurizr.site.generatr\n\nimport kotlinx.cli.*\nimport nl.avisi.structurizr.site.generatr.site.*\nimport java.io.File\n\nclass GenerateSiteCommand : Subcommand(\n    \"generate-site\",\n    \"Generate a site for the selected workspace.\"\n) {\n    private val gitUrl by option(\n        ArgType.String, \"git-url\", \"g\",\n        \"The URL of the Git repository which contains the Structurizr model. \" +\n            \"If a Git repository is provided, it will be cloned and\" +\n            \"--workspace-file and --assets-dir will be treated as paths within the cloned repository. \" +\n            \"If no Git repository is provided, --workspace-file and --assets-dir will be used as-is, and the site\" +\n            \"will only contain one branch, named after the --default-branch option.\"\n    )\n    private val gitUsername by option(\n        ArgType.String, \"git-username\", \"u\",\n        \"Username for the Git repository\"\n    )\n    private val gitPassword by option(\n        ArgType.String, \"git-password\", \"p\",\n        \"Password for the Git repository\"\n    )\n    private val workspaceFile by option(\n        ArgType.String, \"workspace-file\", \"w\",\n        \"Relative path within the Git repository of the workspace file\"\n    ).required()\n    private val assetsDir by option(\n        ArgType.String, \"assets-dir\", \"a\",\n        \"Relative path within the Git repository where static assets are located\"\n    )\n    private val branches by option(\n        ArgType.String, \"branches\", \"b\",\n        \"Comma-separated list of branches to include in the generated site. Not used if '--all-branches' option is set to true\"\n    ).default(\"master\")\n    private val defaultBranch by option(\n        ArgType.String, \"default-branch\", \"d\",\n        \"The default branch\"\n    ).default(\"master\")\n    private val version by option(\n        ArgType.String, \"version\", \"v\",\n        \"The version of the site\"\n    ).default(\"0.0.0\")\n    private val outputDir by option(\n        ArgType.String, \"output-dir\", \"o\",\n        \"Directory where the generated site will be stored. Will be created if it doesn't exist yet.\"\n    ).default(\"build/site\")\n\n    private val allBranches by option(\n        ArgType.Boolean, \"all-branches\", \"all\",\n        \"When set, generate a site for every branch in the git repository\"\n    ).default(value = false)\n    private val excludeBranches by option(\n        ArgType.String, \"exclude-branches\", \"ex\",\n        \"Comma-separated list of branches to exclude from the generated site\"\n    ).default(\"\")\n\n    override fun execute() {\n        val siteDir = File(outputDir).apply { mkdirs() }\n        val gitUrl = gitUrl\n\n        generateRedirectingIndexPage(siteDir, defaultBranch)\n        copySiteWideAssets(siteDir)\n\n        if (gitUrl != null)\n            generateSiteForModelInGitRepository(gitUrl, siteDir)\n        else\n            generateSiteForModel(siteDir)\n    }\n\n    private fun generateSiteForModelInGitRepository(gitUrl: String, siteDir: File) {\n        val cloneDir = File(\"build/model-clone\")\n        val clonedRepository = ClonedRepository(cloneDir, gitUrl, gitUsername, gitPassword).apply {\n            refreshLocalClone()\n        }\n\n        val branchNames = if (allBranches)\n            clonedRepository.getBranchNames(excludeBranches.split(\",\"))\n        else\n            branches.split(\",\")\n\n        println(\"The following branches will be checked for Structurizr Workspaces: $branchNames\")\n\n        val workspaceFileInRepo = File(clonedRepository.cloneDir, workspaceFile)\n        val branchesToGenerate = branchNames.filter { branch ->\n            println(\"Checking branch $branch\")\n            try {\n                clonedRepository.checkoutBranch(branch)\n                createStructurizrWorkspace(workspaceFileInRepo)\n                true\n            } catch (e: Exception) {\n                val errorMessage = e.message ?: \"Unknown error\"\n                println(\"Bad Branch $branch: $errorMessage\")\n                false\n            }\n        }.sortedWith(branchComparator(defaultBranch))\n\n        println(\"The following branches contain a valid Structurizr workspace: $branchesToGenerate\")\n\n        if (!branchesToGenerate.contains(defaultBranch)) {\n            throw Exception(\"$defaultBranch does not contain a valid structurizr workspace. Site generation halted.\")\n        }\n\n        branchesToGenerate.forEach { branch ->\n            println(\"Generating site for branch $branch\")\n            clonedRepository.checkoutBranch(branch)\n\n            val workspace = createStructurizrWorkspace(workspaceFileInRepo)\n            writeStructurizrJson(workspace, File(siteDir, branch))\n            generateDiagrams(workspace, File(siteDir, branch))\n            generateSite(\n                version,\n                workspace,\n                assetsDir?.let { File(cloneDir, it) },\n                siteDir,\n                branchesToGenerate,\n                branch\n            )\n        }\n    }\n\n    private fun generateSiteForModel(siteDir: File) {\n        val workspace = createStructurizrWorkspace(File(workspaceFile))\n        writeStructurizrJson(workspace, File(siteDir, defaultBranch))\n        generateDiagrams(workspace, File(siteDir, defaultBranch))\n        generateSite(\n            version,\n            workspace,\n            assetsDir?.let { File(it) },\n            siteDir,\n            listOf(defaultBranch),\n            defaultBranch\n        )\n    }\n}\n\nfun branchComparator(defaultBranch: String) = Comparator<String> { a, b ->\n    if (a == defaultBranch) -1\n    else if (b == defaultBranch) 1\n    else a.compareTo(b, ignoreCase = true)\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/ServeCommand.kt",
    "content": "@file:OptIn(ExperimentalCli::class)\n\npackage nl.avisi.structurizr.site.generatr\n\nimport kotlinx.cli.*\nimport nl.avisi.structurizr.site.generatr.site.*\nimport org.eclipse.jetty.server.Server\nimport org.eclipse.jetty.server.handler.ContextHandler\nimport org.eclipse.jetty.server.handler.ContextHandlerCollection\nimport org.eclipse.jetty.server.handler.ResourceHandler\nimport org.eclipse.jetty.util.resource.ResourceFactory\nimport org.eclipse.jetty.websocket.api.Callback\nimport org.eclipse.jetty.websocket.api.Session\nimport org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose\nimport org.eclipse.jetty.websocket.api.annotations.OnWebSocketError\nimport org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen\nimport org.eclipse.jetty.websocket.api.annotations.WebSocket\nimport org.eclipse.jetty.websocket.server.WebSocketUpgradeHandler\nimport java.io.File\nimport java.nio.file.*\nimport java.time.Duration\nimport kotlin.io.path.absolutePathString\nimport kotlin.io.path.extension\nimport kotlin.io.path.isDirectory\nimport kotlin.io.path.isHidden\nimport kotlin.io.path.isRegularFile\nimport kotlin.system.measureTimeMillis\n\nclass ServeCommand : Subcommand(\"serve\", \"Start a development server\") {\n    private val workspaceFile by option(\n        ArgType.String, \"workspace-file\", \"w\", \"The workspace file\"\n    ).required()\n    private val assetsDir by option(\n        ArgType.String, \"assets-dir\", \"a\", \"Directory where static assets are located\"\n    )\n    private val siteDir by option(\n        ArgType.String, \"site-dir\", \"s\", \"Directory for the generated site\"\n    ).default(\"build/serve\")\n\n    private val port by option(\n        ArgType.Int, \"port\", \"p\", \"Port the site is served on\"\n    ).default(8080)\n\n    private val eventSockets = mutableListOf<EventSocket>()\n    private val eventSocketsLock = Any()\n    private var updateSiteError: String? = null\n\n    override fun execute() {\n        updateSite()\n        val server = runServer()\n        val watchService = startWatchService()\n\n        try {\n            server.join()\n        } finally {\n            println(\"Stop watching for changes\")\n            watchService.close()\n\n            println(\"Stopping server...\")\n            server.stop()\n            println(\"Server stopped\")\n        }\n    }\n\n    private fun updateSite() {\n        val branch = \"master\"\n        val exportDir = File(siteDir, branch).apply { mkdirs() }\n\n        try {\n            val ms = measureTimeMillis {\n                broadcast(\"site-updating\")\n                val workspace = createStructurizrWorkspace(File(workspaceFile))\n\n                println(\"Generating index page...\")\n                generateRedirectingIndexPage(File(siteDir), branch)\n                println(\"Copying assets...\")\n                copySiteWideAssets(File(siteDir))\n                println(\"Writing workspace.json...\")\n                writeStructurizrJson(workspace, exportDir)\n                println(\"Generating diagrams...\")\n                generateDiagrams(workspace, exportDir)\n                println(\"Generating site...\")\n                generateSite(\n                    \"0.0.0\",\n                    workspace,\n                    assetsDir?.let { File(it) },\n                    File(siteDir),\n                    listOf(branch),\n                    branch,\n                    serving = true\n                )\n\n                updateSiteError = null\n                broadcast(\"site-updated\")\n            }\n            println(\"Successfully generated diagrams and site in ${ms.toDouble() / 1000} seconds\")\n        } catch (e: Exception) {\n            updateSiteError = e.message ?: \"Unknown error\"\n            broadcast(updateSiteError!!)\n            e.printStackTrace()\n        }\n    }\n\n    private fun runServer(): Server =\n        Server(port).also { server ->\n            println(\"Starting server...\")\n\n            server.handler = createRootContextHandler(server)\n            server.start()\n\n            println(\"Server started\")\n            println(\"Open http://localhost:$port in your browser to view the site\")\n        }\n\n    private fun createRootContextHandler(server: Server) = ContextHandlerCollection(\n        ContextHandler(createStaticResourceHandler(), \"/\"),\n        ContextHandler(\"/\").apply {\n            handler = createWebSocketHandler(server, this, \"/_events\")\n        }\n    )\n\n    private fun createStaticResourceHandler() =\n        ResourceHandler().apply {\n            baseResource = ResourceFactory.of(this).newResource(File(siteDir).absolutePath)\n            isUseFileMapping = false\n        }\n\n    private fun createWebSocketHandler(\n        server: Server,\n        context: ContextHandler,\n        @Suppress(\"SameParameterValue\") pathSpec: String\n    ) =\n        WebSocketUpgradeHandler.from(server, context) { container ->\n            container.idleTimeout = Duration.ZERO\n            container.addMapping(pathSpec) { _, _, _ -> EventSocket() }\n        }\n\n    private fun startWatchService(): WatchService {\n        val path = File(workspaceFile).absoluteFile.parentFile.toPath()\n        val watchService = FileSystems.getDefault().newWatchService()\n        val absoluteSiteDir = File(siteDir).absolutePath\n\n        path.watch(watchService)\n        Files.walk(path)\n            .filter { it.isDirectory() && !it.isHidden() && !it.absolutePathString().startsWith(absoluteSiteDir) }\n            .forEach { it.watch(watchService) }\n\n        Thread {\n            try {\n                monitorFileChanges(watchService)\n            } catch (_: ClosedWatchServiceException) {\n                // ignore, server shutdown\n            }\n        }\n            .apply { isDaemon = true }\n            .start()\n\n        return watchService\n    }\n\n    private fun monitorFileChanges(watchService: WatchService) {\n        while (true) {\n            val watchKey = watchService.take()\n            val parentPath = watchKey.watchable() as Path\n\n            Thread.sleep(100) // Throttle the file watcher to give editors time for cleaning up temporary files\n            val events = watchKey.pollEvents().filterNot { isHashFile(it) }\n\n            val fileModified = events.map { parentPath.resolve(it.context() as Path) }\n                .any { it.isRegularFile() }\n            val fileOrDirectoryDeleted = events\n                .any { it.kind() == StandardWatchEventKinds.ENTRY_DELETE }\n\n            events.filter { it.kind() == StandardWatchEventKinds.ENTRY_CREATE }\n                .map { parentPath.resolve(it.context() as Path) }\n                .filter { it.isDirectory() && !it.isHidden() }\n                .forEach { it.watch(watchService) }\n\n            if (fileModified || fileOrDirectoryDeleted) {\n                println(\"Detected a change in ${watchKey.watchable()}, updating site...\")\n                try {\n                    updateSite()\n                } catch (e: Exception) {\n                    System.err.println(\"An error occurred while updating the site\")\n                    e.printStackTrace()\n                }\n            }\n            watchKey.reset()\n        }\n    }\n\n    private fun isHashFile(it: WatchEvent<*>) = (it.context() as Path).extension == \"md5\"\n\n    private fun Path.watch(watchService: WatchService) {\n        register(\n            watchService,\n            arrayOf(\n                StandardWatchEventKinds.ENTRY_CREATE,\n                StandardWatchEventKinds.ENTRY_DELETE,\n                StandardWatchEventKinds.ENTRY_MODIFY\n            )\n        )\n    }\n\n    private fun broadcast(message: String) = synchronized(eventSocketsLock) {\n        eventSockets.forEach { it.send(message) }\n    }\n\n    @Suppress(\"UNUSED_PARAMETER\", \"unused\")\n    @WebSocket\n    inner class EventSocket {\n        private var session: Session? = null\n\n        @OnWebSocketOpen\n        fun onWebSocketConnect(session: Session?) {\n            synchronized(eventSocketsLock) { eventSockets.add(this) }\n            this.session = session\n            updateSiteError?.let { send(it) }\n        }\n\n        @OnWebSocketClose\n        fun onWebSocketClose(statusCode: Int, reason: String?) {\n            session = null\n            synchronized(eventSocketsLock) { eventSockets.remove(this) }\n        }\n\n        @OnWebSocketError\n        fun onWebSocketError(cause: Throwable?) {\n            session = null\n            synchronized(eventSocketsLock) { eventSockets.remove(this) }\n        }\n\n        fun send(message: String) {\n            session?.let {\n                if (it.isOpen)\n                    it.sendText(message, Callback.NOOP)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/StringUtilities.kt",
    "content": "package nl.avisi.structurizr.site.generatr\n\n// based on https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names\nprivate const val reservedChars = \"|\\\\?*<\\\":>+[]/'\"\nprivate val reservedNames = setOf(\n    \"CON\", \"PRN\", \"AUX\", \"NUL\",\n    \"COM1\", \"COM2\", \"COM3\", \"COM4\", \"COM5\", \"COM6\", \"COM7\", \"COM8\", \"COM9\",\n    \"LPT1\", \"LPT2\", \"LPT3\", \"LPT4\", \"LPT5\", \"LPT6\", \"LPT7\", \"LPT8\", \"LPT9\"\n)\n\nfun String.normalize(): String =\n    lowercase()\n        .replace(\"\\\\s+\".toRegex(), \"-\")\n        .filterNot { reservedChars.contains(it) }\n        .trim()\n        .let {\n            if (reservedNames.contains(it.uppercase()))\n                \"${it}-\"\n            else\n                it\n        }\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/StructurizrUtilities.kt",
    "content": "package nl.avisi.structurizr.site.generatr\n\nimport com.structurizr.Workspace\nimport com.structurizr.model.Component\nimport com.structurizr.model.Container\nimport com.structurizr.model.SoftwareSystem\nimport com.structurizr.model.StaticStructureElement\nimport com.structurizr.view.ViewSet\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nval Workspace.includedSoftwareSystems: List<SoftwareSystem>\n    get() = model.softwareSystems.filter {\n        val externalTag = views.configuration.properties.getOrDefault(\"generatr.site.externalTag\", null)\n        if (externalTag != null) !it.tags.contains(externalTag) else true\n    }\n\nfun Workspace.hasImageViews(id: String) = views.imageViews.any { it.elementId == id }\n\nfun Workspace.hasComponentDiagrams(container: Container) = views.componentViews.any { it.container == container}\n\nval SoftwareSystem.hasContainers\n    get() = this.containers.isNotEmpty()\n\nval StaticStructureElement.includedProperties\n    get() = this.properties.filterNot { (name, _) -> name.startsWith(\"structurizr.\") or name.startsWith(\"generatr.\") }\n\nval Container.hasComponents\n    get() = this.components.isNotEmpty()\n\nfun SoftwareSystem.firstContainer(generatorContext: GeneratorContext) = containers\n        .sortedBy { it.name }.firstOrNull { container ->\n            generatorContext.workspace.hasComponentDiagrams(container) or\n                    generatorContext.workspace.hasImageViews(container.id) }\n\nfun SoftwareSystem.hasDecisions(recursive: Boolean = false) = documentation.decisions.isNotEmpty() || (recursive && hasContainerDecisions(recursive = true))\n\nfun SoftwareSystem.hasContainerDecisions(recursive: Boolean = false) = containers.any { it.hasDecisions() || (recursive && hasComponentDecisions()) }\n\nfun SoftwareSystem.hasComponentDecisions() = containers.any { it.hasComponentDecisions() }\n\nfun SoftwareSystem.hasDocumentationSections(recursive: Boolean = false) = documentation.sections.size >= 2 || (recursive && hasContainerDocumentationSections(recursive))\n\nfun SoftwareSystem.hasContainerDocumentationSections(recursive: Boolean = false) = containers.any { it.hasSections(recursive) } || (recursive && hasComponentDocumentationSections())\n\nfun SoftwareSystem.hasComponentDocumentationSections() = containers.any { it.hasComponentsSections() }\n\nfun Container.firstComponent(generatorContext: GeneratorContext) = components\n    .sortedBy { it.name }.firstOrNull {\n        component -> generatorContext.workspace.hasImageViews(component.id) }\n\nfun Container.hasDecisions(recursive: Boolean = false) = documentation.decisions.isNotEmpty() || (recursive && hasComponentDecisions())\n\nfun Container.hasComponentDecisions() = components.any { it.hasDecisions() }\n\nfun Container.hasSections(recursive: Boolean = false) = documentation.sections.isNotEmpty() || (recursive && hasComponentsSections())\n\nfun Container.hasComponentsSections() = components.any { it.hasSections() }\n\nfun Component.hasDecisions() = documentation.decisions.isNotEmpty()\n\nfun Component.hasSections() = documentation.sections.isNotEmpty()\n\nfun ViewSet.hasSystemContextViews(softwareSystem: SoftwareSystem) =\n    systemContextViews.any { it.softwareSystem == softwareSystem }\n\nfun ViewSet.hasContainerViews(workspace: Workspace, softwareSystem: SoftwareSystem) =\n    containerViews.any { it.softwareSystem == softwareSystem } ||\n        workspace.views.imageViews.any { it.elementId == softwareSystem.id }\n\nfun ViewSet.hasComponentViews(workspace: Workspace, softwareSystem: SoftwareSystem) =\n    componentViews.any { it.softwareSystem == softwareSystem } ||\n        workspace.views.imageViews.any { it.elementId in softwareSystem.containers.map { cn -> cn.id } }\n\nfun ViewSet.hasCodeViews(workspace: Workspace, softwareSystem: SoftwareSystem) =\n    componentViews.any { it.softwareSystem == softwareSystem } &&\n        workspace.views.imageViews.any { it.elementId in softwareSystem.containers.flatMap { cn -> cn.components.map { com -> com.id } } }\n\nfun ViewSet.hasDynamicViews(softwareSystem: SoftwareSystem) =\n    dynamicViews.any { it.softwareSystem == softwareSystem }\n\nfun ViewSet.hasDeploymentViews(softwareSystem: SoftwareSystem) = with(deploymentViews) {\n    any { it.softwareSystem == softwareSystem } || any { it.properties[\"generatr.view.deployment.belongsTo\"] == softwareSystem.name }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/VersionCommand.kt",
    "content": "@file:OptIn(ExperimentalCli::class)\n\npackage nl.avisi.structurizr.site.generatr\n\nimport kotlinx.cli.ExperimentalCli\nimport kotlinx.cli.Subcommand\n\nclass VersionCommand : Subcommand(\"version\", \"Print version information\") {\n    override fun execute() {\n        val pkg = javaClass.`package`\n        println(\"${pkg.implementationTitle} v${pkg.implementationVersion}\")\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/DateFormatter.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site\n\nimport java.time.ZoneId\nimport java.util.*\n\nfun formatDate(date: Date): String {\n    val localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()\n    return String.format(\"%02d-%02d-%04d\", localDate.dayOfMonth, localDate.monthValue, localDate.year)\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/DiagramGenerator.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site\n\nimport com.structurizr.Workspace\nimport com.structurizr.export.Diagram\nimport com.structurizr.export.plantuml.PlantUMLDiagram\nimport com.structurizr.view.ModelView\nimport com.structurizr.view.View\nimport net.sourceforge.plantuml.FileFormat\nimport net.sourceforge.plantuml.FileFormatOption\nimport net.sourceforge.plantuml.SourceStringReader\nimport java.io.ByteArrayOutputStream\nimport java.io.File\nimport java.net.URI\nimport java.util.concurrent.ConcurrentHashMap\n\nfun generateDiagrams(workspace: Workspace, exportDir: File) {\n    val pumlDir = pumlDir(exportDir)\n    val svgDir = svgDir(exportDir)\n    val pngDir = pngDir(exportDir)\n\n    val plantUMLDiagrams = generatePlantUMLDiagrams(workspace)\n\n    plantUMLDiagrams.parallelStream()\n        .forEach { diagram ->\n            val plantUMLFile = File(pumlDir, \"${diagram.key}.puml\")\n            if (!plantUMLFile.exists() || plantUMLFile.readText() != diagram.definition) {\n                println(\"${diagram.key}...\")\n                saveAsSvg(diagram, svgDir)\n                saveAsPng(diagram, pngDir)\n                saveAsPUML(diagram, plantUMLFile)\n            } else {\n                println(\"${diagram.key} UP-TO-DATE\")\n            }\n        }\n}\n\nfun generateDiagramWithElementLinks(\n    workspace: Workspace,\n    view: View,\n    url: String,\n    diagramCache: ConcurrentHashMap<String, String>\n): String {\n    val diagram = generatePlantUMLDiagramWithElementLinks(workspace, view, url)\n\n    val name = \"${diagram.key}-${view.key}\"\n    return diagramCache.getOrPut(name) {\n        val reader = SourceStringReader(diagram.withCachedIncludes().definition)\n        val stream = ByteArrayOutputStream()\n\n        reader.outputImage(stream, FileFormatOption(FileFormat.SVG, false))\n        stream.toString(Charsets.UTF_8)\n    }\n}\n\nprivate fun generatePlantUMLDiagrams(workspace: Workspace): Collection<Diagram> {\n    val plantUMLExporter = PlantUmlExporter(workspace)\n\n    return plantUMLExporter.export()\n}\n\nprivate fun saveAsPUML(diagram: Diagram, plantUMLFile: File) {\n    plantUMLFile.writeText(diagram.definition)\n}\n\nprivate fun saveAsSvg(diagram: Diagram, svgDir: File, name: String = diagram.key) {\n    val reader = SourceStringReader(diagram.withCachedIncludes().definition)\n    val svgFile = File(svgDir, \"$name.svg\")\n\n    svgFile.outputStream().use {\n        reader.outputImage(it, FileFormatOption(FileFormat.SVG, false))\n    }\n}\n\nprivate fun saveAsPng(diagram: Diagram, pngDir: File) {\n    val reader = SourceStringReader(diagram.withCachedIncludes().definition)\n    val pngFile = File(pngDir, \"${diagram.key}.png\")\n\n    pngFile.outputStream().use {\n        reader.outputImage(it)\n    }\n}\n\nprivate fun generatePlantUMLDiagramWithElementLinks(workspace: Workspace, view: View, url: String): Diagram {\n    val plantUMLExporter = PlantUmlExporterWithElementLinks(workspace, url)\n\n    return plantUMLExporter.export(view)\n}\n\nprivate fun pumlDir(exportDir: File) = File(exportDir, \"puml\").apply { mkdirs() }\nprivate fun svgDir(exportDir: File) = File(exportDir, \"svg\").apply { mkdirs() }\nprivate fun pngDir(exportDir: File) = File(exportDir, \"png\").apply { mkdirs() }\n\nprivate fun Diagram.withCachedIncludes(): Diagram {\n    val def = definition.replace(\"!include\\\\s+(.*)\".toRegex()) {\n        val cachedInclude = IncludeCache.cachedInclude(it.groupValues[1])\n        \"!include $cachedInclude\"\n    }\n\n    return PlantUMLDiagram(view as ModelView, def)\n}\n\nprivate object IncludeCache {\n    private val cache = mutableMapOf<String, String>()\n    private val cacheDir = File(\"build/puml-cache\").apply { mkdirs() }\n\n    fun cachedInclude(includedFile: String): String {\n        if (!includedFile.startsWith(\"http\"))\n            return includedFile\n\n        return cache.getOrPut(includedFile) {\n            val fileName = includedFile.split(\"/\").last()\n            val cachedFile = File(cacheDir, fileName)\n\n            if (!cachedFile.exists())\n                downloadIncludedFile(includedFile, cachedFile)\n\n            cachedFile.absolutePath\n        }\n    }\n\n    private fun downloadIncludedFile(includedFile: String, cachedFile: File) {\n        URI(includedFile).toURL().openStream().use { inputStream ->\n            cachedFile.outputStream().use { outputStream ->\n                inputStream.copyTo(outputStream)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/GeneratorContext.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site\n\nimport com.structurizr.Workspace\n\ndata class GeneratorContext(\n    val version: String,\n    val workspace: Workspace,\n    val branches: List<String>,\n    val currentBranch: String,\n    val serving: Boolean,\n    val svgFactory: (key: String, url: String) -> String?\n)\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/PlantUmlExporter.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site\n\nimport com.structurizr.Workspace\nimport com.structurizr.export.Diagram\nimport com.structurizr.export.IndentingWriter\nimport com.structurizr.export.plantuml.C4PlantUMLExporter\nimport com.structurizr.export.plantuml.StructurizrPlantUMLExporter\nimport com.structurizr.model.Component\nimport com.structurizr.model.Container\nimport com.structurizr.model.Element\nimport com.structurizr.model.SoftwareSystem\nimport com.structurizr.view.*\nimport nl.avisi.structurizr.site.generatr.*\n\nenum class ExporterType { C4, STRUCTURIZR }\n\nclass PlantUmlExporter(val workspace: Workspace) {\n    private val exporter = when (workspace.exporterType()) {\n        ExporterType.C4 -> C4PlantUMLExporter(ColorScheme.Light)\n        ExporterType.STRUCTURIZR -> StructurizrPlantUMLExporter(ColorScheme.Light)\n    }\n\n    fun export(): Collection<Diagram> = exporter.export(workspace)\n}\n\nclass PlantUmlExporterWithElementLinks(workspace: Workspace, url: String) {\n    private val exporter = when (workspace.exporterType()) {\n        ExporterType.C4 -> C4PlantUmlExporterWithElementLinks(workspace, url, ColorScheme.Light)\n        ExporterType.STRUCTURIZR -> StructurizrPlantUmlExporterWithElementLinks(workspace, url, ColorScheme.Light)\n    }\n\n    init {\n        if (workspace.views.configuration.properties.containsKey(\"generatr.svglink.target\")) {\n            exporter.addSkinParam(\n                \"svgLinkTarget\",\n                workspace.views.configuration.properties.getValue(\"generatr.svglink.target\")\n            )\n        }\n    }\n\n    fun export(view: View): Diagram = when (view) {\n        is CustomView -> exporter.export(view)\n        is SystemLandscapeView -> exporter.export(view)\n        is SystemContextView -> exporter.export(view)\n        is ContainerView -> exporter.export(view)\n        is ComponentView -> exporter.export(view)\n        is DynamicView -> exporter.export(view)\n        is DeploymentView -> exporter.export(view)\n        else -> throw IllegalStateException(\"View ${view.name} has a non-exportable type\")\n    }\n}\n\nfun Workspace.exporterType() = views.configuration.properties\n    .getOrDefault(\"generatr.site.exporter\", \"c4\")\n    .let { ExporterType.valueOf(it.uppercase()) }\n\nprivate class WriterWithElementLinks(\n    private val workspace: Workspace,\n    private val url: String\n) {\n    companion object {\n        const val TEMP_URI = \"https://will-be-changed-to-relative/\"\n    }\n\n    fun writeHeader(\n        view: ModelView,\n        writer: IndentingWriter,\n        writeHeaderFn: (view: ModelView, writer: IndentingWriter) -> Unit\n    ) {\n        writeHeaderFn(view, writer)\n        writer.writeLine(\"skinparam svgDimensionStyle false\")\n        writer.writeLine(\"skinparam preserveAspectRatio meet\")\n    }\n\n    fun writeElement(\n        view: ModelView?,\n        element: Element?,\n        writer: IndentingWriter?,\n        writeElementFn: (view: ModelView?, element: Element?, writer: IndentingWriter?) -> Unit\n    ) {\n        val url = when {\n            needsLinkToSoftwareSystem(element, view) -> getUrlToElement(element, \"context\")\n            needsLinkToContainerViews(element, view, workspace) -> getUrlToElement(element)\n            needsLinkToComponentViews(element, view, workspace) -> getUrlToElement(element)\n            needsLinkToCodeViews(element, workspace) -> getUrlToElement(element)\n            else -> null\n        }\n\n        if (url != null)\n            writeElementWithCustomUrl(element, url, view, writer, writeElementFn)\n        else\n            writeElementFn(view, element, writer)\n    }\n\n    private fun needsLinkToSoftwareSystem(element: Element?, view: ModelView?) =\n        element is SoftwareSystem && workspace.includedSoftwareSystems.contains(element) && element != view?.softwareSystem\n\n    private fun needsLinkToContainerViews(element: Element?, view: ModelView?, workspace: Workspace) =\n        element is SoftwareSystem && workspace.includedSoftwareSystems.contains(element) && element == view?.softwareSystem &&\n            (element.hasContainers || workspace.hasImageViews(element.id))\n\n    private fun needsLinkToComponentViews(element: Element?, view: ModelView?, workspace: Workspace) =\n        element is Container && (element.hasComponents || workspace.hasImageViews(element.id)) && view !is ComponentView\n\n    private fun needsLinkToCodeViews(element: Element?, workspace: Workspace) =\n        element is Component && workspace.hasImageViews(element.id)\n\n    private fun getUrlToElement(element: Element?, page: String? = null): String {\n        val path = when (element) {\n            is SoftwareSystem -> \"/${element.name?.normalize()}/${page?.let { page } ?: \"container\"}/\".asUrlToDirectory(url)\n            is Container -> \"/${element.parent?.name?.normalize()}/${page?.let { page } ?: \"component\"}/${element.name?.normalize()}\".asUrlToDirectory(url)\n            is Component -> \"/${element.parent?.parent?.name?.normalize()}/${page?.let { page } ?: \"code\"}/${element.container?.name?.normalize()}/${element.name?.normalize()}\".asUrlToDirectory(url)\n            else -> throw IllegalStateException(\"Not supported element\")\n        }\n        return \"$TEMP_URI$path\"\n    }\n\n    private fun writeElementWithCustomUrl(\n        element: Element?,\n        url: String?,\n        view: ModelView?,\n        writer: IndentingWriter?,\n        writeElementFn: (view: ModelView?, element: Element?, writer: IndentingWriter?) -> Unit\n    ) {\n        element?.url = url\n        writeModifiedElement(view, element, writer, writeElementFn)\n        element?.url = null\n    }\n\n    private fun writeModifiedElement(\n        view: ModelView?,\n        element: Element?,\n        writer: IndentingWriter?,\n        writeElement: (view: ModelView?, element: Element?, writer: IndentingWriter?) -> Unit\n    ) = IndentingWriter().let {\n        writeElement(view, element, it)\n        it.toString()\n            .replace(TEMP_URI, \"\")\n            .lines()\n            .forEach { line -> writer?.writeLine(line) }\n    }\n}\n\nclass C4PlantUmlExporterWithElementLinks(workspace: Workspace, url: String, colorScheme: ColorScheme = ColorScheme.Light) : C4PlantUMLExporter(colorScheme) {\n    private val withElementLinks = WriterWithElementLinks(workspace, url)\n\n    override fun writeHeader(view: ModelView, writer: IndentingWriter) {\n        withElementLinks.writeHeader(view, writer) { v, w -> super.writeHeader(v, w) }\n    }\n\n    override fun writeElement(view: ModelView?, element: Element?, writer: IndentingWriter?) {\n        withElementLinks.writeElement(view, element, writer) { v, e, w -> super.writeElement(v, e, w) }\n    }\n}\n\nclass StructurizrPlantUmlExporterWithElementLinks(workspace: Workspace, url: String, colorScheme: ColorScheme = ColorScheme.Light) : StructurizrPlantUMLExporter(colorScheme) {\n    private val withElementLinks = WriterWithElementLinks(workspace, url)\n\n    override fun writeHeader(view: ModelView, writer: IndentingWriter) {\n        withElementLinks.writeHeader(view, writer) { v, w -> super.writeHeader(v, w) }\n    }\n\n    override fun writeElement(view: ModelView?, element: Element?, writer: IndentingWriter?) {\n        withElementLinks.writeElement(view, element, writer) { v, e, w -> super.writeElement(v, e, w) }\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/RelativeUrl.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site\n\nimport java.io.File\n\nfun String.asUrlToDirectory(otherUrl: String) = \"${this.asUrlToFile(otherUrl)}/\"\n\nfun String.asUrlToFile(relativeTo: String): String =\n    if (relativeTo == this) \".\"\n    else File(this)\n        .relativeTo(File(relativeTo))\n        .invariantSeparatorsPath\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/SiteGenerator.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site\n\nimport com.structurizr.Workspace\nimport com.structurizr.util.WorkspaceUtils\nimport kotlinx.html.*\nimport kotlinx.html.stream.appendHTML\nimport nl.avisi.structurizr.site.generatr.*\nimport nl.avisi.structurizr.site.generatr.site.model.*\nimport nl.avisi.structurizr.site.generatr.site.views.*\nimport java.io.File\nimport java.math.BigInteger\nimport java.security.MessageDigest\nimport java.util.concurrent.ConcurrentHashMap\n\nfun copySiteWideAssets(exportDir: File) {\n    copySiteWideAsset(exportDir, \"/css/style.css\")\n    copySiteWideAsset(exportDir, \"/js/header.js\")\n    copySiteWideAsset(exportDir, \"/js/svg-modal.js\")\n    copySiteWideAsset(exportDir, \"/js/modal.js\")\n    copySiteWideAsset(exportDir, \"/js/search.js\")\n    copySiteWideAsset(exportDir, \"/js/auto-reload.js\")\n    copySiteWideAsset(exportDir, \"/css/admonition.css\")\n    copySiteWideAsset(exportDir, \"/js/admonition.js\")\n    copySiteWideAsset(exportDir, \"/js/reformat-mermaid.js\")\n    copySiteWideAsset(exportDir, \"/css/treeview.css\")\n    copySiteWideAsset(exportDir, \"/js/treeview.js\")\n    copySiteWideAsset(exportDir, \"/js/katex-render.js\")\n    copySiteWideAsset(exportDir, \"/js/toggle-theme.js\")\n}\n\nprivate fun copySiteWideAsset(exportDir: File, asset: String) {\n    val content = object {}.javaClass.getResource(\"/assets$asset\")?.readText()\n        ?: throw IllegalStateException(\"File $asset not found on classpath\")\n    val file = File(exportDir, asset.substringAfterLast('/'))\n\n    file.writeText(content)\n}\n\nfun generateRedirectingIndexPage(exportDir: File, defaultBranch: String) {\n    val htmlFile = File(exportDir, \"index.html\")\n    htmlFile.writeText(\n        buildString {\n            appendLine(\"<!doctype html>\")\n            appendHTML().html {\n                attributes[\"lang\"] = \"en\"\n                head {\n                    meta {\n                        httpEquiv = \"refresh\"\n                        content = \"0; url=$defaultBranch/\"\n                    }\n                    title { +\"Structurizr site generatr\" }\n                }\n                body()\n            }\n        }\n    )\n}\n\nfun writeStructurizrJson(workspace: Workspace, exportDir: File) {\n    val json = WorkspaceUtils.toJson(workspace, true)\n    File(exportDir, \"workspace.json\")\n        .apply { parentFile.mkdirs() }\n        .writeText(json)\n}\n\nfun generateSite(\n    version: String,\n    workspace: Workspace,\n    assetsDir: File?,\n    exportDir: File,\n    branches: List<String>,\n    currentBranch: String,\n    serving: Boolean = false\n) {\n    val generatorContext = GeneratorContext(version, workspace, branches, currentBranch, serving) { key, url ->\n        val diagramCache = ConcurrentHashMap<String, String>()\n        workspace.views.views.singleOrNull { view -> view.key == key }\n            ?.let { generateDiagramWithElementLinks(workspace, it, url, diagramCache) }\n    }\n\n    val branchDir = File(exportDir, currentBranch)\n\n    deleteOldHashes(branchDir)\n    if (assetsDir != null) copyAssets(assetsDir, branchDir)\n    generateStyle(generatorContext, branchDir)\n    generateHtmlFiles(generatorContext, branchDir)\n}\n\nprivate fun deleteOldHashes(branchDir: File) = branchDir.walk().filter { it.extension == \"md5\" }\n    .forEach { it.delete() }\n\nprivate fun copyAssets(assetsDir: File, branchDir: File) {\n    assetsDir.copyRecursively(branchDir, overwrite = true)\n}\n\nprivate fun generateStyle(context: GeneratorContext, branchDir: File) {\n    val configuration = context.workspace.views.configuration.properties\n    val primary = configuration.getOrDefault(\"generatr.style.colors.primary\", \"#333333\")\n    val secondary = configuration.getOrDefault(\"generatr.style.colors.secondary\", \"#cccccc\")\n\n    val file = File(branchDir, \"style-branding.css\")\n    val content = \"\"\"\n        .navbar .has-site-branding {\n            background-color: $primary!important;\n            color: $secondary!important;\n        }\n        .navbar .has-site-branding::after {\n            border-color: $secondary!important;\n        }\n        .menu .has-site-branding a.is-active {\n            color: $secondary!important;\n            background-color: $primary!important;\n        }\n        .input.has-site-branding:focus {\n            border-color: $secondary!important;\n            box-shadow: 0 0 0 0.125em $secondary;\n        }\n    \"\"\".trimIndent()\n\n    file.writeText(content)\n}\n\nprivate fun generateHtmlFiles(context: GeneratorContext, branchDir: File) {\n    buildList {\n        add { writeHtmlFile(branchDir, HomePageViewModel(context)) }\n        add { writeHtmlFile(branchDir, WorkspaceDecisionsPageViewModel(context)) }\n        add { writeHtmlFile(branchDir, SoftwareSystemsPageViewModel(context)) }\n        add { writeHtmlFile(branchDir, SearchViewModel(context)) }\n\n        context.workspace.documentation.sections\n            .filter { it.order != 1 }\n            .forEach {\n                add { writeHtmlFile(branchDir, WorkspaceDocumentationSectionPageViewModel(context, it)) }\n            }\n        context.workspace.documentation.decisions\n            .forEach {\n                add { writeHtmlFile(branchDir, WorkspaceDecisionPageViewModel(context, it)) }\n            }\n\n        context.workspace.includedSoftwareSystems.forEach {\n            add { writeHtmlFile(branchDir, SoftwareSystemHomePageViewModel(context, it)) }\n            add { writeHtmlFile(branchDir, SoftwareSystemContextPageViewModel(context, it)) }\n            add { writeHtmlFile(branchDir, SoftwareSystemContainerPageViewModel(context, it)) }\n            add { writeHtmlFile(branchDir, SoftwareSystemDynamicPageViewModel(context, it)) }\n            add { writeHtmlFile(branchDir, SoftwareSystemDeploymentPageViewModel(context, it)) }\n            add { writeHtmlFile(branchDir, SoftwareSystemDependenciesPageViewModel(context, it)) }\n            add { writeHtmlFile(branchDir, SoftwareSystemDecisionsPageViewModel(context, it)) }\n            add { writeHtmlFile(branchDir, SoftwareSystemSectionsPageViewModel(context, it)) }\n\n            it.documentation.decisions.forEach { decision ->\n                add { writeHtmlFile(branchDir, SoftwareSystemDecisionPageViewModel(context, it, decision)) }\n            }\n\n            it.containers\n                .filter { container -> container.hasDecisions(recursive = true) }\n                .onEach { container ->\n                    add { writeHtmlFile(branchDir, SoftwareSystemContainerDecisionsPageViewModel(context, container)) }\n                    container.documentation.decisions.forEach { decision ->\n                        add { writeHtmlFile(branchDir, SoftwareSystemContainerDecisionPageViewModel(context, container, decision)) }\n                    }\n                }\n                .flatMap { container -> container.components }\n                .filter { component -> component.hasDecisions() }\n                .forEach { component ->\n                    add { writeHtmlFile(branchDir, SoftwareSystemContainerComponentDecisionsPageViewModel(context, component)) }\n                    component.documentation.decisions.forEach { decision ->\n                        add { writeHtmlFile(branchDir, SoftwareSystemContainerComponentDecisionPageViewModel(context, component, decision)) }\n                    }\n                }\n\n            it.containers\n                .filter { container ->\n                    context.workspace.hasComponentDiagrams(container) or\n                            container.includedProperties.isNotEmpty() or\n                            context.workspace.hasImageViews(container.id) }\n                .forEach { container ->\n                    add { writeHtmlFile(branchDir, SoftwareSystemContainerComponentsPageViewModel(context, container)) } }\n\n            it.containers\n                .filter { container ->\n                    ( context.workspace.hasComponentDiagrams(container) or\n                            context.workspace.hasImageViews(container.id)) }\n                .forEach { container ->\n                    container.components.filter { component ->\n                        context.workspace.hasImageViews(component.id) }\n                    .forEach { component ->\n                        add { writeHtmlFile(branchDir, SoftwareSystemContainerComponentCodePageViewModel(context, container, component)) }\n                    }\n                }\n\n            it.documentation.sections\n                .filter { section -> section.order != 1 }\n                .forEach { section ->\n                    add { writeHtmlFile(branchDir, SoftwareSystemSectionPageViewModel(context, it, section)) }\n                }\n\n            it.containers\n                .filter { container -> container.hasSections(recursive = true) }\n                .onEach { container ->\n                    add { writeHtmlFile(branchDir, SoftwareSystemContainerSectionsPageViewModel(context, container)) }\n                    container.documentation.sections.forEach { section ->\n                        add { writeHtmlFile(branchDir, SoftwareSystemContainerSectionPageViewModel(context, container, section)) }\n                    }\n                }\n                .flatMap { container -> container.components }\n                .filter { component -> component.hasSections() }\n                .forEach { component ->\n                    add { writeHtmlFile(branchDir, SoftwareSystemContainerComponentSectionsPageViewModel(context, component)) }\n                    component.documentation.sections.forEach { section ->\n                        add { writeHtmlFile(branchDir, SoftwareSystemContainerComponentSectionPageViewModel(context, component, section)) }\n                    }\n                }\n        }\n    }\n        .parallelStream()\n        .forEach { it.invoke() }\n}\n\nprivate fun writeHtmlFile(exportDir: File, viewModel: PageViewModel) {\n    val html = buildString {\n        appendLine(\"<!doctype html>\")\n        appendHTML().html {\n            when (viewModel) {\n                is HomePageViewModel -> homePage(viewModel)\n                is SearchViewModel -> searchPage(viewModel)\n                is SoftwareSystemsPageViewModel -> softwareSystemsPage(viewModel)\n                is SoftwareSystemHomePageViewModel -> softwareSystemHomePage(viewModel)\n                is SoftwareSystemContextPageViewModel -> softwareSystemContextPage(viewModel)\n                is SoftwareSystemContainerPageViewModel -> softwareSystemContainerPage(viewModel)\n                is SoftwareSystemContainerDecisionPageViewModel -> softwareSystemContainerDecisionPage(viewModel)\n                is SoftwareSystemContainerDecisionsPageViewModel -> softwareSystemContainerDecisionsPage(viewModel)\n                is SoftwareSystemContainerSectionPageViewModel -> softwareSystemContainerSectionPage(viewModel)\n                is SoftwareSystemContainerSectionsPageViewModel -> softwareSystemContainerSectionsPage(viewModel)\n                is SoftwareSystemContainerComponentsPageViewModel -> softwareSystemContainerComponentsPage(viewModel)\n                is SoftwareSystemContainerComponentCodePageViewModel -> softwareSystemContainerComponentCodePage(viewModel)\n                is SoftwareSystemContainerComponentDecisionPageViewModel -> softwareSystemContainerComponentDecisionPage(viewModel)\n                is SoftwareSystemContainerComponentDecisionsPageViewModel -> softwareSystemContainerComponentDecisionsPage(viewModel)\n                is SoftwareSystemContainerComponentSectionPageViewModel -> softwareSystemContainerComponentSectionPage(viewModel)\n                is SoftwareSystemContainerComponentSectionsPageViewModel -> softwareSystemContainerComponentSectionsPage(viewModel)\n                is SoftwareSystemDynamicPageViewModel -> softwareSystemDynamicPage(viewModel)\n                is SoftwareSystemDeploymentPageViewModel -> softwareSystemDeploymentPage(viewModel)\n                is SoftwareSystemDependenciesPageViewModel -> softwareSystemDependenciesPage(viewModel)\n                is SoftwareSystemDecisionPageViewModel -> softwareSystemDecisionPage(viewModel)\n                is SoftwareSystemDecisionsPageViewModel -> softwareSystemDecisionsPage(viewModel)\n                is SoftwareSystemSectionPageViewModel -> softwareSystemSectionPage(viewModel)\n                is SoftwareSystemSectionsPageViewModel -> softwareSystemSectionsPage(viewModel)\n                is WorkspaceDecisionPageViewModel -> workspaceDecisionPage(viewModel)\n                is WorkspaceDecisionsPageViewModel -> workspaceDecisionsPage(viewModel)\n                is WorkspaceDocumentationSectionPageViewModel -> workspaceDocumentationSectionPage(viewModel)\n            }\n        }\n    }\n\n    val subDirectory = File(exportDir, viewModel.url)\n    val htmlFile = File(subDirectory, \"index.html\")\n    htmlFile.parentFile.mkdirs()\n    htmlFile.writeText(html)\n\n    val hash = MessageDigest.getInstance(\"MD5\").digest(html.toByteArray())\n        .let { BigInteger(1, it).toString(16) }\n\n    val hashFile = File(\"${htmlFile.absolutePath}.md5\")\n    hashFile.writeText(hash)\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/Asciidoctor.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport net.sourceforge.plantuml.FileFormat\nimport net.sourceforge.plantuml.FileFormatOption\nimport net.sourceforge.plantuml.SourceStringReader\nimport org.asciidoctor.Asciidoctor\nimport org.asciidoctor.ast.ContentModel\nimport org.asciidoctor.ast.ContentNode\nimport org.asciidoctor.ast.Document\nimport org.asciidoctor.ast.StructuralNode\nimport org.asciidoctor.converter.ConverterFor\nimport org.asciidoctor.converter.StringConverter\nimport org.asciidoctor.extension.BlockProcessor\nimport org.asciidoctor.extension.Contexts\nimport org.asciidoctor.extension.Name\nimport org.asciidoctor.extension.Reader\nimport java.io.ByteArrayOutputStream\n\nval asciidoctorWithTextConverter: Asciidoctor = Asciidoctor.Factory.create().apply {\n    javaConverterRegistry().register(AsciiDocTextConverter::class.java)\n}\n\nval asciidoctorWithPUMLRenderer: Asciidoctor = Asciidoctor.Factory.create().apply {\n    javaExtensionRegistry().block(PlantUMLBlockProcessor::class.java)\n}\n\n@ConverterFor(\"text\")\nclass AsciiDocTextConverter(\n    backend: String?,\n    opts: Map<String?, Any?>?\n) : StringConverter(backend, opts) {\n    // based on https://docs.asciidoctor.org/asciidoctorj/latest/write-converter/\n\n    override fun convert(node: ContentNode, transform: String?, o: Map<Any?, Any?>?): String? {\n        val transform1 = transform ?: node.nodeName\n        return if (node is Document)\n            node.content.toString()\n        else if (node is org.asciidoctor.ast.Section)\n            \"${node.title}\\n${node.content}\"\n        else if (transform1 == \"preamble\" || transform1 == \"paragraph\")\n            (node as StructuralNode).content as String\n        else\n            null\n    }\n}\n\n@Name(\"plantuml\")\n@Contexts(value = [Contexts.LISTING])\n@ContentModel(ContentModel.VERBATIM)\nclass PlantUMLBlockProcessor : BlockProcessor() {\n    override fun process(parent: StructuralNode?, reader: Reader, attributes: MutableMap<String, Any>?): Any {\n        val plantUMLCode = reader.read()\n        val plantUMLReader = SourceStringReader(plantUMLCode)\n        val stream = ByteArrayOutputStream()\n        plantUMLReader.outputImage(stream, FileFormatOption(FileFormat.SVG, false))\n        return createBlock(parent, \"pass\", \"<div>$stream</div>\")\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/BranchHomeLinkViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport nl.avisi.structurizr.site.generatr.site.asUrlToDirectory\nimport java.net.URLEncoder\nimport java.nio.charset.StandardCharsets\n\ndata class BranchHomeLinkViewModel(\n    private val pageViewModel: PageViewModel,\n    private val branchName: String\n) {\n    val title get() = branchName\n    val relativeHref get() = HomePageViewModel.url().asUrlToDirectory(pageViewModel.url) + \"../${URLEncoder.encode(branchName, StandardCharsets.UTF_8)}/\"\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ComponentTabViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\ndata class ComponentTabViewModel(val pageViewModel: PageViewModel, val title: String, val url: String) {\n    val link = LinkViewModel(pageViewModel, title, url)\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ComponentsTabViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.Container\nimport nl.avisi.structurizr.site.generatr.hasImageViews\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nfun SoftwareSystemPageViewModel.createComponentsTabViewModel(\n    generatorContext: GeneratorContext,\n    container: Container\n) = buildList {\n    container\n        .components\n        .sortedBy { it.name }\n        .filter { component ->\n            generatorContext.workspace.hasImageViews(component.id) }\n        .map {\n            ComponentTabViewModel(\n                this@createComponentsTabViewModel,\n                it.name,\n                SoftwareSystemContainerComponentCodePageViewModel.url(it.container, it)\n            )\n        }\n        .forEach { add(it) }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ContainerTabViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\ndata class ContainerTabViewModel(val pageViewModel: SoftwareSystemPageViewModel, val title: String, val url: String, val match: Match = Match.EXACT) {\n    val link = LinkViewModel(pageViewModel, title, url, match)\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ContainersCodeTabViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.structurizr.site.generatr.hasComponentDiagrams\nimport nl.avisi.structurizr.site.generatr.hasImageViews\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nfun SoftwareSystemPageViewModel.createContainersCodeTabViewModel(\n    generatorContext: GeneratorContext,\n    softwareSystem: SoftwareSystem,\n) = buildList {\n    softwareSystem\n        .containers\n        .sortedBy { it.name }\n        .filter { container ->\n            (generatorContext.workspace.hasComponentDiagrams(container) or\n                    generatorContext.workspace.hasImageViews(container.id)) and\n                    container.components.any { component -> generatorContext.workspace.hasImageViews(component.id) }\n        }\n        .map { container ->\n            ContainerTabViewModel(\n                this@createContainersCodeTabViewModel,\n                container.name,\n                SoftwareSystemContainerComponentCodePageViewModel.url(container, container.components.sortedBy { it.name }.firstOrNull { component -> generatorContext.workspace.hasImageViews(component.id) }),\n                Match.SIBLING\n            )\n        }\n        .forEach { add(it) }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ContainersComponentTabViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.structurizr.site.generatr.hasComponentDiagrams\nimport nl.avisi.structurizr.site.generatr.hasImageViews\nimport nl.avisi.structurizr.site.generatr.includedProperties\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nfun SoftwareSystemPageViewModel.createContainersComponentTabViewModel(\n    generatorContext: GeneratorContext,\n    softwareSystem: SoftwareSystem,\n) = buildList {\n   softwareSystem\n        .containers\n        .sortedBy { it.name }\n        .filter { container ->\n            container.includedProperties.isNotEmpty() or\n                    generatorContext.workspace.hasComponentDiagrams(container) or\n                    generatorContext.workspace.hasImageViews(container.id) }\n        .map {\n            ContainerTabViewModel(\n                this@createContainersComponentTabViewModel,\n                it.name,\n                SoftwareSystemContainerComponentsPageViewModel.url(it)\n            )\n        }\n        .forEach { add(it) }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ContentText.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Decision\nimport com.structurizr.documentation.Format\nimport com.structurizr.documentation.Section\nimport com.vladsch.flexmark.ast.Heading\nimport com.vladsch.flexmark.ast.Paragraph\nimport com.vladsch.flexmark.parser.Parser\nimport org.asciidoctor.Options\nimport org.asciidoctor.SafeMode\n\nprivate val parser = Parser.builder().build()\n\nfun Decision.contentText(): String = when (format) {\n    Format.Markdown -> markdownText(content)\n    Format.AsciiDoc -> asciidocText(content)\n    else -> \"\"\n}\n\nfun Section.contentText() = when (format) {\n    Format.Markdown -> markdownText(content)\n    Format.AsciiDoc -> asciidocText(content)\n    else -> \"\"\n}\n\nprivate fun markdownText(content: String): String {\n    val document = parser.parse(content)\n    if (!document.hasChildren())\n        return \"\"\n\n    return document\n        .children\n        .filterIsInstance<Heading>()\n        .drop(1) // ignore title\n        .joinToString(\" \") { it.text.toString() }\n        .plus(\" \")\n        .plus(\n            document\n                .children\n                .filterIsInstance<Paragraph>()\n                .joinToString(\" \") { it.chars.toString().trim() }\n        )\n        .trim()\n}\n\nprivate fun asciidocText(content: String): String {\n    val options = Options.builder().safe(SafeMode.SERVER).backend(\"text\").build()\n    val text = asciidoctorWithTextConverter.convert(content, options)\n\n    return text.lines().joinToString(\" \")\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ContentTitle.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Format\nimport com.structurizr.documentation.Section\nimport com.vladsch.flexmark.ast.Heading\nimport com.vladsch.flexmark.parser.Parser\nimport org.asciidoctor.Options\nimport org.asciidoctor.SafeMode\n\nprivate val parser = Parser.builder().build()\n\nfun Section.contentTitle(): String = when (format) {\n    Format.Markdown -> markdownTitle()\n    Format.AsciiDoc -> asciidocTitle()\n    else -> \"unsupported document\"\n}\n\nprivate fun Section.markdownTitle(): String {\n    val document = parser.parse(content)\n\n    val header = document.children.firstOrNull { it is Heading }?.let { it as Heading }\n    if (header != null)\n        return header.text.toString().trim()\n\n    return \"untitled document\"\n}\n\nprivate fun Section.asciidocTitle(): String {\n    val options = Options.builder().safe(SafeMode.SERVER).backend(\"text\").build()\n    val document = asciidoctorWithTextConverter.load(content, options)\n\n    if (document.title != null && document.title.isNotEmpty())\n        return document.title.trim()\n\n    return \"untitled document\"\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/CustomStylesheetViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\nimport nl.avisi.structurizr.site.generatr.site.asUrlToFile\n\nclass CustomStylesheetViewModel(generatorContext: GeneratorContext, pageViewModel: PageViewModel) {\n    val resourceURI = getResourceURI(generatorContext)?.let {\n        if (!it.lowercase().startsWith(\"http\"))\n            \"/$it\".asUrlToFile(pageViewModel.url)\n        else\n            it\n    }\n    val includeCustomStylesheet = resourceURI != null\n\n    private fun getResourceURI(generatorContext: GeneratorContext) =\n        generatorContext.workspace.views.configuration.properties\n            .getOrDefault(\n                \"generatr.style.customStylesheet\",\n                null\n            )\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/DecisionTabViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\ndata class DecisionTabViewModel(val pageViewModel: SoftwareSystemPageViewModel, val title: String, val url: String, private val match: Match = Match.EXACT) {\n    val link = LinkViewModel(pageViewModel, title, url, match)\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/DecisionsTableViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Decision\nimport com.structurizr.model.SoftwareSystem\nimport com.structurizr.model.StaticStructureElement\nimport nl.avisi.structurizr.site.generatr.hasDecisions\nimport nl.avisi.structurizr.site.generatr.site.formatDate\n\nfun PageViewModel.createDecisionsTableViewModel(decisions: Collection<Decision>, hrefFactory: (Decision) -> String) =\n    TableViewModel.create {\n        headerRow(\n            headerCellSmall(\"ID\"),\n            headerCell(\"Date\"),\n            headerCell(\"Status\"),\n            headerCellLarge(\"Title\")\n        )\n\n        decisions\n            .sortedBy { it.id.toInt() }\n            .forEach {\n                bodyRow(\n                    cellWithIndex(it.id),\n                    cell(formatDate(it.date)),\n                    cell(it.status),\n                    cellWithLink(this@createDecisionsTableViewModel, it.title, hrefFactory(it))\n                )\n            }\n    }\n\nfun SoftwareSystemPageViewModel.createDecisionsTabViewModel(\n    softwareSystem: SoftwareSystem,\n    tab: SoftwareSystemPageViewModel.Tab,\n    linkMatch: (StaticStructureElement) -> Match = { Match.EXACT }\n) = buildList {\n    if (softwareSystem.hasDecisions()) {\n        add(\n            DecisionTabViewModel(\n                this@createDecisionsTabViewModel,\n                \"System\",\n                SoftwareSystemPageViewModel.url(softwareSystem, tab),\n                linkMatch(softwareSystem)\n            )\n        )\n    }\n    softwareSystem\n        .containers\n        .filter { it.hasDecisions(recursive = true) }\n        .map {\n            DecisionTabViewModel(\n                this@createDecisionsTabViewModel,\n                it.name,\n                SoftwareSystemContainerDecisionsPageViewModel.url(it),\n                linkMatch(it)\n            )\n        }\n        .forEach { add(it) }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/DiagramIndexViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\ndata class IndexEntry(val key: String, val title: String)\n\ndata class DiagramIndexViewModel(\n    private val diagrams: List<DiagramViewModel> = emptyList(),\n    private val images: List<ImageViewViewModel> = emptyList()) {\n\n    val entries = diagrams.map { IndexEntry(it.key, it.indexTitle()) } +\n            images.map { IndexEntry(it.key, it.indexTitle()) }\n\n    val visible: Boolean = (diagrams.count() + images.count()) > 1\n\n    private fun DiagramViewModel.indexTitle() = if (description.isNullOrBlank()) title else \"$title ($description)\"\n    private fun ImageViewViewModel.indexTitle() = if (description.isNullOrBlank()) title else \"$title ($description)\"\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/DiagramViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.view.View\n\ndata class DiagramViewModel(\n    val key: String,\n    val title: String,\n    val description: String?,\n    val svg: String?,\n    val diagramWidthInPixels: Int?,\n    val svgLocation: ImageViewModel,\n    val pngLocation: ImageViewModel,\n    val pumlLocation: ImageViewModel\n) {\n    companion object {\n        fun forView(pageViewModel: PageViewModel, view: View, svgFactory: (key: String, url: String) -> String?) =\n            forView(pageViewModel, view.key, view.name, view.title, view.description.ifBlank { null }, svgFactory)\n\n        fun forView(\n            pageViewModel: PageViewModel,\n            key: String,\n            name: String,\n            title: String?,\n            description: String?,\n            svgFactory: (key: String, url: String) -> String?\n        ): DiagramViewModel {\n            val svg = svgFactory(key, pageViewModel.url)\n            return DiagramViewModel(\n                key,\n                title ?: name,\n                description,\n                svg,\n                extractDiagramWidthInPixels(svg),\n                ImageViewModel(pageViewModel, \"/svg/$key.svg\"),\n                ImageViewModel(pageViewModel, \"/png/$key.png\"),\n                ImageViewModel(pageViewModel, \"/puml/$key.puml\")\n            )\n        }\n\n        private fun extractDiagramWidthInPixels(svg: String?) =\n            if (svg != null)\n                \"viewBox=\\\"\\\\d+ \\\\d+ (\\\\d+) \\\\d+\\\"\".toRegex()\n                    .find(svg)\n                    ?.let { it.groupValues[1].toInt() }\n                    ?: throw IllegalStateException(\"No viewBox attribute found in SVG!\")\n            else null\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ExternalLinkViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\ndata class ExternalLinkViewModel(\n    val title: String,\n    val href: String,\n)\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/FaviconViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\nimport nl.avisi.structurizr.site.generatr.site.asUrlToFile\nimport java.io.File\n\nclass FaviconViewModel(generatorContext: GeneratorContext, pageViewModel: PageViewModel) {\n    val url = faviconPath(generatorContext)?.let { \"/$it\".asUrlToFile(pageViewModel.url) }\n    val type = extractType()\n    val includeFavicon = url != null\n\n    private fun faviconPath(generatorContext: GeneratorContext) =\n        generatorContext.workspace.views.configuration.properties\n            .getOrDefault(\n                \"generatr.style.faviconPath\",\n                null\n            )\n\n    private fun extractType(): String? {\n        return url?.let {\n            val extension = File(it).extension.lowercase()\n            if (extension.isBlank() || !arrayOf(\"ico\", \"png\", \"gif\").contains(extension))\n                throw IllegalArgumentException(\"Favicon must be a valid *.ico, *.png of *.gif file\")\n\n            \"image/$extension\"\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/FlexmarkConfig.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.Workspace\n\nimport com.vladsch.flexmark.util.misc.Extension\nimport com.vladsch.flexmark.util.data.DataHolder\nimport com.vladsch.flexmark.util.data.MutableDataSet\nimport com.vladsch.flexmark.ext.emoji.EmojiExtension\nimport com.vladsch.flexmark.ext.emoji.EmojiImageType\nimport com.vladsch.flexmark.ext.gitlab.GitLabExtension\nimport com.vladsch.flexmark.parser.Parser\n\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\ndata class FlexmarkConfig(\n    val flexmarkExtensionsProperty: String,\n    val selectedExtensionMap: Map<String, Extension>,\n    val flexmarkOptions: DataHolder\n)\n\nval availableExtensionMap: MutableMap<String, Extension> = mutableMapOf(\n    \"Abbreviation\" to com.vladsch.flexmark.ext.abbreviation.AbbreviationExtension.create(),\n    \"Admonition\" to com.vladsch.flexmark.ext.admonition.AdmonitionExtension.create(),\n    \"AnchorLink\" to com.vladsch.flexmark.ext.anchorlink.AnchorLinkExtension.create(),\n    \"Aside\" to com.vladsch.flexmark.ext.aside.AsideExtension.create(),\n    \"Attributes\" to com.vladsch.flexmark.ext.attributes.AttributesExtension.create(),\n    \"Autolink\" to com.vladsch.flexmark.ext.autolink.AutolinkExtension.create(),\n    \"Definition\" to com.vladsch.flexmark.ext.definition.DefinitionExtension.create(),\n    // Docx Converter: render doxc from markdown\n    \"Emoji\" to com.vladsch.flexmark.ext.emoji.EmojiExtension.create(),\n    \"EnumeratedReference\" to com.vladsch.flexmark.ext.enumerated.reference.EnumeratedReferenceExtension.create(),\n    \"Footnotes\" to com.vladsch.flexmark.ext.footnotes.FootnoteExtension.create(),\n    \"GfmIssues\" to com.vladsch.flexmark.ext.gfm.issues.GfmIssuesExtension.create(),\n    \"GfmStrikethrough\" to com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension.create(),\n    \"GfmSubscript\" to com.vladsch.flexmark.ext.gfm.strikethrough.SubscriptExtension.create(),\n    \"GfmStrikethroughSubscript\" to com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension.create(),\n    \"GfmTaskList\" to com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension.create(),\n    \"GfmUsers\" to com.vladsch.flexmark.ext.gfm.users.GfmUsersExtension.create(),\n    \"GitLab\" to com.vladsch.flexmark.ext.gitlab.GitLabExtension.create(),\n    // Html To Markdown: render markdown from html\n    \"Ins\" to com.vladsch.flexmark.ext.ins.InsExtension.create(),\n    // Jekyll Tags: render jekyll tags from markdown\n    // Jira-Converter: render jira markup from markdown\n    \"Macros\" to com.vladsch.flexmark.ext.macros.MacrosExtension.create(),\n    \"MediaTags\" to com.vladsch.flexmark.ext.media.tags.MediaTagsExtension.create(),\n    \"ResizableImage\" to com.vladsch.flexmark.ext.resizable.image.ResizableImageExtension.create(),\n    // \"SpecExample\" to com.vladsch.flexmark.ext.spec.example.SpecExampleExtension.create(),\n    \"Superscript\" to com.vladsch.flexmark.ext.superscript.SuperscriptExtension.create(),\n    \"Tables\" to com.vladsch.flexmark.ext.tables.TablesExtension.create(),\n    \"TableOfContents\" to com.vladsch.flexmark.ext.toc.TocExtension.create(),\n    \"SimulatedTableOfContents\" to com.vladsch.flexmark.ext.toc.SimTocExtension.create(),\n    \"Typographic\" to com.vladsch.flexmark.ext.typographic.TypographicExtension.create(),\n    \"WikiLinks\" to com.vladsch.flexmark.ext.wikilink.WikiLinkExtension.create(),\n    \"XWikiMacro\" to com.vladsch.flexmark.ext.xwiki.macros.MacroExtension.create(),\n    \"YAMLFrontMatter\" to com.vladsch.flexmark.ext.yaml.front.matter.YamlFrontMatterExtension.create(),\n    // \"YouTrackConverter\" to com.vladsch.flexmark.ext.youtrack.converter.YouTrackConverterExtension.create(),\n    \"YouTubeLink\" to com.vladsch.flexmark.ext.youtube.embedded.YouTubeLinkExtension.create(),\n)\n\nfun buildFlexmarkConfig(context: GeneratorContext): FlexmarkConfig {\n    val configuration = context.workspace.views.configuration.properties\n    val flexmarkExtensionString = configuration.getOrDefault(\"generatr.markdown.flexmark.extensions\", \"Tables\")\n\n    val flexmarkExtensionNames = flexmarkExtensionString.split(\",\")\n\n    val selectedExtensionMap: MutableMap<String, Extension> = mutableMapOf();\n\n    flexmarkExtensionNames.forEach {\n        val extensionName = it.trim()\n        if (availableExtensionMap.containsKey(extensionName)) {\n            selectedExtensionMap[extensionName] = availableExtensionMap[extensionName] as Extension\n        } else {\n            println(\"Unknown flexmark extension requested in generatr.markdown.flexmark.extensions: Skipping $extensionName.\")\n        }\n    }\n\n    val flexmarkOptions = MutableDataSet()\n    flexmarkOptions.set(Parser.EXTENSIONS, selectedExtensionMap.values)\n\n    if (selectedExtensionMap.containsKey(\"Emoji\")) {\n        flexmarkOptions.set(EmojiExtension.USE_IMAGE_TYPE, EmojiImageType.UNICODE_ONLY)\n    }\n    if (selectedExtensionMap.containsKey(\"GitLab\")) {\n        flexmarkOptions.set(GitLabExtension.RENDER_BLOCK_MERMAID, false)\n    }\n\n    return FlexmarkConfig(flexmarkExtensionString, selectedExtensionMap, flexmarkOptions)\n}\n\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/HeaderBarViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nclass HeaderBarViewModel(pageViewModel: PageViewModel, generatorContext: GeneratorContext) {\n    val url = pageViewModel.url\n    val logo = logoPath(generatorContext)?.let { ImageViewModel(pageViewModel, \"/$it\") }\n    val hasLogo = logo != null\n    val titleLink = LinkViewModel(pageViewModel, generatorContext.workspace.name, HomePageViewModel.url())\n    val searchLink = LinkViewModel(pageViewModel, generatorContext.workspace.name, SearchViewModel.url())\n    val branches = generatorContext.branches\n        .map { BranchHomeLinkViewModel(pageViewModel, it) }\n    val currentBranch = generatorContext.currentBranch\n    val version = generatorContext.version\n    val allowToggleTheme = pageViewModel.allowToggleTheme\n\n    private fun logoPath(generatorContext: GeneratorContext) =\n        generatorContext.workspace.views.configuration.properties\n            .getOrDefault(\n                \"generatr.style.logoPath\",\n                null\n            )\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/HomePageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Format\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\nimport org.intellij.lang.annotations.Language\n\nclass HomePageViewModel(generatorContext: GeneratorContext) : PageViewModel(generatorContext) {\n    override val pageSubTitle = if (generatorContext.workspace.name.isNotBlank()) \"\" else \"Home\"\n    override val url = url()\n\n    val content = toHtml(\n        this,\n        content = generatorContext.workspace.documentation.sections\n            .firstOrNull { it.order == 1 }?.content ?: DEFAULT_HOMEPAGE_CONTENT,\n        format = generatorContext.workspace.documentation.sections\n            .firstOrNull { it.order == 1 }?.format ?: Format.Markdown,\n        svgFactory = generatorContext.svgFactory\n    )\n\n    companion object {\n        fun url() = \"/\"\n\n        @Language(\"markdown\")\n        const val DEFAULT_HOMEPAGE_CONTENT = \"\"\"\n## Home\n\nThis is the default home page. You can customize this home page by adding documentation pages to your workspace. For\nexample (see the [Structurizr DSL](https://github.com/structurizr/dsl/blob/master/docs/language-reference.md#documentation)\ndocumentation for more information):\n\n```text\nworkspace \"Example workspace\" {\n    !docs docs\n}\n```\n\nIn the above example, the first document in the `docs` directory (after sorting alphabetically), will be used as the\nhomepage of the generated site.\n\"\"\"\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ImageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport nl.avisi.structurizr.site.generatr.site.asUrlToFile\n\ndata class ImageViewModel(val pageViewModel: PageViewModel, val href: String) {\n    val relativeHref get() = href.asUrlToFile(pageViewModel.url)\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ImageViewViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.Component\nimport com.structurizr.model.Container\nimport com.structurizr.model.SoftwareSystem\nimport com.structurizr.view.ImageView\n\ndata class ImageViewViewModel(val imageView: ImageView) {\n    val key: String = imageView.key\n    val title: String = imageView.title ?: generateName()\n    val description: String? = imageView.description\n    val content: String = imageView.content\n\n    private fun generateName(): String {\n        val elementName = generateSequence(imageView.element) {\n            it.parent\n        }.map { it.name }.toList().reversed().joinToString(\" - \")\n\n        val elementType = when (imageView.element) {\n            is SoftwareSystem -> \"Containers\"\n            is Container -> \"Components\"\n            is Component -> \"Code\"\n            else -> throw IllegalStateException(\"Not supported element\")\n        }\n\n        return \"$elementName - $elementType\"\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/LinkViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport nl.avisi.structurizr.site.generatr.site.asUrlToDirectory\n\ndata class LinkViewModel(\n    private val pageViewModel: PageViewModel,\n    val title: String,\n    val href: String,\n    val match: Match = Match.EXACT\n) {\n    val relativeHref get() = href.asUrlToDirectory(pageViewModel.url)\n    val active get() =\n        when (match) {\n            Match.EXACT -> isHrefOfContainingPage\n            Match.CHILD -> isChildHrefOfContainingPage\n            Match.SIBLING -> isSiblingHrefOfContainingPage\n            Match.SIBLING_CHILD -> isSiblingChildHrefOfContainingPage\n        }\n\n    private val isHrefOfContainingPage get() = href == pageViewModel.url\n    private val isChildHrefOfContainingPage get() = pageViewModel.url == href || pageViewModel.url.startsWith(\"$href/\")\n    private val isSiblingHrefOfContainingPage get() = pageViewModel.url.trimEnd('/').dropLastWhile { it != '/' } == href.trimEnd('/').dropLastWhile { it != '/' }\n    private val isSiblingChildHrefOfContainingPage get() =\n        pageViewModel.url.trimEnd('/').dropLastWhile { it != '/' }.trimEnd('/').dropLastWhile { it != '/' } ==\n            href.trimEnd('/').dropLastWhile { it != '/' }.trimEnd('/').dropLastWhile { it != '/' }\n}\nenum class Match {\n    EXACT,\n    CHILD,\n    SIBLING,\n    SIBLING_CHILD\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuNodeViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\ndata class MenuNodeViewModel(val name: String, val children: List<MenuNodeViewModel>)\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nclass MenuViewModel(generatorContext: GeneratorContext, private val pageViewModel: PageViewModel) {\n    val generalItems = sequence {\n        yield(createMenuItem(\"Home\", HomePageViewModel.url()))\n\n        if (generatorContext.workspace.documentation.decisions.isNotEmpty())\n            yield(createMenuItem(\"Decisions\", WorkspaceDecisionsPageViewModel.url(), Match.CHILD))\n\n        if (generatorContext.workspace.model.softwareSystems.isNotEmpty())\n            yield(createMenuItem(\"Software Systems\", SoftwareSystemsPageViewModel.url()))\n\n        generatorContext.workspace.documentation.sections\n            .sortedBy { it.order }\n            .drop(1)\n            .forEach { yield(createMenuItem(it.contentTitle(), WorkspaceDocumentationSectionPageViewModel.url(it))) }\n    }.toList()\n\n    val softwareSystemItems = pageViewModel.includedSoftwareSystems\n        .sortedBy { it.name.lowercase() }\n        .map {\n            createMenuItem(it.name, SoftwareSystemPageViewModel.url(it, SoftwareSystemPageViewModel.Tab.HOME), Match.CHILD)\n        }\n\n    private val groupSeparator = generatorContext.workspace.model.properties[\"structurizr.groupSeparator\"] ?: \"/\"\n\n    private val softwareSystemPaths = pageViewModel.includedSoftwareSystems\n        .map { \"${it.group ?: \"\"}$groupSeparator${it.name}\" }\n        .sortedBy { it.lowercase() }\n\n    private fun createMenuItem(title: String, href: String, match: Match = Match.EXACT) =\n        LinkViewModel(pageViewModel, title, href, match)\n\n    fun softwareSystemNodes(): MenuNodeViewModel {\n        data class MutableMenuNode(val name: String, val children: MutableList<MutableMenuNode>) {\n            fun toMenuNode(): MenuNodeViewModel = MenuNodeViewModel(name, children.map { it.toMenuNode() })\n        }\n\n        val rootNode = MutableMenuNode(\"\", mutableListOf())\n\n        softwareSystemPaths.forEach { path ->\n            var currentNode = rootNode\n            path.split(groupSeparator).forEach { part ->\n                val existingNode = currentNode.children.find { it.name == part }\n                currentNode = if (existingNode == null) {\n                    val newNode = MutableMenuNode(part, mutableListOf())\n                    currentNode.children.add(newNode)\n                    newNode\n                } else {\n                    existingNode\n                }\n            }\n        }\n\n        return rootNode.toMenuNode()\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/PageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport nl.avisi.structurizr.site.generatr.includedSoftwareSystems\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\nimport nl.avisi.structurizr.site.generatr.site.views.CDN\n\nabstract class PageViewModel(protected val generatorContext: GeneratorContext) {\n    val pageTitle: String by lazy {\n        if (pageSubTitle.isNotBlank() && generatorContext.workspace.name.isNotBlank())\n            \"$pageSubTitle | ${generatorContext.workspace.name}\"\n        else if (generatorContext.workspace.name.isNotBlank())\n            generatorContext.workspace.name\n        else\n            pageSubTitle\n    }\n    val cdn by lazy { CDN(generatorContext.workspace) }\n    val favicon by lazy { FaviconViewModel(generatorContext, this) }\n    val customStylesheet by lazy { CustomStylesheetViewModel(generatorContext, this) }\n    val headerBar by lazy { HeaderBarViewModel(this, generatorContext) }\n    val menu by lazy { MenuViewModel(generatorContext, this) }\n    val includeAutoReloading = generatorContext.serving\n    val flexmarkConfig by lazy { buildFlexmarkConfig(generatorContext) }\n    val includeAdmonition = flexmarkConfig.selectedExtensionMap.containsKey(\"Admonition\")\n    val includeKatex = flexmarkConfig.selectedExtensionMap.containsKey(\"GitLab\")\n    val includedSoftwareSystems = generatorContext.workspace.includedSoftwareSystems\n    val configuration = generatorContext.workspace.views.configuration.properties\n    val includeTreeview = configuration.getOrDefault(\"generatr.site.nestGroups\", \"false\").toBoolean()\n    val theme = configuration.getOrDefault(\"generatr.site.theme\", \"light\").toTheme()\n    val allowToggleTheme = theme == Theme.AUTO\n\n    abstract val url: String\n    abstract val pageSubTitle: String\n}\n\nfun String.toTheme() = when (this) {\n    \"light\" -> Theme.LIGHT\n    \"dark\" -> Theme.DARK\n    \"auto\" -> Theme.AUTO\n    else -> throw IllegalArgumentException(\"Unknown theme '$this', allowed values are 'light', 'dark' or 'auto'\")\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/PropertiesTableViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.util.Url\n\nfun createPropertiesTableViewModel(properties: Map<String, String>) =\n    TableViewModel.create {\n        headerRow(\n            headerCellMedium(\"Name\"),\n            headerCell(\"Value\")\n        )\n\n        properties\n            .toSortedMap()\n            .forEach { (name, value) ->\n                bodyRow(\n                    cell(name),\n                    if (Url.isUrl(value))\n                        cellWithExternalLink(value, value)\n                    else\n                        cell(value)\n                )\n            }\n    }\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SearchViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\nimport nl.avisi.structurizr.site.generatr.site.model.indexing.*\n\nclass SearchViewModel(generatorContext: GeneratorContext) : PageViewModel(generatorContext) {\n    override val pageSubTitle = \"Search results\"\n    override val url = url()\n\n    val language: String = generatorContext.workspace.views\n        .configuration.properties.getOrDefault(\"generatr.search.language\", \"\")\n\n    val documents = buildList {\n        add(home(generatorContext.workspace.documentation, this@SearchViewModel))\n        addAll(workspaceDecisions(generatorContext.workspace.documentation, this@SearchViewModel))\n        addAll(workspaceSections(generatorContext.workspace.documentation, this@SearchViewModel))\n        addAll(\n            includedSoftwareSystems\n                .flatMap {\n                    buildList {\n                        add(softwareSystemHome(it, this@SearchViewModel))\n                        add(softwareSystemContext(it, this@SearchViewModel))\n                        add(softwareSystemContainers(it, this@SearchViewModel))\n                        add(softwareSystemComponents(it, this@SearchViewModel))\n                        add(softwareSystemRelationships(it, this@SearchViewModel))\n                        addAll(softwareSystemDecisions(it, this@SearchViewModel))\n                        addAll(softwareSystemSections(it, this@SearchViewModel))\n                    }\n                }\n                .mapNotNull { it }\n        )\n        addAll(\n            includedSoftwareSystems\n                .flatMap {\n                    buildList {\n                        it.containers.forEach {\n                            addAll(softwareSystemContainerSections(it, this@SearchViewModel))\n                            addAll(softwareSystemContainerDecisions(it, this@SearchViewModel))\n                        }\n                    }\n                }\n                .mapNotNull { it }\n        )\n    }.mapNotNull { it }\n\n    companion object {\n        fun url() = \"/search\"\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SectionTabViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\ndata class SectionTabViewModel(val pageViewModel: SoftwareSystemPageViewModel, val title: String, val url: String, private val match: Match = Match.EXACT) {\n    val link = LinkViewModel(pageViewModel, title, url, match)\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SectionsTableViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Section\nimport com.structurizr.model.SoftwareSystem\nimport com.structurizr.model.StaticStructureElement\nimport nl.avisi.structurizr.site.generatr.hasDocumentationSections\nimport nl.avisi.structurizr.site.generatr.hasSections\n\nfun PageViewModel.createSectionsTableViewModel(\n    sections: Collection<Section>,\n    dropFirst: Boolean = true,\n    hrefFactory: (Section) -> String\n): TableViewModel {\n    val rows = sections\n        .sortedBy { it.order }\n        .drop(if (dropFirst) 1 else 0)\n        .associateWith { if (dropFirst) it.order - 1 else it.order }\n\n    return if (rows.isEmpty()) {\n        TableViewModel(emptyList(), emptyList())\n    } else {\n        TableViewModel.create {\n            headerRow(\n                headerCellSmall(\"#\"),\n                headerCell(\"Title\")\n            )\n\n            rows.forEach { (section, index) ->\n                bodyRow(\n                    cellWithIndex(index.toString()),\n                    cellWithLink(this@createSectionsTableViewModel, section.contentTitle(), hrefFactory(section))\n                )\n            }\n        }\n    }\n}\n\nfun BaseSoftwareSystemSectionsPageViewModel.createSectionsTabViewModel(\n    softwareSystem: SoftwareSystem,\n    tab: SoftwareSystemPageViewModel.Tab,\n    linkMatch: (StaticStructureElement) -> Match = { Match.EXACT }\n) = buildList {\n    if (softwareSystem.hasDocumentationSections()) {\n        add(\n            SectionTabViewModel(\n                this@createSectionsTabViewModel,\n                \"System\",\n                SoftwareSystemPageViewModel.url(softwareSystem, tab),\n                linkMatch(softwareSystem)\n            )\n        )\n    }\n    softwareSystem\n        .containers\n        .filter { it.hasSections(recursive = true) }\n        .map {\n            SectionTabViewModel(\n                this@createSectionsTabViewModel,\n                it.name,\n                SoftwareSystemContainerSectionsPageViewModel.url(it),\n                linkMatch(it)\n            )\n        }\n        .forEach { add(it) }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentCodePageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.Container\nimport com.structurizr.model.Component\nimport nl.avisi.structurizr.site.generatr.normalize\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nclass SoftwareSystemContainerComponentCodePageViewModel(generatorContext: GeneratorContext, container: Container, component: Component) :\n    SoftwareSystemPageViewModel(generatorContext, container.softwareSystem, Tab.CODE) {\n    override val url = url(container, component)\n    val images = generatorContext.workspace.views.imageViews\n        .filter { it.element == component }\n        .map { ImageViewViewModel(it) }\n\n    val visible = images.isNotEmpty()\n    val containerTabs = createContainersCodeTabViewModel(generatorContext, container.softwareSystem)\n    val componentTabs = createComponentsTabViewModel(generatorContext, container)\n    val diagramIndex = DiagramIndexViewModel(images = images)\n\n    companion object {\n        fun url(container: Container, component: Component?) = \"${url(container.softwareSystem, Tab.CODE)}/${container.name.normalize()}/${component?.name?.normalize()}\"\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentDecisionPageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Decision\nimport com.structurizr.model.Component\nimport nl.avisi.structurizr.site.generatr.normalize\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nclass SoftwareSystemContainerComponentDecisionPageViewModel(\n    generatorContext: GeneratorContext, component: Component, decision: Decision\n) : SoftwareSystemPageViewModel(generatorContext, component.container.softwareSystem, Tab.DECISIONS) {\n    override val url = url(component, decision)\n\n    val content = toHtml(this, transformADRLinks(decision.content, component), decision.format, generatorContext.svgFactory)\n\n    private fun transformADRLinks(content: String, component: Component) =\n        content.replace(\"\\\\[(.*)]\\\\(#(\\\\d+)\\\\)\".toRegex()) {\n            \"[${it.groupValues[1]}](${url(component.container.softwareSystem, Tab.DECISIONS)}/${component.container.name.normalize()}/${component.name.normalize()}/${it.groupValues[2]})\"\n        }\n\n    companion object {\n        fun url(component: Component, decision: Decision) =\n            \"${url(component.container.softwareSystem, Tab.DECISIONS)}/${component.container.name.normalize()}/${component.name.normalize()}/${decision.id}\"\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentDecisionsPageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Decision\nimport com.structurizr.model.Component\nimport com.structurizr.model.Container\nimport nl.avisi.structurizr.site.generatr.hasDecisions\nimport nl.avisi.structurizr.site.generatr.normalize\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nclass SoftwareSystemContainerComponentDecisionsPageViewModel(\n    generatorContext: GeneratorContext,\n    component: Component\n) : BaseSoftwareSystemContainerDecisionsPageViewModel(\n    generatorContext,\n    component.container\n) {\n    val visible = component.hasDecisions()\n\n    override val url = url(component)\n    val decisionsTable = createDecisionsTableViewModel(component.documentation.decisions) {\n        \"$url/${it.id}\"\n    }\n\n    override fun decisionTableItemUrl(container: Container, decision: Decision?) = decision?.let { \"${url(container)}/${decision.id}\" } ?: url(container)\n    override fun componentDecisionItemUrl(component: Component) = \"${url(component.container)}/${component.name.normalize()}\"\n\n    companion object {\n        fun url(container: Container) = \"${url(container.softwareSystem, Tab.DECISIONS)}/${container.name.normalize()}\"\n        fun url(component: Component) = \"${url(component.container.softwareSystem, Tab.DECISIONS)}/${component.container.name.normalize()}/${component.name.normalize()}\"\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentSectionPageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Section\nimport com.structurizr.model.Component\nimport com.structurizr.model.Container\nimport nl.avisi.structurizr.site.generatr.normalize\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nclass SoftwareSystemContainerComponentSectionPageViewModel(\n    generatorContext: GeneratorContext,\n    component: Component,\n    section: Section\n) : SoftwareSystemPageViewModel(generatorContext, component.container.softwareSystem, Tab.SECTIONS) {\n\n    override val url = url(component, section)\n\n    val content = toHtml(this, section.content, section.format, generatorContext.svgFactory)\n\n    companion object {\n        fun url(component: Component, section: Section) =\n            \"${SoftwareSystemContainerComponentSectionsPageViewModel.url(component)}/${section.contentTitle().normalize()}\"\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentSectionsPageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Section\nimport com.structurizr.model.Component\nimport com.structurizr.model.Container\nimport nl.avisi.structurizr.site.generatr.hasComponentsSections\nimport nl.avisi.structurizr.site.generatr.hasSections\nimport nl.avisi.structurizr.site.generatr.normalize\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nclass SoftwareSystemContainerComponentSectionsPageViewModel(generatorContext: GeneratorContext, component: Component) :\n    BaseSoftwareSystemContainerSectionsPageViewModel(generatorContext, component.container) {\n    val visible = component.hasSections()\n\n    override val url = url(component)\n\n    override fun sectionTableItemUrl(container: Container, section: Section?): String =\n        SoftwareSystemContainerSectionsPageViewModel.url(container, section)\n\n    override fun componentSectionItemUrl(component: Component): String = url(component)\n\n    val componentSectionTable = createSectionsTableViewModel(component.documentation.sections, dropFirst = false) {\n        SoftwareSystemContainerComponentSectionPageViewModel.url(component, it)\n    }\n\n    companion object {\n        fun url(component: Component) =\n            \"${SoftwareSystemContainerSectionsPageViewModel.url(component.container)}/${component.name.normalize()}\"\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentsPageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.Container\nimport nl.avisi.structurizr.site.generatr.includedProperties\nimport nl.avisi.structurizr.site.generatr.normalize\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nclass SoftwareSystemContainerComponentsPageViewModel(generatorContext: GeneratorContext, container: Container) :\n    SoftwareSystemPageViewModel(generatorContext, container.softwareSystem, Tab.COMPONENT) {\n    override val url = url(container)\n    val diagrams = generatorContext.workspace.views.componentViews\n        .filter { it.container == container }\n        .map { DiagramViewModel.forView(this, it, generatorContext.svgFactory) }\n    val images = generatorContext.workspace.views.imageViews\n        .filter { it.element == container }\n        .map { ImageViewViewModel(it) }\n\n    val hasProperties = container.includedProperties.isNotEmpty()\n    val propertiesTable = createPropertiesTableViewModel(container.includedProperties)\n    val visible = diagrams.isNotEmpty() or images.isNotEmpty() or hasProperties\n    val containerTabs = createContainersComponentTabViewModel(generatorContext, container.softwareSystem)\n    val diagramIndex = DiagramIndexViewModel(diagrams, images)\n\n    companion object {\n        fun url(container: Container) = \"${url(container.softwareSystem, Tab.COMPONENT)}/${container.name.normalize()}\"\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerDecisionPageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Decision\nimport com.structurizr.model.Container\nimport nl.avisi.structurizr.site.generatr.normalize\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nclass SoftwareSystemContainerDecisionPageViewModel(\n    generatorContext: GeneratorContext, container: Container, decision: Decision\n) : SoftwareSystemPageViewModel(generatorContext, container.softwareSystem, Tab.DECISIONS) {\n    override val url = url(container, decision)\n\n    val content = toHtml(this, transformADRLinks(decision.content, container), decision.format, generatorContext.svgFactory)\n\n    private fun transformADRLinks(content: String, container: Container) =\n        content.replace(\"\\\\[(.*)]\\\\(#(\\\\d+)\\\\)\".toRegex()) {\n            \"[${it.groupValues[1]}](${url(container.softwareSystem, Tab.DECISIONS)}/${container.name.normalize()}/${it.groupValues[2]})\"\n        }\n\n    companion object {\n        fun url(container: Container, decision: Decision) =\n            \"${url(container.softwareSystem, Tab.DECISIONS)}/${container.name.normalize()}/${decision.id}\"\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerDecisionsPageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Decision\nimport com.structurizr.model.Component\nimport com.structurizr.model.Container\nimport nl.avisi.structurizr.site.generatr.hasComponentDecisions\nimport nl.avisi.structurizr.site.generatr.hasDecisions\nimport nl.avisi.structurizr.site.generatr.normalize\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nabstract class BaseSoftwareSystemContainerDecisionsPageViewModel(\n    generatorContext: GeneratorContext,\n    container: Container\n) : BaseSoftwareSystemDecisionsPageViewModel(\n    generatorContext,\n    container.softwareSystem\n) {\n    val componentDecisionsTabs by lazy {\n        val components = container.components.filter { it.hasDecisions() }\n        buildList(components.size) {\n            if (container.hasDecisions())\n                add(\n                    DecisionTabViewModel(\n                        this@BaseSoftwareSystemContainerDecisionsPageViewModel,\n                        \"Container\",\n                        decisionTableItemUrl(container),\n                        Match.EXACT\n                    )\n                )\n            addAll(\n                components.map { component ->\n                    DecisionTabViewModel(\n                        this@BaseSoftwareSystemContainerDecisionsPageViewModel,\n                        component.name,\n                        componentDecisionItemUrl(component)\n                    )\n                }\n            )\n        }\n    }\n\n    abstract fun decisionTableItemUrl(container: Container, decision: Decision? = null): String\n    abstract fun componentDecisionItemUrl(component: Component): String\n}\n\nclass SoftwareSystemContainerDecisionsPageViewModel(generatorContext: GeneratorContext, container: Container) :\n    BaseSoftwareSystemContainerDecisionsPageViewModel(generatorContext, container) {\n    override val url = url(container)\n    val decisionsTable = createDecisionsTableViewModel(container.documentation.decisions) {\n        \"$url/${it.id}\"\n    }\n\n    private val componentDecisionsVisible = container.hasComponentDecisions()\n    val containerDecisionsVisible = container.hasDecisions()\n\n    val visible = componentDecisionsVisible or containerDecisionsVisible\n    val onlyComponentDecisionsVisible = !containerDecisionsVisible and componentDecisionsVisible\n\n    override fun decisionTableItemUrl(container: Container, decision: Decision?) = decision?.let { \"${url(container)}/${decision.id}\" } ?: url(container)\n    override fun componentDecisionItemUrl(component: Component) = \"${url(component.container)}/${component.name.normalize()}\"\n\n    companion object {\n        fun url(container: Container) = \"${url(container.softwareSystem, Tab.DECISIONS)}/${container.name.normalize()}\"\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerPageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.structurizr.site.generatr.hasContainerViews\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nclass SoftwareSystemContainerPageViewModel(generatorContext: GeneratorContext, softwareSystem: SoftwareSystem) :\n    SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.CONTAINER) {\n    val diagrams = generatorContext.workspace.views.containerViews\n        .filter { it.softwareSystem == softwareSystem }\n        .map { DiagramViewModel.forView(this, it, generatorContext.svgFactory) }\n    val images = generatorContext.workspace.views.imageViews\n        .filter { it.element == softwareSystem }\n        .map { ImageViewViewModel(it) }\n    val visible = generatorContext.workspace.views.hasContainerViews(generatorContext.workspace, softwareSystem) || images.isNotEmpty()\n    val diagramIndex = DiagramIndexViewModel(diagrams, images)\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerSectionPageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Section\nimport com.structurizr.model.Container\nimport nl.avisi.structurizr.site.generatr.normalize\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nclass SoftwareSystemContainerSectionPageViewModel(\n    generatorContext: GeneratorContext,\n    container: Container,\n    section: Section\n) : SoftwareSystemPageViewModel(generatorContext, container.softwareSystem, Tab.SECTIONS) {\n    override val url = url(container, section)\n\n    val content = toHtml(this, section.content, section.format, generatorContext.svgFactory)\n\n    companion object {\n        fun url(container: Container, section: Section) =\n            \"${url(container.softwareSystem, Tab.SECTIONS)}/${container.name.normalize()}/${section.contentTitle().normalize()}\"\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerSectionsPageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Section\nimport com.structurizr.model.Component\nimport com.structurizr.model.Container\nimport nl.avisi.structurizr.site.generatr.hasComponentsSections\nimport nl.avisi.structurizr.site.generatr.hasSections\nimport nl.avisi.structurizr.site.generatr.normalize\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nabstract class BaseSoftwareSystemContainerSectionsPageViewModel(generatorContext: GeneratorContext, container: Container) :\n    BaseSoftwareSystemSectionsPageViewModel(generatorContext, container.softwareSystem) {\n    val sectionsTable: TableViewModel = createSectionsTableViewModel(container.documentation.sections, dropFirst = false) {\n        sectionTableItemUrl(container, it)\n    }\n\n    val componentSectionsTabs by lazy {\n        val components = container.components.filter { it.hasSections() }\n        buildList(components.size) {\n            if (container.hasSections()) {\n                add(\n                    SectionTabViewModel(\n                        this@BaseSoftwareSystemContainerSectionsPageViewModel,\n                        \"Container\",\n                        sectionTableItemUrl(container),\n                        Match.EXACT\n                    )\n                )\n            }\n\n            addAll(\n                components.map { component ->\n                    SectionTabViewModel(this@BaseSoftwareSystemContainerSectionsPageViewModel, component.name, componentSectionItemUrl(component))\n                }\n            )\n        }\n    }\n\n    abstract fun sectionTableItemUrl(container: Container, section: Section? = null): String\n    abstract fun componentSectionItemUrl(component: Component): String\n}\n\nclass SoftwareSystemContainerSectionsPageViewModel(generatorContext: GeneratorContext, container: Container) :\n    BaseSoftwareSystemContainerSectionsPageViewModel(generatorContext, container) {\n    private val containerDocumentationSectionsVisible = container.hasSections()\n    private val componentsDocumentationSectionsVisible = container.hasComponentsSections()\n\n    val visible = containerDocumentationSectionsVisible or componentsDocumentationSectionsVisible\n    val onlyComponentsDocumentationSectionsVisible = !containerDocumentationSectionsVisible and componentsDocumentationSectionsVisible\n\n    override val url = url(container)\n\n    override fun sectionTableItemUrl(container: Container, section: Section?): String = url(container, section)\n    override fun componentSectionItemUrl(component: Component): String = SoftwareSystemContainerComponentSectionsPageViewModel.url(component)\n\n    companion object {\n        fun url(container: Container) = \"${url(container.softwareSystem, Tab.SECTIONS)}/${container.name.normalize()}\"\n        fun url(container: Container, section: Section?) = section?.let { \"${url(container)}/${it.contentTitle().normalize()}\" } ?: url(container)\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContextPageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.structurizr.site.generatr.hasSystemContextViews\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nclass SoftwareSystemContextPageViewModel(generatorContext: GeneratorContext, softwareSystem: SoftwareSystem) :\n    SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.SYSTEM_CONTEXT) {\n    val diagrams = generatorContext.workspace.views.systemContextViews\n        .filter { it.softwareSystem == softwareSystem }\n        .map { DiagramViewModel.forView(this, it, generatorContext.svgFactory) }\n    val visible = generatorContext.workspace.views.hasSystemContextViews(softwareSystem)\n    val diagramIndex = DiagramIndexViewModel(diagrams)\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDecisionPageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Decision\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nclass SoftwareSystemDecisionPageViewModel(\n    generatorContext: GeneratorContext, softwareSystem: SoftwareSystem, decision: Decision\n) : SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.DECISIONS) {\n    override val url = url(softwareSystem, decision)\n\n    val content = toHtml(\n        this,\n        transformADRLinks(decision.content, softwareSystem),\n        decision.format,\n        generatorContext.svgFactory\n    )\n\n    private fun transformADRLinks(content: String, softwareSystem: SoftwareSystem) =\n        content.replace(\"\\\\[(.*)]\\\\(#(\\\\d+)\\\\)\".toRegex()) {\n            \"[${it.groupValues[1]}](${url(softwareSystem, Tab.DECISIONS)}/${it.groupValues[2]})\"\n        }\n\n    companion object {\n        fun url(softwareSystem: SoftwareSystem, decision: Decision) =\n            \"${url(softwareSystem, Tab.DECISIONS)}/${decision.id}\"\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDecisionsPageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.Container\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.structurizr.site.generatr.hasContainerDecisions\nimport nl.avisi.structurizr.site.generatr.hasDecisions\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nabstract class BaseSoftwareSystemDecisionsPageViewModel(\n    generatorContext: GeneratorContext,\n    softwareSystem: SoftwareSystem\n) : SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.DECISIONS) {\n    val decisionTabs = createDecisionsTabViewModel(softwareSystem, Tab.DECISIONS) {\n        if (it is Container) Match.CHILD else Match.EXACT\n    }\n}\n\nclass SoftwareSystemDecisionsPageViewModel(generatorContext: GeneratorContext, softwareSystem: SoftwareSystem) :\n    BaseSoftwareSystemDecisionsPageViewModel(generatorContext, softwareSystem) {\n\n    val decisionsTable = createDecisionsTableViewModel(softwareSystem.documentation.decisions) {\n        \"$url/${it.id}\"\n    }\n\n    private val containerDecisionsVisible = softwareSystem.hasContainerDecisions(recursive = true)\n    val softwareSystemDecisionsVisible = softwareSystem.hasDecisions()\n\n    val visible = softwareSystemDecisionsVisible or containerDecisionsVisible\n    val onlyContainersDecisionsVisible = !softwareSystemDecisionsVisible and containerDecisionsVisible\n}\n\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDependenciesPageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.Relationship\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nclass SoftwareSystemDependenciesPageViewModel(\n    generatorContext: GeneratorContext,\n    private val softwareSystem: SoftwareSystem\n) : SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.DEPENDENCIES) {\n\n    val dependenciesInboundTable = TableViewModel.create {\n        header()\n        incomingRelationships.forEach {inboundBodyRow(it)}\n    }\n\n    val dependenciesOutboundTable = TableViewModel.create {\n        header()\n        outgoingRelationships.forEach {outboundBodyRow(it)}\n    }\n\n    private val incomingRelationships\n        get() = generatorContext.workspace.model.relationships.asSequence()\n            .filter { it.destination == softwareSystem }\n            .plus(softwareSystem.relationships)\n            .filter { it.source is SoftwareSystem && it.destination is SoftwareSystem}\n            .filter { it.destination == softwareSystem}\n            .sortedBy { it.source.name.lowercase() }\n\n    private val outgoingRelationships\n        get() = generatorContext.workspace.model.relationships.asSequence()\n            .filter { it.destination == softwareSystem }\n            .plus(softwareSystem.relationships)\n            .filter { it.source is SoftwareSystem && it.destination is SoftwareSystem }\n            .filter { it.source == softwareSystem}\n            .sortedBy { it.destination.name.lowercase() }\n\n    private fun TableViewModel.TableViewInitializerContext.header() {\n        headerRow(\n            headerCellMedium(\"System\"),\n            headerCellLarge(\"Description\"),\n            headerCell(\"Technology\")\n        )\n    }\n\n    private fun TableViewModel.TableViewInitializerContext.inboundBodyRow(relationship: Relationship) {\n        bodyRow(\n            softwareSystemDependencyCell(relationship.source as SoftwareSystem),\n            cell(relationship.description),\n            cell(relationship.technology)\n        )\n    }\n\n    private fun TableViewModel.TableViewInitializerContext.outboundBodyRow(relationship: Relationship) {\n        bodyRow(\n            softwareSystemDependencyCell(relationship.destination as SoftwareSystem),\n            cell(relationship.description),\n            cell(relationship.technology)\n        )\n    }\n\n    private fun TableViewModel.TableViewInitializerContext.softwareSystemDependencyCell(system: SoftwareSystem) =\n        if (system == softwareSystem)\n            headerCell(system.name)\n        else\n            softwareSystemCell(this@SoftwareSystemDependenciesPageViewModel, system)\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDeploymentPageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.structurizr.site.generatr.hasDeploymentViews\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nclass SoftwareSystemDeploymentPageViewModel(generatorContext: GeneratorContext, softwareSystem: SoftwareSystem) :\n    SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.DEPLOYMENT) {\n    val diagrams = generatorContext.workspace.views.deploymentViews\n        .filter {\n            it.softwareSystem == softwareSystem || it.properties[\"generatr.view.deployment.belongsTo\"] == softwareSystem.name\n        }\n        .map { DiagramViewModel.forView(this, it, generatorContext.svgFactory) }\n    val visible = generatorContext.workspace.views.hasDeploymentViews(softwareSystem)\n    val diagramIndex = DiagramIndexViewModel(diagrams)\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDynamicPageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.structurizr.site.generatr.hasDynamicViews\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nclass SoftwareSystemDynamicPageViewModel(generatorContext: GeneratorContext, softwareSystem: SoftwareSystem) :\n    SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.DYNAMIC) {\n    val diagrams = generatorContext.workspace.views.dynamicViews\n        .filter { it.softwareSystem == softwareSystem }\n        .map { DiagramViewModel.forView(this, it, generatorContext.svgFactory) }\n    val visible = generatorContext.workspace.views.hasDynamicViews(softwareSystem)\n    val diagramIndex = DiagramIndexViewModel(diagrams)\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemHomePageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Format\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.structurizr.site.generatr.includedProperties\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nclass SoftwareSystemHomePageViewModel(generatorContext: GeneratorContext, softwareSystem: SoftwareSystem) :\n    SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME) {\n    val hasProperties = softwareSystem.includedProperties.any()\n    val propertiesTable = createPropertiesTableViewModel(softwareSystem.includedProperties)\n    val content = toHtml(this, softwareSystem.info(), softwareSystem.format(), generatorContext.svgFactory)\n\n    private fun SoftwareSystem.info() = documentation.sections\n        .minByOrNull { it.order }\n        ?.content\n        ?: \"# Description${System.lineSeparator()}${description}\"\n\n    private fun SoftwareSystem.format() = documentation.sections\n        .minByOrNull { it.order }\n        ?.format\n        ?: Format.Markdown\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemPageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.structurizr.site.generatr.*\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nopen class SoftwareSystemPageViewModel(\n    generatorContext: GeneratorContext,\n    private val softwareSystem: SoftwareSystem,\n    tab: Tab\n) : PageViewModel(generatorContext) {\n    enum class Tab { HOME, SYSTEM_CONTEXT, CONTAINER, COMPONENT, CODE, DYNAMIC, DEPLOYMENT, DEPENDENCIES, DECISIONS, SECTIONS }\n\n    inner class TabViewModel(val tab: Tab, match: Match = Match.EXACT) {\n        val link = when (tab) {\n            Tab.COMPONENT -> LinkViewModel(\n                this@SoftwareSystemPageViewModel,\n                title,\n                \"${url(softwareSystem, tab)}/${softwareSystem.firstContainer(generatorContext)?.name?.normalize()}\",\n                match\n            )\n            Tab.CODE -> LinkViewModel(\n                this@SoftwareSystemPageViewModel,\n                title,\n                \"${url(softwareSystem, tab)}/${softwareSystem.firstContainer(generatorContext)?.name?.normalize()}/${softwareSystem.firstContainer(generatorContext)?.firstComponent(generatorContext)?.name?.normalize()}\",\n                match\n            )\n            else -> LinkViewModel(this@SoftwareSystemPageViewModel, title, url(softwareSystem, tab), match)\n    }\n\n        private val title\n            get() = when (tab) {\n                Tab.HOME -> \"Info\"\n                Tab.SYSTEM_CONTEXT -> \"Context views\"\n                Tab.CONTAINER -> \"Container views\"\n                Tab.COMPONENT -> \"Component views\"\n                Tab.CODE -> \"Code views\"\n                Tab.DYNAMIC -> \"Dynamic views\"\n                Tab.DEPLOYMENT -> \"Deployment views\"\n                Tab.DEPENDENCIES -> \"Dependencies\"\n                Tab.DECISIONS -> \"Decisions\"\n                Tab.SECTIONS -> \"Documentation\"\n            }\n\n        val visible\n            get() = when (tab) {\n                Tab.HOME -> true\n                Tab.DEPENDENCIES -> true\n                Tab.SYSTEM_CONTEXT -> generatorContext.workspace.views.hasSystemContextViews(softwareSystem)\n                Tab.CONTAINER -> generatorContext.workspace.views.hasContainerViews(generatorContext.workspace, softwareSystem)\n                Tab.COMPONENT -> generatorContext.workspace.views.hasComponentViews(generatorContext.workspace, softwareSystem)\n                Tab.CODE -> generatorContext.workspace.views.hasCodeViews(generatorContext.workspace, softwareSystem)\n                Tab.DYNAMIC -> generatorContext.workspace.views.hasDynamicViews(softwareSystem)\n                Tab.DEPLOYMENT -> generatorContext.workspace.views.hasDeploymentViews(softwareSystem)\n                Tab.DECISIONS -> softwareSystem.hasDecisions(recursive = true)\n                Tab.SECTIONS -> softwareSystem.hasDocumentationSections(recursive = true)\n            }\n    }\n\n    override val url: String = url(softwareSystem, tab)\n    override val pageSubTitle: String = softwareSystem.name\n\n    val tabs = listOf(\n        TabViewModel(Tab.HOME),\n        TabViewModel(Tab.SYSTEM_CONTEXT),\n        TabViewModel(Tab.CONTAINER),\n        TabViewModel(Tab.COMPONENT, Match.SIBLING),\n        TabViewModel(Tab.CODE, Match.SIBLING_CHILD),\n        TabViewModel(Tab.DYNAMIC),\n        TabViewModel(Tab.DEPLOYMENT),\n        TabViewModel(Tab.DEPENDENCIES),\n        TabViewModel(Tab.DECISIONS, Match.CHILD),\n        TabViewModel(Tab.SECTIONS, Match.CHILD)\n    )\n\n    val description: String = softwareSystem.description\n\n    companion object {\n        fun url(softwareSystem: SoftwareSystem, tab: Tab): String {\n            val home = \"/${softwareSystem.name.normalize()}\"\n            return when (tab) {\n                Tab.HOME -> home\n                Tab.SYSTEM_CONTEXT -> \"$home/context\"\n                Tab.CONTAINER -> \"$home/container\"\n                Tab.COMPONENT -> \"$home/component\"\n                Tab.CODE -> \"$home/code\"\n                Tab.DYNAMIC -> \"$home/dynamic\"\n                Tab.DEPLOYMENT -> \"$home/deployment\"\n                Tab.DEPENDENCIES -> \"$home/dependencies\"\n                Tab.DECISIONS -> \"$home/decisions\"\n                Tab.SECTIONS -> \"$home/sections\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemSectionPageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Section\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.structurizr.site.generatr.normalize\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nclass SoftwareSystemSectionPageViewModel(\n    generatorContext: GeneratorContext,\n    softwareSystem: SoftwareSystem,\n    section: Section\n) : SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.SECTIONS) {\n    override val url = url(softwareSystem, section)\n\n    val content = toHtml(this, section.content, section.format, generatorContext.svgFactory)\n\n    companion object {\n        fun url(softwareSystem: SoftwareSystem, section: Section) =\n            \"${url(softwareSystem, Tab.SECTIONS)}/${section.contentTitle().normalize()}\"\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemSectionsPageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.Container\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.structurizr.site.generatr.hasContainerDocumentationSections\nimport nl.avisi.structurizr.site.generatr.hasDocumentationSections\nimport nl.avisi.structurizr.site.generatr.normalize\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nabstract class BaseSoftwareSystemSectionsPageViewModel(generatorContext: GeneratorContext, softwareSystem: SoftwareSystem) :\n    SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.SECTIONS) {\n\n    val sectionsTabs: List<SectionTabViewModel> = createSectionsTabViewModel(softwareSystem, Tab.SECTIONS) {\n        if (it is Container) Match.CHILD else Match.EXACT\n    }\n}\n\nclass SoftwareSystemSectionsPageViewModel(\n    generatorContext: GeneratorContext,\n    softwareSystem: SoftwareSystem\n) : BaseSoftwareSystemSectionsPageViewModel(\n    generatorContext,\n    softwareSystem\n) {\n    private val softwareSystemDocumentationSectionsVisible = softwareSystem.hasDocumentationSections()\n    private val containerDocumentationSectionsVisible = softwareSystem.hasContainerDocumentationSections(recursive = true)\n\n    val visible = softwareSystemDocumentationSectionsVisible or containerDocumentationSectionsVisible\n    val onlyContainersDocumentationSectionsVisible = !softwareSystemDocumentationSectionsVisible and containerDocumentationSectionsVisible\n\n    val sectionsTable = createSectionsTableViewModel(softwareSystem.documentation.sections) {\n        \"$url/${it.contentTitle().normalize()}\"\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemTableUtilities.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.model.SoftwareSystem\n\nfun TableViewModel.TableViewInitializerContext.softwareSystemCell(\n    pageViewModel: PageViewModel,\n    system: SoftwareSystem\n) = if (pageViewModel.includedSoftwareSystems.contains(system))\n    cellWithSoftwareSystemLink(\n        pageViewModel,\n        system.name,\n        SoftwareSystemPageViewModel.url(system, SoftwareSystemPageViewModel.Tab.HOME)\n    )\nelse\n    cellWithExternalSoftwareSystem(\"${system.name} (External)\")\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemsPageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nclass SoftwareSystemsPageViewModel(generatorContext: GeneratorContext) : PageViewModel(generatorContext) {\n    override val url = url()\n    override val pageSubTitle = \"Software Systems\"\n\n    val softwareSystemsTable: TableViewModel = TableViewModel.create {\n        headerRow(\n            headerCellMedium(\"Name\"),\n            headerCell(\"Description\")\n        )\n\n        generatorContext.workspace.model.softwareSystems\n            .sortedBy { it.name.lowercase() }\n            .forEach {\n                bodyRow(\n                    softwareSystemCell(this@SoftwareSystemsPageViewModel, it),\n                    cell(it.description)\n                )\n            }\n    }\n\n    companion object {\n        fun url() = \"/software-systems\"\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/TableViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nenum class CellWidth {\n    UNSPECIFIED,\n    ONE_TENTH,\n    ONE_FOURTH,\n    TWO_FOURTH,\n}\n\ndata class TableViewModel(val headerRows: List<RowViewModel>, val bodyRows: List<RowViewModel>) {\n    sealed interface CellViewModel {\n        val isHeader: Boolean\n    }\n\n    data class TextCellViewModel(\n        val title: String,\n        override val isHeader: Boolean,\n        val greyText: Boolean = false,\n        val boldText: Boolean = false,\n        val width: CellWidth = CellWidth.UNSPECIFIED\n    ) : CellViewModel {\n        override fun toString() = if (isHeader)\n            \"headerCell($title, greyText=$greyText)\"\n        else\n            \"cell($title, greyText=$greyText, boldText=$boldText, width=${width.name})\"\n    }\n\n    data class LinkCellViewModel(\n        val link: LinkViewModel,\n        override val isHeader: Boolean,\n        val boldText: Boolean = false\n    ) : CellViewModel {\n        override fun toString() = if (isHeader)\n            \"headerCell($link), boldText=$boldText,\"\n        else\n            \"cell($link), boldText=$boldText,\"\n    }\n\n    data class ExternalLinkCellViewModel(\n        val link: ExternalLinkViewModel,\n        override val isHeader: Boolean\n    ) : CellViewModel {\n        override fun toString() = if (isHeader) \"headerCell($link)\" else \"cell($link)\"\n    }\n\n    data class RowViewModel(val columns: List<CellViewModel>) {\n        override fun toString() =\n            columns.joinToString(separator = \", \", prefix = \"row { \", postfix = \" }\") { it.toString() }\n    }\n\n    class TableViewInitializerContext(\n        private val headerRows: MutableList<RowViewModel>,\n        private val bodyRows: MutableList<RowViewModel>\n    ) {\n        fun headerRow(vararg cells: TextCellViewModel) {\n            headerRows.add(RowViewModel(cells.toList()))\n        }\n\n        fun bodyRow(vararg cells: CellViewModel) {\n            bodyRows.add(RowViewModel(cells.toList()))\n        }\n\n        fun headerCell(title: String) =\n            TextCellViewModel(title, true)\n        fun headerCellSmall(title: String): TextCellViewModel =\n            TextCellViewModel(title, isHeader = true, width = CellWidth.ONE_TENTH)\n        fun headerCellMedium(title: String): TextCellViewModel =\n            TextCellViewModel(title, isHeader = true, width = CellWidth.ONE_FOURTH)\n        fun headerCellLarge(title: String): TextCellViewModel =\n            TextCellViewModel(title, isHeader = true, width = CellWidth.TWO_FOURTH)\n\n        fun cell(title: String, greyText: Boolean = false) =\n            TextCellViewModel(title, false, greyText, false)\n        fun cellWithIndex(title: String) =\n            TextCellViewModel(title, false, boldText = true)\n        fun cellWithExternalSoftwareSystem(title: String) =\n            TextCellViewModel(title, false, greyText = true, boldText = true)\n        fun cellWithLink(pageViewModel: PageViewModel, title: String, href: String) =\n            LinkCellViewModel(LinkViewModel(pageViewModel, title, href), false)\n        fun cellWithSoftwareSystemLink(pageViewModel: PageViewModel, title: String, href: String) =\n            LinkCellViewModel(LinkViewModel(pageViewModel, title, href), false, boldText = true)\n        fun cellWithExternalLink(title: String, href: String) =\n            ExternalLinkCellViewModel(ExternalLinkViewModel(title, href), false)\n    }\n\n    override fun toString(): String {\n        val builder = StringBuilder()\n        builder.appendLine(\"header {\")\n        headerRows.forEach {\n            builder.append(\"  \")\n            builder.append(it)\n            builder.appendLine()\n        }\n        builder.appendLine(\"}\")\n        builder.appendLine(\"body {\")\n        bodyRows.forEach {\n            builder.append(\"  \")\n            builder.append(it)\n            builder.appendLine()\n        }\n        builder.appendLine(\"}\")\n        return builder.toString()\n    }\n\n    companion object {\n        fun create(initializer: TableViewInitializerContext.() -> Unit): TableViewModel {\n            val headerRows = mutableListOf<RowViewModel>()\n            val bodyRows = mutableListOf<RowViewModel>()\n            val ctx = TableViewInitializerContext(headerRows, bodyRows)\n            ctx.initializer()\n\n            return TableViewModel(headerRows, bodyRows)\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/Theme.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nenum class Theme {\n    LIGHT, DARK, AUTO\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ToHtml.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Format\nimport com.vladsch.flexmark.ast.FencedCodeBlock\nimport com.vladsch.flexmark.html.HtmlRenderer\nimport com.vladsch.flexmark.html.HtmlWriter\nimport com.vladsch.flexmark.html.LinkResolver\nimport com.vladsch.flexmark.html.LinkResolverFactory\nimport com.vladsch.flexmark.html.renderer.*\nimport com.vladsch.flexmark.parser.Parser\nimport com.vladsch.flexmark.util.ast.Node\nimport kotlinx.html.div\nimport kotlinx.html.stream.createHTML\nimport net.sourceforge.plantuml.FileFormat\nimport net.sourceforge.plantuml.FileFormatOption\nimport net.sourceforge.plantuml.SourceStringReader\nimport nl.avisi.structurizr.site.generatr.site.asUrlToDirectory\nimport nl.avisi.structurizr.site.generatr.site.asUrlToFile\nimport nl.avisi.structurizr.site.generatr.site.views.diagram\nimport org.asciidoctor.Options\nimport org.asciidoctor.SafeMode\nimport org.jsoup.Jsoup\nimport org.jsoup.nodes.Element\nimport java.io.ByteArrayOutputStream\n\nconst val embedPrefix = \"embed:\"\n\nfun toHtml(\n    pageViewModel: PageViewModel,\n    content: String,\n    format: Format,\n    svgFactory: (key: String, url: String) -> String?\n): String = when (format) {\n    Format.Markdown -> markdownToHtml(pageViewModel, content, svgFactory)\n    Format.AsciiDoc -> asciidocToHtml(pageViewModel, content, svgFactory)\n}\n\nprivate fun markdownToHtml(\n    pageViewModel: PageViewModel,\n    markdown: String,\n    svgFactory: (key: String, url: String) -> String?\n): String {\n    val flexmarkConfig = pageViewModel.flexmarkConfig\n    val options = flexmarkConfig.flexmarkOptions\n\n    val parser = Parser.builder(options).build()\n    val renderer = HtmlRenderer.builder(options)\n        .linkResolverFactory(CustomLinkResolver.Factory(pageViewModel))\n        .nodeRendererFactory { FencedCodeBlockRenderer() }\n        .build()\n    val markDownDocument = parser.parse(markdown)\n    val html = renderer.render(markDownDocument)\n\n    return Jsoup.parse(html)\n        .apply { body().transformEmbeddedDiagramElements(pageViewModel, svgFactory) }\n        .body()\n        .html()\n}\n\nprivate class FencedCodeBlockRenderer : NodeRenderer {\n    override fun getNodeRenderingHandlers(): MutableSet<NodeRenderingHandler<*>> =\n        mutableSetOf(NodeRenderingHandler(FencedCodeBlock::class.java, this::render))\n\n    private fun render(fencedCodeBlock: FencedCodeBlock, nodeRendererContext: NodeRendererContext, htmlWriter: HtmlWriter) {\n        if (fencedCodeBlock.info.toString() == \"puml\") {\n            htmlWriter.tag(\"div\") {\n                val reader = SourceStringReader(fencedCodeBlock.contentChars.toString())\n                val stream = ByteArrayOutputStream()\n                reader.outputImage(stream, FileFormatOption(FileFormat.SVG, false))\n                htmlWriter.raw(stream.toString())\n            }\n        } else\n            nodeRendererContext.delegateRender()\n    }\n}\n\nprivate fun asciidocToHtml(\n    pageViewModel: PageViewModel,\n    asciidoc: String,\n    svgFactory: (key: String, url: String) -> String?\n): String {\n    val options = Options.builder()\n        .safe(SafeMode.SERVER)\n        // Docs dir needs to be exposed from structurizr using `.baseDir(File(\"./docs/example/workspace-docs\"))`,\n        // which is not the case at the moment.\n        // Needed for partial include `include::partial.adoc[]`, which structurizr also does not support.\n        // see https://docs.asciidoctor.org/asciidoc/latest/directives/include/\n        // another option could be https://docs.asciidoctor.org/asciidoctorj/latest/locating-files/#globdirectorywalker-class\n        .backend(\"html5\")\n        .build()\n    val html = asciidoctorWithPUMLRenderer.convert(asciidoc, options)\n\n    return Jsoup.parse(html)\n        .apply {\n            body().transformEmbeddedDiagramElements(pageViewModel, svgFactory)\n            body().transformAsciiDocContent()\n            body().transformAsciiDocAdmonition()\n            body().transformAsciiDocImgSrc(pageViewModel)\n        }\n        .body()\n        .html()\n}\n\n/**\n * Adds css class content to several asciidoc tags, as the default asciidoc structure is not compatible with bulma css.\n * [asciidoctor html5 template](https://github.com/asciidoctor/asciidoctor/blob/main/lib/asciidoctor/converter/html5.rb)\n */\nprivate fun Element.transformAsciiDocContent() = this\n    .select(\".sect0, .sect1, .sect2, .sect3, .sect4, .sect5, .paragraph, .toc, .olist, .ulist, .dlist, .colist, .listingblock, .tableblock, .literalblock, .quoteblock, .stemblock, .verseblock, .imageblock, .videoblock, .audioblock, .admonitionblock\")\n    .addClass(\"content\")\n\nprivate fun Element.transformAsciiDocAdmonition() = this\n    .select(\".admonitionblock\").addClass(\"adm-block\")\n    .select(\"td.icon\").addClass(\"adm-heading\").parents()\n    .select(\".admonitionblock.note\").addClass(\"adm-note\").parents()\n    .select(\".admonitionblock.tip\").addClass(\"adm-tip\").parents()\n    .select(\".admonitionblock.important\").addClass(\"adm-example\").parents()\n    .select(\".admonitionblock.caution\").addClass(\"adm-danger\").parents()\n    .select(\".admonitionblock.warning\").addClass(\"adm-warning\")\n\nprivate fun Element.transformAsciiDocImgSrc(pageViewModel: PageViewModel) = this\n    .select(\"img\").map {\n        it.attr(\"src\", \"/${it.attr(\"src\").dropWhile { it == '/' }}\".asUrlToFile(pageViewModel.url))\n    }\n\nprivate class CustomLinkResolver(private val pageViewModel: PageViewModel) : LinkResolver {\n    override fun resolveLink(node: Node, context: LinkResolverBasicContext, link: ResolvedLink): ResolvedLink {\n        if (link.url.startsWith(embedPrefix)) {\n            return link\n                .withStatus(LinkStatus.VALID)\n                .withUrl(link.url)\n        }\n        if (link.url.matches(\"https?://.*\".toRegex()))\n            return link.withTarget(\"blank\")\n\n        return link.withStatus(LinkStatus.VALID)\n            .withUrl(\"/${link.url.dropWhile { it == '/' }}\"\n                .let {\n                    if (it.endsWith('/'))\n                        it.asUrlToDirectory(pageViewModel.url)\n                    else\n                        it.asUrlToFile(pageViewModel.url)\n                }\n            )\n    }\n\n    class Factory(private val viewModel: PageViewModel) : LinkResolverFactory {\n        override fun apply(context: LinkResolverBasicContext): LinkResolver {\n            return CustomLinkResolver(viewModel)\n        }\n\n        override fun getAfterDependents(): MutableSet<Class<*>>? = null\n\n        override fun getBeforeDependents(): MutableSet<Class<*>>? = null\n\n        override fun affectsGlobalScope() = false\n    }\n}\n\nfun Element.transformEmbeddedDiagramElements(\n    pageViewModel: PageViewModel,\n    svgFactory: (key: String, url: String) -> String?\n) = this.allElements\n    .toList()\n    .filter { it.tag().name == \"img\" && it.attr(\"src\").startsWith(embedPrefix) }\n    .forEach {\n        val key = it.attr(\"src\").substring(embedPrefix.length)\n        val name = it.attr(\"alt\").ifBlank { key }\n        val html = createHTML().div {\n            diagram(DiagramViewModel.forView(pageViewModel, key, name, null, null, svgFactory))\n        }\n\n        it.parent()?.append(html)\n        it.remove()\n    }\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/WorkspaceDecisionPageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Decision\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nclass WorkspaceDecisionPageViewModel(generatorContext: GeneratorContext, decision: Decision) :\n    PageViewModel(generatorContext) {\n    override val url = url(decision)\n    override val pageSubTitle: String = decision.title\n\n    val content = toHtml(\n        this,\n        transformADRLinks(decision.content),\n        decision.format,\n        generatorContext.svgFactory\n    )\n\n    private fun transformADRLinks(content: String) =\n        content.replace(\"\\\\[(.*)]\\\\(#(\\\\d+)\\\\)\".toRegex()) {\n            \"[${it.groupValues[1]}](decisions/${it.groupValues[2]}/)\"\n        }\n\n    companion object {\n        fun url(decision: Decision) = \"/decisions/${decision.id}\"\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/WorkspaceDecisionsPageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nclass WorkspaceDecisionsPageViewModel(generatorContext: GeneratorContext) : PageViewModel(generatorContext) {\n    override val url = url()\n    override val pageSubTitle = \"Decisions\"\n\n    val decisionsTable = createDecisionsTableViewModel(generatorContext.workspace.documentation.decisions) {\n        WorkspaceDecisionPageViewModel.url(it)\n    }\n\n    companion object {\n        fun url() = \"/decisions\"\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/WorkspaceDocumentationSectionPageViewModel.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.documentation.Section\nimport nl.avisi.structurizr.site.generatr.normalize\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\n\nclass WorkspaceDocumentationSectionPageViewModel(generatorContext: GeneratorContext, section: Section) :\n    PageViewModel(generatorContext) {\n    override val url = url(section)\n    override val pageSubTitle: String = section.contentTitle()\n\n    val content = toHtml(this, section.content, section.format, generatorContext.svgFactory)\n\n    companion object {\n        fun url(section: Section): String {\n            return \"/${section.contentTitle().normalize()}\"\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/Document.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model.indexing\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class Document(val href: String, val type: String, val title: String, val text: String)\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/Home.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model.indexing\n\nimport com.structurizr.documentation.Documentation\nimport nl.avisi.structurizr.site.generatr.site.asUrlToDirectory\nimport nl.avisi.structurizr.site.generatr.site.model.HomePageViewModel\nimport nl.avisi.structurizr.site.generatr.site.model.PageViewModel\nimport nl.avisi.structurizr.site.generatr.site.model.contentText\nimport nl.avisi.structurizr.site.generatr.site.model.contentTitle\n\nfun home(documentation: Documentation, viewModel: PageViewModel) = documentation.sections.firstOrNull()\n    ?.let { section ->\n        Document(\n            HomePageViewModel.url().asUrlToDirectory(viewModel.url),\n            \"Home\",\n            section.contentTitle(),\n            \"${section.contentTitle()} ${section.contentText()}\".trim()\n        )\n    }\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/SoftwareSystemComponents.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model.indexing\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.structurizr.site.generatr.site.asUrlToDirectory\nimport nl.avisi.structurizr.site.generatr.site.model.PageViewModel\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemPageViewModel\n\nfun softwareSystemComponents(softwareSystem: SoftwareSystem, viewModel: PageViewModel) = softwareSystem.containers\n    .sortedBy { it.name }\n    .flatMap { container ->\n        container.components\n            .sortedBy { it.name }\n            .flatMap { component ->\n                listOf(component.name, component.description, component.technology)\n            }\n    }\n    .filter { it != null && it.isNotBlank() }\n    .joinToString(\" \")\n    .ifBlank { null }\n    ?.let {\n        Document(\n            SoftwareSystemPageViewModel.url(softwareSystem, SoftwareSystemPageViewModel.Tab.COMPONENT)\n                .asUrlToDirectory(viewModel.url),\n            \"Component views\",\n            \"${softwareSystem.name} | Component views\",\n            it\n        )\n    }\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/SoftwareSystemContainerDecisions.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model.indexing\n\nimport com.structurizr.model.Container\nimport nl.avisi.structurizr.site.generatr.site.asUrlToDirectory\nimport nl.avisi.structurizr.site.generatr.site.model.PageViewModel\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerDecisionPageViewModel\nimport nl.avisi.structurizr.site.generatr.site.model.contentText\n\nfun softwareSystemContainerDecisions(container: Container, viewModel: PageViewModel) = container\n    .documentation\n    .decisions\n    .map { decision ->\n        Document(\n            SoftwareSystemContainerDecisionPageViewModel.url(\n                container,\n                decision\n            ).asUrlToDirectory(viewModel.url),\n            \"Container Decision\",\n            \"${container.name} | ${decision.title}\",\n            \"${decision.title} ${decision.contentText()}\".trim()\n        )\n    }\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/SoftwareSystemContainerSections.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model.indexing\n\nimport com.structurizr.model.Container\nimport nl.avisi.structurizr.site.generatr.site.asUrlToDirectory\nimport nl.avisi.structurizr.site.generatr.site.model.*\n\nfun softwareSystemContainerSections(container: Container, viewModel: PageViewModel) = container\n    .documentation\n    .sections\n    .map { section ->\n        Document(\n            SoftwareSystemContainerSectionPageViewModel.url(\n                container,\n                section\n            ).asUrlToDirectory(viewModel.url),\n            \"Container Documentation\",\n            \"${container.name} | ${section.contentTitle()}\",\n            \"${section.contentTitle()} ${section.contentText()}\".trim()\n        )\n    }\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/SoftwareSystemContainers.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model.indexing\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.structurizr.site.generatr.site.asUrlToDirectory\nimport nl.avisi.structurizr.site.generatr.site.model.PageViewModel\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemPageViewModel\n\nfun softwareSystemContainers(softwareSystem: SoftwareSystem, viewModel: PageViewModel) = softwareSystem.containers\n    .sortedBy { it.name }\n    .flatMap { container ->\n        listOf(container.name, container.description, container.technology)\n    }\n    .filter { it != null && it.isNotBlank() }\n    .joinToString(\" \")\n    .ifBlank { null }\n    ?.let {\n        Document(\n            SoftwareSystemPageViewModel.url(softwareSystem, SoftwareSystemPageViewModel.Tab.CONTAINER)\n                .asUrlToDirectory(viewModel.url),\n            \"Container views\",\n            \"${softwareSystem.name} | Container views\",\n            it\n        )\n    }\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/SoftwareSystemContext.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model.indexing\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.structurizr.site.generatr.site.asUrlToDirectory\nimport nl.avisi.structurizr.site.generatr.site.model.PageViewModel\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemPageViewModel\n\nfun softwareSystemContext(softwareSystem: SoftwareSystem, viewModel: PageViewModel) = Document(\n    SoftwareSystemPageViewModel.url(softwareSystem, SoftwareSystemPageViewModel.Tab.SYSTEM_CONTEXT)\n        .asUrlToDirectory(viewModel.url),\n    \"Context views\",\n    \"${softwareSystem.name} | Context views\",\n    \"${softwareSystem.name} ${softwareSystem.description}\".trim()\n)\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/SoftwareSystemDecisions.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model.indexing\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.structurizr.site.generatr.site.asUrlToDirectory\nimport nl.avisi.structurizr.site.generatr.site.model.PageViewModel\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemDecisionPageViewModel\nimport nl.avisi.structurizr.site.generatr.site.model.contentText\n\nfun softwareSystemDecisions(softwareSystem: SoftwareSystem, viewModel: PageViewModel) = softwareSystem.documentation\n    .decisions\n    .map { decision ->\n        Document(\n            SoftwareSystemDecisionPageViewModel.url(\n                softwareSystem,\n                decision\n            ).asUrlToDirectory(viewModel.url),\n            \"Software System Decision\",\n            \"${softwareSystem.name} | ${decision.title}\",\n            \"${decision.title} ${decision.contentText()}\".trim()\n        )\n    }\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/SoftwareSystemHome.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model.indexing\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.structurizr.site.generatr.site.asUrlToDirectory\nimport nl.avisi.structurizr.site.generatr.site.model.PageViewModel\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemPageViewModel\nimport nl.avisi.structurizr.site.generatr.site.model.contentText\nimport nl.avisi.structurizr.site.generatr.site.model.contentTitle\n\nfun softwareSystemHome(softwareSystem: SoftwareSystem, viewModel: PageViewModel): Document? {\n    val (contentTitle, contentText) = softwareSystem.section()\n    val propertyValues = softwareSystem.propertyValues()\n\n    if (contentTitle.isBlank() && contentText.isBlank() && propertyValues.isBlank())\n        return null\n\n    return Document(\n        SoftwareSystemPageViewModel.url(softwareSystem, SoftwareSystemPageViewModel.Tab.HOME)\n            .asUrlToDirectory(viewModel.url),\n        \"Software System Info\",\n        \"${softwareSystem.name} | ${contentTitle.ifEmpty { \"Info\" }}\",\n        \"$contentTitle $contentText $propertyValues\".trim(),\n    )\n}\n\nprivate fun SoftwareSystem.section() = documentation.sections.firstOrNull()?.let {\n    it.contentTitle() to it.contentText()\n} ?: (\"\" to \"\")\n\nprivate fun SoftwareSystem.propertyValues() = properties.values.joinToString(\" \")\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/SoftwareSystemRelationships.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model.indexing\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.structurizr.site.generatr.site.asUrlToDirectory\nimport nl.avisi.structurizr.site.generatr.site.model.PageViewModel\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemPageViewModel\n\nfun softwareSystemRelationships(softwareSystem: SoftwareSystem, viewModel: PageViewModel) = softwareSystem.relationships\n    .filter { r -> r.source == softwareSystem }\n    .sortedBy { it.destination.name }\n    .flatMap { relationship ->\n        listOf(relationship.destination.name, relationship.description, relationship.technology)\n    }\n    .plus(\n        softwareSystem.model.softwareSystems\n            .sortedBy { it.name }\n            .filterNot { system -> system == softwareSystem }\n            .flatMap { other ->\n                other.relationships\n                    .filter { r -> r.destination == softwareSystem }\n                    .sortedBy { it.source.name }\n                    .flatMap { relationship ->\n                        listOf(relationship.source.name, relationship.description, relationship.technology)\n                    }\n            }\n    )\n    .filter { it != null && it.isNotBlank() }\n    .joinToString(\" \")\n    .ifBlank { null }\n    ?.let {\n        Document(\n            SoftwareSystemPageViewModel.url(softwareSystem, SoftwareSystemPageViewModel.Tab.DEPENDENCIES)\n                .asUrlToDirectory(viewModel.url),\n            \"Dependencies\",\n            \"${softwareSystem.name} | Dependencies\",\n            it\n        )\n    }\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/SoftwareSystemSections.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model.indexing\n\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.structurizr.site.generatr.site.asUrlToDirectory\nimport nl.avisi.structurizr.site.generatr.site.model.*\n\nfun softwareSystemSections(softwareSystem: SoftwareSystem, viewModel: PageViewModel) = softwareSystem.documentation\n    .sections\n    .drop(1) // Drop software system home\n    .map { section ->\n        Document(\n            SoftwareSystemSectionPageViewModel.url(\n                softwareSystem,\n                section\n            ).asUrlToDirectory(viewModel.url),\n            \"Software System Documentation\",\n            \"${softwareSystem.name} | ${section.contentTitle()}\",\n            \"${section.contentTitle()} ${section.contentText()}\".trim()\n        )\n    }\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/WorkspaceDecisions.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model.indexing\n\nimport com.structurizr.documentation.Documentation\nimport nl.avisi.structurizr.site.generatr.site.asUrlToDirectory\nimport nl.avisi.structurizr.site.generatr.site.model.PageViewModel\nimport nl.avisi.structurizr.site.generatr.site.model.WorkspaceDecisionPageViewModel\nimport nl.avisi.structurizr.site.generatr.site.model.contentText\n\nfun workspaceDecisions(documentation: Documentation, viewModel: PageViewModel) = documentation.decisions\n    .map { decision ->\n        Document(\n            WorkspaceDecisionPageViewModel.url(decision)\n                .asUrlToDirectory(viewModel.url),\n            \"Workspace Decision\",\n            decision.title,\n            \"${decision.title} ${decision.contentText()}\".trim()\n        )\n    }\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/indexing/WorkspaceSections.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model.indexing\n\nimport com.structurizr.documentation.Documentation\nimport nl.avisi.structurizr.site.generatr.normalize\nimport nl.avisi.structurizr.site.generatr.site.asUrlToDirectory\nimport nl.avisi.structurizr.site.generatr.site.model.PageViewModel\nimport nl.avisi.structurizr.site.generatr.site.model.WorkspaceDocumentationSectionPageViewModel\nimport nl.avisi.structurizr.site.generatr.site.model.contentText\nimport nl.avisi.structurizr.site.generatr.site.model.contentTitle\n\nfun workspaceSections(documentation: Documentation, viewModel: PageViewModel) = documentation.sections\n    .drop(1) // Drop home\n    .map { section ->\n        Document(\n            WorkspaceDocumentationSectionPageViewModel.url(section)\n                .asUrlToDirectory(viewModel.url),\n            \"Workspace Documentation\",\n            section.contentTitle(),\n            \"${section.contentTitle()} ${section.contentText()}\".trim()\n        )\n    }\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/AutoReloading.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.site.asUrlToFile\nimport nl.avisi.structurizr.site.generatr.site.model.PageViewModel\n\nfun HEAD.autoReloadScript(viewModel: PageViewModel) {\n    script(\n        type = ScriptType.textJavaScript,\n        src = \"../\" + \"/auto-reload.js\".asUrlToFile(viewModel.url)\n    ) { }\n}\n\nfun BODY.updatingSiteProgressBar() {\n    div(classes = \"is-overlay is-front-centered\") {\n        progress(classes = \"progress is-small mt-1 is-hidden\") {\n            id = \"updating-site\"\n            value = \"0\"\n        }\n    }\n}\n\nfun BODY.updateSiteErrorHero() {\n    div(classes = \"container is-hidden\") {\n        id = \"update-site-error\"\n        div(classes = \"hero is-danger mt-6\") {\n            div(classes = \"hero-body\") {\n                p(classes = \"title\") {\n                    text(\"Error loading workspace file\")\n                }\n                p(classes = \"subtitle\") {\n                    id = \"update-site-error-message\"\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/CDN.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport com.structurizr.Workspace\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.json.jsonObject\nimport kotlinx.serialization.json.jsonPrimitive\nimport java.lang.IllegalStateException\n\nclass CDN(val workspace: Workspace) {\n    private val cdnBaseURL = getCdnBaseUrl()\n    private val json = Json { ignoreUnknownKeys = true }\n\n    private val packageJson = object {}.javaClass.getResource(\"/package.json\")?.readText()\n        ?: throw IllegalStateException(\"package.json not found\")\n\n    private val dependencies = json.parseToJsonElement(packageJson).jsonObject[\"dependencies\"]\n        ?.jsonObject\n        ?.map { Dependency(cdnBaseURL, it.key, it.value.jsonPrimitive.content) }\n        ?: throw IllegalStateException(\"dependencies element not found in package.json\")\n\n    fun bulmaCss() = dependencies.single { it.name == \"bulma\" }.let {\n        \"${it.baseUrl()}/css/bulma.min.css\"\n    }\n\n    fun katexJs() = dependencies.single { it.name == \"katex\" }.let {\n        \"${it.baseUrl()}/dist/katex.min.js\"\n    }\n\n    fun katexCss() = dependencies.single { it.name == \"katex\" }.let {\n        \"${it.baseUrl()}/dist/katex.min.css\"\n    }\n\n    fun lunrJs() = dependencies.single { it.name == \"lunr\" }.let {\n        \"${it.baseUrl()}/lunr.min.js\"\n    }\n\n    fun lunrLanguagesStemmerJs() = dependencies.single { it.name == \"lunr-languages\" }.let {\n        \"${it.baseUrl()}/min/lunr.stemmer.support.min.js\"\n    }\n\n    fun lunrLanguagesJs(language: String) = dependencies.single { it.name == \"lunr-languages\" }.let {\n        \"${it.baseUrl()}/min/lunr.$language.min.js\"\n    }\n\n    fun mermaidJs() = dependencies.single { it.name == \"mermaid\" }.let {\n        \"${it.baseUrl()}/dist/mermaid.esm.min.mjs\"\n    }\n\n    fun svgpanzoomJs() = dependencies.single { it.name == \"svg-pan-zoom\" }.let {\n        \"${it.baseUrl()}/dist/svg-pan-zoom.min.js\"\n    }\n\n    fun webfontloaderJs() = dependencies.single { it.name == \"webfontloader\" }.let {\n        \"${it.baseUrl()}/webfontloader.js\"\n    }\n\n    fun getCdnBaseUrl() = workspace.views.configuration.properties\n        .getOrDefault(\"generatr.site.cdn\", \"https://cdn.jsdelivr.net/npm\")\n        .trimEnd('/')\n\n    @Serializable\n    data class Dependency(val cdnBaseURL: String, val name: String, val version: String) {\n        fun baseUrl() = \"$cdnBaseURL/$name@$version\"\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/ContentDiv.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\n\nfun DIV.contentDiv(block: DIV.() -> Unit) {\n    div(classes = \"content p-3\") {\n        block()\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Diagram.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.site.model.DiagramViewModel\n\nfun FlowContent.diagram(viewModel: DiagramViewModel) {\n    if (viewModel.svg != null) {\n        val dialogId = \"${viewModel.key}-modal\"\n        val svgId = \"${viewModel.key}-svg\"\n\n        figure {\n            style = \"width: min(100%, ${viewModel.diagramWidthInPixels}px);\"\n            attributes[\"id\"] = viewModel.key\n            rawHtml(viewModel.svg)\n            figcaption {\n                a {\n                    onClick = \"openSvgModal('$dialogId', '$svgId')\"\n                    +viewModel.title\n                    if (!viewModel.description.isNullOrBlank()) {\n                        br\n                        +viewModel.description\n                    }\n                }\n            }\n        }\n        modal(dialogId) {\n            // TODO: no links in this SVG\n            rawHtml(viewModel.svg, svgId, \"modal-box-content\")\n            div(classes = \"has-text-centered\") {\n                +viewModel.title\n                +\" [\"\n                a(href = viewModel.svgLocation.relativeHref, target = \"_blank\") { +\"svg\" }\n                +\"|\"\n                a(href = viewModel.pngLocation.relativeHref, target = \"_blank\") { +\"png\" }\n                +\"|\"\n                a(href = viewModel.pumlLocation.relativeHref, target = \"_blank\") { +\"puml\" }\n                +\"]\"\n            }\n        }\n    } else\n        div(classes = \"notification is-danger\") {\n            +\"No view with key\"\n            span(classes = \"has-text-weight-bold\") { +\" ${viewModel.key} \" }\n            +\"found!\"\n        }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/DiagramIndex.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.site.model.DiagramIndexViewModel\n\nfun FlowContent.diagramIndex(viewModel: DiagramIndexViewModel) {\n    if(viewModel.visible) {\n        p(classes = \"subtitle is-6\") {\n            +\"Index:\"\n        }\n        ul {\n            viewModel.entries.forEach {\n                li {\n                    a(href = \"#${it.key}\") {\n                        +it.title\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/ExternalLink.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.FlowOrPhrasingContent\nimport kotlinx.html.a\nimport nl.avisi.structurizr.site.generatr.site.model.ExternalLinkViewModel\n\nfun FlowOrPhrasingContent.externalLink(viewModel: ExternalLinkViewModel) {\n    a(\n        href = viewModel.href,\n        target = \"_blank\",\n    ) {\n        +viewModel.title\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Favicon.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HEAD\nimport kotlinx.html.link\nimport nl.avisi.structurizr.site.generatr.site.model.FaviconViewModel\n\nfun HEAD.favicon(viewModel: FaviconViewModel) {\n    link(\n        rel = \"icon\",\n        type = viewModel.type,\n        href = viewModel.url\n    )\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/HomePage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.site.model.HomePageViewModel\n\nfun HTML.homePage(viewModel: HomePageViewModel) {\n    page(viewModel) {\n        contentDiv {\n            rawHtml(viewModel.content)\n        }\n    }\n}\n\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Image.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.site.model.ImageViewViewModel\n\nfun FlowContent.image(viewModel: ImageViewViewModel) {\n    val dialogId = \"${viewModel.key}-modal\"\n\n    figure {\n        style = \"width: fit-content;\"\n        attributes[\"id\"] = viewModel.key\n\n        p(classes = \"has-text-weight-bold\") { +viewModel.title }\n        img { src = viewModel.content }\n        figcaption {\n            a {\n                onClick = \"openModal('$dialogId')\"\n                +viewModel.title\n                if (!viewModel.description.isNullOrBlank()) {\n                    br\n                    +viewModel.description\n                }\n            }\n        }\n    }\n    modal(dialogId) {\n        img(classes = \"modal-box-content modal-image\") { src = viewModel.content }\n        div(classes = \"has-text-centered\") { +viewModel.title }\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Link.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.FlowOrPhrasingContent\nimport kotlinx.html.a\nimport nl.avisi.structurizr.site.generatr.site.model.LinkViewModel\n\nfun FlowOrPhrasingContent.link(viewModel: LinkViewModel, classes: String = \"\") {\n    a(\n        href = viewModel.relativeHref,\n        classes = \"$classes ${if (viewModel.active) \"is-active\" else \"\"}\".trim()\n    ) {\n        +viewModel.title\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/MarkdownExtension.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.site.asUrlToFile\nimport nl.avisi.structurizr.site.generatr.site.model.PageViewModel\n\nfun HEAD.markdownAdmonitionStylesheet(viewModel: PageViewModel) {\n    link(\n        rel = \"stylesheet\",\n        href = \"../\" + \"/admonition.css\".asUrlToFile(viewModel.url)\n    )\n}\n\nfun BODY.markdownAdmonitionScript(viewModel: PageViewModel) {\n    script(\n        type = ScriptType.textJavaScript,\n        src = \"../\" + \"/admonition.js\".asUrlToFile(viewModel.url)\n    ) { }\n}\n\nfun HEAD.katexStylesheet(cdn: CDN) {\n    // loading KaTeX as global on a webpage: https://katex.org/docs/browser.html#loading-as-global\n    unsafe {\n        raw(\"\"\"\n            <link rel=\"stylesheet\" href=\"${cdn.katexCss()}\" crossorigin=\"anonymous\">\n        \"\"\")\n    }\n}\n\nfun HEAD.katexScript(cdn: CDN) {\n    // loading KaTeX as global on a webpage: https://katex.org/docs/browser.html#loading-as-global\n    unsafe {\n        raw(\"\"\"\n            <script defer src=\"${cdn.katexJs()}\" crossorigin=\"anonymous\"></script>\n        \"\"\")\n    }\n}\n\nfun HEAD.katexFonts(cdn: CDN) {\n    // loading KaTeX as global on a webpage: https://katex.org/docs/browser.html#loading-as-global\n    unsafe {\n        raw(\"\"\"\n            <script>\n            window.WebFontConfig = {\n                custom: {\n                families: ['KaTeX_AMS', 'KaTeX_Caligraphic:n4,n7', 'KaTeX_Fraktur:n4,n7',\n                    'KaTeX_Main:n4,n7,i4,i7', 'KaTeX_Math:i4,i7', 'KaTeX_Script',\n                    'KaTeX_SansSerif:n4,n7,i4', 'KaTeX_Size1', 'KaTeX_Size2', 'KaTeX_Size3',\n                    'KaTeX_Size4', 'KaTeX_Typewriter'],\n                },\n            };\n            </script>\n            <script defer src=\"${cdn.webfontloaderJs()}\" crossorigin=\"anonymous\"></script>\n        \"\"\")\n    }\n}\n\nfun BODY.katexRenderScript(viewModel: PageViewModel) {\n    // Fix to support math rendering in markdown:\n    script(\n        type = ScriptType.textJavaScript,\n        src = \"../\" + \"/katex-render.js\".asUrlToFile(viewModel.url)\n    ) { }\n}\n\nfun BODY.mermaidScript(viewModel: PageViewModel) {\n    // Fix to support mermaid diagrams in markdown:\n    script(\n        type = ScriptType.textJavaScript,\n        src = \"../\" + \"/reformat-mermaid.js\".asUrlToFile(viewModel.url)\n    ) { }\n    // Simple full example, how to include Mermaid: https://mermaid.js.org/config/usage.html#simple-full-example\n    script(type = \"module\") {\n        unsafe {\n            raw(\"import mermaid from '${viewModel.cdn.mermaidJs()}';\")\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Menu.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.site.model.LinkViewModel\nimport nl.avisi.structurizr.site.generatr.site.model.MenuNodeViewModel\nimport nl.avisi.structurizr.site.generatr.site.model.MenuViewModel\n\nfun DIV.menu(viewModel: MenuViewModel, nestGroups: Boolean) {\n    aside(classes = \"menu p-3\") {\n        generalSection(viewModel.generalItems)\n        softwareSystemsSection(viewModel, nestGroups)\n    }\n}\n\nprivate fun ASIDE.generalSection(items: List<LinkViewModel>) {\n    p(classes = \"menu-label\") { +\"General\" }\n    menuItemLinks(items)\n}\n\nprivate fun ASIDE.softwareSystemsSection(viewModel: MenuViewModel, nestGroups: Boolean) {\n    p(classes = \"menu-label\") { +\"Software systems\" }\n    if (nestGroups) {\n        ul(classes = \"listree menu-list has-site-branding\") {\n            buildHtmlTree(viewModel.softwareSystemNodes(), viewModel).invoke(this)\n        }\n    } else {\n        menuItemLinks(viewModel.softwareSystemItems)\n    }\n}\n\nprivate fun ASIDE.menuItemLinks(items: List<LinkViewModel>) {\n    ul(classes = \"menu-list has-site-branding\") {\n        li {\n            items.forEach {\n                link(it)\n            }\n        }\n    }\n}\n\nprivate fun buildHtmlTree(node: MenuNodeViewModel, viewModel: MenuViewModel): UL.() -> Unit = {\n    if (node.name.isNotEmpty() && node.children.isEmpty()) {\n        val itemLink = viewModel.softwareSystemItems.find { it.title == node.name }\n        li {\n            if (itemLink != null) {\n                link(itemLink)\n            }\n        }\n    }\n\n    if (node.name.isNotEmpty() && node.children.isNotEmpty()) {\n        li {\n            div(classes = \"listree-submenu-heading\") {\n                +node.name\n            }\n            ul(classes = \"listree-submenu-items\") {\n                for (child in node.children) {\n                    buildHtmlTree(child, viewModel).invoke(this)\n                }\n            }\n        }\n    }\n\n    if (node.name.isEmpty() && node.children.isNotEmpty()) {\n        ul(classes = \"listree-submenu-items\") {\n            for (child in node.children) {\n                buildHtmlTree(child, viewModel).invoke(this)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Modal.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.FlowContent\nimport kotlinx.html.button\nimport kotlinx.html.div\nimport kotlinx.html.id\nimport kotlinx.html.onClick\n\nfun FlowContent.modal(dialogId: String, block: FlowContent.() -> Unit) {\n    div(classes = \"modal\") {\n        id = dialogId\n\n        div(classes = \"modal-background\") {\n            onClick = \"closeModal('$dialogId')\"\n        }\n        div(classes = \"modal-content\") {\n            div(classes = \"box\") {\n                block()\n            }\n        }\n        button(classes = \"modal-close is-large\") {\n            attributes[\"aria-label\"] = \"close\"\n            onClick = \"closeModal('$dialogId')\"\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Page.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.site.asUrlToFile\nimport nl.avisi.structurizr.site.generatr.site.model.PageViewModel\nimport nl.avisi.structurizr.site.generatr.site.model.Theme\n\nfun HTML.page(viewModel: PageViewModel, block: DIV.() -> Unit) {\n    attributes[\"lang\"] = \"en\"\n    when (viewModel.theme) {\n        Theme.LIGHT -> {\n            attributes[\"data-theme\"] = \"light\"\n        }\n        Theme.DARK -> {\n            attributes[\"data-theme\"] = \"dark\"\n        }\n        Theme.AUTO -> { }\n    }\n\n    headFragment(viewModel)\n    bodyFragment(viewModel, block)\n}\n\nprivate fun HTML.headFragment(viewModel: PageViewModel) {\n    head {\n        meta(charset = \"utf-8\")\n        meta(name = \"viewport\", content = \"width=device-width, initial-scale=1\")\n        title { +viewModel.pageTitle }\n        link(rel = \"stylesheet\", href = viewModel.cdn.bulmaCss())\n        link(rel = \"stylesheet\", href = \"../\" + \"/style.css\".asUrlToFile(viewModel.url))\n        link(rel = \"stylesheet\", href = \"./\" + \"/style-branding.css\".asUrlToFile(viewModel.url))\n        script(type = ScriptType.textJavaScript, src = \"../\" + \"/modal.js\".asUrlToFile(viewModel.url)) { }\n        script(type = ScriptType.textJavaScript, src = \"../\" + \"/svg-modal.js\".asUrlToFile(viewModel.url)) { }\n        script(type = ScriptType.textJavaScript, src = viewModel.cdn.svgpanzoomJs()) { }\n        if (viewModel.allowToggleTheme)\n            script(type = ScriptType.textJavaScript, src = \"../\" + \"/toggle-theme.js\".asUrlToFile(viewModel.url)) { }\n\n        if (viewModel.includeTreeview)\n            link(rel = \"stylesheet\", href = \"../\" + \"/treeview.css\".asUrlToFile(viewModel.url))\n\n        if (viewModel.includeAdmonition)\n            markdownAdmonitionStylesheet(viewModel)\n\n        if (viewModel.includeKatex)\n            katexStylesheet(viewModel.cdn)\n        if (viewModel.includeKatex)\n            katexScript(viewModel.cdn)\n        if (viewModel.includeKatex)\n            katexFonts(viewModel.cdn)\n\n        if (viewModel.favicon.includeFavicon)\n            favicon(viewModel.favicon)\n\n        if (viewModel.includeAutoReloading)\n            autoReloadScript(viewModel)\n\n        if (viewModel.customStylesheet.includeCustomStylesheet)\n            link(rel = \"stylesheet\", href = viewModel.customStylesheet.resourceURI)\n    }\n}\n\nprivate fun HTML.bodyFragment(viewModel: PageViewModel, block: DIV.() -> Unit) {\n    body {\n        if (viewModel.includeAutoReloading)\n            updatingSiteProgressBar()\n\n        pageHeader(viewModel.headerBar)\n\n        div(classes = \"site-layout\") {\n            id = \"site\"\n            menu(viewModel.menu, viewModel.includeTreeview)\n            div(classes = \"container is-fluid\") {\n                block()\n            }\n        }\n\n        if (viewModel.includeAutoReloading)\n            updateSiteErrorHero()\n        if (viewModel.includeAdmonition)\n            markdownAdmonitionScript(viewModel)\n\n        mermaidScript(viewModel)\n\n        if (viewModel.includeTreeview) {\n            script(type = ScriptType.textJavaScript, src = \"../\" + \"/treeview.js\".asUrlToFile(viewModel.url)) { }\n            script(type = ScriptType.textJavaScript) { unsafe { +\"listree();\" } }\n        }\n\n        if (viewModel.includeKatex)\n            katexRenderScript(viewModel)\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/PageHeader.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.site.asUrlToFile\nimport nl.avisi.structurizr.site.generatr.site.model.HeaderBarViewModel\nimport nl.avisi.structurizr.site.generatr.site.model.ImageViewModel\nimport nl.avisi.structurizr.site.generatr.site.model.LinkViewModel\n\nfun BODY.pageHeader(viewModel: HeaderBarViewModel) {\n    script(\n        type = ScriptType.textJavaScript,\n        src = \"../\" + \"/header.js\".asUrlToFile(viewModel.url)\n    ) { }\n    nav(classes = \"navbar\") {\n        role = \"navigation\"\n        attributes[\"aria-label\"] = \"main navigation\"\n\n        div(classes = \"navbar-brand has-site-branding\") {\n            if (viewModel.hasLogo)\n                logo(viewModel.titleLink, viewModel.logo!!)\n\n            navbarLink(viewModel.titleLink)\n        }\n        div(classes = \"navbar-menu has-site-branding\") {\n            div(classes = \"navbar-end\") {\n                div(classes = \"navbar-item\") {\n                    input(classes = \"input is-small is-rounded\") {\n                        id = \"search\"\n                        type = InputType.search\n                        size = \"30\"\n                        maxLength = \"50\"\n                        placeholder = \"Search...\"\n                        onKeyUp = \"redirect(event, value, '${viewModel.searchLink.relativeHref}')\"\n                    }\n                }\n                div(classes = \"navbar-item has-dropdown is-hoverable\") {\n                    a(classes = \"navbar-link has-site-branding\") {\n                        +viewModel.currentBranch\n                    }\n                    div(classes = \"navbar-dropdown is-right\") {\n                        viewModel.branches.forEach { branchLink ->\n                            a(\n                                classes = \"navbar-item\",\n                                href = branchLink.relativeHref\n                            ) {\n                                +branchLink.title\n                            }\n                        }\n                        if (viewModel.allowToggleTheme) {\n                            hr(classes = \"navbar-divider\")\n                            a(\n                                classes = \"navbar-item\",\n                            ) {\n                                onClick = \"toggleTheme()\"\n                                +\"Toggle theme\"\n                            }\n                        }\n                        hr(classes = \"navbar-divider\")\n                        div(classes = \"navbar-item has-text-grey-light\") {\n                            span { +\"v\" }\n                            span { +viewModel.version }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\nprivate fun DIV.logo(linkViewModel: LinkViewModel, imageViewModel: ImageViewModel) {\n    a(\n        classes = \"navbar-item\",\n        href = linkViewModel.relativeHref\n    ) {\n        img {\n            alt = linkViewModel.title\n            src = imageViewModel.relativeHref\n        }\n    }\n}\n\nprivate fun DIV.navbarLink(viewModel: LinkViewModel) {\n    a(\n        classes = \"navbar-item\",\n        href = viewModel.relativeHref\n    ) {\n        span(classes = \"has-text-weight-semibold has-site-branding\") { +viewModel.title }\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/RawHtml.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\n\nfun FlowContent.rawHtml(html: String, contentId: String? = null, contentClass: String? = null) {\n    div {\n        if (contentId != null) id = contentId\n        if (contentClass != null) classes = setOf(contentClass)\n\n        unsafe {\n            +html\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/RedirectRelative.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport kotlinx.html.body\nimport kotlinx.html.head\nimport kotlinx.html.meta\nimport kotlinx.html.title\n\nfun HTML.redirectRelative(appendUrl: String) {\n    attributes[\"lang\"] = \"en\"\n    head {\n        meta {\n            httpEquiv = \"refresh\"\n            content = \"0; url=./$appendUrl\"\n        }\n        title { +\"Structurizr site generatr\" }\n    }\n    body()\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/RedirectUpPage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport kotlinx.html.body\nimport kotlinx.html.head\nimport kotlinx.html.meta\nimport kotlinx.html.title\n\nfun HTML.redirectUpPage() {\n    attributes[\"lang\"] = \"en\"\n    head {\n        meta {\n            httpEquiv = \"refresh\"\n            content = \"0; url=../\"\n        }\n        title { +\"Structurizr site generatr\" }\n    }\n    body()\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SearchPage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport kotlinx.serialization.encodeToString\nimport kotlinx.serialization.json.Json\nimport nl.avisi.structurizr.site.generatr.site.asUrlToFile\nimport nl.avisi.structurizr.site.generatr.site.model.SearchViewModel\n\nfun HTML.searchPage(viewModel: SearchViewModel) {\n    val language = if (viewModel.language == \"en\") \"\" else viewModel.language\n    if (!supportedLanguages.contains(language))\n        throw IllegalArgumentException(\n            \"Indexing language $language is not supported, available languages are $supportedLanguages\"\n        )\n\n    page(viewModel) {\n        contentDiv {\n            h2 { +viewModel.pageSubTitle }\n            script(\n                type = ScriptType.textJavaScript,\n                src = viewModel.cdn.lunrJs()\n            ) { }\n            script(\n                type = ScriptType.textJavaScript,\n                src = viewModel.cdn.lunrLanguagesStemmerJs()\n            ) { }\n            if (language.isNotBlank())\n                script(\n                    type = ScriptType.textJavaScript,\n                    src = viewModel.cdn.lunrLanguagesJs(language)\n                ) { }\n\n            script(type = ScriptType.textJavaScript) {\n                unsafe {\n                    +\"const documents = ${Json.encodeToString(viewModel.documents)};\"\n                    +\"const idx = lunr(function () {\"\n                    if (viewModel.language.isNotBlank())\n                        +\"this.use(lunr.${viewModel.language});\"\n                    +\"\"\"\n                        this.ref('href')\n                        this.field('text')\n\n                        documents.forEach(function (doc) {\n                          this.add(doc)\n                        }, this)\n                      });\n                    \"\"\"\n                }\n            }\n            script(\n                type = ScriptType.textJavaScript,\n                src = \"../\" + \"/search.js\".asUrlToFile(viewModel.url)\n            ) { }\n            div { id = \"search-results\" }\n        }\n    }\n}\n\n// From https://github.com/olivernn/lunr-languages\nprivate val supportedLanguages = listOf(\n    \"\",\n    \"ar\",\n    \"da\",\n    \"de\",\n    \"du\",\n    \"es\",\n    \"fi\",\n    \"fr\",\n    \"hi\",\n    \"hu\",\n    \"it\",\n    \"ja\",\n    \"jp\",\n    \"ko\",\n    \"nl\",\n    \"no\",\n    \"pt\",\n    \"ro\",\n    \"ru\",\n    \"sv\",\n    \"ta\",\n    \"th\",\n    \"tr\",\n    \"vi\",\n    \"zh\",\n)\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerComponentCodePage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport kotlinx.html.div\nimport kotlinx.html.li\nimport kotlinx.html.ul\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerComponentCodePageViewModel\n\nfun HTML.softwareSystemContainerComponentCodePage(viewModel: SoftwareSystemContainerComponentCodePageViewModel) {\n    if (viewModel.visible) {\n        softwareSystemPage(viewModel) {\n            div(classes = \"tabs\") {\n                ul(classes = \"m-0 is-flex-wrap-wrap is-flex-shrink-1 is-flex-grow-0\") {\n                    viewModel.containerTabs\n                        .forEach {\n                            li(classes = if (it.link.active) \"is-active\" else null) {\n                                link(it.link)\n                            }\n                        }\n                }\n            }\n            div(classes = \"tabs is-size-7\") {\n                ul(classes = \"m-0 is-flex-wrap-wrap is-flex-shrink-1 is-flex-grow-0\") {\n                    viewModel.componentTabs\n                        .forEach {\n                            li(classes = if (it.link.active) \"is-active\" else null) {\n                                link(it.link)\n                            }\n                        }\n                }\n            }\n            diagramIndex(viewModel.diagramIndex)\n            viewModel.images.forEach { image(it) }\n        }\n    } else\n        redirectUpPage()\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerComponentDecisionPage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerComponentDecisionPageViewModel\n\nfun HTML.softwareSystemContainerComponentDecisionPage(viewModel: SoftwareSystemContainerComponentDecisionPageViewModel) {\n    softwareSystemPage(viewModel) {\n        rawHtml(viewModel.content)\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerComponentDecisionsPage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerComponentDecisionsPageViewModel\n\nfun HTML.softwareSystemContainerComponentDecisionsPage(\n    viewModel: SoftwareSystemContainerComponentDecisionsPageViewModel\n) {\n    if (viewModel.visible) {\n        softwareSystemPage(viewModel) {\n            decisionTabs(viewModel)\n            componentDecisionTabs(viewModel)\n            table(viewModel.decisionsTable)\n        }\n    } else {\n        redirectUpPage()\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerComponentSectionPage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerComponentSectionPageViewModel\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerSectionPageViewModel\n\nfun HTML.softwareSystemContainerComponentSectionPage(viewModel: SoftwareSystemContainerComponentSectionPageViewModel) {\n    softwareSystemPage(viewModel) {\n        rawHtml(viewModel.content)\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerComponentSectionsPage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerComponentSectionsPageViewModel\n\nfun HTML.softwareSystemContainerComponentSectionsPage(viewModel: SoftwareSystemContainerComponentSectionsPageViewModel) {\n    if (viewModel.visible)\n        softwareSystemPage(viewModel) {\n            softwareSystemContainerSectionsBody(viewModel)\n            table(viewModel.componentSectionTable)\n        }\n    else\n        redirectUpPage()\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerComponentsPage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerComponentsPageViewModel\n\nfun HTML.softwareSystemContainerComponentsPage(viewModel: SoftwareSystemContainerComponentsPageViewModel) {\n     if (viewModel.visible) {\n        softwareSystemPage(viewModel) {\n            div(classes = \"tabs\") {\n                ul(classes = \"m-0 is-flex-wrap-wrap is-flex-shrink-1 is-flex-grow-0\") {\n                    viewModel.containerTabs\n                        .forEach {\n                            li(classes = if (it.link.active) \"is-active\" else null) {\n                                link(it.link)\n                            }\n                        }\n                }\n            }\n            diagramIndex(viewModel.diagramIndex)\n            viewModel.diagrams.forEach { diagram(it) }\n            viewModel.images.forEach { image(it) }\n\n            if(viewModel.hasProperties) {\n                h3 { +\"Properties\" }\n                table(viewModel.propertiesTable)\n            }\n        }\n    } else\n        redirectUpPage()\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerDecisionPage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerDecisionPageViewModel\n\nfun HTML.softwareSystemContainerDecisionPage(viewModel: SoftwareSystemContainerDecisionPageViewModel) {\n    softwareSystemPage(viewModel) {\n        rawHtml(viewModel.content)\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerDecisionsPage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.FlowContent\nimport kotlinx.html.HTML\nimport kotlinx.html.div\nimport kotlinx.html.li\nimport kotlinx.html.ul\nimport nl.avisi.structurizr.site.generatr.site.model.BaseSoftwareSystemContainerDecisionsPageViewModel\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerDecisionsPageViewModel\n\nfun HTML.softwareSystemContainerDecisionsPage(viewModel: SoftwareSystemContainerDecisionsPageViewModel) {\n    if (viewModel.onlyComponentDecisionsVisible) {\n        redirectRelative(\n            viewModel.componentDecisionsTabs.first().link.relativeHref\n        )\n    } else if (viewModel.visible) {\n        softwareSystemPage(viewModel) {\n            decisionTabs(viewModel)\n            componentDecisionTabs(viewModel)\n            table(viewModel.decisionsTable)\n        }\n    } else {\n        redirectUpPage()\n    }\n}\n\nfun FlowContent.componentDecisionTabs(viewModel: BaseSoftwareSystemContainerDecisionsPageViewModel) {\n    div(classes = \"tabs is-size-7\") {\n        ul(classes = \"m-0 is-flex-wrap-wrap is-flex-shrink-1 is-flex-grow-0\") {\n            viewModel.componentDecisionsTabs.forEach { tab ->\n                li(classes = if (tab.link.active) \"is-active\" else null) {\n                    link(tab.link)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerPage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerPageViewModel\n\nfun HTML.softwareSystemContainerPage(viewModel: SoftwareSystemContainerPageViewModel) {\n    if (viewModel.visible)\n        softwareSystemPage(viewModel) {\n            diagramIndex(viewModel.diagramIndex)\n            viewModel.diagrams.forEach { diagram(it) }\n            viewModel.images.forEach { image(it) }\n        }\n    else\n        redirectUpPage()\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerSectionPage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerSectionPageViewModel\n\nfun HTML.softwareSystemContainerSectionPage(viewModel: SoftwareSystemContainerSectionPageViewModel) {\n    softwareSystemPage(viewModel) {\n        rawHtml(viewModel.content)\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerSectionsPage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.site.model.BaseSoftwareSystemContainerSectionsPageViewModel\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerSectionsPageViewModel\n\nfun HTML.softwareSystemContainerSectionsPage(viewModel: SoftwareSystemContainerSectionsPageViewModel) {\n    if (viewModel.onlyComponentsDocumentationSectionsVisible)\n        redirectRelative(\n            viewModel.componentSectionsTabs.first().link.relativeHref\n        )\n    else if (viewModel.visible)\n        softwareSystemPage(viewModel) {\n            softwareSystemContainerSectionsBody(viewModel)\n            table(viewModel.sectionsTable)\n        }\n    else\n        redirectUpPage()\n}\n\nfun FlowContent.softwareSystemContainerSectionsBody(viewModel: BaseSoftwareSystemContainerSectionsPageViewModel) {\n    softwareSystemSectionsBody(viewModel)\n\n    div(classes = \"tabs is-size-7\") {\n        ul(classes = \"m-0 is-flex-wrap-wrap is-flex-shrink-1 is-flex-grow-0\") {\n            viewModel.componentSectionsTabs.forEach { tab ->\n                li(classes = if (tab.link.active) \"is-active\" else null) {\n                    link(tab.link)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContextPage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContextPageViewModel\n\nfun HTML.softwareSystemContextPage(viewModel: SoftwareSystemContextPageViewModel) {\n    if (viewModel.visible)\n        softwareSystemPage(viewModel) {\n            diagramIndex(viewModel.diagramIndex)\n            viewModel.diagrams.forEach { diagram(it) }\n        }\n    else\n        redirectUpPage()\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemDecisionPage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemDecisionPageViewModel\n\nfun HTML.softwareSystemDecisionPage(viewModel: SoftwareSystemDecisionPageViewModel) {\n    softwareSystemPage(viewModel) {\n        rawHtml(viewModel.content)\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemDecisionsPage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.FlowContent\nimport kotlinx.html.HTML\nimport kotlinx.html.div\nimport kotlinx.html.li\nimport kotlinx.html.ul\nimport nl.avisi.structurizr.site.generatr.site.model.BaseSoftwareSystemDecisionsPageViewModel\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemDecisionsPageViewModel\nimport kotlin.collections.forEach\n\nfun HTML.softwareSystemDecisionsPage(viewModel: SoftwareSystemDecisionsPageViewModel) {\n    if (viewModel.onlyContainersDecisionsVisible) {\n        redirectRelative(\n            viewModel.decisionTabs.first().link.relativeHref\n        )\n    } else if (viewModel.visible) {\n        softwareSystemPage(viewModel) {\n            decisionTabs(viewModel)\n            if (viewModel.softwareSystemDecisionsVisible) {\n                table(viewModel.decisionsTable)\n            }\n        }\n    } else {\n        redirectUpPage()\n    }\n}\n\nfun FlowContent.decisionTabs(viewModel: BaseSoftwareSystemDecisionsPageViewModel) {\n    div(classes = \"tabs\") {\n        ul(classes = \"m-0 is-flex-wrap-wrap is-flex-shrink-1 is-flex-grow-0\") {\n            viewModel.decisionTabs\n                .forEach {\n                    li(classes = if (it.link.active) \"is-active\" else null) {\n                        link(it.link)\n                    }\n                }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemDependenciesPage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport kotlinx.html.h2\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemDependenciesPageViewModel\n\nfun HTML.softwareSystemDependenciesPage(viewModel: SoftwareSystemDependenciesPageViewModel) {\n    softwareSystemPage(viewModel) {\n        h2 { +\"Inbound\" }\n        table(viewModel.dependenciesInboundTable)\n        h2 { +\"Outbound\" }\n        table(viewModel.dependenciesOutboundTable)\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemDeploymentPage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemDeploymentPageViewModel\n\nfun HTML.softwareSystemDeploymentPage(viewModel: SoftwareSystemDeploymentPageViewModel) {\n    if (viewModel.visible)\n        softwareSystemPage(viewModel) {\n            diagramIndex(viewModel.diagramIndex)\n            viewModel.diagrams.forEach { diagram(it) }\n        }\n    else\n        redirectUpPage()\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemDynamicPage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemDynamicPageViewModel\n\nfun HTML.softwareSystemDynamicPage(viewModel: SoftwareSystemDynamicPageViewModel) {\n    if (viewModel.visible)\n        softwareSystemPage(viewModel) {\n            diagramIndex(viewModel.diagramIndex)\n            viewModel.diagrams.forEach { diagram(it) }\n        }\n    else\n        redirectUpPage()\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemHomePage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport kotlinx.html.h2\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemHomePageViewModel\n\nfun HTML.softwareSystemHomePage(viewModel: SoftwareSystemHomePageViewModel) {\n    softwareSystemPage(viewModel) {\n        rawHtml(viewModel.content)\n        if (viewModel.hasProperties) {\n            h2 { +\"Properties\" }\n            table(viewModel.propertiesTable)\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemPage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemPageViewModel\n\nfun HTML.softwareSystemPage(viewModel: SoftwareSystemPageViewModel, block: FlowContent.() -> Unit) {\n    page(viewModel) {\n        h1(classes = \"title mt-3\") { +viewModel.pageSubTitle }\n        h2(classes = \"subtitle\") { +viewModel.description }\n\n        div(classes = \"tabs mt-3\") {\n            ul(classes = \"is-flex-wrap-wrap is-flex-shrink-1 is-flex-grow-0\") {\n                viewModel.tabs\n                    .filter { it.visible }\n                    .forEach {\n                        li(classes = if (it.link.active) \"is-active\" else null) {\n                            link(it.link)\n                        }\n                    }\n            }\n        }\n        contentDiv {\n            block()\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemSectionPage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemSectionPageViewModel\n\nfun HTML.softwareSystemSectionPage(viewModel: SoftwareSystemSectionPageViewModel) {\n    softwareSystemPage(viewModel) {\n        rawHtml(viewModel.content)\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemSectionsPage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.site.model.BaseSoftwareSystemSectionsPageViewModel\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemSectionsPageViewModel\n\nfun HTML.softwareSystemSectionsPage(viewModel: SoftwareSystemSectionsPageViewModel) {\n    if (viewModel.onlyContainersDocumentationSectionsVisible) {\n        redirectRelative(\n            viewModel.sectionsTabs.first().link.relativeHref\n        )\n    } else if (viewModel.visible)\n        softwareSystemPage(viewModel) {\n            softwareSystemSectionsBody(viewModel)\n            table(viewModel.sectionsTable)\n        }\n    else\n        redirectUpPage()\n}\n\nfun FlowContent.softwareSystemSectionsBody(viewModel: BaseSoftwareSystemSectionsPageViewModel) {\n    div(classes = \"tabs\") {\n        ul(classes = \"m-0 is-flex-wrap-wrap is-flex-shrink-1 is-flex-grow-0\") {\n            viewModel.sectionsTabs\n                .forEach {\n                    li(classes = if (it.link.active) \"is-active\" else null) {\n                        link(it.link)\n                    }\n                }\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemsPage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport kotlinx.html.h2\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemsPageViewModel\n\nfun HTML.softwareSystemsPage(viewModel: SoftwareSystemsPageViewModel) {\n    page(viewModel = viewModel) {\n        contentDiv {\n            h2 { +viewModel.pageSubTitle }\n            table(viewModel.softwareSystemsTable)\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/Table.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.*\nimport nl.avisi.structurizr.site.generatr.site.model.TableViewModel\nimport nl.avisi.structurizr.site.generatr.site.model.CellWidth\n\nfun FlowContent.table(viewModel: TableViewModel) {\n    table (classes = \"table is-fullwidth\") {\n        thead {\n            viewModel.headerRows.forEach {\n                row(it)\n            }\n        }\n        tbody {\n            viewModel.bodyRows.forEach {\n                row(it)\n            }\n        }\n    }\n}\n\nprivate fun THEAD.row(viewModel: TableViewModel.RowViewModel) {\n    tr {\n        viewModel.columns.forEach {\n            cell(it)\n        }\n    }\n}\n\nprivate fun TBODY.row(viewModel: TableViewModel.RowViewModel) {\n    tr {\n        viewModel.columns.forEach {\n            cell(it)\n        }\n    }\n}\n\nprivate fun TR.cell(viewModel: TableViewModel.CellViewModel) {\n    when (viewModel) {\n        is TableViewModel.TextCellViewModel -> {\n            val classes = when (viewModel.width) {\n                CellWidth.UNSPECIFIED -> null\n                CellWidth.ONE_TENTH -> \"is-one-tenth\"\n                CellWidth.ONE_FOURTH -> \"is-one-fourth\"\n                CellWidth.TWO_FOURTH -> \"is-two-fourth\"\n            }\n\n            var spanClasses = \"\"\n            if (viewModel.greyText) spanClasses += \"has-text-grey \"\n            if (viewModel.boldText) spanClasses += \"has-text-weight-bold\"\n\n            if (viewModel.isHeader)\n                th(classes = classes) { span(classes = spanClasses) { +viewModel.title } }\n            else\n                td(classes = classes) { span(classes = spanClasses) { +viewModel.title } }\n        }\n        is TableViewModel.LinkCellViewModel -> {\n            val classes = if (viewModel.boldText) \"has-text-weight-bold\" else null\n            if (viewModel.isHeader)\n                th(classes = classes)  { link(viewModel.link) }\n            else\n                td(classes = classes)  { link(viewModel.link) }\n        }\n        is TableViewModel.ExternalLinkCellViewModel ->\n            if (viewModel.isHeader)\n                th { externalLink(viewModel.link) }\n            else\n                td { externalLink(viewModel.link) }\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/WorkspaceDecisionPage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generatr.site.model.WorkspaceDecisionPageViewModel\n\nfun HTML.workspaceDecisionPage(viewModel: WorkspaceDecisionPageViewModel) {\n    page(viewModel) {\n        contentDiv {\n            rawHtml(viewModel.content)\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/WorkspaceDecisionsPage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport kotlinx.html.h2\nimport nl.avisi.structurizr.site.generatr.site.model.WorkspaceDecisionsPageViewModel\n\nfun HTML.workspaceDecisionsPage(viewModel: WorkspaceDecisionsPageViewModel) {\n    page(viewModel) {\n        contentDiv {\n            h2 { +viewModel.pageSubTitle }\n            table(viewModel.decisionsTable)\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/WorkspaceDocumentationSectionPage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport kotlinx.html.HTML\nimport nl.avisi.structurizr.site.generatr.site.model.WorkspaceDocumentationSectionPageViewModel\n\nfun HTML.workspaceDocumentationSectionPage(viewModel: WorkspaceDocumentationSectionPageViewModel) {\n    page(viewModel) {\n        contentDiv {\n            rawHtml(viewModel.content)\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/resources/assets/css/admonition.css",
    "content": ".adm-block {\n    display: block;\n    width: 99%;\n    border-radius: 6px;\n    padding-left: 10px;\n    margin-bottom: 1em;\n    border: 1px solid;\n    border-left-width: 4px;\n    box-shadow: 2px 2px 6px #cdcdcd;\n}\n\n.adm-heading {\n    display: block;\n    font-weight: bold;\n    font-size: 0.9em;\n    height: 1.8em;\n    padding-top: 0.3em;\n    padding-bottom: 2em;\n    border-bottom: solid 1px;\n    padding-left: 10px;\n    margin-left: -10px;\n}\n\n.adm-body {\n    display: block;\n    padding-bottom: 0.5em;\n    padding-top: 0.5em;\n    margin-left: 1.5em;\n    margin-right: 1.5em;\n}\n\n.adm-heading > span {\n    color: initial;\n}\n\n.adm-icon {\n    height: 1.6em;\n    width: 1.6em;\n    display: inline-block;\n    vertical-align: middle;\n    margin-right: 0.25em;\n    margin-left: -0.25em;\n}\n\n.adm-hidden {\n    display: none !important;\n}\n\n.adm-block.adm-collapsed > .adm-heading, .adm-block.adm-open > .adm-heading {\n    position: relative;\n    cursor: pointer;\n}\n\n.adm-block.adm-collapsed > .adm-heading {\n    margin-bottom: 0;\n}\n\n.adm-block.adm-collapsed .adm-body {\n    display: none !important;\n}\n\n.adm-block.adm-open > .adm-heading:after,\n.adm-block.adm-collapsed > .adm-heading:after {\n    display: inline-block;\n    position: absolute;\n    top:calc(50% - .65em);\n    right: 0.5em;\n    font-size: 1.3em;\n    content: '▼';\n}\n\n.adm-block.adm-collapsed > .adm-heading:after {\n    right: 0.50em;\n    top:calc(50% - .75em);\n    transform: rotate(90deg);\n}\n\n/* default scheme */\n\n.adm-block {\n    border-color: #ebebeb;\n    border-bottom-color: #bfbfbf;\n}\n\n.adm-block.adm-abstract {\n    border-left-color: #48C4FF;\n}\n\n.adm-block .adm-block.adm-abstract .adm-heading,\n.adm-block.adm-abstract .adm-heading {\n    background: #E8F7FF;\n    color: #48C4FF;\n    border-bottom-color: #dbf3ff;\n}\n\n.adm-block.adm-abstract.adm-open > .adm-heading:after,\n.adm-block.adm-abstract.adm-collapsed > .adm-heading:after {\n    color: #80d9ff;\n}\n\n\n.adm-block.adm-bug {\n    border-left-color: #F50057;\n}\n\n.adm-block .adm-block.adm-bug .adm-heading,\n.adm-block.adm-bug .adm-heading {\n    background: #FEE7EE;\n    color: #F50057;\n    border-bottom-color: #fcd9e4;\n}\n\n.adm-block.adm-bug.adm-open > .adm-heading:after,\n.adm-block.adm-bug.adm-collapsed > .adm-heading:after {\n    color: #f57aab;\n}\n\n.adm-block.adm-danger {\n    border-left-color: #FE1744;\n}\n\n.adm-block .adm-block.adm-danger .adm-heading,\n.adm-block.adm-danger .adm-heading {\n    background: #FFE9ED;\n    color: #FE1744;\n    border-bottom-color: #ffd9e0;\n}\n\n.adm-block.adm-danger.adm-open > .adm-heading:after,\n.adm-block.adm-danger.adm-collapsed > .adm-heading:after {\n    color: #fc7e97;\n}\n\n.adm-block.adm-example {\n    border-left-color: #7940ff;\n}\n\n.adm-block .adm-block.adm-example .adm-heading,\n.adm-block.adm-example .adm-heading {\n    background: #EFEBFF;\n    color: #7940ff;\n    border-bottom-color: #e0d9ff;\n}\n\n.adm-block.adm-example.adm-open > .adm-heading:after,\n.adm-block.adm-example.adm-collapsed > .adm-heading:after {\n    color: #b199ff;\n}\n\n.adm-block.adm-fail {\n    border-left-color: #FE5E5E;\n}\n\n.adm-block .adm-block.adm-fail .adm-heading,\n.adm-block.adm-fail .adm-heading {\n    background: #FFEEEE;\n    color: #Fe5e5e;\n    border-bottom-color: #ffe3e3;\n}\n\n.adm-block.adm-fail.adm-open > .adm-heading:after,\n.adm-block.adm-fail.adm-collapsed > .adm-heading:after {\n    color: #fcb1b1;\n}\n\n.adm-block.adm-faq {\n    border-left-color: #5ED116;\n}\n\n.adm-block .adm-block.adm-faq .adm-heading,\n.adm-block.adm-faq .adm-heading {\n    background: #EEFAE8;\n    color: #5ED116;\n    border-bottom-color: #e6fadc;\n}\n\n.adm-block.adm-faq.adm-open > .adm-heading:after,\n.adm-block.adm-faq.adm-collapsed > .adm-heading:after {\n    color: #98cf72;\n}\n\n.adm-block.adm-info {\n    border-left-color: #00B8D4;\n}\n\n.adm-block .adm-block.adm-info .adm-heading,\n.adm-block.adm-info .adm-heading {\n    background: #E8F7FA;\n    color: #00B8D4;\n    border-bottom-color: #dcf5fa;\n}\n\n.adm-block.adm-info.adm-open > .adm-heading:after,\n.adm-block.adm-info.adm-collapsed > .adm-heading:after {\n    color: #83ced6;\n}\n\n.adm-block.adm-note {\n    border-left-color: #448AFF;\n}\n\n.adm-block.adm-block.adm-note .adm-heading,\n.adm-block.adm-note .adm-heading {\n    background: #EDF4FF;\n    color: #448AFF;\n    border-bottom-color: #e0edff;\n}\n\n.adm-block.adm-note.adm-open > .adm-heading:after,\n.adm-block.adm-note.adm-collapsed > .adm-heading:after {\n    color: #8cb8ff;\n}\n\n.adm-block.adm-quote {\n    border-left-color: #9E9E9E;\n}\n\n.adm-block .adm-block.adm-quote .adm-heading,\n.adm-block.adm-quote .adm-heading {\n    background: #F4F4F4;\n    color: #9E9E9E;\n    border-bottom-color: #e8e8e8;\n}\n\n.adm-block.adm-quote.adm-open > .adm-heading:after,\n.adm-block.adm-quote.adm-collapsed > .adm-heading:after {\n    color: #b3b3b3;\n}\n\n.adm-block.adm-success {\n    border-left-color: #1DCD63;\n}\n\n.adm-block .adm-block.adm-success .adm-heading,\n.adm-block.adm-success .adm-heading {\n    background: #E9F8EE;\n    color: #1DCD63;\n    border-bottom-color: #dcf7e5;\n}\n\n.adm-block.adm-success.adm-open > .adm-heading:after,\n.adm-block.adm-success.adm-collapsed > .adm-heading:after {\n    color: #7acc98;\n}\n\n.adm-block.adm-tip {\n    border-left-color: #01BFA5;\n}\n\n.adm-block .adm-block.adm-tip .adm-heading,\n.adm-block.adm-tip .adm-heading {\n    background: #E9F9F6;\n    color: #01BFA5;\n    border-bottom-color: #dcf7f2;\n}\n\n.adm-block.adm-tip.adm-open > .adm-heading:after,\n.adm-block.adm-tip.adm-collapsed > .adm-heading:after {\n    color: #7dd1c0;\n}\n\n.adm-block.adm-warning {\n    border-left-color: #FF9001;\n}\n\n.adm-block .adm-block.adm-warning .adm-heading,\n.adm-block.adm-warning .adm-heading {\n    background: #FEF3E8;\n    color: #FF9001;\n    border-bottom-color: #Fef3e8;\n}\n\n.adm-block.adm-warning.adm-open > .adm-heading:after,\n.adm-block.adm-warning.adm-collapsed > .adm-heading:after {\n    color: #fcbb6a;\n}\n\n/* Assciidoc adaptions */\n.admonitionblock .icon > .title {\n    word-break: normal;\n    font-size: 1em;\n}\n\n.admonitionblock table,\n.admonitionblock thead,\n.admonitionblock tbody,\n.admonitionblock th,\n.admonitionblock td,\n.admonitionblock tr {\n    display: block;\n}\n\n.admonitionblock td.icon {\n    width: initial;\n    height: initial;\n    margin-left: -10px;\n}\n\n.admonitionblock .title {\n    font-size: 1.5em;\n}\n"
  },
  {
    "path": "src/main/resources/assets/css/style.css",
    "content": "body {\n    height: 100vh;\n    display: flex;\n    flex-direction: column;\n}\n\n.site-layout {\n    display: grid;\n    grid-template-columns: 300px 1fr;\n    flex-grow: 1;\n}\n\n.menu {\n    border-right: 1px solid #cccccc;\n}\n\n.menu-list a {\n    background-color: transparent;\n}\n\n.is-front-centered {\n    width: 40%;\n    margin-left: 30%;\n    height: fit-content;\n    z-index: 50;\n}\n\n.is-one-tenth {\n    width: 10%;\n}\n\n.is-one-fourth {\n    width: 25%;\n}\n\n.is-two-fourth {\n    width: 50%;\n}\n\nsvg a:hover {\n    opacity: 90%;\n}\n\n.modal-content {\n    width: calc(100vw - 120px);\n}\n\n.modal-box-content {\n    width: 100%;\n    height: calc(100vh - 120px);\n}\n\n.modal-svg {\n    display: inline;\n    width: inherit;\n    min-width: inherit;\n    max-width: inherit;\n    height: inherit;\n    min-height: inherit;\n    max-height: inherit;\n}\n\na {\n    color: #485fc7; /* Pre Bulma 1.x colour code for headers, tabs and links */\n}\n\n.modal-image {\n    object-fit: contain;\n}\n\na.navbar-item:hover {\n    background-color: transparent;\n}\n\n.tabs li.is-active a {\n    color: #485fc7;\n    border-bottom-color: #485fc7;\n}\n\n.tabs li {\n    font-weight: bold;\n}\n\n.input {\n    color: dimgrey!important;\n    background-color: white!important;\n}\n.input::placeholder {\n    color: darkgrey!important;\n}\n"
  },
  {
    "path": "src/main/resources/assets/css/treeview.css",
    "content": ".listree-submenu-heading {\n    cursor: pointer;\n    padding: .5em .75em;\n}\n\nul.listree {\n    list-style: none\n}\n\nul.listree-submenu-items {\n    list-style: none;\n    white-space: nowrap;\n    padding-left: 10px;\n}\n\ndiv.listree-submenu-heading.collapsed:before {\n    content: \"\\25B6\";\n    display: inline-block;\n    min-width: 20px;\n}\n\ndiv.listree-submenu-heading.expanded:before {\n    content: \"\\25BC\";\n    display: inline-block;\n    min-width: 20px;\n}\n\n.scrollable-menu {\n    height: auto;\n    max-width: 800px;\n    overflow-y: hidden\n}\n\n.menu-list li ul {\n    border-left: 0;\n    margin: .25em;\n    padding-left: .75em;\n}\n"
  },
  {
    "path": "src/main/resources/assets/js/admonition.js",
    "content": "(() => {\n    let divs = document.getElementsByClassName(\"adm-block\");\n    for (let i = 0; i < divs.length; i++) {\n        let div = divs[i];\n        if (div.classList.contains(\"adm-collapsed\") || div.classList.contains(\"adm-open\")) {\n            let headings = div.getElementsByClassName(\"adm-heading\");\n            if (headings.length > 0) {\n                headings[0].addEventListener(\"click\", event => {\n                    let el = div;\n                    event.preventDefault();\n                    event.stopImmediatePropagation();\n                    if (el.classList.contains(\"adm-collapsed\")) {\n                        console.debug(\"Admonition Open\", event.srcElement);\n                        el.classList.remove(\"adm-collapsed\");\n                        el.classList.add(\"adm-open\");\n                    } else {\n                        console.debug(\"Admonition Collapse\", event.srcElement);\n                        el.classList.add(\"adm-collapsed\");\n                        el.classList.remove(\"adm-open\");\n                    }\n                });\n            }\n        }\n    }\n})();\n"
  },
  {
    "path": "src/main/resources/assets/js/auto-reload.js",
    "content": "let hash = undefined;\n\nfunction connectToWs() {\n    const ws = new WebSocket(\"ws://\" + location.host + \"/_events\");\n    ws.onopen = () => {\n        console.log(\"Connected to socket.\");\n        reloadIfNeeded();\n    }\n    ws.onclose = (event) => {\n        console.log('Socket has been closed. Attempting to reconnect ...', event.reason);\n        setTimeout(() => connectToWs(), 1000);\n    };\n    ws.onmessage = (event) => {\n        if (event.data === \"site-updating\") {\n            console.log(\"Site updating ...\")\n            showUpdatingSite()\n        } else if (event.data === \"site-updated\") {\n            console.log(\"Site update detected, detect page content change ...\")\n            hideUpdatingSite()\n            hideUpdateSiteError()\n            showSite()\n            reloadIfNeeded();\n        } else {\n            console.log(event.data)\n            hideUpdatingSite()\n            hideSite()\n            showUpdateSiteError(event.data)\n        }\n    }\n}\n\nfunction reloadIfNeeded() {\n    fetch(window.location + \"index.html.md5\")\n        .then((response) => {\n            if (!response.ok) {\n                throw Error(response.statusText.toLowerCase())\n            }\n            return response.text()\n        })\n        .then((content) => {\n            if (!hash) {\n                hash = content;\n            } else if (hash !== content) {\n                console.log(\"Page content change detected, reloading ...\")\n                location.reload()\n            } else {\n                console.log(\"Page content has not been changed.\")\n            }\n        })\n        .catch((error) => {\n            if (error.message === \"not found\") {\n                location.href = location.origin\n            } else {\n                console.log(error)\n            }\n        });\n}\n\nfunction showUpdatingSite() {\n    document.getElementById(\"updating-site\").classList.remove(\"is-hidden\")\n    document.getElementById(\"updating-site\").attributes.removeNamedItem(\"value\")\n}\n\nfunction hideUpdatingSite() {\n    document.getElementById(\"updating-site\").classList.add(\"is-hidden\")\n    document.getElementById(\"updating-site\").value = \"0\"\n}\n\nfunction showUpdateSiteError(content) {\n    document.getElementById(\"update-site-error-message\").innerText = content\n    document.getElementById(\"update-site-error\").classList.remove(\"is-hidden\")\n}\n\nfunction hideUpdateSiteError() {\n    document.getElementById(\"update-site-error-message\").innerText = \"\"\n    document.getElementById(\"update-site-error\").classList.add(\"is-hidden\")\n}\n\nfunction showSite() {\n    document.getElementById(\"site\").classList.remove(\"is-hidden\");\n}\n\nfunction hideSite() {\n    document.getElementById(\"site\").classList.add(\"is-hidden\");\n}\n\nconnectToWs();\n"
  },
  {
    "path": "src/main/resources/assets/js/header.js",
    "content": "function redirect(event, value, href) {\n  if (event.key === 'Enter' && value.length >= 1) window.location.href = href + '?q=' + value;\n}\n"
  },
  {
    "path": "src/main/resources/assets/js/katex-render.js",
    "content": "// Script taken from flexmark wiki: https://github.com/vsch/flexmark-java/wiki/Extensions#gitlab-flavoured-markdown\n(function () {\n    document.addEventListener(\"DOMContentLoaded\", function () {\n        var mathElems = document.getElementsByClassName(\"katex\");\n        var elems = [];\n        for (const i in mathElems) {\n            if (mathElems.hasOwnProperty(i)) elems.push(mathElems[i]);\n        }\n\n        elems.forEach(elem => {\n            katex.render(elem.textContent, elem, { throwOnError: false, displayMode: elem.nodeName !== 'SPAN', });\n        });\n    });\n})();\n\n"
  },
  {
    "path": "src/main/resources/assets/js/modal.js",
    "content": "function openModal(id) {\n  document.getElementById(id).classList.add('is-active');\n}\n\nfunction closeModal(id) {\n  window.removeEventListener('resize', resetPz);\n  document.getElementById(id).classList.remove('is-active');\n}\n\n// Add a keyboard event to close all modals\ndocument.addEventListener('keydown', (event) => {\n  if (event.code === 'Escape') {\n    (document.querySelectorAll('.modal') || []).forEach((modal) => {\n      if (modal.classList.contains('is-active')) {\n        closeModal(modal.id);\n      }\n    });\n  }\n});\n"
  },
  {
    "path": "src/main/resources/assets/js/reformat-mermaid.js",
    "content": "// solution from https://css-tricks.com/making-mermaid-diagrams-in-markdown/\n\n// select <pre class=\"mermaid\"> _and_ <pre><code class=\"language-mermaid\">\ndocument.querySelectorAll(\"pre.mermaid, pre>code.language-mermaid, div.mermaid\").forEach($el => {\n    // if the second selector got a hit, reference the parent <pre>\n    if ($el.tagName === \"CODE\")\n      $el = $el.parentElement\n    // put the Mermaid contents in the expected <div class=\"mermaid\">\n    // plus keep the original contents in a nice <details>\n    $el.outerHTML = `\n      <div class=\"mermaid\">${$el.textContent}</div>\n      <details>\n        <summary>Diagram source</summary>\n        <pre>${$el.textContent}</pre>\n      </details>\n    `\n  })"
  },
  {
    "path": "src/main/resources/assets/js/search.js",
    "content": "window.onpageshow = function() {\n  const el = document.getElementById('search');\n  el.focus();\n\n  const params = new URLSearchParams(window.location.search);\n  const terms = params.get('q');\n\n  if (terms) {\n    el.value = terms;\n    search(terms);\n  }\n};\n\nfunction search(terms) {\n  const results = idx.search(terms).map(item => {\n    const document = documents.find(post => item.ref === post.href);\n    return { score: item.score, href: document.href, type: document.type, title: document.title };\n  });\n\n  const div = document.getElementById('search-results');\n  clearResultElements(div);\n\n  if (results.length === 0) {\n    div.appendChild(createNoResultsElement());\n  } else {\n    results.forEach(result => {\n      div.appendChild(createResultElement(result));\n    });\n  }\n}\n\nfunction clearResultElements(div) {\n  while (div.firstChild) {\n    div.removeChild(div.firstChild);\n  }\n}\n\nfunction createResultElement(result) {\n  const p = document.createElement('p');\n\n  const link = document.createElement('a');\n  link.href = result.href;\n  const title = document.createElement('p');\n  title.innerText = result.title;\n  title.className = 'mb-0';\n\n  const type = document.createElement('small');\n  type.innerText = result.type;\n\n  link.appendChild(title);\n  p.appendChild(link);\n  p.appendChild(type);\n  return p;\n}\n\nfunction createNoResultsElement() {\n  const p = document.createElement('p');\n  const small = document.createElement('small');\n  small.className = 'is-italic';\n  small.innerText = 'Your search yielded no results';\n\n  p.appendChild(small);\n  return p;\n}\n"
  },
  {
    "path": "src/main/resources/assets/js/svg-modal.js",
    "content": "let pz = undefined;\n\nfunction resetPz() {\n  if (pz) {\n    pz.resize();\n    pz.center();\n    pz.reset();\n  }\n}\n\nfunction openSvgModal(id, svgId) {\n  openModal(id);\n\n  const svgElement = document.getElementById(svgId).firstElementChild;\n  svgElement.classList.add('modal-svg');\n\n  pz = svgPanZoom(svgElement, {\n    zoomEnabled: true,\n    controlIconsEnabled: true,\n    fit: true,\n    center: true,\n    minZoom: 1,\n    maxZoom: 5\n  });\n  resetPz();\n\n  // Reset position on window resize\n  window.addEventListener('resize', resetPz);\n}\n\nfunction closeSvgModal(id) {\n  if (pz) {\n    pz.destroy();\n  }\n  closeModal(id);\n}\n"
  },
  {
    "path": "src/main/resources/assets/js/toggle-theme.js",
    "content": "if (!localStorage.getItem(\"data-theme\")) {\n  const prefersDarkMode = window.matchMedia &&\n    window.matchMedia('(prefers-color-scheme: dark)').matches;\n\n  localStorage.setItem(\"data-theme\", prefersDarkMode ? \"dark\" : \"light\");\n}\n\ndocument.documentElement.setAttribute(\"data-theme\", localStorage.getItem(\"data-theme\"));\n\nfunction toggleTheme() {\n  if (localStorage.getItem(\"data-theme\") === \"light\") {\n    document.documentElement.setAttribute(\"data-theme\", \"dark\");\n    localStorage.setItem(\"data-theme\", \"dark\");\n  } else {\n    document.documentElement.setAttribute(\"data-theme\", \"light\");\n    localStorage.setItem(\"data-theme\", \"light\");\n  }\n}\n"
  },
  {
    "path": "src/main/resources/assets/js/treeview.js",
    "content": "function listree() {\n\n  const subMenuHeadingClass = \"listree-submenu-heading\";\n  const expandedClass = \"expanded\";\n  const collapsedClass = \"collapsed\";\n  const activeClass = \"is-active\";\n  const subMenuHeadings = document.getElementsByClassName(subMenuHeadingClass);\n\n  Array\n    .from(subMenuHeadings)\n    .forEach(function(subMenuHeading) {\n      // Collapse all the subMenuHeadings while searching for is-active class\n      let foundActive = false;\n      subMenuHeading.classList.add(collapsedClass);\n      subMenuHeading.nextElementSibling.style.display = \"none\";\n\n      // Check if this sub-menu heading is active\n      const liElements = subMenuHeading.nextElementSibling.children;\n      for (let i = 0; i < liElements.length; i++) {\n        if (liElements[i].hasChildNodes()) {\n          if (liElements[i].children[0].tagName === \"A\") {\n            if (liElements[i].children[0].classList.contains(activeClass)) {\n              foundActive = true;\n              break;\n            }\n          }\n        }\n      }\n\n      // Expand all parent sub-menus until root menu is reached\n      if (foundActive) {\n        subMenuHeading.classList.remove(collapsedClass);\n        subMenuHeading.classList.add(expandedClass);\n        subMenuHeading.nextElementSibling.style.display = \"block\";\n\n        let currentSubMenu = subMenuHeading.parentElement;\n        while (currentSubMenu !== null && !currentSubMenu.classList.contains(\"listree\")) {\n          if(currentSubMenu.tagName === \"UL\"){\n            if(currentSubMenu.previousElementSibling != null) {\n              currentSubMenu.previousElementSibling.classList.remove(collapsedClass);\n              currentSubMenu.previousElementSibling.classList.add(expandedClass);\n              currentSubMenu.style.display = \"block\";\n            }\n          }\n          currentSubMenu = currentSubMenu.parentElement\n        }\n      }\n\n      // Add the eventlistener to all subMenuHeadings\n      subMenuHeading.addEventListener(\"click\", function(event) {\n        event.preventDefault();\n        const subMenuList = event.target.nextElementSibling;\n        if (subMenuList.style.display === \"none\") {\n          subMenuHeading.classList.remove(collapsedClass);\n          subMenuHeading.classList.add(expandedClass);\n          subMenuHeading.nextElementSibling.style.display = \"block\";\n        } else {\n          subMenuHeading.classList.remove(expandedClass);\n          subMenuHeading.classList.add(collapsedClass);\n          subMenuHeading.nextElementSibling.style.display = \"none\";\n        }\n        event.stopPropagation();\n      });\n    });\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/BranchComparatorTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site\n\nimport assertk.assertThat\nimport assertk.assertions.containsExactly\nimport nl.avisi.structurizr.site.generatr.branchComparator\nimport org.junit.jupiter.api.DynamicTest\nimport org.junit.jupiter.api.TestFactory\n\nclass BranchComparatorTest {\n    @TestFactory\n    fun `default branch first then by string`() = listOf(\n        listOf(\"a\", \"B\", \"main\"),\n        listOf(\"a\", \"main\", \"B\"),\n        listOf(\"B\", \"a\", \"main\"),\n        listOf(\"B\", \"main\", \"a\"),\n        listOf(\"main\", \"a\", \"B\"),\n        listOf(\"main\", \"B\", \"a\"),\n    ).map { branches ->\n        DynamicTest.dynamicTest(branches.toString()) {\n            assertThat(branches.sortedWith(branchComparator(\"main\")))\n                .containsExactly(\"main\", \"a\", \"B\")\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/PlantUmlExporterTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site\n\nimport assertk.assertThat\nimport assertk.assertions.contains\nimport assertk.assertions.isEqualTo\nimport com.structurizr.Workspace\nimport org.junit.jupiter.api.DynamicTest\nimport org.junit.jupiter.api.TestFactory\nimport kotlin.test.Test\n\nclass PlantUmlExporterTest {\n    @TestFactory\n    fun `adds skinparam to remove explicit size from generated svg`() = exporters().map { (type, exporterFn) ->\n        DynamicTest.dynamicTest(\"($type)\") {\n            val workspace = createWorkspaceWithOneSystem()\n\n            val diagram = exporterFn(workspace, \"/landscape/\").export(workspace.views.systemContextViews.first())\n\n            assertThat(diagram.definition)\n                .contains(\"skinparam svgDimensionStyle false\")\n        }\n    }\n\n    @TestFactory\n    fun `adds skinparam to preserve the aspect ratio of the generated svg`() = exporters().map { (type, exporterFn) ->\n        DynamicTest.dynamicTest(\"($type)\") {\n            val workspace = createWorkspaceWithOneSystem()\n\n            val diagram = exporterFn(workspace, \"/landscape/\")\n                .export(workspace.views.systemContextViews.first())\n\n            assertThat(diagram.definition)\n                .contains(\"skinparam preserveAspectRatio meet\")\n        }\n    }\n\n    private fun exporters() = listOf(\n        ExporterType.C4.name.lowercase() to\n            { workspace: Workspace, url: String -> C4PlantUmlExporterWithElementLinks(workspace, url) },\n        ExporterType.STRUCTURIZR.name.lowercase() to\n            { workspace: Workspace, url: String -> StructurizrPlantUmlExporterWithElementLinks(workspace, url) }\n    )\n\n    @Test\n    fun `renders diagram (c4)`() {\n        val workspace = createWorkspaceWithOneSystem()\n\n        val diagram = C4PlantUmlExporterWithElementLinks(workspace, \"/landscape/\")\n            .export(workspace.views.systemContextViews.first())\n\n        assertThat(diagram.definition.withoutC4HeaderAndFooter()).isEqualTo(\n            \"\"\"\n            System(System1, \"System 1\", ${'$'}descr=\"\", ${'$'}tags=\"\", ${'$'}link=\"\")\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `renders diagram (structurizr)`() {\n        val workspace = createWorkspaceWithOneSystem()\n\n        val diagram = StructurizrPlantUmlExporterWithElementLinks(workspace, \"/landscape/\")\n            .export(workspace.views.systemContextViews.first())\n\n        assertThat(diagram.definition.withoutStructurizrHeaderAndFooter()).isEqualTo(\n            \"\"\"\n            rectangle \"==System 1\\n<size:16>[Software System]</size>\" <<Element-RWxlbWVudA==>> as System1\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `renders System Diagram with link to container (c4)`() {\n        val workspace = createWorkspaceWithOneSystemWithContainers()\n\n        val diagram = C4PlantUmlExporterWithElementLinks(workspace, \"/container/\")\n            .export(workspace.views.systemContextViews.first())\n\n        assertThat(diagram.definition.withoutC4HeaderAndFooter()).isEqualTo(\n            \"\"\"\n            System(System1, \"System 1\", ${'$'}descr=\"\", ${'$'}tags=\"\", ${'$'}link=\"../system-1/container/\")\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `renders System Diagram with link to container (structurizr)`() {\n        val workspace = createWorkspaceWithOneSystemWithContainers()\n\n        val diagram = StructurizrPlantUmlExporterWithElementLinks(workspace, \"/container/\")\n            .export(workspace.views.systemContextViews.first())\n\n        assertThat(diagram.definition.withoutStructurizrHeaderAndFooter()).isEqualTo(\n            \"\"\"\n            rectangle \"==System 1\\n<size:16>[Software System]</size>\" <<Element-RWxlbWVudA==>> as System1 [[../system-1/container/]]\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `renders System Diagram with link to other system and link to container (c4)`() {\n        val workspace = createSystemContextViewForWorkspaceWithTwoSystemWithContainers()\n\n        val diagram = C4PlantUmlExporterWithElementLinks(workspace, \"/container/\")\n            .export(workspace.views.systemContextViews.first())\n\n        assertThat(diagram.definition.withoutC4HeaderAndFooter()).isEqualTo(\n            \"\"\"\n            System(System1, \"System 1\", ${'$'}descr=\"\", ${'$'}tags=\"\", ${'$'}link=\"../system-1/container/\")\n            System(System2, \"System 2\", ${'$'}descr=\"\", ${'$'}tags=\"\", ${'$'}link=\"../system-2/context/\")\n\n            Rel(System2, System1, \"uses\", ${'$'}techn=\"\", ${'$'}tags=\"\", ${'$'}link=\"\")\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `renders System Diagram with link to other system and link to container (structurizr)`() {\n        val workspace = createSystemContextViewForWorkspaceWithTwoSystemWithContainers()\n\n        val diagram = StructurizrPlantUmlExporterWithElementLinks(workspace, \"/container/\")\n            .export(workspace.views.systemContextViews.first())\n\n        assertThat(diagram.definition.withoutStructurizrHeaderAndFooter()).isEqualTo(\n            \"\"\"\n            rectangle \"==System 1\\n<size:16>[Software System]</size>\" <<Element-RWxlbWVudA==>> as System1 [[../system-1/container/]]\n            rectangle \"==System 2\\n<size:16>[Software System]</size>\" <<Element-RWxlbWVudA==>> as System2 [[../system-2/context/]]\n\n            System2 --> System1 <<Relationship-UmVsYXRpb25zaGlw>> : \"uses\"\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `renders Container Diagram with link to component diagram (c4)`() {\n        val workspace = createWorkspaceWithOneSystemWithContainersAndComponents()\n\n        val diagram = C4PlantUmlExporterWithElementLinks(workspace, \"/container/\")\n            .export(workspace.views.containerViews.first())\n\n        assertThat(diagram.definition.withoutC4HeaderAndFooter()).isEqualTo(\n            \"\"\"\n            System_Boundary(\"System1_boundary\", \"System 1\", ${'$'}tags=\"\") {\n              Container(System1.Container1, \"Container 1\", ${'$'}techn=\"\", ${'$'}descr=\"\", ${'$'}tags=\"\", ${'$'}link=\"../system-1/component/container-1/\")\n            }\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `renders Container Diagram with link to component diagram (structurizr)`() {\n        val workspace = createWorkspaceWithOneSystemWithContainersAndComponents()\n\n        val diagram = StructurizrPlantUmlExporterWithElementLinks(workspace, \"/container/\")\n            .export(workspace.views.containerViews.first())\n\n        assertThat(diagram.definition.withoutStructurizrHeaderAndFooter()).isEqualTo(\n            \"\"\"\n            rectangle \"System 1\\n<size:16>[Software System]</size>\" <<Boundary-U3lzdGVtIDE=>> {\n              rectangle \"==Container 1\\n<size:16>[Container]</size>\" <<Element-RWxlbWVudA==>> as System1.Container1 [[../system-1/component/container-1/]]\n            }\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `renders Container Diagram with link to code view (c4)`() {\n        val workspace = createWorkspaceWithOneSystemWithContainersComponentAndImage()\n\n        val diagram = C4PlantUmlExporterWithElementLinks(workspace, \"/container/\")\n            .export(workspace.views.componentViews.first())\n\n        assertThat(diagram.definition.withoutC4HeaderAndFooter()).isEqualTo(\n            \"\"\"\n            System_Boundary(\"System1_boundary\", \"System 1\", ${'$'}tags=\"\") {\n              Container_Boundary(\"System1.Container1_boundary\", \"Container 1\", ${'$'}tags=\"\") {\n                Component(System1.Container1.Component1, \"Component 1\", ${'$'}techn=\"\", ${'$'}descr=\"\", ${'$'}tags=\"\", ${'$'}link=\"../system-1/code/container-1/component-1/\")\n                Component(System1.Container1.Component2, \"Component 2\", ${'$'}techn=\"\", ${'$'}descr=\"\", ${'$'}tags=\"\", ${'$'}link=\"\")\n                Component(System1.Container1.Component3, \"Component 3\", ${'$'}techn=\"\", ${'$'}descr=\"\", ${'$'}tags=\"\", ${'$'}link=\"\")\n              }\n\n            }\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `renders Container Diagram with link to code view (structurizr)`() {\n        val workspace = createWorkspaceWithOneSystemWithContainersComponentAndImage()\n\n        val diagram = StructurizrPlantUmlExporterWithElementLinks(workspace, \"/container/\")\n            .export(workspace.views.componentViews.first())\n\n        assertThat(diagram.definition.withoutStructurizrHeaderAndFooter()).isEqualTo(\n            \"\"\"\n            rectangle \"System 1\\n<size:16>[Software System]</size>\" <<Boundary-U3lzdGVtIDE=>> {\n              rectangle \"Container 1\\n<size:16>[Container]</size>\" <<Boundary-Q29udGFpbmVyIDE=>> {\n                rectangle \"==Component 1\\n<size:16>[Component]</size>\" <<Element-RWxlbWVudA==>> as System1.Container1.Component1 [[../system-1/code/container-1/component-1/]]\n                rectangle \"==Component 2\\n<size:16>[Component]</size>\" <<Element-RWxlbWVudA==>> as System1.Container1.Component2\n                rectangle \"==Component 3\\n<size:16>[Component]</size>\" <<Element-RWxlbWVudA==>> as System1.Container1.Component3\n              }\n\n            }\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `renders Components Diagram without link to other Component Diagram (c4)`() {\n        val workspace = Workspace(\"workspace name\", \"\")\n        val system = workspace.model.addSoftwareSystem(\"System 1\")\n        val container1 = system.addContainer(\"Container 1\")\n        val container2 = system.addContainer(\"Container 2\")\n        container1.addComponent(\"Component 1\").apply { uses(container2, \"uses\") }\n        container2.addComponent(\"Component 2\")\n\n        val view = workspace.views.createComponentView(container1, \"Component2\", \"\")\n            .apply { addAllElements() }\n\n        val diagram = C4PlantUmlExporterWithElementLinks(workspace, \"/system-1/component/\")\n            .export(view)\n\n        assertThat(diagram.definition.withoutC4HeaderAndFooter()).isEqualTo(\n            \"\"\"\n            System_Boundary(\"System1_boundary\", \"System 1\", ${'$'}tags=\"\") {\n              Container_Boundary(\"System1.Container1_boundary\", \"Container 1\", ${'$'}tags=\"\") {\n                Component(System1.Container1.Component1, \"Component 1\", ${'$'}techn=\"\", ${'$'}descr=\"\", ${'$'}tags=\"\", ${'$'}link=\"\")\n              }\n\n              Container(System1.Container2, \"Container 2\", ${'$'}techn=\"\", ${'$'}descr=\"\", ${'$'}tags=\"\", ${'$'}link=\"\")\n            }\n\n            Rel(System1.Container1.Component1, System1.Container2, \"uses\", ${'$'}techn=\"\", ${'$'}tags=\"\", ${'$'}link=\"\")\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `renders Components Diagram without link to other Component Diagram (structurizr)`() {\n        val workspace = Workspace(\"workspace name\", \"\")\n        val system = workspace.model.addSoftwareSystem(\"System 1\")\n        val container1 = system.addContainer(\"Container 1\")\n        val container2 = system.addContainer(\"Container 2\")\n        container1.addComponent(\"Component 1\").apply { uses(container2, \"uses\") }\n        container2.addComponent(\"Component 2\")\n\n        val view = workspace.views.createComponentView(container1, \"Component2\", \"\")\n            .apply { addAllElements() }\n\n        val diagram = StructurizrPlantUmlExporterWithElementLinks(workspace, \"/system-1/component/\")\n            .export(view)\n\n        assertThat(diagram.definition.withoutStructurizrHeaderAndFooter()).isEqualTo(\n            \"\"\"\n            rectangle \"System 1\\n<size:16>[Software System]</size>\" <<Boundary-U3lzdGVtIDE=>> {\n              rectangle \"Container 1\\n<size:16>[Container]</size>\" <<Boundary-Q29udGFpbmVyIDE=>> {\n                rectangle \"==Component 1\\n<size:16>[Component]</size>\" <<Element-RWxlbWVudA==>> as System1.Container1.Component1\n              }\n\n              rectangle \"==Container 2\\n<size:16>[Container]</size>\" <<Element-RWxlbWVudA==>> as System1.Container2\n            }\n\n            System1.Container1.Component1 --> System1.Container2 <<Relationship-UmVsYXRpb25zaGlw>> : \"uses\"\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `link to other software system (c4)`() {\n        val workspace = createWorkspaceWithTwoSystems()\n\n        val diagram = C4PlantUmlExporterWithElementLinks(workspace, \"/landscape/\")\n            .export(workspace.views.systemContextViews.first())\n\n        assertThat(diagram.definition.withoutC4HeaderAndFooter()).isEqualTo(\n            \"\"\"\n            System(System1, \"System 1\", ${'$'}descr=\"\", ${'$'}tags=\"\", ${'$'}link=\"\")\n            System(System2, \"System 2\", ${'$'}descr=\"\", ${'$'}tags=\"\", ${'$'}link=\"../system-2/context/\")\n\n            Rel(System2, System1, \"uses\", ${'$'}techn=\"\", ${'$'}tags=\"\", ${'$'}link=\"\")\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `link to other software system (structurizr)`() {\n        val workspace = createWorkspaceWithTwoSystems()\n\n        val diagram = StructurizrPlantUmlExporterWithElementLinks(workspace, \"/landscape/\")\n            .export(workspace.views.systemContextViews.first())\n\n        assertThat(diagram.definition.withoutStructurizrHeaderAndFooter()).isEqualTo(\n            \"\"\"\n            rectangle \"==System 1\\n<size:16>[Software System]</size>\" <<Element-RWxlbWVudA==>> as System1\n            rectangle \"==System 2\\n<size:16>[Software System]</size>\" <<Element-RWxlbWVudA==>> as System2 [[../system-2/context/]]\n\n            System2 --> System1 <<Relationship-UmVsYXRpb25zaGlw>> : \"uses\"\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `external software system (declared external by tag) (c4)`() {\n        val workspace = createWorkspaceWithTwoSystems()\n        workspace.views.configuration.addProperty(\"generatr.site.externalTag\", \"External System\")\n        workspace.model.softwareSystems.single { it.name == \"System 2\" }.addTags(\"External System\")\n        val view = workspace.views.systemContextViews.first()\n\n        val diagram = C4PlantUmlExporterWithElementLinks(workspace, \"/landscape/\")\n            .export(view)\n\n        assertThat(diagram.definition.withoutC4HeaderAndFooter()).isEqualTo(\n            \"\"\"\n            System(System1, \"System 1\", ${'$'}descr=\"\", ${'$'}tags=\"\", ${'$'}link=\"\")\n            System(System2, \"System 2\", ${'$'}descr=\"\", ${'$'}tags=\"\", ${'$'}link=\"\")\n\n            Rel(System2, System1, \"uses\", ${'$'}techn=\"\", ${'$'}tags=\"\", ${'$'}link=\"\")\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `external software system (declared external by tag) (structurizr)`() {\n        val workspace = createWorkspaceWithTwoSystems()\n        workspace.views.configuration.addProperty(\"generatr.site.externalTag\", \"External System\")\n        workspace.model.softwareSystems.single { it.name == \"System 2\" }.addTags(\"External System\")\n        val view = workspace.views.systemContextViews.first()\n\n        val diagram = StructurizrPlantUmlExporterWithElementLinks(workspace, \"/landscape/\")\n            .export(view)\n\n        assertThat(diagram.definition.withoutStructurizrHeaderAndFooter()).isEqualTo(\n            \"\"\"\n            rectangle \"==System 1\\n<size:16>[Software System]</size>\" <<Element-RWxlbWVudA==>> as System1\n            rectangle \"==System 2\\n<size:16>[Software System]</size>\" <<Element-RWxlbWVudA==>> as System2\n\n            System2 --> System1 <<Relationship-UmVsYXRpb25zaGlw>> : \"uses\"\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `link to other software system from two path segments deep (c4)`() {\n        val workspace = createWorkspaceWithTwoSystems()\n\n        val diagram = C4PlantUmlExporterWithElementLinks(workspace, \"/system-1/context/\")\n            .export(workspace.views.systemContextViews.first())\n\n        assertThat(diagram.definition.withoutC4HeaderAndFooter()).isEqualTo(\n            \"\"\"\n            System(System1, \"System 1\", ${'$'}descr=\"\", ${'$'}tags=\"\", ${'$'}link=\"\")\n            System(System2, \"System 2\", ${'$'}descr=\"\", ${'$'}tags=\"\", ${'$'}link=\"../../system-2/context/\")\n\n            Rel(System2, System1, \"uses\", ${'$'}techn=\"\", ${'$'}tags=\"\", ${'$'}link=\"\")\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `link to other software system from two path segments deep (structurizr)`() {\n        val workspace = createWorkspaceWithTwoSystems()\n\n        val diagram = StructurizrPlantUmlExporterWithElementLinks(workspace, \"/system-1/context/\")\n            .export(workspace.views.systemContextViews.first())\n\n        assertThat(diagram.definition.withoutStructurizrHeaderAndFooter()).isEqualTo(\n            \"\"\"\n            rectangle \"==System 1\\n<size:16>[Software System]</size>\" <<Element-RWxlbWVudA==>> as System1\n            rectangle \"==System 2\\n<size:16>[Software System]</size>\" <<Element-RWxlbWVudA==>> as System2 [[../../system-2/context/]]\n\n            System2 --> System1 <<Relationship-UmVsYXRpb25zaGlw>> : \"uses\"\n            \"\"\".trimIndent()\n        )\n    }\n\n    private fun createWorkspaceWithOneSystem(): Workspace {\n        val workspace = Workspace(\"workspace name\", \"\").apply {\n            views.configuration.addProperty(\"generatr.site.excludedTag\", \"External System\")\n        }\n        val system = workspace.model.addSoftwareSystem(\"System 1\")\n\n        workspace.views.createSystemContextView(system, \"Context1\", \"\")\n            .apply { addAllElements() }\n\n        return workspace\n    }\n\n    private fun createWorkspaceWithOneSystemWithContainers(): Workspace {\n        val workspace = Workspace(\"workspace name\", \"\")\n        val system = workspace.model.addSoftwareSystem(\"System 1\")\n        system.addContainer(\"Container 1\")\n        system.addContainer(\"Container 2\")\n\n        workspace.views.createSystemContextView(system, \"Context 1\", \"\")\n            .apply { addAllElements() }\n\n        workspace.views.createContainerView(system, \"Container1\", \"\")\n            .apply { addAllElements() }\n\n        return workspace\n    }\n\n    private fun createSystemContextViewForWorkspaceWithTwoSystemWithContainers(): Workspace {\n        val workspace = Workspace(\"workspace name\", \"\")\n        val system = workspace.model.addSoftwareSystem(\"System 1\")\n        workspace.model.addSoftwareSystem(\"System 2\").apply { uses(system, \"uses\") }\n        system.addContainer(\"Container 1\")\n        system.addContainer(\"Container 2\")\n\n        workspace.views.createSystemContextView(system, \"Context 1\", \"\")\n            .apply { addAllElements() }\n\n        workspace.views.createContainerView(system, \"Container1\", \"\")\n            .apply { addAllElements() }\n\n        return workspace\n    }\n\n    private fun createWorkspaceWithOneSystemWithContainersAndComponents(): Workspace {\n        val workspace = Workspace(\"workspace name\", \"\")\n        val system = workspace.model.addSoftwareSystem(\"System 1\")\n        val container = system.addContainer(\"Container 1\")\n        container.addComponent(\"Component 1\")\n        container.addComponent(\"Component 2\")\n        container.addComponent(\"Component 3\")\n\n        workspace.views.createSystemContextView(system, \"Context 1\", \"\")\n            .apply { addAllElements() }\n\n        workspace.views.createContainerView(system, \"Container1\", \"\")\n            .apply { addAllElements() }\n\n        workspace.views.createComponentView(container, \"Component1\", \"\")\n            .apply { addAllElements() }\n\n        return workspace\n    }\n\n    private fun createWorkspaceWithOneSystemWithContainersComponentAndImage() = createWorkspaceWithOneSystemWithContainersAndComponents().apply {\n        views.createImageView(model.softwareSystems.single().containers.single().components.single { it.name == \"Component 1\" }, \"imageview-001\").also {\n            it.description = \"Image View Description\"\n            it.title = \"Image View Title\"\n            it.contentType = \"image/png\"\n            it.content = \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=\"\n        }\n    }\n\n    private fun createWorkspaceWithTwoSystems(): Workspace {\n        val workspace = Workspace(\"workspace name\", \"\")\n        val system = workspace.model.addSoftwareSystem(\"System 1\")\n        workspace.model.addSoftwareSystem(\"System 2\").apply { uses(system, \"uses\") }\n\n        workspace.views.createSystemContextView(system, \"Context 1\", \"\")\n            .apply { addAllElements() }\n\n        return workspace\n    }\n\n    private fun String.withoutC4HeaderAndFooter() = this\n        .split(System.lineSeparator())\n        .dropWhile { !it.startsWith(\"System\") && !it.startsWith(\"Container\") }\n        .dropLast(3)\n        .joinToString(System.lineSeparator())\n        .trimEnd()\n\n    private fun String.withoutStructurizrHeaderAndFooter() = this\n        .split(System.lineSeparator())\n        .dropWhile { !it.startsWith(\"rectangle\") }\n        .dropLast(1)\n        .joinToString(System.lineSeparator())\n        .trimEnd()\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/StringUtilitiesTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport nl.avisi.structurizr.site.generatr.normalize\nimport org.junit.jupiter.api.DynamicTest\nimport org.junit.jupiter.api.TestFactory\n\nclass StringUtilitiesTest {\n\n    @TestFactory\n    fun `normalize strips invalid chars`() = listOf(\n        listOf(\"doc\", \"doc\"),\n        listOf(\"d c\", \"d-c\"),\n        listOf(\"doc:\", \"doc\"),\n        listOf(\" doc \", \"-doc-\"),\n        listOf(\"aux\", \"aux-\"),\n    ).map { (actual, expected) ->\n        DynamicTest.dynamicTest(\"normalize replaces $actual with $expected\") {\n            assertThat(actual.normalize()).isEqualTo(expected)\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/StructurizrUtilitiesTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport com.structurizr.Workspace\nimport nl.avisi.structurizr.site.generatr.includedProperties\nimport kotlin.test.Test\n\nclass StructurizrUtilitiesTest {\n\n    private val svgFactory = { _: String, _: String -> \"\" }\n\n    private fun generatorContext(\n        workspaceName: String = \"Workspace name\",\n        branches: List<String> = listOf(\"main\"),\n        currentBranch: String = \"main\",\n        version: String = \"1.0.0\"\n    ) = GeneratorContext(version, Workspace(workspaceName, \"\"), branches, currentBranch, false, svgFactory)\n\n    @Test\n    fun `includedProperties filters out structurizr and generatr properties`() {\n        val element = generatorContext().workspace.model.addSoftwareSystem(\"System 1\")\n        element.addProperty(\"structurizr.key\", \"value\")\n        element.addProperty(\"generatr.key\", \"value\")\n        element.addProperty(\"structurizrnotquite.key\", \"value\")\n        element.addProperty(\"generatrbut.not.key\", \"value\")\n        element.addProperty(\"other.key\", \"value\")\n\n        val includedProperties = element.includedProperties\n\n        assertThat(includedProperties.size).isEqualTo(3)\n        assertThat(includedProperties.keys).isEqualTo(setOf(\"structurizrnotquite.key\", \"generatrbut.not.key\", \"other.key\"))\n    }\n\n    @Test\n    fun `includedProperties returns all properties when none are filtered out`() {\n        val element = generatorContext().workspace.model.addSoftwareSystem(\"System 1\")\n        element.addProperty(\"key1\", \"value1\")\n        element.addProperty(\"key2\", \"value2\")\n\n        val includedProperties = element.includedProperties\n\n        assertThat(includedProperties.size).isEqualTo(2)\n        assertThat(includedProperties.keys.first()).isEqualTo(\"key1\")\n        assertThat(includedProperties.keys.last()).isEqualTo(\"key2\")\n    }\n\n    @Test\n    fun `includedProperties returns empty map when no properties are set`() {\n        val element = generatorContext().workspace.model.addSoftwareSystem(\"System 1\")\n        val includedProperties = element.includedProperties\n        assertThat(includedProperties.size).isEqualTo(0)\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/E2ETestFixture.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.e2e\n\nimport assertk.assertThat\nimport assertk.assertions.isTrue\nimport com.microsoft.playwright.Browser\nimport com.microsoft.playwright.BrowserContext\nimport com.microsoft.playwright.Page\nimport com.microsoft.playwright.Playwright\nimport nl.avisi.structurizr.site.generatr.main\nimport org.junit.jupiter.api.*\nimport org.junit.jupiter.api.extension.ExtendWith\nimport java.io.IOException\nimport java.net.BindException\nimport java.net.URI\nimport java.net.http.HttpClient\nimport java.net.http.HttpRequest\nimport java.net.http.HttpResponse\nimport java.time.Duration\n\n@ExtendWith(RetryExtension::class)\n@TestInstance(TestInstance.Lifecycle.PER_CLASS)\nabstract class E2ETestFixture {\n    companion object {\n        @JvmStatic\n        protected val PORT = 9999\n\n        @JvmStatic\n        protected val SITE_URL = \"http://127.0.0.1:$PORT\"\n    }\n\n    private lateinit var serverThread: Thread\n    private lateinit var playwright: Playwright\n    private lateinit var browser: Browser\n    private lateinit var context: BrowserContext\n    protected lateinit var page: Page\n\n    @BeforeAll\n    @Order(1)\n    fun serveExampleSite() {\n        serverThread = Thread(::serve).apply {\n            isDaemon = true\n            start()\n        }\n\n        var reachable = false\n        var count = 0\n        while (!reachable && count++ < 1000) {\n            reachable = siteIsReachable()\n\n            Thread.sleep(Duration.ofMillis(100))\n        }\n        assertThat(reachable, name = \"site reachable\").isTrue()\n    }\n\n    @BeforeAll\n    @Order(2)\n    fun launchBrowser() {\n        playwright = Playwright.create()\n        browser = playwright.chromium().launch()\n    }\n\n    @BeforeEach\n    @Order(1)\n    fun createContextAndPage() {\n        context = browser.newContext()\n        page = context.newPage()\n    }\n\n    @AfterEach\n    fun closeContext() {\n        context.close()\n    }\n\n    @AfterAll\n    fun closeBrowser() {\n        playwright.close()\n        serverThread.interrupt()\n    }\n\n    private fun serve() {\n        try {\n            main(arrayOf(\"serve\", \"-w\", \"docs/example/workspace.dsl\", \"-p\", PORT.toString()))\n        } catch (_: InterruptedException) {\n            // ignore, server shutdown\n        } catch (exception: IOException) {\n            if (exception.cause !is BindException) {\n                throw exception\n            }\n        }\n    }\n\n    private fun siteIsReachable(): Boolean {\n        try {\n            val response = HttpClient\n                .newBuilder()\n                .connectTimeout(Duration.ofMillis(250))\n                .build()\n                .send(HttpRequest.newBuilder(URI.create(\"http://127.0.0.1:$PORT\")).GET().build(), HttpResponse.BodyHandlers.ofString())\n            return response.statusCode() == 200 && response.body().contains(\"Structurizr site generatr\")\n        } catch (_: IOException) {\n            return false\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/PageTestHelper.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.e2e\n\nimport com.microsoft.playwright.Locator\nimport com.microsoft.playwright.Page\nimport com.microsoft.playwright.assertions.LocatorAssertions\nimport com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat\n\nclass PageTestHelper {\n    fun testMenu(page: Page, additionalActions: (Locator) -> Unit) {\n        val menu = page.locator(\".menu > *\")\n\n        val general = menu.nth(0)\n        val softwareSystems = menu.nth(2)\n\n        assertThat(general).containsText(\"GENERAL\", LocatorAssertions.ContainsTextOptions().setUseInnerText(true))\n        assertThat(softwareSystems).containsText(\"SOFTWARE SYSTEMS\", LocatorAssertions.ContainsTextOptions().setUseInnerText(true))\n\n        val generalMenu = menu.nth(1).locator(\"a\")\n        val softwareSystemsMenu = menu.nth(3).locator(\"a\")\n\n        val generalMenuTexts = listOf(\n            \"Home\",\n            \"Decisions\",\n            \"Software Systems\",\n            \"Embedding diagrams and images\",\n            \"Extended Markdown features\",\n            \"AsciiDoc features\"\n        )\n\n        generalMenuTexts.forEachIndexed { index, text ->\n            assertThat(generalMenu.nth(index)).containsText(text)\n        }\n\n        val softwareSystemsMenuTexts = listOf(\n            \"ATM\",\n            \"E-mail System\",\n            \"Internet Banking System\",\n            \"Mainframe Banking System\"\n        )\n\n        softwareSystemsMenuTexts.forEachIndexed { index, text ->\n            assertThat(softwareSystemsMenu.nth(index)).containsText(text)\n        }\n\n        additionalActions(menu)\n    }\n\n    fun testTitleAndSubtitle(page: Page, title: String, subtitle: String) {\n        val titleLocator = page.locator(\".container > .title\")\n        val subtitleLocator = page.locator(\".container > .subtitle\")\n\n        assertThat(titleLocator).containsText(title)\n        assertThat(subtitleLocator).containsText(subtitle)\n    }\n\n    fun testTabs(page: Page, additionalActions: (Locator) -> Unit) {\n        val tabs = page.locator(\".container > .tabs > ul > li\")\n\n        val tabTexts = listOf(\n            \"Info\",\n            \"Context views\",\n            \"Container views\",\n            \"Component views\",\n            \"Code views\",\n            \"Dynamic views\",\n            \"Deployment views\",\n            \"Dependencies\",\n            \"Decisions\",\n            \"Documentation\"\n        )\n\n        tabTexts.forEachIndexed { index, text ->\n            assertThat(tabs.nth(index)).containsText(text)\n        }\n\n        additionalActions(tabs)\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/RetryExtension.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.e2e\n\nimport org.junit.jupiter.api.extension.ExtensionContext\nimport org.junit.jupiter.api.extension.TestExecutionExceptionHandler\n\nclass RetryExtension : TestExecutionExceptionHandler {\n    companion object {\n        private const val MAX_RETRIES = 3\n\n        private val namespace: ExtensionContext.Namespace = ExtensionContext.Namespace.create(\"RetryExtension\")\n    }\n\n    override fun handleTestExecutionException(\n        context: ExtensionContext,\n        throwable: Throwable\n    ) {\n        val store = context.getStore(namespace)\n        val retries = store.getOrDefault(\"retries\", Int::class.java, 0)\n\n        if (retries < MAX_RETRIES) {\n            store.put(\"retries\", retries + 1)\n\n            println(\"Retrying test \" + context.displayName + \", attempt \" + store.get(\"retries\"))\n        } else {\n            throw throwable\n        }\n    }\n}"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/SearchPageTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.e2e\n\nimport com.microsoft.playwright.Locator\nimport com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Order\nimport kotlin.test.Test\n\nclass SearchPageTest : E2ETestFixture() {\n    private val pageTestHelper = PageTestHelper()\n\n    @BeforeEach\n    @Order(2)\n    fun `navigate to page`() {\n        page.navigate(SITE_URL)\n\n        val search = page.locator(\".navbar input\").and(page.getByPlaceholder(\"Search...\"))\n\n        search.fill(\"internet banking system\")\n        search.press(\"Enter\")\n    }\n\n    @Test\n    fun `title is correct`() {\n        assertThat(page).hasTitle(\"Search results | Big Bank plc\")\n    }\n\n    @Test\n    fun `menu renders correctly`() {\n        pageTestHelper.testMenu(page) { menu ->\n            assertThat(menu.and(page.locator(\".is-active\"))).hasCount(0)\n        }\n    }\n\n    @Test\n    fun `content renders correctly`() {\n        val content = page.locator(\".content > *\").filter(Locator.FilterOptions().setVisible(true))\n\n        assertThat(content.nth(0)).containsText(\"Search results\")\n\n        val expectedSearchResults = listOf(\n            listOf(\"Internet Banking System | Context views\", \"Context views\"),\n            listOf(\"E-mail System | Dependencies\", \"Dependencies\"),\n            listOf(\"Internet Banking System | Description\", \"Software System Info\"),\n            listOf(\"Mainframe Banking System | Dependencies\", \"Dependencies\"),\n            listOf(\"Internet Banking System | Component views\", \"Component views\"),\n            listOf(\"Internet Banking System | Container views\", \"Container views\"),\n            listOf(\"Internet Banking System | Record Internet Banking System architecture decisions\", \"Software System Decision\"),\n            listOf(\"ATM | Dependencies\", \"Dependencies\"),\n            listOf(\"Mainframe Banking System | Context views\", \"Context views\"),\n            listOf(\"Internet Banking System | Dependencies\", \"Dependencies\"),\n            listOf(\"E-mail System | Context views\", \"Context views\"),\n            listOf(\"Big Bank plc architecture\", \"Home\"),\n            listOf(\"Embedding diagrams and images\", \"Workspace Documentation\"),\n        )\n\n        expectedSearchResults.forEachIndexed { rowIndex, row ->\n            row.forEachIndexed { colIndex, text ->\n                assertThat(content.nth(1).locator(\"p:not(.mb-0)\").also {\n                    it.nth(0).waitFor()\n                }.nth(rowIndex).locator(\"> *\").nth(colIndex)).containsText(text)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/SoftwareSystemDependenciesPageTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.e2e\n\nimport com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Order\nimport kotlin.test.Test\n\nclass SoftwareSystemDependenciesPageTest : E2ETestFixture() {\n    private val pageTestHelper = PageTestHelper()\n\n    @BeforeEach\n    @Order(2)\n    fun `navigate to page`() {\n        page.navigate(SITE_URL)\n\n        page.locator(\".menu a\").and(page.getByText(\"Internet Banking System\")).click()\n        page.locator(\".container > .tabs a\").and(page.getByText(\"Dependencies\")).click()\n    }\n\n    @Test\n    fun `title is correct`() {\n        assertThat(page).hasTitle(\"Internet Banking System | Big Bank plc\")\n    }\n\n    @Test\n    fun `menu renders correctly`() {\n        pageTestHelper.testMenu(page) { menu ->\n            assertThat(menu.locator(\".is-active\")).containsText(\"Internet Banking System\")\n        }\n    }\n\n    @Test\n    fun `title and subtitle render correctly`() {\n        pageTestHelper.testTitleAndSubtitle(\n            page,\n            \"Internet Banking System\",\n            \"Allows customers to view information about their bank accounts, and make payments.\"\n        )\n    }\n\n    @Test\n    fun `tabs render correctly`() {\n        pageTestHelper.testTabs(page) { tabs ->\n            assertThat(tabs.and(page.locator(\".is-active\"))).containsText(\"Dependencies\")\n        }\n    }\n\n    @Test\n    fun `content renders correctly`() {\n        val content = page.locator(\".content > *\")\n\n        assertThat(content.nth(0)).containsText(\"Inbound\")\n        assertThat(content.nth(2)).containsText(\"Outbound\")\n\n        val expectedTableHeadings = listOf(\"System\", \"Description\", \"Technology\")\n\n        expectedTableHeadings.forEachIndexed { index, text ->\n            assertThat(content.nth(1).locator(\"thead th\").nth(index)).containsText(text)\n        }\n        expectedTableHeadings.forEachIndexed { index, text ->\n            assertThat(content.nth(3).locator(\"thead th\").nth(index)).containsText(text)\n        }\n\n        val outboundData = listOf(\n            listOf(\n                \"E-mail System\",\n                \"Sends e-mail using\",\n                \"\"\n            ),\n            listOf(\n                \"Mainframe Banking System\",\n                \"Gets account information from, and makes payments using\",\n                \"\"\n            )\n        )\n\n        outboundData.forEachIndexed { rowIndex, data ->\n            data.forEachIndexed { cellIndex, text ->\n                assertThat(content.nth(3).locator(\"tbody tr\").nth(rowIndex).locator(\"td\").nth(cellIndex)).containsText(text)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/SoftwareSystemHomePageTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.e2e\n\nimport com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Order\nimport kotlin.test.Test\n\nclass SoftwareSystemHomePageTest : E2ETestFixture() {\n    private val pageTestHelper = PageTestHelper()\n\n    @BeforeEach\n    @Order(2)\n    fun `navigate to page`() {\n        page.navigate(SITE_URL)\n\n        page.locator(\".menu a\").and(page.getByText(\"Internet Banking System\")).click()\n    }\n\n    @Test\n    fun `title is correct`() {\n        assertThat(page).hasTitle(\"Internet Banking System | Big Bank plc\")\n    }\n\n    @Test\n    fun `menu renders correctly`() {\n        pageTestHelper.testMenu(page) { menu ->\n            assertThat(menu.locator(\".is-active\")).containsText(\"Internet Banking System\")\n        }\n    }\n\n    @Test\n    fun `title and subtitle render correctly`() {\n        pageTestHelper.testTitleAndSubtitle(\n            page,\n            \"Internet Banking System\",\n            \"Allows customers to view information about their bank accounts, and make payments.\"\n        )\n    }\n\n    @Test\n    fun `tabs render correctly`() {\n        pageTestHelper.testTabs(page) { tabs ->\n            assertThat(tabs.and(page.locator(\".is-active\"))).containsText(\"Info\")\n        }\n    }\n\n    @Test\n    fun `content renders correctly`() {\n        val content = page.locator(\".content > div > *\")\n\n        assertThat(content.nth(0)).containsText(\"Description\")\n        assertThat(content.nth(2)).containsText(\"Vision\")\n        assertThat(content.nth(4)).containsText(\"References\")\n\n        assertThat(content.nth(1)).containsText(\"This is our fancy Internet Banking System.\")\n        assertThat(content.nth(3)).containsText(\"One system to rule them all!\")\n        assertThat(content.nth(5)).containsText(\"Source Code on GitHub: [https://github.com/avisi-cloud/structurizr-site-generatr]\")\n    }\n\n    @Test\n    fun `properties render correctly`() {\n        val properties = page.locator(\".content > :not(div)\")\n\n        assertThat(properties.nth(0)).containsText(\"Properties\")\n\n        val table = properties.and(page.locator(\".table\"))\n        val tableHeadings = table.locator(\"thead th\")\n\n        assertThat(tableHeadings.nth(0)).containsText(\"Name\")\n        assertThat(tableHeadings.nth(1)).containsText(\"Value\")\n\n        val expectedPropertyValues = listOf(\n            listOf(\n                \"Development Team\",\n                \"Dev/Internet Services\"\n            ),\n            listOf(\n                \"Owner\",\n                \"Customer Services\"\n            ),\n            listOf(\n                \"Url\",\n                \"https://en.wikipedia.org/wiki/Online_banking\"\n            ),\n        )\n\n        expectedPropertyValues.forEachIndexed { rowIndex, row ->\n            row.forEachIndexed { colIndex, text ->\n                assertThat(table.locator(\"tbody tr\").nth(rowIndex).locator(\"td\").nth(colIndex)).containsText(text)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/SoftwareSystemsPageTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.e2e\n\nimport com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Order\nimport kotlin.test.Test\n\nclass SoftwareSystemsPageTest : E2ETestFixture() {\n    private val pageTestHelper = PageTestHelper()\n\n    @BeforeEach\n    @Order(2)\n    fun `navigate to page`() {\n        page.navigate(SITE_URL)\n\n        page.locator(\".menu a\").and(page.getByText(\"Software Systems\")).click()\n    }\n\n    @Test\n    fun `title is correct`() {\n        assertThat(page).hasTitle(\"Software Systems | Big Bank plc\")\n    }\n\n    @Test\n    fun `menu renders correctly`() {\n        pageTestHelper.testMenu(page) { menu ->\n            assertThat(menu.locator(\".is-active\")).containsText(\"Software Systems\")\n        }\n    }\n\n    @Test\n    fun `content renders correctly`() {\n        val content = page.locator(\".content > *\")\n\n        val title = content.nth(0)\n\n        assertThat(title).containsText(\"Software Systems\")\n\n        val table = content.and(page.locator(\".table\"))\n\n        val tableHeadings = table.locator(\"thead th\")\n\n        assertThat(tableHeadings.nth(0)).containsText(\"Name\")\n        assertThat(tableHeadings.nth(1)).containsText(\"Description\")\n\n        val expectedData = listOf(\n            listOf(\n                \"Acquirer (External)\",\n                \"Facilitates PIN transactions for merchants.\"\n            ),\n            listOf(\n                \"ATM\",\n                \"Allows customers to withdraw cash.\"\n            ),\n            listOf(\n                \"E-mail System\",\n                \"The internal Microsoft Exchange e-mail system.\"\n            ),\n            listOf(\n                \"Internet Banking System\",\n                \"Allows customers to view information about their bank accounts, and make payments.\"\n            ),\n            listOf(\n                \"Mainframe Banking System\",\n                \"Stores all of the core banking information about customers, accounts, transactions, etc.\"\n            )\n        )\n\n        expectedData.forEachIndexed { rowIndex, row ->\n            row.forEachIndexed { colIndex, text ->\n                assertThat(table.locator(\"tbody tr\").nth(rowIndex).locator(\"td\").nth(colIndex)).containsText(text)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/WorkspaceHomePageTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.e2e\n\nimport com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Order\nimport kotlin.test.Test\n\nclass WorkspaceHomePageTest : E2ETestFixture() {\n    private val pageTestHelper = PageTestHelper()\n\n    @BeforeEach\n    @Order(2)\n    fun `navigate to page`() {\n        page.navigate(SITE_URL)\n    }\n\n    @Test\n    fun `title is correct`() {\n        assertThat(page).hasTitle(\"Big Bank plc\")\n    }\n\n    @Test\n    fun `menu renders correctly`() {\n        pageTestHelper.testMenu(page) { menu ->\n            assertThat(menu.locator(\".is-active\")).hasText(\"Home\")\n        }\n    }\n\n    @Test\n    fun `content renders correctly`() {\n        val content = page.locator(\".content > div > *\")\n\n        val title = content.nth(0)\n        val paragraph1 = content.nth(1)\n        val paragraph2 = content.nth(2)\n\n        assertThat(title).containsText(\"Big Bank plc architecture\")\n        assertThat(paragraph1).containsText(\"This site contains the C4 architecture model for Big Bank plc.\")\n        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.\")\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/decisions/DecisionPageTestHelper.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.e2e.decisions\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport com.microsoft.playwright.Locator\nimport com.microsoft.playwright.Page\n\nclass DecisionPageTestHelper {\n    fun testDecisionContent(page: Page, additionalActions: (List<Locator>) -> Unit) {\n        val content = page.locator(\".content > div > *\")\n\n        val title = content.nth(0)\n        val date = content.nth(1)\n        val status = content.nth(3)\n        val context = content.nth(5)\n        val decision = content.nth(7)\n        val consequences = content.nth(9)\n\n        assertThat(content.nth(2).innerText()).isEqualTo(\"Status\")\n        assertThat(content.nth(4).innerText()).isEqualTo(\"Context\")\n        assertThat(content.nth(6).innerText()).isEqualTo(\"Decision\")\n        assertThat(content.nth(8).innerText()).isEqualTo(\"Consequences\")\n\n        additionalActions(listOf(\n            title,\n            date,\n            status,\n            context,\n            decision,\n            consequences\n        ))\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/decisions/DecisionsPageTestHelper.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.e2e.decisions\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport com.microsoft.playwright.Locator\nimport com.microsoft.playwright.Page\n\nclass DecisionsPageTestHelper {\n    fun testDecisionTabs(page: Page, additionalActions: (Locator) -> Unit) {\n        val tabs = page.locator(\".content .tabs:not(.is-size-7) a\")\n\n        additionalActions(tabs)\n    }\n\n    fun testComponentDecisionTabs(page: Page, additionalActions: (Locator) -> Unit) {\n        val tabs = page.locator(\".content .tabs.is-size-7 a\")\n\n        additionalActions(tabs)\n    }\n\n    fun testDecisionsTable(page: Page, additionalActions: (List<Locator>) -> Unit) {\n        val tableHeadings = page.locator(\".table thead th\")\n        val tableRows = page.locator(\".table tbody tr\")\n\n        val locators = tableRows.all().map { row -> row.locator(\"td\") }\n\n        assertThat(tableHeadings.count()).isEqualTo(4)\n        assertThat(tableHeadings.nth(0).innerText()).isEqualTo(\"ID\")\n        assertThat(tableHeadings.nth(1).innerText()).isEqualTo(\"Date\")\n        assertThat(tableHeadings.nth(2).innerText()).isEqualTo(\"Status\")\n        assertThat(tableHeadings.nth(3).innerText()).isEqualTo(\"Title\")\n\n        additionalActions(locators)\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/decisions/SoftwareSystemContainerComponentDecisionPageTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.e2e.decisions\n\nimport com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat\nimport nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture\nimport nl.avisi.structurizr.site.generatr.site.e2e.PageTestHelper\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Order\nimport kotlin.test.Test\n\nclass SoftwareSystemContainerComponentDecisionPageTest : E2ETestFixture() {\n    private val pageTestHelper = PageTestHelper()\n    private val decisionPageTestHelper = DecisionPageTestHelper()\n\n    @BeforeEach\n    @Order(2)\n    fun `navigate to page`() {\n        page.navigate(SITE_URL)\n\n        page.locator(\".menu a\").and(page.getByText(\"Internet Banking System\")).click()\n        page.locator(\".container > .tabs a\").and(page.getByText(\"Decisions\")).click()\n        page.locator(\".content > .tabs a\").and(page.getByText(\"API Application\")).click()\n        page.locator(\".content > .tabs a\").and(page.getByText(\"E-mail Component\")).click()\n        page.locator(\".table a\").and(page.getByText(\"Record Email Component architecture decision\")).click()\n    }\n\n    @Test\n    fun `title is correct`() {\n        assertThat(page).hasTitle(\"Internet Banking System | Big Bank plc\")\n    }\n\n    @Test\n    fun `menu renders correctly`() {\n        pageTestHelper.testMenu(page) { menu ->\n            assertThat(menu.locator(\".is-active\")).containsText(\"Internet Banking System\")\n        }\n    }\n\n    @Test\n    fun `title and subtitle render correctly`() {\n        pageTestHelper.testTitleAndSubtitle(\n            page,\n            \"Internet Banking System\",\n            \"Allows customers to view information about their bank accounts, and make payments.\"\n        )\n    }\n\n    @Test\n    fun `tabs render correctly`() {\n        pageTestHelper.testTabs(page) { tabs ->\n            assertThat(tabs.and(page.locator(\".is-active\"))).containsText(\"Decisions\")\n        }\n    }\n\n    @Test\n    fun `content renders correctly`() {\n        decisionPageTestHelper.testDecisionContent(page) { locators ->\n            val expectedData = listOf(\n                \"1. Record Email Component architecture decision\",\n                \"Date: 2022-06-21\",\n                \"Accepted\",\n                \"We need to record the architectural decisions made on this project.\",\n                \"We will use Architecture Decision Records, as described by Michael Nygard.\",\n                \"See Michael Nygard’s article, linked above. For a lightweight ADR toolset, see Nat Pryce’s adr-tools.\"\n            )\n\n            expectedData.forEachIndexed { index, text ->\n                assertThat(locators[index]).containsText(text)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/decisions/SoftwareSystemContainerComponentDecisionsPageTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.e2e.decisions\n\nimport com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat\nimport nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Order\nimport kotlin.test.Test\n\nclass SoftwareSystemContainerComponentDecisionsPageTest : E2ETestFixture() {\n    private val decisionPageTestHelper = DecisionsPageTestHelper()\n\n    @BeforeEach\n    @Order(2)\n    fun `navigate to page`() {\n        page.navigate(SITE_URL)\n\n        page.locator(\".menu a\").and(page.getByText(\"Internet Banking System\")).click()\n        page.locator(\".container > .tabs a\").and(page.getByText(\"Decisions\")).click()\n        page.locator(\".content > .tabs a\").and(page.getByText(\"API Application\")).click()\n        page.locator(\".content > .tabs a\").and(page.getByText(\"E-mail Component\")).click()\n    }\n\n    @Test\n    fun `title is correct`() {\n        assertThat(page).hasTitle(\"Internet Banking System | Big Bank plc\")\n    }\n\n    @Test\n    fun `decision tabs render correctly`() {\n        decisionPageTestHelper.testDecisionTabs(page) { tabs ->\n            assertThat(tabs).hasCount(3)\n\n            assertThat(tabs.nth(0)).containsText(\"System\")\n            assertThat(tabs.nth(1)).containsText(\"API Application\")\n            assertThat(tabs.nth(2)).containsText(\"Database\")\n\n            assertThat(tabs.nth(1)).hasClass(\"is-active\")\n        }\n    }\n\n    @Test\n    fun `component decision tabs render correctly`() {\n        decisionPageTestHelper.testComponentDecisionTabs(page) { tabs ->\n            assertThat(tabs).hasCount(2)\n\n            assertThat(tabs.nth(0)).containsText(\"Mainframe Banking System Facade\")\n            assertThat(tabs.nth(1)).containsText(\"E-mail Component\")\n\n            assertThat(tabs.nth(1)).hasClass(\"is-active\")\n        }\n    }\n\n    @Test\n    fun `decisions table renders correctly`() {\n        decisionPageTestHelper.testDecisionsTable(page) { locators ->\n            val expectedData = listOf(\n                listOf(\"1\", \"21-06-2022\", \"Accepted\", \"Record Email Component architecture decision\"),\n                listOf(\"2\", \"17-12-2024\", \"Superseded\", \"Implement Feature 1\"),\n                listOf(\"3\", \"17-12-2024\", \"Accepted\", \"Another Realisation of Feature 1\"),\n            )\n\n            expectedData.forEachIndexed { rowIndex, row ->\n                row.forEachIndexed { colIndex, text ->\n                    assertThat(locators[rowIndex].nth(colIndex)).containsText(text)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/decisions/SoftwareSystemContainerDecisionPage.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.e2e.decisions\n\nimport com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat\nimport nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture\nimport nl.avisi.structurizr.site.generatr.site.e2e.PageTestHelper\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Order\nimport kotlin.test.Test\n\nclass SoftwareSystemContainerDecisionPage : E2ETestFixture() {\n    private val pageTestHelper = PageTestHelper()\n    private val decisionPageTestHelper = DecisionPageTestHelper()\n\n    @BeforeEach\n    @Order(2)\n    fun `navigate to page`() {\n        page.navigate(SITE_URL)\n\n        page.locator(\".menu a\").and(page.getByText(\"Internet Banking System\")).click()\n        page.locator(\".container > .tabs a\").and(page.getByText(\"Decisions\")).click()\n        page.locator(\".content > .tabs a\").and(page.getByText(\"Database\")).click()\n        page.locator(\".table a\").and(page.getByText(\"Using Oracle database schema\")).click()\n    }\n\n    @Test\n    fun `title is correct`() {\n        assertThat(page).hasTitle(\"Internet Banking System | Big Bank plc\")\n    }\n\n    @Test\n    fun `menu renders correctly`() {\n        pageTestHelper.testMenu(page) { menu ->\n            assertThat(menu.locator(\".is-active\")).containsText(\"Internet Banking System\")\n        }\n    }\n\n    @Test\n    fun `title and subtitle render correctly`() {\n        pageTestHelper.testTitleAndSubtitle(\n            page,\n            \"Internet Banking System\",\n            \"Allows customers to view information about their bank accounts, and make payments.\"\n        )\n    }\n\n    @Test\n    fun `tabs render correctly`() {\n        pageTestHelper.testTabs(page) { tabs ->\n            assertThat(tabs.and(page.locator(\".is-active\"))).containsText(\"Decisions\")\n        }\n    }\n\n    @Test\n    fun `content renders correctly`() {\n        decisionPageTestHelper.testDecisionContent(page) { locators ->\n            val expected = listOf(\n                \"4. Using Oracle database schema\",\n                \"Date: 2025-11-13\",\n                \"Accepted\",\n                \"The issue motivating this decision, and any context that influences or constrains the decision.\",\n                \"The change that we’re proposing or have agreed to implement.\",\n                \"What becomes easier or more difficult to do and any risks introduced by the change that will need to be mitigated.\"\n            )\n\n            expected.forEachIndexed { index, text ->\n                assertThat(locators[index]).containsText(text)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/decisions/SoftwareSystemContainerDecisionsPageTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.e2e.decisions\n\nimport com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat\nimport nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture\nimport nl.avisi.structurizr.site.generatr.site.e2e.PageTestHelper\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Order\nimport kotlin.test.Test\n\nclass SoftwareSystemContainerDecisionsPageTest : E2ETestFixture() {\n    private val pageTestHelper = PageTestHelper()\n    private val decisionPageTestHelper = DecisionsPageTestHelper()\n\n    @BeforeEach\n    @Order(2)\n    fun `navigate to page`() {\n        page.navigate(SITE_URL)\n\n        page.locator(\".menu a\").and(page.getByText(\"Internet Banking System\")).click()\n        page.locator(\".container > .tabs a\").and(page.getByText(\"Decisions\")).click()\n        page.locator(\".content > .tabs a\").and(page.getByText(\"Database\")).click()\n    }\n\n    @Test\n    fun `title is correct`() {\n        assertThat(page).hasTitle(\"Internet Banking System | Big Bank plc\")\n    }\n\n    @Test\n    fun `menu renders correctly`() {\n        pageTestHelper.testMenu(page) { menu ->\n            assertThat(menu.locator(\".is-active\")).containsText(\"Internet Banking System\")\n        }\n    }\n\n    @Test\n    fun `title and subtitle render correctly`() {\n        pageTestHelper.testTitleAndSubtitle(\n            page,\n            \"Internet Banking System\",\n            \"Allows customers to view information about their bank accounts, and make payments.\"\n        )\n    }\n\n    @Test\n    fun `tabs render correctly`() {\n        pageTestHelper.testTabs(page) { tabs ->\n            assertThat(tabs.and(page.locator(\".is-active\"))).containsText(\"Decisions\")\n        }\n    }\n\n    @Test\n    fun `decision tabs render correctly`() {\n        decisionPageTestHelper.testDecisionTabs(page) { tabs ->\n            assertThat(tabs).hasCount(3)\n\n            assertThat(tabs.nth(0)).containsText(\"System\")\n            assertThat(tabs.nth(1)).containsText(\"API Application\")\n            assertThat(tabs.nth(2)).containsText(\"Database\")\n\n            assertThat(tabs.nth(2)).hasClass(\"is-active\")\n        }\n    }\n\n    @Test\n    fun `component decision tabs render correctly`() {\n        decisionPageTestHelper.testComponentDecisionTabs(page) { tabs ->\n            assertThat(tabs).hasCount(1)\n\n            assertThat(tabs.nth(0)).containsText(\"Container\")\n\n            assertThat(tabs.nth(0)).hasClass(\"is-active\")\n        }\n    }\n\n    @Test\n    fun `decisions table renders correctly`() {\n        decisionPageTestHelper.testDecisionsTable(page) { locators ->\n            val expectedData = listOf(\n                listOf(\"4\", \"13-11-2025\", \"Accepted\", \"Using Oracle database schema\")\n            )\n\n            expectedData.forEachIndexed { rowIndex, row ->\n                row.forEachIndexed { colIndex, text ->\n                    assertThat(locators[rowIndex].nth(colIndex)).containsText(text)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/decisions/SoftwareSystemDecisionPageTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.e2e.decisions\n\nimport com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat\nimport nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture\nimport nl.avisi.structurizr.site.generatr.site.e2e.PageTestHelper\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Order\nimport kotlin.test.Test\n\nclass SoftwareSystemDecisionPageTest : E2ETestFixture() {\n    private val pageTestHelper = PageTestHelper()\n    private val decisionPageTestHelper = DecisionPageTestHelper()\n\n    @BeforeEach\n    @Order(2)\n    fun `navigate to page`() {\n        page.navigate(SITE_URL)\n\n        page.locator(\".menu a\").and(page.getByText(\"Internet Banking System\")).click()\n        page.locator(\".container > .tabs a\").and(page.getByText(\"Decisions\")).click()\n        page.locator(\".table a\").and(page.getByText(\"Record Internet Banking System architecture decisions\")).click()\n    }\n\n    @Test\n    fun `title is correct`() {\n        assertThat(page).hasTitle(\"Internet Banking System | Big Bank plc\")\n    }\n\n    @Test\n    fun `menu renders correctly`() {\n        pageTestHelper.testMenu(page) { menu ->\n            assertThat(menu.locator(\".is-active\")).containsText(\"Internet Banking System\")\n        }\n    }\n\n    @Test\n    fun `title and subtitle render correctly`() {\n        pageTestHelper.testTitleAndSubtitle(\n            page,\n            \"Internet Banking System\",\n            \"Allows customers to view information about their bank accounts, and make payments.\"\n        )\n    }\n\n    @Test\n    fun `tabs render correctly`() {\n        pageTestHelper.testTabs(page) { tabs ->\n            assertThat(tabs.and(page.locator(\".is-active\"))).containsText(\"Decisions\")\n        }\n    }\n\n    @Test\n    fun `content renders correctly`() {\n        decisionPageTestHelper.testDecisionContent(page) { locators ->\n            val expectedData = listOf(\n                \"1. Record Internet Banking System architecture decisions\",\n                \"Date: 2022-06-21\",\n                \"Accepted\",\n                \"We need to record the architectural decisions made on this project.\",\n                \"We will use Architecture Decision Records, as described by Michael Nygard.\",\n                \"See Michael Nygard’s article, linked above. For a lightweight ADR toolset, see Nat Pryce’s adr-tools.\"\n            )\n\n            expectedData.forEachIndexed { index, text ->\n                assertThat(locators[index]).containsText(text)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/decisions/SoftwareSystemDecisionsPageTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.e2e.decisions\n\nimport com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat\nimport nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture\nimport nl.avisi.structurizr.site.generatr.site.e2e.PageTestHelper\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Order\nimport kotlin.test.Test\n\nclass SoftwareSystemDecisionsPageTest : E2ETestFixture() {\n    private val pageTestHelper = PageTestHelper()\n    private val decisionPageTestHelper = DecisionsPageTestHelper()\n\n    @BeforeEach\n    @Order(2)\n    fun `navigate to page`() {\n        page.navigate(SITE_URL)\n\n        page.locator(\".menu a\").and(page.getByText(\"Internet Banking System\")).click()\n        page.locator(\".container > .tabs a\").and(page.getByText(\"Decisions\")).click()\n    }\n\n    @Test\n    fun `title is correct`() {\n        assertThat(page).hasTitle(\"Internet Banking System | Big Bank plc\")\n    }\n\n    @Test\n    fun `menu renders correctly`() {\n        pageTestHelper.testMenu(page) { menu ->\n            assertThat(menu.locator(\".is-active\")).containsText(\"Internet Banking System\")\n        }\n    }\n\n    @Test\n    fun `title and subtitle render correctly`() {\n        pageTestHelper.testTitleAndSubtitle(\n            page,\n            \"Internet Banking System\",\n            \"Allows customers to view information about their bank accounts, and make payments.\"\n        )\n    }\n\n    @Test\n    fun `tabs render correctly`() {\n        pageTestHelper.testTabs(page) { tabs ->\n            assertThat(tabs.and(page.locator(\".is-active\"))).containsText(\"Decisions\")\n        }\n    }\n\n    @Test\n    fun `decision tabs render correctly`() {\n        decisionPageTestHelper.testDecisionTabs(page) { tabs ->\n            assertThat(tabs).hasCount(3)\n\n            assertThat(tabs.nth(0)).containsText(\"System\")\n            assertThat(tabs.nth(1)).containsText(\"API Application\")\n            assertThat(tabs.nth(2)).containsText(\"Database\")\n\n            assertThat(tabs.nth(0)).containsClass(\"is-active\")\n        }\n    }\n\n    @Test\n    fun `component decision tabs render correctly`() {\n        decisionPageTestHelper.testComponentDecisionTabs(page) { locators ->\n            assertThat(locators).hasCount(0)\n        }\n    }\n\n    @Test\n    fun `decisions table renders correctly`() {\n        decisionPageTestHelper.testDecisionsTable(page) { locators ->\n            val expectedData = listOf(\n                listOf(\"1\", \"21-06-2022\", \"Accepted\", \"Record Internet Banking System architecture decisions\")\n            )\n\n            expectedData.forEachIndexed { rowIndex, row ->\n                row.forEachIndexed { colIndex, text ->\n                    assertThat(locators[rowIndex].nth(colIndex)).containsText(text)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/decisions/WorkspaceDecisionPageTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.e2e.decisions\n\nimport com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat\nimport nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture\nimport nl.avisi.structurizr.site.generatr.site.e2e.PageTestHelper\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Order\nimport kotlin.test.Test\n\nclass WorkspaceDecisionPageTest : E2ETestFixture() {\n    private val pageTestHelper = PageTestHelper()\n    private val decisionPageTestHelper = DecisionPageTestHelper()\n\n    @BeforeEach\n    @Order(2)\n    fun `navigate to page`() {\n        page.navigate(SITE_URL)\n\n        page.locator(\".menu a\").and(page.getByText(\"Decisions\")).click()\n        page.locator(\".content a\").and(page.getByText(\"Record architecture decisions\")).click()\n    }\n\n    @Test\n    fun `title is correct`() {\n        page.onDOMContentLoaded {\n            assertThat(page).hasTitle(\"Record architecture decisions | Big Bank plc\")\n        }\n    }\n\n    @Test\n    fun `menu renders correctly`() {\n        pageTestHelper.testMenu(page) { menu ->\n            assertThat(menu.locator(\".is-active\")).containsText(\"Decisions\")\n        }\n    }\n\n    @Test\n    fun `content renders correctly`() {\n        decisionPageTestHelper.testDecisionContent(page) { locators ->\n            val expectedData = listOf(\n                \"1. Record architecture decisions\",\n                \"Date: 2022-06-21\",\n                \"Accepted\",\n                \"We need to record the architectural decisions made on this project.\",\n                \"We will use Architecture Decision Records, as described by Michael Nygard.\",\n                \"See Michael Nygard’s article, linked above. For a lightweight ADR toolset, see Nat Pryce’s adr-tools.\"\n            )\n\n            expectedData.forEachIndexed { index, text ->\n                assertThat(locators[index]).containsText(text)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/decisions/WorkspaceDecisionsPageTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.e2e.decisions\n\nimport com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat\nimport nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture\nimport nl.avisi.structurizr.site.generatr.site.e2e.PageTestHelper\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Order\nimport kotlin.test.Test\n\nclass WorkspaceDecisionsPageTest : E2ETestFixture() {\n    private val pageTestHelper = PageTestHelper()\n    private val decisionsPageTestHelper = DecisionsPageTestHelper()\n\n    @BeforeEach\n    @Order(2)\n    fun `navigate to page`() {\n        page.navigate(SITE_URL)\n\n        page.locator(\".menu a\").and(page.getByText(\"Decisions\")).click()\n    }\n\n    @Test\n    fun `title is correct`() {\n        assertThat(page).hasTitle(\"Decisions | Big Bank plc\")\n    }\n\n    @Test\n    fun `menu renders correctly`() {\n        pageTestHelper.testMenu(page) { menu ->\n            assertThat(menu.locator(\".is-active\")).containsText(\"Decisions\")\n        }\n    }\n\n    @Test\n    fun `decisions table renders correctly`() {\n        decisionsPageTestHelper.testDecisionsTable(page) { locators ->\n            val expectedData = listOf(\n                listOf(\"1\", \"21-06-2022\", \"Accepted\", \"Record architecture decisions\"),\n                listOf(\"2\", \"17-12-2024\", \"Superseded\", \"Implement Feature 1\"),\n                listOf(\"3\", \"17-12-2024\", \"Accepted\", \"Another Realisation of Feature 1\"),\n                listOf(\"4\", \"13-11-2025\", \"Accepted\", \"Using Oracle database schema\"),\n            )\n\n            expectedData.forEachIndexed { rowIndex, row ->\n                row.forEachIndexed { colIndex, text ->\n                    assertThat(locators[rowIndex].nth(colIndex)).containsText(text)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/sections/SectionPageTestHelper.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.e2e.sections\n\nimport com.microsoft.playwright.Locator\nimport com.microsoft.playwright.Page\n\nclass SectionPageTestHelper {\n    fun testSectionContent(page: Page, additionalActions: (List<Locator>) -> Unit) {\n        val content = page.locator(\".content > div > *\")\n\n        val title = content.nth(0)\n        val notes = content.nth(1)\n\n        additionalActions(listOf(\n            title,\n            notes\n        ))\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/sections/SectionsPageTestHelper.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.e2e.sections\n\nimport com.microsoft.playwright.Locator\nimport com.microsoft.playwright.Page\nimport com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat\n\nclass SectionsPageTestHelper {\n    fun testSectionTabs(page: Page, additionalActions: (Locator) -> Unit) {\n        val tabs = page.locator(\".content .tabs:not(.is-size-7) a\")\n\n        additionalActions(tabs)\n    }\n\n    fun testComponentSectionTabs(page: Page, additionalActions: (Locator) -> Unit) {\n        val tabs = page.locator(\".content .tabs.is-size-7 a\")\n\n        additionalActions(tabs)\n    }\n\n    fun testSectionsTable(page: Page, additionalActions: (List<Locator>) -> Unit) {\n        val tableHeadings = page.locator(\".table thead th\")\n        val tableRows = page.locator(\".table tbody tr\")\n\n        val locators = tableRows.all().map { row -> row.locator(\"td\") }\n\n        assertThat(tableHeadings).hasCount(2)\n\n        assertThat(tableHeadings.nth(0)).containsText(\"#\")\n        assertThat(tableHeadings.nth(1)).containsText(\"Title\")\n\n        additionalActions(locators)\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/sections/SoftwareSystemContainerComponentSectionPageTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.e2e.sections\n\nimport com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat\nimport nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture\nimport nl.avisi.structurizr.site.generatr.site.e2e.PageTestHelper\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Order\nimport kotlin.test.Test\n\nclass SoftwareSystemContainerComponentSectionPageTest : E2ETestFixture() {\n    private val pageTestHelper = PageTestHelper()\n    private val sectionPageTestHelper = SectionPageTestHelper()\n\n    @BeforeEach\n    @Order(2)\n    fun `navigate to page`() {\n        page.navigate(SITE_URL)\n\n        page.locator(\".menu a\").and(page.getByText(\"Internet Banking System\")).click()\n        page.locator(\".container > .tabs a\").and(page.getByText(\"Documentation\")).click()\n        page.locator(\".content > .tabs a\").and(page.getByText(\"API Application\")).click()\n        page.locator(\".content > .tabs a\").and(page.getByText(\"E-mail Component\")).click()\n        page.locator(\".table a\").and(page.getByText(\"Email Component inner workings\")).click()\n    }\n\n    @Test\n    fun `title is correct`() {\n        assertThat(page).hasTitle(\"Internet Banking System | Big Bank plc\")\n    }\n\n    @Test\n    fun `menu renders correctly`() {\n        pageTestHelper.testMenu(page) { menu ->\n            assertThat(menu.locator(\".is-active\")).containsText(\"Internet Banking System\")\n        }\n    }\n\n    @Test\n    fun `title and subtitle render correctly`() {\n        pageTestHelper.testTitleAndSubtitle(\n            page,\n            \"Internet Banking System\",\n            \"Allows customers to view information about their bank accounts, and make payments.\"\n        )\n    }\n\n    @Test\n    fun `tabs render correctly`() {\n        pageTestHelper.testTabs(page) { tabs ->\n            assertThat(tabs.and(page.locator(\".is-active\"))).containsText(\"Documentation\")\n        }\n    }\n\n    @Test\n    fun `sections table renders correctly`() {\n        sectionPageTestHelper.testSectionContent(page) { locators ->\n            assertThat(locators[0]).containsText(\"Email Component inner workings\")\n            assertThat(locators[1]).containsText(\"This is how the e-mail component works.\")\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/sections/SoftwareSystemContainerComponentSectionsPageTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.e2e.sections\n\nimport com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat\nimport nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture\nimport nl.avisi.structurizr.site.generatr.site.e2e.PageTestHelper\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Order\nimport kotlin.test.Test\n\nclass SoftwareSystemContainerComponentSectionsPageTest : E2ETestFixture() {\n    private val pageTestHelper = PageTestHelper()\n    private val sectionsPageTestHelper = SectionsPageTestHelper()\n\n    @BeforeEach\n    @Order(2)\n    fun `navigate to page`() {\n        page.navigate(SITE_URL)\n\n        page.locator(\".menu a\").and(page.getByText(\"Internet Banking System\")).click()\n        page.locator(\".container > .tabs a\").and(page.getByText(\"Documentation\")).click()\n        page.locator(\".content > .tabs a\").and(page.getByText(\"API Application\")).click()\n        page.locator(\".content > .tabs a\").and(page.getByText(\"E-mail Component\")).click()\n    }\n\n    @Test\n    fun `title is correct`() {\n        assertThat(page).hasTitle(\"Internet Banking System | Big Bank plc\")\n    }\n\n    @Test\n    fun `menu renders correctly`() {\n        pageTestHelper.testMenu(page) { menu ->\n            assertThat(menu.locator(\".is-active\")).containsText(\"Internet Banking System\")\n        }\n    }\n\n    @Test\n    fun `title and subtitle render correctly`() {\n        pageTestHelper.testTitleAndSubtitle(\n            page,\n            \"Internet Banking System\",\n            \"Allows customers to view information about their bank accounts, and make payments.\"\n        )\n    }\n\n    @Test\n    fun `tabs render correctly`() {\n        pageTestHelper.testTabs(page) { tabs ->\n            assertThat(tabs.and(page.locator(\".is-active\"))).containsText(\"Documentation\")\n        }\n    }\n\n    @Test\n    fun `section tabs render correctly`() {\n        sectionsPageTestHelper.testSectionTabs(page) { tabs ->\n            assertThat(tabs).hasCount(3)\n\n            assertThat(tabs.nth(0)).containsText(\"System\")\n            assertThat(tabs.nth(1)).containsText(\"API Application\")\n            assertThat(tabs.nth(2)).containsText(\"Database\")\n\n            assertThat(tabs.nth(1)).hasClass(\"is-active\")\n        }\n    }\n\n    @Test\n    fun `component section tabs render correctly`() {\n        sectionsPageTestHelper.testComponentSectionTabs(page) { tabs ->\n            assertThat(tabs).hasCount(2)\n\n            assertThat(tabs.nth(0)).containsText(\"Mainframe Banking System Facade\")\n            assertThat(tabs.nth(1)).containsText(\"E-mail Component\")\n\n            assertThat(tabs.nth(1)).hasClass(\"is-active\")\n        }\n    }\n\n    @Test\n    fun `sections table renders correctly`() {\n        sectionsPageTestHelper.testSectionsTable(page) { locators ->\n            val expectedData = listOf(\n                listOf(\"1\", \"Email Component inner workings\")\n            )\n\n            expectedData.forEachIndexed  { rowIndex, row ->\n                row.forEachIndexed { colIndex, text ->\n                    assertThat(locators[rowIndex].nth(colIndex)).containsText(text)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/sections/SoftwareSystemContainerSectionPageTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.e2e.sections\n\nimport com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat\nimport nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture\nimport nl.avisi.structurizr.site.generatr.site.e2e.PageTestHelper\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Order\nimport kotlin.test.Test\n\nclass SoftwareSystemContainerSectionPageTest : E2ETestFixture() {\n    private val pageTestHelper = PageTestHelper()\n    private val sectionPageTestHelper = SectionPageTestHelper()\n\n    @BeforeEach\n    @Order(2)\n    fun `navigate to page`() {\n        page.navigate(SITE_URL)\n\n        page.locator(\".menu a\").and(page.getByText(\"Internet Banking System\")).click()\n        page.locator(\".container > .tabs a\").and(page.getByText(\"Documentation\")).click()\n        page.locator(\".content > .tabs a\").and(page.getByText(\"Database\")).click()\n        page.locator(\".table a\").and(page.getByText(\"Usage\")).click()\n    }\n\n    @Test\n    fun `title is correct`() {\n        assertThat(page).hasTitle(\"Internet Banking System | Big Bank plc\")\n    }\n\n    @Test\n    fun `menu renders correctly`() {\n        pageTestHelper.testMenu(page) { menu ->\n            assertThat(menu.locator(\".is-active\")).containsText(\"Internet Banking System\")\n        }\n    }\n\n    @Test\n    fun `title and subtitle render correctly`() {\n        pageTestHelper.testTitleAndSubtitle(\n            page,\n            \"Internet Banking System\",\n            \"Allows customers to view information about their bank accounts, and make payments.\"\n        )\n    }\n\n    @Test\n    fun `tabs render correctly`() {\n        pageTestHelper.testTabs(page) { tabs ->\n            assertThat(tabs.and(page.locator(\".is-active\"))).containsText(\"Documentation\")\n        }\n    }\n\n    @Test\n    fun `sections table renders correctly`() {\n        sectionPageTestHelper.testSectionContent(page) { locators ->\n            assertThat(locators[0]).containsText(\"Usage\")\n            assertThat(locators[1]).containsText(\"This is how we use this thing.\")\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/sections/SoftwareSystemContainerSectionsPageTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.e2e.sections\n\nimport com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat\nimport nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture\nimport nl.avisi.structurizr.site.generatr.site.e2e.PageTestHelper\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Order\nimport kotlin.test.Test\n\nclass SoftwareSystemContainerSectionsPageTest : E2ETestFixture() {\n    private val pageTestHelper = PageTestHelper()\n    private val sectionsPageTestHelper = SectionsPageTestHelper()\n\n    @BeforeEach\n    @Order(2)\n    fun `navigate to page`() {\n        page.navigate(SITE_URL)\n\n        page.locator(\".menu a\").and(page.getByText(\"Internet Banking System\")).click()\n        page.locator(\".container > .tabs a\").and(page.getByText(\"Documentation\")).click()\n        page.locator(\".content > .tabs a\").and(page.getByText(\"Database\")).click()\n    }\n\n    @Test\n    fun `title is correct`() {\n        assertThat(page).hasTitle(\"Internet Banking System | Big Bank plc\")\n    }\n\n    @Test\n    fun `menu renders correctly`() {\n        pageTestHelper.testMenu(page) { menu ->\n            assertThat(menu.locator(\".is-active\")).containsText(\"Internet Banking System\")\n        }\n    }\n\n    @Test\n    fun `title and subtitle render correctly`() {\n        pageTestHelper.testTitleAndSubtitle(\n            page,\n            \"Internet Banking System\",\n            \"Allows customers to view information about their bank accounts, and make payments.\"\n        )\n    }\n\n    @Test\n    fun `tabs render correctly`() {\n        pageTestHelper.testTabs(page) { tabs ->\n            assertThat(tabs.and(page.locator(\".is-active\"))).containsText(\"Documentation\")\n        }\n    }\n\n    @Test\n    fun `section tabs render correctly`() {\n        sectionsPageTestHelper.testSectionTabs(page) { tabs ->\n            assertThat(tabs).hasCount(3)\n\n            assertThat(tabs.nth(0)).containsText(\"System\")\n            assertThat(tabs.nth(1)).containsText(\"API Application\")\n            assertThat(tabs.nth(2)).containsText(\"Database\")\n\n            assertThat(tabs.nth(2)).hasClass(\"is-active\")\n        }\n    }\n\n    @Test\n    fun `component section tabs render correctly`() {\n        sectionsPageTestHelper.testComponentSectionTabs(page) { tabs ->\n            assertThat(tabs).hasCount(1)\n\n            assertThat(tabs.nth(0)).containsText(\"Container\")\n\n            assertThat(tabs.nth(0)).hasClass(\"is-active\")\n        }\n    }\n\n    @Test\n    fun `sections table renders correctly`() {\n        sectionsPageTestHelper.testSectionsTable(page) { locators ->\n            val expectedData = listOf(\n                listOf(\"1\", \"Usage\")\n            )\n\n            expectedData.forEachIndexed { rowIndex, row ->\n                row.forEachIndexed { colIndex, text ->\n                    assertThat(locators[rowIndex].nth(colIndex)).containsText(text)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/sections/SoftwareSystemSectionPageTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.e2e.sections\n\nimport com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat\nimport nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture\nimport nl.avisi.structurizr.site.generatr.site.e2e.PageTestHelper\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Order\nimport kotlin.test.Test\n\nclass SoftwareSystemSectionPageTest : E2ETestFixture() {\n    private val pageTestHelper = PageTestHelper()\n    private val sectionPageTestHelper = SectionPageTestHelper()\n\n    @BeforeEach\n    @Order(2)\n    fun `navigate to page`() {\n        page.navigate(SITE_URL)\n\n        page.locator(\".menu a\").and(page.getByText(\"Internet Banking System\")).click()\n        page.locator(\".container > .tabs a\").and(page.getByText(\"Documentation\")).click()\n        page.locator(\".table a\").and(page.getByText(\"History\")).click()\n    }\n\n    @Test\n    fun `title is correct`() {\n        assertThat(page).hasTitle(\"Internet Banking System | Big Bank plc\")\n    }\n\n    @Test\n    fun `menu renders correctly`() {\n        pageTestHelper.testMenu(page) { menu ->\n            assertThat(menu.locator(\".is-active\")).containsText(\"Internet Banking System\")\n        }\n    }\n\n    @Test\n    fun `title and subtitle render correctly`() {\n        pageTestHelper.testTitleAndSubtitle(\n            page,\n            \"Internet Banking System\",\n            \"Allows customers to view information about their bank accounts, and make payments.\"\n        )\n    }\n\n    @Test\n    fun `tabs render correctly`() {\n        pageTestHelper.testTabs(page) { tabs ->\n            assertThat(tabs.and(page.locator(\".is-active\"))).containsText(\"Documentation\")\n        }\n    }\n\n    @Test\n    fun `content renders correctly`() {\n        sectionPageTestHelper.testSectionContent(page) { locators ->\n            assertThat(locators[0]).containsText(\"History\")\n            assertThat(locators[1]).containsText(\"Some notes how we got to the current state.\")\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/e2e/sections/SoftwareSystemSectionsPageTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.e2e.sections\n\nimport com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat\nimport nl.avisi.structurizr.site.generatr.site.e2e.E2ETestFixture\nimport nl.avisi.structurizr.site.generatr.site.e2e.PageTestHelper\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Order\nimport kotlin.test.Test\n\nclass SoftwareSystemSectionsPageTest : E2ETestFixture() {\n    private val pageTestHelper = PageTestHelper()\n    private val sectionsPageTestHelper = SectionsPageTestHelper()\n\n    @BeforeEach\n    @Order(2)\n    fun `navigate to page`() {\n        page.navigate(SITE_URL)\n\n        page.locator(\".menu a\").and(page.getByText(\"Internet Banking System\")).click()\n        page.locator(\".container > .tabs a\").and(page.getByText(\"Documentation\")).click()\n    }\n\n    @Test\n    fun `title is correct`() {\n        assertThat(page).hasTitle(\"Internet Banking System | Big Bank plc\")\n    }\n\n    @Test\n    fun `menu renders correctly`() {\n        pageTestHelper.testMenu(page) { menu ->\n            assertThat(menu.locator(\".is-active\")).containsText(\"Internet Banking System\")\n        }\n    }\n\n    @Test\n    fun `title and subtitle render correctly`() {\n        pageTestHelper.testTitleAndSubtitle(\n            page,\n            \"Internet Banking System\",\n            \"Allows customers to view information about their bank accounts, and make payments.\"\n        )\n    }\n\n    @Test\n    fun `tabs render correctly`() {\n        pageTestHelper.testTabs(page) { tabs ->\n            assertThat(tabs.and(page.locator(\".is-active\"))).containsText(\"Documentation\")\n        }\n    }\n\n    @Test\n    fun `section tabs render correctly`() {\n        sectionsPageTestHelper.testSectionTabs(page) { tabs ->\n            assertThat(tabs).hasCount(3)\n\n            assertThat(tabs.nth(0)).containsText(\"System\")\n            assertThat(tabs.nth(1)).containsText(\"API Application\")\n            assertThat(tabs.nth(2)).containsText(\"Database\")\n\n            assertThat(tabs.nth(0)).hasClass(\"is-active\")\n        }\n    }\n\n    @Test\n    fun `component section tabs render correctly`() {\n        sectionsPageTestHelper.testComponentSectionTabs(page) { tabs ->\n            assertThat(tabs).hasCount(0)\n        }\n    }\n\n    @Test\n    fun `sections table renders correctly`() {\n        sectionsPageTestHelper.testSectionsTable(page) { locators ->\n            val expectedData = listOf(\n                listOf(\"1\", \"History\"),\n                listOf(\"2\", \"Usage\"),\n            )\n\n            expectedData.forEachIndexed { rowIndex, row ->\n                row.forEachIndexed { colIndex, text ->\n                    assertThat(locators[rowIndex].nth(colIndex)).containsText(text)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/AsciidocToHtmlTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport assertk.assertions.matches\nimport com.structurizr.documentation.Format\nimport org.junit.jupiter.api.Test\n\nclass AsciidocToHtmlTest : ViewModelTest() {\n\n    @Test\n    fun `translates asciidoc`() {\n        val generatorContext = generatorContext()\n        val viewModel = HomePageViewModel(generatorContext)\n\n        val html = toHtml(\n            viewModel,\n            \"\"\"\n            == header\n            content\n            \"\"\".trimIndent(),\n            Format.AsciiDoc,\n            svgFactory\n        )\n\n        assertThat(html).isEqualTo(\n            \"\"\"\n            <div class=\"sect1 content\">\n             <h2 id=\"_header\">header</h2>\n             <div class=\"sectionbody\">\n              <div class=\"paragraph content\">\n               <p>content</p>\n              </div>\n             </div>\n            </div>\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `translates asciidoc table`() {\n        val generatorContext = generatorContext()\n        val viewModel = HomePageViewModel(generatorContext)\n\n        val html = toHtml(\n            viewModel,\n            \"\"\"\n            == header\n\n            |===\n            |header1 |header2\n\n            |content\n            |content\n            |===\n            \"\"\".trimIndent(),\n            Format.AsciiDoc,\n            svgFactory\n        )\n\n        assertThat(html).isEqualTo(\n            \"\"\"\n            <div class=\"sect1 content\">\n             <h2 id=\"_header\">header</h2>\n             <div class=\"sectionbody\">\n              <table class=\"tableblock frame-all grid-all stretch content\">\n               <colgroup>\n                <col style=\"width: 50%;\">\n                <col style=\"width: 50%;\">\n               </colgroup>\n               <thead>\n                <tr>\n                 <th class=\"tableblock halign-left valign-top content\">header1</th>\n                 <th class=\"tableblock halign-left valign-top content\">header2</th>\n                </tr>\n               </thead>\n               <tbody>\n                <tr>\n                 <td class=\"tableblock halign-left valign-top content\">\n                  <p class=\"tableblock content\">content</p>\n                 </td>\n                 <td class=\"tableblock halign-left valign-top content\">\n                  <p class=\"tableblock content\">content</p>\n                 </td>\n                </tr>\n               </tbody>\n              </table>\n             </div>\n            </div>\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `translates asciidoc admonition block`() {\n        val generatorContext = generatorContext()\n        val viewModel = HomePageViewModel(generatorContext)\n\n        val html = toHtml(\n            viewModel,\n            \"\"\"\n            == header\n\n            NOTE: This is a note.\n            \"\"\".trimIndent(),\n            Format.AsciiDoc,\n            svgFactory\n        )\n\n        assertThat(html).isEqualTo(\n            \"\"\"\n            <div class=\"sect1 content\">\n             <h2 id=\"_header\">header</h2>\n             <div class=\"sectionbody\">\n              <div class=\"admonitionblock note content adm-block adm-note\">\n               <table>\n                <tbody>\n                 <tr>\n                  <td class=\"icon adm-heading\">\n                   <div class=\"title\">Note</div>\n                  </td>\n                  <td class=\"content\">This is a note.</td>\n                 </tr>\n                </tbody>\n               </table>\n              </div>\n             </div>\n            </div>\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `renders PlantUML code to SVG`() {\n        val generatorContext = generatorContext()\n        val viewModel = HomePageViewModel(generatorContext)\n\n        val html = toHtml(\n            viewModel,\n            \"\"\"\n                [plantuml]\n                ----\n                @startuml\n                class Foo\n                @enduml\n                ----\n            \"\"\".trimIndent(),\n            Format.AsciiDoc,\n            svgFactory\n        )\n\n        assertThat(html).matches(\"<div>.*<svg.*Foo.*</svg>.*</div>\".toRegex(RegexOption.DOT_MATCHES_ALL))\n    }\n\n    @Test\n    fun `translates mermaid graphings in asciidoc`() {\n        val generatorContext = generatorContext()\n        val viewModel = HomePageViewModel(generatorContext)\n\n        val html = toHtml(\n            viewModel,\n            \"\"\"\n            == Mermaid\n\n            [source, mermaid]\n            ----\n            graph TD;\n            A-->B;\n            A-->C;\n            B-->D;\n            C-->D;\n            ----\n            \"\"\".trimIndent(),\n            Format.AsciiDoc,\n            svgFactory\n        )\n\n        assertThat(html).isEqualTo(\n            \"\"\"\n            <div class=\"sect1 content\">\n             <h2 id=\"_mermaid\">Mermaid</h2>\n             <div class=\"sectionbody\">\n              <div class=\"listingblock content\">\n               <div class=\"content\">\n                <pre class=\"highlight\"><code class=\"language-mermaid\" data-lang=\"mermaid\">graph TD;\n            A--&gt;B;\n            A--&gt;C;\n            B--&gt;D;\n            C--&gt;D;</code></pre>\n               </div>\n              </div>\n             </div>\n            </div>\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `embedded diagram`() {\n        val generatorContext = generatorContext()\n        val viewModel = HomePageViewModel(generatorContext)\n\n        val html = toHtml(viewModel, \"image::embed:SystemLandscape[System Landscape Diagram]\", Format.AsciiDoc, svgFactory)\n\n        assertThat(html).isEqualTo(\n            \"\"\"\n            <div class=\"imageblock content\">\n             <div class=\"content\">\n              <div>\n               <figure style=\"width: min(100%, 800px);\" id=\"SystemLandscape\">\n                <div>\n                 <svg viewBox=\"0 0 800 900\"></svg>\n                </div>\n                <figcaption>\n                 <a onclick=\"openSvgModal('SystemLandscape-modal', 'SystemLandscape-svg')\">System Landscape Diagram</a>\n                </figcaption>\n               </figure>\n               <div class=\"modal\" id=\"SystemLandscape-modal\">\n                <div class=\"modal-background\" onclick=\"closeModal('SystemLandscape-modal')\"></div>\n                <div class=\"modal-content\">\n                 <div class=\"box\">\n                  <div id=\"SystemLandscape-svg\" class=\"modal-box-content\">\n                   <svg viewBox=\"0 0 800 900\"></svg>\n                  </div>\n                  <div class=\"has-text-centered\">\n                   System Landscape Diagram [<a href=\"svg/SystemLandscape.svg\" target=\"_blank\">svg</a>|<a href=\"png/SystemLandscape.png\" target=\"_blank\">png</a>|<a href=\"puml/SystemLandscape.puml\" target=\"_blank\">puml</a>]\n                  </div>\n                 </div>\n                </div>\n                <button class=\"modal-close is-large\" aria-label=\"close\" onclick=\"closeModal('SystemLandscape-modal')\"></button>\n               </div>\n              </div>\n             </div>\n            </div>\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `embedded diagram key not found`() {\n        val generatorContext = generatorContext()\n        val viewModel = HomePageViewModel(generatorContext)\n\n        val html = toHtml(viewModel, \"image::embed:non-existing[Diagram]\", Format.AsciiDoc) { _, _ ->\n            null\n        }\n\n        assertThat(html).isEqualTo(\n            \"\"\"\n            <div class=\"imageblock content\">\n             <div class=\"content\">\n              <div>\n               <div class=\"notification is-danger\">\n                No view with key<span class=\"has-text-weight-bold\"> non-existing </span>found!\n               </div>\n              </div>\n             </div>\n            </div>\n            \"\"\".trimIndent()\n        )\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/BranchHomeLinkViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport kotlin.test.Test\n\nclass BranchHomeLinkViewModelTest : ViewModelTest() {\n\n    @Test\n    fun `title is branch name`() {\n        val viewModel = BranchHomeLinkViewModel(pageViewModel(), \"branch-1\")\n\n        assertThat(viewModel.title)\n            .isEqualTo(\"branch-1\")\n    }\n\n    @Test\n    fun `relative href to`() {\n        val viewModel = BranchHomeLinkViewModel(pageViewModel(\"/master/decisions/1\"), \"branch-1\")\n\n        assertThat(viewModel.relativeHref)\n            .isEqualTo(\"../../../../branch-1/\")\n    }\n\n    @Test\n    fun `relative href from home`() {\n        val viewModel = BranchHomeLinkViewModel(pageViewModel(\"/\"), \"master\")\n\n        assertThat(viewModel.relativeHref)\n            .isEqualTo(\"./../master/\")\n    }\n\n    @Test\n    fun `title is branch name when branch name contains slash`() {\n        val viewModel = BranchHomeLinkViewModel(pageViewModel(), \"feat/branch-1\")\n\n        assertThat(viewModel.title)\n            .isEqualTo(\"feat/branch-1\")\n    }\n\n    @Test\n    fun `relative href from home when branch name contains slash`() {\n        val viewModel = BranchHomeLinkViewModel(pageViewModel(\"/\"), \"feat/branch-1\")\n\n        assertThat(viewModel.relativeHref)\n            .isEqualTo(\"./../feat%2Fbranch-1/\")\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/ContentTextTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport com.structurizr.documentation.Format\nimport com.structurizr.documentation.Section\nimport org.junit.jupiter.api.DynamicTest\nimport org.junit.jupiter.api.Test\nimport org.junit.jupiter.api.TestFactory\nimport org.junit.jupiter.params.ParameterizedTest\nimport org.junit.jupiter.params.provider.ValueSource\n\nclass ContentTextTest {\n\n    @TestFactory\n    fun `no content`() = listOf(Format.Markdown, Format.AsciiDoc)\n        .map { format ->\n            DynamicTest.dynamicTest(format.name) {\n                val section = Section(format, \"\")\n                assertThat(section.contentText()).isEqualTo(\"\")\n            }\n        }\n\n    @TestFactory\n    fun `only whitespaces`() = listOf(Format.Markdown, Format.AsciiDoc)\n        .map { format ->\n            DynamicTest.dynamicTest(format.name) {\n                val section = Section(format, \" \\n \")\n                assertThat(section.contentText()).isEqualTo(\"\")\n            }\n        }\n\n    @ParameterizedTest\n    @ValueSource(strings = [\"# header\", \"## header\", \"### header\"])\n    fun `ignores markdown title`(content: String) {\n        val section = Section(Format.Markdown, content)\n        assertThat(section.contentText()).isEqualTo(\"\")\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = [\"= header\"])\n    fun `ignores asciidoc title`(content: String) {\n        val section = Section(Format.AsciiDoc, content)\n        assertThat(section.contentText()).isEqualTo(\"\")\n    }\n\n    @TestFactory\n    fun `simple paragraph`() = listOf(Format.Markdown, Format.AsciiDoc)\n        .map { format ->\n            DynamicTest.dynamicTest(format.name) {\n                val section = Section(format, \"some content\")\n                assertThat(section.contentText()).isEqualTo(\"some content\")\n            }\n        }\n\n    @Test\n    fun `markdown headers and paragraphs`() {\n        val section = Section(Format.Markdown, \"# header\\n\\nsome content\\n\\n## subheader\\n\\nmore content\")\n        assertThat(section.contentText()).isEqualTo(\"subheader some content more content\")\n    }\n\n    @Test\n    fun `asciidoc headers and paragraphs`() {\n        val section = Section(Format.AsciiDoc, \"= header\\n\\nsome content\\n\\n== subheader\\n\\nmore content\")\n        assertThat(section.contentText()).isEqualTo(\"some content subheader more content\")\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/ContentTitleTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport com.structurizr.documentation.Format\nimport com.structurizr.documentation.Section\nimport org.junit.jupiter.api.DynamicTest\nimport org.junit.jupiter.api.TestFactory\nimport org.junit.jupiter.params.ParameterizedTest\nimport org.junit.jupiter.params.provider.ValueSource\nimport kotlin.test.Test\n\nclass ContentTitleTest {\n\n    @TestFactory\n    fun `no content`() = listOf(Format.Markdown, Format.AsciiDoc)\n        .map { format ->\n            DynamicTest.dynamicTest(format.name) {\n                val section = Section(format, \"\")\n                assertThat(section.contentTitle()).isEqualTo(\"untitled document\")\n            }\n        }\n\n    @TestFactory\n    fun `only whitespaces`() = listOf(Format.Markdown, Format.AsciiDoc)\n        .map { format ->\n            DynamicTest.dynamicTest(format.name) {\n                val section = Section(format, \" \\n \")\n                assertThat(section.contentTitle()).isEqualTo(\"untitled document\")\n            }\n        }\n\n    @TestFactory\n    fun `simple paragraph`() = listOf(Format.Markdown, Format.AsciiDoc)\n        .map { format ->\n            DynamicTest.dynamicTest(format.name) {\n                val section = Section(format, \"some content\")\n                assertThat(section.contentTitle()).isEqualTo(\"untitled document\")\n            }\n        }\n\n    @ParameterizedTest\n    @ValueSource(strings = [\"# header\", \"## header\", \"### header\"])\n    fun `with markdown heading`(content: String) {\n        val section = Section(Format.Markdown, content)\n        assertThat(section.contentTitle()).isEqualTo(\"header\")\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = [\"= header\", \"== header\", \"=== header\"])\n    fun `with asciidoc heading`(content: String) {\n        val section = Section(Format.AsciiDoc, content)\n        assertThat(section.contentTitle()).isEqualTo(\"header\")\n    }\n\n    @Test\n    fun `with asciidoc heading including logo`() {\n        val section = Section(Format.AsciiDoc, \"= image:logo.png[logo] header\")\n        assertThat(section.contentTitle()).isEqualTo(\"header\")\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/CustomStylesheetViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport org.junit.jupiter.api.Test\n\nclass CustomStylesheetViewModelTest : ViewModelTest() {\n    private val generatorContext = generatorContext()\n    private val viewModel = pageViewModel()\n\n    @Test\n    fun `no custom stylesheet`() {\n        val customStylesheetViewModel = CustomStylesheetViewModel(generatorContext, viewModel)\n        assertThat(customStylesheetViewModel.resourceURI).isEqualTo(null)\n    }\n\n    @Test\n    fun `custom stylesheet set to URL`() {\n        generatorContext.workspace.views.configuration.addProperty(\"generatr.style.customStylesheet\",\"http://example.uri/custom.css\")\n        val customStylesheetViewModel = CustomStylesheetViewModel(generatorContext, viewModel)\n        assertThat(customStylesheetViewModel.resourceURI).isEqualTo(\"http://example.uri/custom.css\")\n    }\n\n    @Test\n    fun `custom stylesheet set to local asset`() {\n        generatorContext.workspace.views.configuration.addProperty(\"generatr.style.customStylesheet\",\"site/custom.css\")\n        val customStylesheetViewModel = CustomStylesheetViewModel(generatorContext, viewModel)\n        assertThat(customStylesheetViewModel.resourceURI).isEqualTo(\"../site/custom.css\")\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/DecisionTabViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport kotlin.test.Test\n\nclass DecisionTabViewModelTest : ViewModelTest() {\n\n    @Test\n    fun `no decision tabs`() {\n        val context = generatorContext()\n        val softwareSystem = context.workspace.model.addSoftwareSystem(\"Software system\")\n\n        val softwareSystemPageViewModel = SoftwareSystemPageViewModel(context, softwareSystem, SoftwareSystemPageViewModel.Tab.DECISIONS)\n        val decisionTabViewModel = softwareSystemPageViewModel.createDecisionsTabViewModel(softwareSystem, SoftwareSystemPageViewModel.Tab.DECISIONS)\n\n        assertThat(decisionTabViewModel.size).isEqualTo(0)\n    }\n\n    @Test\n    fun `software system decision tab available`() {\n        val context = generatorContext()\n        val softwareSystem = context.workspace.model.addSoftwareSystem(\"Software system\")\n        softwareSystem.documentation.addDecision(createDecision(\"1\", \"Accepted\"))\n\n        val softwareSystemPageViewModel = SoftwareSystemPageViewModel(context, softwareSystem, SoftwareSystemPageViewModel.Tab.DECISIONS)\n        val decisionTabViewModel = softwareSystemPageViewModel.createDecisionsTabViewModel(softwareSystem, SoftwareSystemPageViewModel.Tab.DECISIONS)\n\n        assertThat(decisionTabViewModel.size).isEqualTo(1)\n        assertThat(decisionTabViewModel.first().title).isEqualTo(\"System\")\n    }\n\n    @Test\n    fun `container decision tab available `() {\n        val context = generatorContext()\n        val softwareSystem = context.workspace.model.addSoftwareSystem(\"Software system\")\n        softwareSystem.addContainer(\"Some Container\").documentation.addDecision(createDecision(\"2\", \"Proposed\"))\n\n        val softwareSystemPageViewModel = SoftwareSystemPageViewModel(context, softwareSystem, SoftwareSystemPageViewModel.Tab.DECISIONS)\n        val decisionTabViewModel = softwareSystemPageViewModel.createDecisionsTabViewModel(softwareSystem, SoftwareSystemPageViewModel.Tab.DECISIONS)\n\n        assertThat(decisionTabViewModel.size).isEqualTo(1)\n        assertThat(decisionTabViewModel.first().title).isEqualTo(\"Some Container\")\n\n    }\n    @Test\n    fun `container & system decision tabs available`() {\n        val context = generatorContext()\n        val softwareSystem = context.workspace.model.addSoftwareSystem(\"Software system\")\n        softwareSystem.documentation.addDecision(createDecision(\"1\", \"Accepted\"))\n        softwareSystem.addContainer(\"Some Container\").documentation.addDecision(createDecision(\"2\", \"Proposed\"))\n\n        val softwareSystemPageViewModel = SoftwareSystemPageViewModel(context, softwareSystem, SoftwareSystemPageViewModel.Tab.DECISIONS)\n        val decisionTabViewModel = softwareSystemPageViewModel.createDecisionsTabViewModel(softwareSystem, SoftwareSystemPageViewModel.Tab.DECISIONS)\n\n        assertThat(decisionTabViewModel.size).isEqualTo(2)\n        assertThat(decisionTabViewModel.first().title).isEqualTo(\"System\")\n        assertThat(decisionTabViewModel.last().title).isEqualTo(\"Some Container\")\n    }\n\n    @Test\n    fun `check container decisions tabs are filtered`() {\n        val context = generatorContext()\n        val softwareSystem = context.workspace.model.addSoftwareSystem(\"Software system\")\n        softwareSystem.addContainer(\"Some Container\").documentation.addDecision(createDecision(\"2\", \"Proposed\"))\n        softwareSystem.addContainer(\"Another Container\")\n\n        val softwareSystemPageViewModel = SoftwareSystemPageViewModel(context, softwareSystem, SoftwareSystemPageViewModel.Tab.DECISIONS)\n        val decisionTabViewModel = softwareSystemPageViewModel.createDecisionsTabViewModel(softwareSystem, SoftwareSystemPageViewModel.Tab.DECISIONS)\n\n        assertThat(decisionTabViewModel.size).isEqualTo(1)\n        assertThat(decisionTabViewModel.last().title).isEqualTo(\"Some Container\")\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/DecisionsTableViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport java.time.LocalDate\nimport java.time.Month\nimport kotlin.test.Test\n\nclass DecisionsTableViewModelTest : ViewModelTest() {\n\n    @Test\n    fun `no decisions available`() {\n        assertThat(pageViewModel().createDecisionsTableViewModel(emptySet()) { \"href\" })\n            .isEqualTo(\n                TableViewModel.create {\n                    decisionsTableHeaderRow()\n                }\n            )\n    }\n\n    @Test\n    fun `many decisions sorted by integer value of id`() {\n        val decision1 = createDecision(\"10\", \"Accepted\", LocalDate.of(2022, Month.JANUARY, 1))\n        val decision2 = createDecision(\"2\", \"Proposed\", LocalDate.of(2022, Month.JANUARY, 2))\n\n        val pageViewModel = pageViewModel()\n        assertThat(pageViewModel.createDecisionsTableViewModel(setOf(decision1, decision2)) { it.id }).isEqualTo(\n            TableViewModel.create {\n                decisionsTableHeaderRow()\n                bodyRow(\n                    cellWithIndex(\"2\"),\n                    cell(\"02-01-2022\"),\n                    cell(\"Proposed\"),\n                    cellWithLink(pageViewModel, \"Decision 2\", decision2.id)\n                )\n                bodyRow(\n                    cellWithIndex(\"10\"),\n                    cell(\"01-01-2022\"),\n                    cell(\"Accepted\"),\n                    cellWithLink(pageViewModel, \"Decision 10\", decision1.id)\n                )\n            }\n        )\n    }\n\n    private fun TableViewModel.TableViewInitializerContext.decisionsTableHeaderRow() {\n        headerRow(headerCellSmall(\"ID\"), headerCell(\"Date\"), headerCell(\"Status\"), headerCellLarge(\"Title\"))\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/DiagramIndexViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport assertk.assertions.isFalse\nimport assertk.assertions.isTrue\nimport kotlin.test.Test\n\nclass DiagramIndexViewModelTest : ViewModelTest() {\n    private val generatorContext = generatorContext()\n\n    @Test\n    fun `index without diagrams` () {\n        val viewModel = DiagramIndexViewModel(diagramViewModels())\n\n        assertThat(viewModel.entries).isEqualTo(listOf())\n    }\n\n    @Test\n    fun `index with diagram` () {\n        val softwareSystem = generatorContext.workspace.model.addSoftwareSystem(\"Software system\")\n        generatorContext.workspace.views.createSystemContextView(softwareSystem, \"system-1\", \"\")\n        val viewModel = DiagramIndexViewModel(diagramViewModels())\n\n        assertThat(viewModel.entries).isEqualTo(listOf(IndexEntry(\"system-1\", \"System Context View: Software system\")))\n    }\n\n    @Test\n    fun `index with diagram with description` () {\n        val softwareSystem = generatorContext.workspace.model.addSoftwareSystem(\"Software system\")\n        generatorContext.workspace.views.createSystemContextView(softwareSystem, \"system-1\", \"Some description\")\n        val viewModel = DiagramIndexViewModel(diagramViewModels())\n\n        assertThat(viewModel.entries).isEqualTo(listOf(IndexEntry(\"system-1\", \"System Context View: Software system (Some description)\")))\n    }\n\n    @Test\n    fun `index with exactly one diagram` () {\n        val softwareSystem = generatorContext.workspace.model.addSoftwareSystem(\"Software system\")\n        generatorContext.workspace.views.createSystemContextView(softwareSystem, \"system-1\", \"\")\n        val viewModel = DiagramIndexViewModel(diagramViewModels())\n\n        assertThat(viewModel.visible).isFalse()\n    }\n\n    @Test\n    fun `index with diagram and image with description` () {\n        val softwareSystem = generatorContext.workspace.model.addSoftwareSystem(\"Software system\")\n        generatorContext.workspace.views.createSystemContextView(softwareSystem, \"system-1\", \"Some description\")\n        generatorContext.workspace.views.createImageView(softwareSystem, \"image-1\")\n        val viewModel = DiagramIndexViewModel(diagramViewModels(), imageViewViewModels())\n\n        assertThat(viewModel.visible).isTrue()\n        assertThat(viewModel.entries).isEqualTo(listOf(\n            IndexEntry(\"system-1\", \"System Context View: Software system (Some description)\"),\n            IndexEntry(\"imageview-2\", \"Image View Title (Image View Description)\")\n        ))\n    }\n\n    private fun diagramViewModels() = generatorContext.workspace.views.systemContextViews\n        .map { DiagramViewModel.forView(this.pageViewModel(), it, generatorContext.svgFactory) }\n\n    private fun imageViewViewModels() = listOf(\n        ImageViewViewModel(createImageView(generatorContext.workspace, generatorContext.workspace.model.addSoftwareSystem(\"Software system2\")))\n    )\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/FaviconViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertFailure\nimport assertk.assertThat\nimport assertk.assertions.*\nimport kotlin.test.Test\n\nclass FaviconViewModelTest : ViewModelTest() {\n    private val generatorContext = generatorContext(\"some workspace\")\n\n    @Test\n    fun favicon() {\n        generatorContext.workspace.views.configuration.addProperty(\n            \"generatr.style.faviconPath\",\n            \"site/favicon.png\"\n        )\n        val faviconViewModel = faviconViewModel()\n\n        assertThat(faviconViewModel.includeFavicon).isTrue()\n        assertThat(faviconViewModel.type).isEqualTo(\"image/png\")\n        assertThat(faviconViewModel.url).isEqualTo(\"../../site/favicon.png\")\n    }\n\n    @Test\n    fun `invalid favicon`() {\n        generatorContext.workspace.views.configuration.addProperty(\n            \"generatr.style.faviconPath\",\n            \"favicon\"\n        )\n\n        assertFailure { faviconViewModel() }.hasMessage(\"Favicon must be a valid *.ico, *.png of *.gif file\")\n    }\n\n    @Test\n    fun `no favicon`() {\n        val faviconViewModel = faviconViewModel()\n\n        assertThat(faviconViewModel.includeFavicon).isFalse()\n    }\n\n    private fun faviconViewModel() = object : PageViewModel(generatorContext) {\n        override val url: String = \"/master/system\"\n        override val pageSubTitle: String = \"subtitle\"\n    }.favicon\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/HeaderBarViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.containsExactly\nimport assertk.assertions.isEqualTo\nimport assertk.assertions.isFalse\nimport assertk.assertions.isTrue\nimport org.junit.jupiter.api.DynamicTest\nimport org.junit.jupiter.api.TestFactory\nimport kotlin.test.Test\n\nclass HeaderBarViewModelTest : ViewModelTest() {\n    private val generatorContext = generatorContext(\n        \"some workspace\", branches = listOf(\"main\", \"branch-2\"), version = \"0.42.1337\"\n    )\n    private val pageViewModel = object : PageViewModel(generatorContext) {\n        override val url: String = \"/master/system\"\n        override val pageSubTitle: String = \"subtitle\"\n    }\n\n    @Test\n    fun `workspace title link`() {\n        val viewModel = HeaderBarViewModel(pageViewModel, generatorContext)\n\n        assertThat(viewModel.titleLink).isEqualTo(\n            LinkViewModel(pageViewModel, \"some workspace\", HomePageViewModel.url())\n        )\n    }\n\n    @Test\n    fun `current branch`() {\n        val viewModel = HeaderBarViewModel(pageViewModel, generatorContext)\n\n        assertThat(viewModel.currentBranch).isEqualTo(generatorContext.currentBranch)\n    }\n\n    @Test\n    fun `branch pulldown menu`() {\n        val viewModel = HeaderBarViewModel(pageViewModel, generatorContext)\n\n        assertThat(viewModel.branches).containsExactly(\n            BranchHomeLinkViewModel(pageViewModel, \"main\"),\n            BranchHomeLinkViewModel(pageViewModel, \"branch-2\")\n        )\n    }\n\n    @Test\n    fun `version number`() {\n        val viewModel = HeaderBarViewModel(pageViewModel, generatorContext)\n\n        assertThat(viewModel.version).isEqualTo(\"0.42.1337\")\n    }\n\n    @Test\n    fun logo() {\n        generatorContext.workspace.views.configuration.addProperty(\n            \"generatr.style.logoPath\",\n            \"site/logo.png\"\n        )\n        val viewModel = HeaderBarViewModel(pageViewModel, generatorContext)\n\n        assertThat(viewModel.hasLogo).isTrue()\n        assertThat(viewModel.logo).isEqualTo(ImageViewModel(pageViewModel, \"/site/logo.png\"))\n    }\n\n    @Test\n    fun `no logo`() {\n        val viewModel = HeaderBarViewModel(pageViewModel, generatorContext)\n\n        assertThat(viewModel.hasLogo).isFalse()\n    }\n\n    @TestFactory\n    fun `no dark mode`() = listOf(\n        \"light\" to false,\n        \"dark\" to false,\n        \"auto\" to true,\n    ).map { (theme, allowToggle) ->\n        DynamicTest.dynamicTest(theme) {\n            generatorContext.workspace.views.configuration.addProperty(\n                \"generatr.site.theme\",\n                theme\n            )\n\n            val viewModel = HeaderBarViewModel(object : PageViewModel(generatorContext) {\n                override val url: String = \"/master/system\"\n                override val pageSubTitle: String = \"subtitle\"\n            }, generatorContext)\n\n            assertThat(viewModel.allowToggleTheme).isEqualTo(allowToggle)\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/HomePageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport assertk.assertions.isTrue\nimport com.structurizr.documentation.Format\nimport nl.avisi.structurizr.site.generatr.site.model.HomePageViewModel.Companion.DEFAULT_HOMEPAGE_CONTENT\nimport org.junit.jupiter.params.ParameterizedTest\nimport org.junit.jupiter.params.provider.ValueSource\nimport kotlin.test.Test\n\nclass HomePageViewModelTest : ViewModelTest() {\n    @Test\n    fun url() {\n        assertThat(HomePageViewModel.url())\n            .isEqualTo(\"/\")\n    }\n\n    @Test\n    fun `default homepage`() {\n        val generatorContext = generatorContext()\n        val viewModel = HomePageViewModel(generatorContext)\n\n        assertThat(viewModel.content)\n            .isEqualTo(toHtml(viewModel, DEFAULT_HOMEPAGE_CONTENT, Format.Markdown, svgFactory))\n    }\n\n    @Test\n    fun `homepage with workspace docs`() {\n        val generatorContext = generatorContext()\n        generatorContext.workspace.documentation.addSection(\n            createSection(\"Section content\")\n        )\n        val viewModel = HomePageViewModel(generatorContext)\n\n        assertThat(viewModel.content)\n            .isEqualTo(toHtml(viewModel, \"Section content\", Format.Markdown, svgFactory))\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = [\"\", \"  \"])\n    fun `page title with blank workspace name`(workspaceName: String) {\n        val generatorContext = generatorContext(workspaceName)\n        val viewModel = HomePageViewModel(generatorContext)\n\n        assertThat(viewModel.pageTitle).isEqualTo(\"Home\")\n    }\n\n    @Test\n    fun `page title with workspace name`() {\n        val generatorContext = generatorContext(\"Workspace name\")\n        val viewModel = HomePageViewModel(generatorContext)\n\n        assertThat(viewModel.pageTitle).isEqualTo(\"Workspace name\")\n    }\n\n    @Test\n    fun `header bar`() {\n        val generatorContext = generatorContext(\"Workspace name\")\n        val viewModel = HomePageViewModel(generatorContext)\n\n        assertThat(viewModel.headerBar.titleLink.title).isEqualTo(\"Workspace name\")\n    }\n\n    @Test\n    fun menu() {\n        val generatorContext = generatorContext(\"Workspace name\")\n        val viewModel = HomePageViewModel(generatorContext)\n\n        assertThat(viewModel.menu.generalItems[0].active).isTrue()\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/ImageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport kotlin.test.Test\n\nclass ImageViewModelTest : ViewModelTest() {\n    @Test\n    fun `relative href`() {\n        val pageViewModel = pageViewModel(\"/some-page/some-subpage/\")\n        val viewModel = ImageViewModel(pageViewModel, \"/svg/image.svg\")\n\n        assertThat(viewModel.relativeHref).isEqualTo(\"../../svg/image.svg\")\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/ImageViewViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport com.structurizr.model.SoftwareSystem\nimport kotlin.test.Test\n\nclass ImageViewViewModelTest : ViewModelTest() {\n    private val generatorContext = generatorContext()\n    private val softwareSystem: SoftwareSystem = generatorContext.workspace.model.addSoftwareSystem(\"Software system 1\")\n    private val container = softwareSystem.addContainer(\"Container 1\")\n    private val component = container.addComponent(\"Component 1\")\n\n    @Test\n    fun `image name for software system`() {\n        val imageView = ImageViewViewModel(createImageView(generatorContext.workspace, softwareSystem, title = null))\n        assertThat(imageView.title).isEqualTo(\"Software system 1 - Containers\")\n    }\n\n    @Test\n    fun `image name for container`() {\n        val imageView = ImageViewViewModel(createImageView(generatorContext.workspace, container, title = null))\n        assertThat(imageView.title).isEqualTo(\"Software system 1 - Container 1 - Components\")\n    }\n\n    @Test\n    fun `image name for component`() {\n        val imageView = ImageViewViewModel(createImageView(generatorContext.workspace, component, title = null))\n        assertThat(imageView.title).isEqualTo(\"Software system 1 - Container 1 - Component 1 - Code\")\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/IndexingTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.containsAll\nimport assertk.assertions.containsAtLeast\nimport assertk.assertions.isEmpty\nimport assertk.assertions.isEqualTo\nimport assertk.assertions.isNull\nimport com.structurizr.documentation.Format\nimport com.structurizr.documentation.Section\nimport nl.avisi.structurizr.site.generatr.site.model.indexing.*\nimport kotlin.test.Test\n\nclass IndexingTest : ViewModelTest() {\n    val workspace = this.generatorContext().workspace\n\n    @Test\n    fun `no home`() {\n        val document = home(workspace.documentation, this.pageViewModel())\n\n        assertThat(document).isNull()\n    }\n\n    @Test\n    fun `indexes home`() {\n        workspace.documentation.addSection(createSection(\"# Home\\nSome info\"))\n        val document = home(workspace.documentation, this.pageViewModel())\n\n        assertThat(document).isEqualTo(Document(\"../\", \"Home\", \"Home\", \"Home Some info\"))\n    }\n\n    @Test\n    fun `no workspace sections`() {\n        val documents = workspaceSections(workspace.documentation, this.pageViewModel())\n\n        assertThat(documents).isEmpty()\n    }\n\n    @Test\n    fun `indexes non home workspace sections`() {\n        workspace.documentation.addSection(createSection(\"# Home\\nSome info\"))\n        workspace.documentation.addSection(createSection(\"# Landscape\\nMore info\"))\n        val documents = workspaceSections(workspace.documentation, this.pageViewModel())\n\n        assertThat(documents).containsAtLeast(\n            Document(\n                \"../landscape/\",\n                \"Workspace Documentation\",\n                \"Landscape\",\n                \"Landscape More info\"\n            )\n        )\n    }\n\n    @Test\n    fun `no workspace decisions`() {\n        val documents = workspaceDecisions(workspace.documentation, this.pageViewModel())\n\n        assertThat(documents).isEmpty()\n    }\n\n    @Test\n    fun `indexes workspace decisions`() {\n        workspace.documentation.addDecision(createDecision())\n        val documents = workspaceDecisions(workspace.documentation, this.pageViewModel())\n\n        assertThat(documents).containsAtLeast(\n            Document(\n                \"../decisions/1/\",\n                \"Workspace Decision\",\n                \"Decision 1\",\n                \"Decision 1 Decision 1 content\"\n            )\n        )\n    }\n\n    @Test\n    fun `no software system home or properties`() {\n        workspace.model.addSoftwareSystem(\"Software System 1\", \"One system to rule them all\")\n        val document = softwareSystemHome(workspace.model.softwareSystems.single(), this.pageViewModel())\n\n        assertThat(document).isNull()\n    }\n\n    @Test\n    fun `indexes software system with home section`() {\n        workspace.model.addSoftwareSystem(\"Software System 1\", \"One system to rule them all\").apply {\n            documentation.addSection(Section(Format.Markdown, \"# Introduction\\nSome info\"))\n        }\n        val document = softwareSystemHome(workspace.model.softwareSystems.single(), this.pageViewModel())\n\n        assertThat(document).isEqualTo(\n            Document(\n                \"../software-system-1/\",\n                \"Software System Info\",\n                \"Software System 1 | Introduction\",\n                \"Introduction Some info\"\n            )\n        )\n    }\n\n    @Test\n    fun `indexes software system with properties`() {\n        workspace.model.addSoftwareSystem(\"Software System 1\", \"One system to rule them all\").apply {\n            addProperty(\"Prop1\", \"Value 1\")\n        }\n        val document = softwareSystemHome(workspace.model.softwareSystems.single(), this.pageViewModel())\n\n        assertThat(document).isEqualTo(\n            Document(\n                \"../software-system-1/\",\n                \"Software System Info\",\n                \"Software System 1 | Info\",\n                \"Value 1\"\n            )\n        )\n    }\n\n    @Test\n    fun `indexes software system with home section and properties`() {\n        workspace.model.addSoftwareSystem(\"Software System 1\", \"One system to rule them all\").apply {\n            documentation.addSection(Section(Format.Markdown, \"# Introduction\\nSome info\"))\n            addProperty(\"Prop 1\", \"Value 1\")\n            addProperty(\"Prop 2\", \"Value 2\")\n        }\n        val document = softwareSystemHome(workspace.model.softwareSystems.single(), this.pageViewModel())\n\n        assertThat(document).isEqualTo(\n            Document(\n                \"../software-system-1/\",\n                \"Software System Info\",\n                \"Software System 1 | Introduction\",\n                \"Introduction Some info Value 1 Value 2\"\n            )\n        )\n    }\n\n    @Test\n    fun `indexes software system context`() {\n        workspace.model.addSoftwareSystem(\"Software System 1\", \"One system to rule them all\")\n        val document = softwareSystemContext(workspace.model.softwareSystems.single(), this.pageViewModel())\n\n        assertThat(document).isEqualTo(\n            Document(\n                \"../software-system-1/context/\",\n                \"Context views\",\n                \"Software System 1 | Context views\",\n                \"Software System 1 One system to rule them all\"\n            )\n        )\n    }\n\n    @Test\n    fun `no software system containers`() {\n        workspace.model.addSoftwareSystem(\"Software System 1\", \"One system to rule them all\")\n        val document = softwareSystemContainers(workspace.model.softwareSystems.single(), this.pageViewModel())\n\n        assertThat(document).isNull()\n    }\n\n    @Test\n    fun `indexes software system containers`() {\n        workspace.model.addSoftwareSystem(\"Software System 1\", \"One system to rule them all\").apply {\n            addContainer(\"Container 1\", \"a container\")\n            addContainer(\"Container 2\", \"a second container\", \"tech 1\")\n        }\n        val document = softwareSystemContainers(workspace.model.softwareSystems.single(), this.pageViewModel())\n\n        assertThat(document).isEqualTo(\n            Document(\n                \"../software-system-1/container/\",\n                \"Container views\",\n                \"Software System 1 | Container views\",\n                \"Container 1 a container Container 2 a second container tech 1\"\n            )\n        )\n    }\n\n    @Test\n    fun `no software system components`() {\n        workspace.model.addSoftwareSystem(\"Software System 1\", \"One system to rule them all\")\n        val document = softwareSystemComponents(workspace.model.softwareSystems.single(), this.pageViewModel())\n\n        assertThat(document).isNull()\n    }\n\n    @Test\n    fun `indexes software system components`() {\n        workspace.model.addSoftwareSystem(\"Software System 1\", \"One system to rule them all\").apply {\n            addContainer(\"Container 1\", \"a container\").apply {\n                addComponent(\"Component 1\", \"a component\")\n                addComponent(\"Component 2\", \"a second component\")\n            }\n            addContainer(\"Container 2\", \"a second container\", \"tech 1\").apply {\n                addComponent(\"Component 3\", \"a third component\", \"tech 1\")\n            }\n        }\n        val document = softwareSystemComponents(workspace.model.softwareSystems.single(), this.pageViewModel())\n\n        assertThat(document).isEqualTo(\n            Document(\n                \"../software-system-1/component/\",\n                \"Component views\",\n                \"Software System 1 | Component views\",\n                \"Component 1 a component Component 2 a second component Component 3 a third component tech 1\"\n            )\n        )\n    }\n\n    @Test\n    fun `no software system relationships`() {\n        workspace.model.addSoftwareSystem(\"Software System 1\", \"One system to rule them all\")\n        val document = softwareSystemRelationships(workspace.model.softwareSystems.single(), this.pageViewModel())\n\n        assertThat(document).isNull()\n    }\n\n    @Test\n    fun `indexes software system relationships`() {\n        val system1 = workspace.model.addSoftwareSystem(\"Software System 1\", \"One system to rule them all\")\n        val system2 = workspace.model.addSoftwareSystem(\"Software System 2\", \"One system to rule the others\")\n        system1.uses(system2, \"reads\")\n        system2.uses(system1, \"writes\", \"tech 1\")\n        val document = softwareSystemRelationships(\n            workspace.model.softwareSystems.single { it.name == \"Software System 1\" },\n            this.pageViewModel()\n        )\n\n        assertThat(document).isEqualTo(\n            Document(\n                \"../software-system-1/dependencies/\",\n                \"Dependencies\",\n                \"Software System 1 | Dependencies\",\n                \"Software System 2 reads Software System 2 writes tech 1\"\n            )\n        )\n    }\n\n    @Test\n    fun `no software system decisions`() {\n        workspace.model.addSoftwareSystem(\"Software System 1\", \"One system to rule them all\")\n        val documents = softwareSystemDecisions(workspace.model.softwareSystems.single(), this.pageViewModel())\n\n        assertThat(documents).isEmpty()\n    }\n\n    @Test\n    fun `indexes software system decisions`() {\n        workspace.model.addSoftwareSystem(\"Software System 1\", \"One system to rule them all\").apply {\n            documentation.addDecision(createDecision(\"1\"))\n            documentation.addDecision(createDecision(\"2\"))\n        }\n        val documents = softwareSystemDecisions(workspace.model.softwareSystems.single(), this.pageViewModel())\n\n        assertThat(documents).containsAtLeast(\n            Document(\n                \"../software-system-1/decisions/1/\",\n                \"Software System Decision\",\n                \"Software System 1 | Decision 1\",\n                \"Decision 1 Decision 1 content\"\n            ),\n            Document(\n                \"../software-system-1/decisions/2/\",\n                \"Software System Decision\",\n                \"Software System 1 | Decision 2\",\n                \"Decision 2 Decision 2 content\"\n            )\n        )\n    }\n\n    @Test\n    fun `no software system sections`() {\n        workspace.model.addSoftwareSystem(\"Software System 1\", \"One system to rule them all\")\n        val documents = softwareSystemSections(workspace.model.softwareSystems.single(), this.pageViewModel())\n\n        assertThat(documents).isEmpty()\n    }\n\n    @Test\n    fun `only software system section for software system home`() {\n        workspace.model.addSoftwareSystem(\"Software System 1\", \"One system to rule them all\").apply {\n            documentation.addSection(Section(Format.Markdown, \"# Introduction\\nSome info\"))\n        }\n        val documents = softwareSystemSections(workspace.model.softwareSystems.single(), this.pageViewModel())\n\n        assertThat(documents).isEmpty()\n    }\n\n    @Test\n    fun `indexes software system sections`() {\n        workspace.model.addSoftwareSystem(\"Software System 1\", \"One system to rule them all\").apply {\n            documentation.addSection(Section(Format.Markdown, \"# Introduction\\nSome info\"))\n            documentation.addSection(Section(Format.Markdown, \"# Usage\\nThat's how it works\"))\n            documentation.addSection(Section(Format.Markdown, \"# History\\nThat's how we got here\"))\n        }\n        val documents = softwareSystemSections(workspace.model.softwareSystems.single(), this.pageViewModel())\n\n        assertThat(documents).containsAtLeast(\n            Document(\n                \"../software-system-1/sections/usage/\",\n                \"Software System Documentation\",\n                \"Software System 1 | Usage\",\n                \"Usage That's how it works\"\n            ),\n            Document(\n                \"../software-system-1/sections/history/\",\n                \"Software System Documentation\",\n                \"Software System 1 | History\",\n                \"History That's how we got here\"\n            )\n        )\n    }\n\n    @Test\n    fun `no container sections`() {\n        val softwareSystem = workspace.model.addSoftwareSystem(\"Software System 1\", \"One system to rule them all\")\n        softwareSystem.addContainer(\"Container 1\", \"a container\")\n        val documents = softwareSystemContainerSections(softwareSystem.containers.single(), this.pageViewModel())\n\n        assertThat(documents).isEmpty()\n    }\n\n    @Test\n    fun `no container decisions`() {\n        val softwareSystem = workspace.model.addSoftwareSystem(\"Software System 1\", \"One system to rule them all\")\n        softwareSystem.addContainer(\"Container 1\", \"a container\")\n        val documents = softwareSystemContainerDecisions(softwareSystem.containers.single(), this.pageViewModel())\n\n        assertThat(documents).isEmpty()\n    }\n\n    @Test\n    fun `indexes container decisions`() {\n        val softwareSystem = workspace.model.addSoftwareSystem(\"Software System 1\", \"One system to rule them all\")\n        val container = softwareSystem.addContainer(\"Container 1\", \"a container\").apply {\n            documentation.addDecision(createDecision(\"1\"))\n            documentation.addDecision(createDecision(\"2\"))\n        }\n        val documents = softwareSystemContainerDecisions(container, this.pageViewModel())\n\n        assertThat(documents).containsAtLeast(\n            Document(\n                \"../software-system-1/decisions/container-1/1/\",\n                \"Container Decision\",\n                \"Container 1 | Decision 1\",\n                \"Decision 1 Decision 1 content\"\n            ),\n            Document(\n                \"../software-system-1/decisions/container-1/2/\",\n                \"Container Decision\",\n                \"Container 1 | Decision 2\",\n                \"Decision 2 Decision 2 content\"\n            )\n        )\n    }\n\n    @Test\n    fun `indexes container sections`() {\n        val softwareSystem = workspace.model.addSoftwareSystem(\"Software System 1\", \"One system to rule them all\")\n        val container = softwareSystem.addContainer(\"Container 1\", \"a container\").apply {\n            documentation.addSection(Section(Format.Markdown, \"# Introduction\\nSome info\"))\n            documentation.addSection(Section(Format.Markdown, \"# Usage\\nThat's how it works\"))\n            documentation.addSection(Section(Format.Markdown, \"# History\\nThat's how we got here\"))\n        }\n        val documents = softwareSystemContainerSections(container, this.pageViewModel())\n\n        assertThat(documents).containsAtLeast(\n            Document(\n                \"../software-system-1/sections/container-1/usage/\",\n                \"Container Documentation\",\n                \"Container 1 | Usage\",\n                \"Usage That's how it works\"\n            ),\n            Document(\n                \"../software-system-1/sections/container-1/history/\",\n                \"Container Documentation\",\n                \"Container 1 | History\",\n                \"History That's how we got here\"\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/LinkViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport assertk.assertions.isFalse\nimport assertk.assertions.isTrue\nimport org.junit.jupiter.params.ParameterizedTest\nimport org.junit.jupiter.params.provider.ValueSource\nimport kotlin.test.Test\n\nclass LinkViewModelTest : ViewModelTest() {\n    @Test\n    fun `exact links are active when the page url matches exactly`() {\n        val pageViewModel = pageViewModel(\"/some-page\")\n        val viewModel = LinkViewModel(pageViewModel, \"Some page\", \"/some-page\")\n        assertThat(viewModel.active).isTrue()\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = [\"/some-page/1\", \"/\", \"/some-other-page\"])\n    fun `exact links are not active when the page url matches exactly`(href: String) {\n        val pageViewModel = pageViewModel(\"/some-page\")\n        val viewModel = LinkViewModel(pageViewModel, \"Some page\", href)\n        assertThat(viewModel.active).isFalse()\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = [\"/some-page\", \"/some-page/1\", \"/some-page/subpage/subsubpage\"])\n    fun `non-exact links are active when the page url matches partially`(pageHref: String) {\n        val pageViewModel = pageViewModel(pageHref)\n        val viewModel = LinkViewModel(pageViewModel, \"Some page\", \"/some-page\", Match.CHILD)\n        assertThat(viewModel.active).isTrue()\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = [\"/\", \"/some-other-page/1\", \"/some-other-page/subpage/subsubpage\"])\n    fun `non-exact links are not active when the page url doesn't match partially`(pageHref: String) {\n        val pageViewModel = pageViewModel(pageHref)\n        val viewModel = LinkViewModel(pageViewModel, \"Some page\", \"/some-page\", Match.CHILD)\n        assertThat(viewModel.active).isFalse()\n    }\n\n    @Test\n    fun `non-exact links are only active when page url is a subdirectory of the link`() {\n        val expectInactivePartialMatch = LinkViewModel(pageViewModel(\"/page-two\"), \"Some page\", \"/page\", Match.CHILD)\n        val expectActivePartialMatch = LinkViewModel(pageViewModel(\"/page/two\"), \"Some page\", \"/page\", Match.CHILD)\n        assertThat(expectInactivePartialMatch.active).isFalse()\n        assertThat(expectActivePartialMatch.active).isTrue()\n    }\n\n    @Test\n    fun `relative href`() {\n        val pageViewModel = pageViewModel(\"/some-page/some-subpage\")\n        val viewModel = LinkViewModel(pageViewModel, \"Some other page\", \"/some-other-page\")\n        assertThat(viewModel.relativeHref).isEqualTo(\"../../some-other-page/\")\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = [\"/some-page/sibling-page-1/\", \"/some-page/sibling-page-2/\"])\n    fun `sibling links are active when the previous url path matches`(pageHref: String) {\n        val pageViewModel = pageViewModel(pageHref)\n        val viewModel = LinkViewModel(pageViewModel, \"Some page\", \"/some-page/sibling-page-3/\", Match.SIBLING)\n        assertThat(viewModel.active).isTrue()\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = [\"/some-other-page/sibling-page-1/\", \"/some-other-page/sibling-page-2/\"])\n    fun `sibling links are not active when the previous url path doesn't match`(pageHref: String) {\n        val pageViewModel = pageViewModel(pageHref)\n        val viewModel = LinkViewModel(pageViewModel, \"Some page\", \"/some-page/sibling-page-1\", Match.SIBLING)\n        assertThat(viewModel.active).isFalse()\n    }\n\n    @Test\n    fun `sibling links are only active when previous page url path matches previous href path url`() {\n        val expectInactiveSiblingMatch = LinkViewModel(pageViewModel(\"/page/two/\"), \"Some page\", \"/some-page/\", Match.SIBLING)\n        val expectActiveSiblingMatch = LinkViewModel(pageViewModel(\"/page/two\"), \"Some page\", \"/page/one\", Match.SIBLING)\n        assertThat(expectInactiveSiblingMatch.active).isFalse()\n        assertThat(expectActiveSiblingMatch.active).isTrue()\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = [\"/some-page/sibling/child/\", \"/some-page/other-sibling/other-child/\"])\n    fun `sibling child links are active when two url paths back matches`(pageHref: String) {\n        val pageViewModel = pageViewModel(pageHref)\n        val viewModel = LinkViewModel(pageViewModel, \"Some page\", \"/some-page/another-sibling/another-child\", Match.SIBLING_CHILD)\n        assertThat(viewModel.active).isTrue()\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = [\"/some-page/sibling/child/\", \"/some-page/other-sibling/other-child/\"])\n    fun `sibling child links are not active when two url paths back doesnt match`(pageHref: String) {\n        val pageViewModel = pageViewModel(pageHref)\n        val viewModel = LinkViewModel(pageViewModel, \"Some other page\", \"/some-other-page/not-a-sibling/child/\", Match.SIBLING_CHILD)\n        assertThat(viewModel.active).isFalse()\n    }\n\n    @Test\n    fun `sibling child links are only active when two url paths back matches with two url paths back from href`() {\n        val expectInactiveSiblingChildMatch = LinkViewModel(pageViewModel(\"/page/one/description\"), \"Some page\", \"/some-page/\", Match.SIBLING_CHILD)\n        val expectActiveSiblingChildMatch = LinkViewModel(pageViewModel(\"/page/one/description\"), \"Some page\", \"/page/two/title-screen\", Match.SIBLING_CHILD)\n        assertThat(expectInactiveSiblingChildMatch.active).isFalse()\n        assertThat(expectActiveSiblingChildMatch.active).isTrue()\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/MarkdownToHtmlTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport assertk.assertions.matches\nimport com.structurizr.documentation.Format\nimport org.junit.jupiter.api.Test\n\nclass MarkdownToHtmlTest : ViewModelTest() {\n\n    @Test\n    fun `translates markdown`() {\n        val generatorContext = generatorContext()\n        val viewModel = HomePageViewModel(generatorContext)\n\n        val html = toHtml(\n            viewModel,\n            \"\"\"\n                ## header\n                content\n            \"\"\".trimIndent(),\n            Format.Markdown,\n            svgFactory\n        )\n\n        assertThat(html).isEqualTo(\n            \"\"\"\n                <h2>header</h2>\n                <p>content</p>\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `translates markdown table as default`() {\n        val generatorContext = generatorContext()\n        val viewModel = HomePageViewModel(generatorContext)\n\n        val html = toHtml(\n            viewModel,\n            \"\"\"\n                ## header\n\n                | header1 | header2 |\n                | ------- | ------- |\n                | content | content |\n            \"\"\".trimIndent(),\n            Format.Markdown,\n            svgFactory\n        )\n\n        assertThat(html).isEqualTo(\n            \"\"\"\n                <h2>header</h2>\n                <table>\n                 <thead>\n                  <tr>\n                   <th>header1</th>\n                   <th>header2</th>\n                  </tr>\n                 </thead>\n                 <tbody>\n                  <tr>\n                   <td>content</td>\n                   <td>content</td>\n                  </tr>\n                 </tbody>\n                </table>\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `doesnt translate markdown table with extension property without Tables`() {\n        val generatorContext = generatorContext().apply {\n            workspace.views.configuration.addProperty(\"generatr.markdown.flexmark.extensions\", \"Admonition\")\n        }\n        val viewModel = HomePageViewModel(generatorContext)\n\n        val html = toHtml(\n            viewModel,\n            \"\"\"\n                ## header\n\n                | header1 | header2 |\n                | ------- | ------- |\n                | content | content |\n            \"\"\".trimIndent(),\n            Format.Markdown,\n            svgFactory\n        )\n\n        assertThat(html).isEqualTo(\n            \"\"\"\n                <h2>header</h2>\n                <p>| header1 | header2 | | ------- | ------- | | content | content |</p>\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `translates markdown table with extension property containing Tables`() {\n        val generatorContext = generatorContext().apply {\n            workspace.views.configuration.addProperty(\"generatr.markdown.flexmark.extensions\", \"Admonition, Tables\")\n        }\n        val viewModel = HomePageViewModel(generatorContext)\n\n        val html = toHtml(\n            viewModel,\n            \"\"\"\n                ## header\n\n                | header1 | header2 |\n                | ------- | ------- |\n                | content | content |\n            \"\"\".trimIndent(),\n            Format.Markdown,\n            svgFactory\n        )\n\n        assertThat(html).isEqualTo(\n            \"\"\"\n                <h2>header</h2>\n                <table>\n                 <thead>\n                  <tr>\n                   <th>header1</th>\n                   <th>header2</th>\n                  </tr>\n                 </thead>\n                 <tbody>\n                  <tr>\n                   <td>content</td>\n                   <td>content</td>\n                  </tr>\n                 </tbody>\n                </table>\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `translates markdown admonition block with extension property containing Admonition`() {\n        val generatorContext = generatorContext().apply {\n            workspace.views.configuration.addProperty(\"generatr.markdown.flexmark.extensions\", \"Admonition, Tables\")\n        }\n        val viewModel = HomePageViewModel(generatorContext)\n\n        val html = toHtml(\n            viewModel,\n            \"\"\"\n                ## header\n\n                !!! faq \"FAQ\"\n                    This is a FAQ.\n            \"\"\".trimIndent(),\n            Format.Markdown,\n            svgFactory\n        )\n\n        assertThat(html).isEqualTo(\n            \"\"\"\n                <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"adm-hidden\">\n                 <symbol id=\"adm-faq\">\n                  <svg viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\n                   <path d=\"m20.4 12.2c0 4.5-3.7 8.2-8.2 8.2s-8.2-3.7-8.2-8.2 3.7-8.2 8.2-8.2 8.2 3.7 8.2 8.2zm-4.2-2.9c-.1-1.3-1-2.5-2.3-2.9-.8-.2-1.6-.1-2.4.1-.6.1-1.5.2-1.8.6-.4.5.2 1 .6 1.3.6.4 1.1-.1 1.8-.3s3-.7 2.4.8c-.5 1.1-2 1.9-3 2.6-.4.3-.9.6-1.2 1-.5.6.1 1 .7 1.4.4.3.9.2 1.2-.1.5-.5 1-.9 1.6-1.3 1.1-.7 2.5-1.6 2.4-3.2-.2-1.3 0 .7 0 0zm-2.8 6.8c-.3-.5-1.3-1.3-1.9-1.1-.4.1-.4.4-.5.8-.1.2-.4.6-.3.8.1.4 1.5 1.3 1.9 1 .1-.1.9-1.4.8-1.5 0-.1.1.1 0 0z\" fill=\"currentColor\" />\n                  </svg>\n                 </symbol>\n                </svg>\n                <h2>header</h2>\n                <div class=\"adm-block adm-faq\">\n                 <div class=\"adm-heading\">\n                  <svg class=\"adm-icon\">\n                   <use xlink:href=\"#adm-faq\" />\n                  </svg>\n                  <span>FAQ</span>\n                 </div>\n                 <div class=\"adm-body\">\n                  <p>This is a FAQ.</p>\n                 </div>\n                </div>\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `renders PlantUML code to SVG`() {\n        val generatorContext = generatorContext()\n        val viewModel = HomePageViewModel(generatorContext)\n\n        val html = toHtml(\n            viewModel,\n            \"\"\"\n                ```puml\n                @startuml\n                class Foo\n                @enduml\n                ```\n            \"\"\".trimIndent(),\n            Format.Markdown,\n            svgFactory\n        )\n\n        assertThat(html).matches(\"<div>.*<svg.*Foo.*</svg>.*</div>\".toRegex(RegexOption.DOT_MATCHES_ALL))\n    }\n\n    @Test\n    fun `translates mermaid graphings in markdown with extension property containing GitLab`() {\n        val generatorContext = generatorContext().apply {\n            workspace.views.configuration.addProperty(\"generatr.markdown.flexmark.extensions\", \"Admonition, GitLab, Tables\")\n        }\n        val viewModel = HomePageViewModel(generatorContext)\n\n        val html = toHtml(\n            viewModel,\n            \"\"\"\n                ## Mermaid\n\n                ```mermaid\n                graph TD;\n                A-->B;\n                A-->C;\n                B-->D;\n                C-->D;\n                ```\n            \"\"\".trimIndent(),\n            Format.Markdown,\n            svgFactory\n        )\n\n        assertThat(html).isEqualTo(\n            \"\"\"\n            <h2>Mermaid</h2>\n            <pre><code class=\"language-mermaid\">graph TD;\n            A--&gt;B;\n            A--&gt;C;\n            B--&gt;D;\n            C--&gt;D;\n            </code></pre>\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `translates mermaid graphings in markdown without extension property containing GitLab`() {\n        val generatorContext = generatorContext().apply {\n            workspace.views.configuration.addProperty(\"generatr.markdown.flexmark.extensions\", \"Admonition, Tables\")\n        }\n        val viewModel = HomePageViewModel(generatorContext)\n\n        val html = toHtml(\n            viewModel,\n            \"\"\"\n                ## Mermaid\n\n                ```mermaid\n                graph TD;\n                A-->B;\n                A-->C;\n                B-->D;\n                C-->D;\n                ```\n            \"\"\".trimIndent(),\n            Format.Markdown,\n            svgFactory\n        )\n\n        assertThat(html).isEqualTo(\n            \"\"\"\n            <h2>Mermaid</h2>\n            <pre><code class=\"language-mermaid\">graph TD;\n            A--&gt;B;\n            A--&gt;C;\n            B--&gt;D;\n            C--&gt;D;\n            </code></pre>\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `embedded diagram`() {\n        val generatorContext = generatorContext()\n        val viewModel = HomePageViewModel(generatorContext)\n\n        val html = toHtml(viewModel, \"![System Landscape Diagram](embed:SystemLandscape)\", Format.Markdown, svgFactory)\n\n        assertThat(html).isEqualTo(\n            \"\"\"\n                <p>\n                 <div>\n                  <figure style=\"width: min(100%, 800px);\" id=\"SystemLandscape\">\n                   <div>\n                    <svg viewBox=\"0 0 800 900\"></svg>\n                   </div>\n                   <figcaption>\n                    <a onclick=\"openSvgModal('SystemLandscape-modal', 'SystemLandscape-svg')\">System Landscape Diagram</a>\n                   </figcaption>\n                  </figure>\n                  <div class=\"modal\" id=\"SystemLandscape-modal\">\n                   <div class=\"modal-background\" onclick=\"closeModal('SystemLandscape-modal')\"></div>\n                   <div class=\"modal-content\">\n                    <div class=\"box\">\n                     <div id=\"SystemLandscape-svg\" class=\"modal-box-content\">\n                      <svg viewBox=\"0 0 800 900\"></svg>\n                     </div>\n                     <div class=\"has-text-centered\">\n                      System Landscape Diagram [<a href=\"svg/SystemLandscape.svg\" target=\"_blank\">svg</a>|<a href=\"png/SystemLandscape.png\" target=\"_blank\">png</a>|<a href=\"puml/SystemLandscape.puml\" target=\"_blank\">puml</a>]\n                     </div>\n                    </div>\n                   </div>\n                   <button class=\"modal-close is-large\" aria-label=\"close\" onclick=\"closeModal('SystemLandscape-modal')\"></button>\n                  </div>\n                 </div>\n                </p>\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `embedded diagram key not found`() {\n        val generatorContext = generatorContext()\n        val viewModel = HomePageViewModel(generatorContext)\n\n        val html = toHtml(viewModel, \"![Diagram](embed:non-existing)\", Format.Markdown) { _, _ ->\n            null\n        }\n\n        assertThat(html).isEqualTo(\n            \"\"\"\n                <p>\n                 <div>\n                  <div class=\"notification is-danger\">\n                   No view with key<span class=\"has-text-weight-bold\"> non-existing </span>found!\n                  </div>\n                 </div>\n                </p>\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `links to pages`() {\n        val generatorContext = generatorContext()\n        val viewModel = HomePageViewModel(generatorContext)\n\n        val html = toHtml(\n            viewModel,\n            \"\"\"\n                ## header\n                Link: [Some decision](/decisions/1/),\n            \"\"\".trimIndent(),\n            Format.Markdown,\n            svgFactory\n        )\n\n        assertThat(html).isEqualTo(\n            \"\"\"\n                <h2>header</h2>\n                <p>Link: <a href=\"decisions/1/\">Some decision</a>,</p>\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `links to files`() {\n        val generatorContext = generatorContext()\n        val viewModel = HomePageViewModel(generatorContext)\n\n        val html = toHtml(\n            viewModel,\n            \"\"\"\n                ## header\n                Link: [Some document](/document.md),\n            \"\"\".trimIndent(),\n            Format.Markdown,\n            svgFactory\n        )\n\n        assertThat(html).isEqualTo(\n            \"\"\"\n                <h2>header</h2>\n                <p>Link: <a href=\"document.md\">Some document</a>,</p>\n            \"\"\".trimIndent()\n        )\n    }\n\n    @Test\n    fun `links to external sites`() {\n        val generatorContext = generatorContext()\n        val viewModel = HomePageViewModel(generatorContext)\n\n        val html = toHtml(\n            viewModel,\n            \"\"\"\n                ## header\n                Link: [Wikipedia](https://www.wikipedia.org/),\n            \"\"\".trimIndent(),\n            Format.Markdown,\n            svgFactory\n        )\n\n        assertThat(html).isEqualTo(\n            \"\"\"\n                <h2>header</h2>\n                <p>Link: <a href=\"https://www.wikipedia.org/\" target=\"blank\">Wikipedia</a>,</p>\n            \"\"\".trimIndent()\n        )\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.containsExactly\nimport assertk.assertions.hasSize\nimport assertk.assertions.isEqualTo\nimport assertk.assertions.isTrue\nimport com.structurizr.documentation.Decision\nimport com.structurizr.documentation.Format\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\nimport org.junit.jupiter.api.Test\nimport org.junit.jupiter.params.ParameterizedTest\nimport org.junit.jupiter.params.provider.ValueSource\n\nclass MenuViewModelTest : ViewModelTest() {\n    @ParameterizedTest\n    @ValueSource(strings = [\"main\", \"branch-2\"])\n    fun `fixed general items`(currentBranch: String) {\n        val generatorContext = generatorContext(branches = listOf(\"main\", \"branch-2\"), currentBranch = currentBranch)\n        val pageViewModel = createPageViewModel(generatorContext)\n        val viewModel = MenuViewModel(generatorContext, pageViewModel)\n\n        assertThat(viewModel.generalItems).containsExactly(\n            LinkViewModel(pageViewModel, \"Home\", HomePageViewModel.url())\n        )\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = [\"main\", \"branch-2\"])\n    fun `software systems menu item if available`(currentBranch: String) {\n        val generatorContext = generatorContext(branches = listOf(\"main\", \"branch-2\"), currentBranch = currentBranch)\n            .apply {\n                workspace.model.addSoftwareSystem(\"Software system 1\")\n            }\n        val pageViewModel = createPageViewModel(generatorContext)\n        val viewModel = MenuViewModel(generatorContext, pageViewModel)\n\n        assertThat(viewModel.generalItems[1]).isEqualTo(\n            LinkViewModel(pageViewModel, \"Software Systems\", SoftwareSystemsPageViewModel.url())\n        )\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = [\"main\", \"branch-2\"])\n    fun `decisions menu item if available`(currentBranch: String) {\n        val generatorContext =\n            generatorContext(branches = listOf(\"main\", \"branch-2\"), currentBranch = currentBranch).apply {\n                workspace.documentation.addDecision(Decision(\"1\").apply {\n                    title = \"Decision 1\"\n                    status = \"Proposed\"\n                    format = Format.Markdown\n                    content = \"Content\"\n                })\n            }\n        val pageViewModel = createPageViewModel(generatorContext)\n        val viewModel = MenuViewModel(generatorContext, pageViewModel)\n\n        assertThat(viewModel.generalItems[1]).isEqualTo(\n            LinkViewModel(pageViewModel, \"Decisions\", WorkspaceDecisionsPageViewModel.url(), Match.CHILD)\n        )\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = [\"main\", \"branch-2\"])\n    fun `workspace-level documentation in general section`(currentBranch: String) {\n        val generatorContext = generatorContext(branches = listOf(\"main\", \"branch-2\"), currentBranch = currentBranch)\n        generatorContext.workspace.documentation.addSection(createSection(\"# Home\"))\n        val section1 = createSection(\"# Doc 1\")\n            .also { generatorContext.workspace.documentation.addSection(it) }\n        val section2 = createSection(\"# Doc Title 2\")\n            .also { generatorContext.workspace.documentation.addSection(it) }\n        val pageViewModel = createPageViewModel(generatorContext)\n        val viewModel = MenuViewModel(generatorContext, pageViewModel)\n\n        assertThat(viewModel.generalItems.drop(1)).containsExactly(\n            LinkViewModel(pageViewModel, \"Doc 1\", WorkspaceDocumentationSectionPageViewModel.url(section1)),\n            LinkViewModel(pageViewModel, \"Doc Title 2\", WorkspaceDocumentationSectionPageViewModel.url(section2))\n        )\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = [\"main\", \"branch-2\"])\n    fun `links to software system pages sorted alphabetically case insensitive`(currentBranch: String) {\n        val generatorContext = generatorContext(branches = listOf(\"main\", \"branch-2\"), currentBranch = currentBranch)\n        val system2 = generatorContext.workspace.model.addSoftwareSystem(\"System 2\", \"\")\n        val system1 = generatorContext.workspace.model.addSoftwareSystem(\"system 1\", \"\")\n        val pageViewModel = createPageViewModel(generatorContext)\n        val viewModel = MenuViewModel(generatorContext, pageViewModel)\n\n        assertThat(viewModel.softwareSystemItems).containsExactly(\n            LinkViewModel(\n                pageViewModel,\n                \"system 1\",\n                SoftwareSystemPageViewModel.url(system1, SoftwareSystemPageViewModel.Tab.HOME),\n                Match.CHILD\n            ),\n            LinkViewModel(\n                pageViewModel,\n                \"System 2\",\n                SoftwareSystemPageViewModel.url(system2, SoftwareSystemPageViewModel.Tab.HOME),\n                Match.CHILD\n            )\n        )\n    }\n\n    @ParameterizedTest\n    @ValueSource(strings = [\"main\", \"branch-2\"])\n    fun `active links`(currentBranch: String) {\n        val generatorContext = generatorContext(branches = listOf(\"main\", \"branch-2\"), currentBranch = currentBranch)\n        val system = generatorContext.workspace.model.addSoftwareSystem(\"System 1\", \"\")\n\n        MenuViewModel(generatorContext, createPageViewModel(generatorContext, url = HomePageViewModel.url()))\n            .let { assertThat(it.generalItems[0].active).isTrue() }\n        MenuViewModel(\n            generatorContext, createPageViewModel(\n                generatorContext, url = SoftwareSystemPageViewModel.url(\n                    system,\n                    SoftwareSystemPageViewModel.Tab.HOME\n                )\n            )\n        )\n            .let { assertThat(it.softwareSystemItems[0].active).isTrue() }\n    }\n\n    @Test\n    fun `do not show menu entries for software systems with an external location (declared external by tag)`() {\n        val generatorContext = generatorContext(branches = listOf(\"main\", \"branch-2\"), currentBranch = \"main\")\n        generatorContext.workspace.views.configuration.addProperty(\"generatr.site.externalTag\", \"External System\")\n        generatorContext.workspace.model.addSoftwareSystem(\"System 1\").apply { group = \"Group 1\" }\n        generatorContext.workspace.model.addSoftwareSystem(\"External system\").apply { addTags(\"External System\") }\n\n        MenuViewModel(generatorContext, createPageViewModel(generatorContext, url = HomePageViewModel.url()))\n            .let {\n                assertThat(it.softwareSystemNodes().children).hasSize(1)\n                assertThat(it.softwareSystemNodes().children[0].name).isEqualTo(\"Group 1\")\n                assertThat(it.softwareSystemNodes().children[0].children).hasSize(1)\n                assertThat(it.softwareSystemNodes().children[0].children[0].name).isEqualTo(\"System 1\")\n            }\n    }\n\n    @Test\n    fun `show software systems in nested groups from single group in software systems list`() {\n        val generatorContext = generatorContext(branches = listOf(\"main\", \"branch-2\"), currentBranch = \"main\")\n        generatorContext.workspace.views.configuration.addProperty(\"generatr.site.nestGroups\", \"true\")\n        generatorContext.workspace.model.addSoftwareSystem(\"System 1\").apply { group = \"Group 1\" }\n\n        MenuViewModel(generatorContext, createPageViewModel(generatorContext, url = HomePageViewModel.url()))\n            .let {\n                assertThat(it.softwareSystemNodes().children).hasSize(1)\n                assertThat(it.softwareSystemNodes().children[0].name).isEqualTo(\"Group 1\")\n                assertThat(it.softwareSystemNodes().children[0].children).hasSize(1)\n                assertThat(it.softwareSystemNodes().children[0].children[0].name).isEqualTo(\"System 1\")\n            }\n    }\n\n    @Test\n    fun `show nested groups in software systems list`() {\n        val generatorContext = generatorContext(branches = listOf(\"main\", \"branch-2\"), currentBranch = \"main\")\n        generatorContext.workspace.views.configuration.addProperty(\"generatr.site.nestGroups\", \"true\")\n        generatorContext.workspace.model.addProperty(\"structurizr.groupSeparator\", \"/\")\n        generatorContext.workspace.model.addSoftwareSystem(\"System 1\").apply { group = \"Group 1\" }\n        generatorContext.workspace.model.addSoftwareSystem(\"System 2\").apply { group = \"Group 1\" }\n        generatorContext.workspace.model.addSoftwareSystem(\"System 3\").apply { group = \"Group 2\" }\n        generatorContext.workspace.model.addSoftwareSystem(\"System 4\").apply { group = \"Group 1/Group 3\" }\n\n        MenuViewModel(generatorContext, createPageViewModel(generatorContext, url = HomePageViewModel.url()))\n            .let {\n                assertThat(it.softwareSystemNodes().children).hasSize(2)\n                assertThat(it.softwareSystemNodes().children[0].name).isEqualTo(\"Group 1\")\n                assertThat(it.softwareSystemNodes().children[0].children).hasSize(3)\n                assertThat(it.softwareSystemNodes().children[0].children[0].name).isEqualTo(\"Group 3\")\n                assertThat(it.softwareSystemNodes().children[0].children[0].children[0].name).isEqualTo(\"System 4\")\n            }\n    }\n\n    private fun createPageViewModel(generatorContext: GeneratorContext, url: String = \"/master/page\"): PageViewModel {\n        return object : PageViewModel(generatorContext) {\n            override val url = url\n            override val pageSubTitle = \"subtitle\"\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/PageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport org.junit.jupiter.api.Test\n\nclass PageViewModelTest : ViewModelTest() {\n\n    @Test\n    fun `page title`() {\n        assertThat(pageViewModel().pageTitle).isEqualTo(\"Some page | Workspace name\")\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/PropertiesTableViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport kotlin.test.Test\n\nclass PropertiesTableViewModelTest : ViewModelTest() {\n\n    @Test\n    fun `no properties available`() {\n        assertThat(createPropertiesTableViewModel(emptyMap()))\n            .isEqualTo(\n                TableViewModel.create {\n                    propertiesTableHeaderRow()\n                }\n            )\n    }\n\n    @Test\n    fun `many properties sorted by key`() {\n        val properties = mapOf(\n            \"b name\" to \"value\",\n            \"a name\" to \"value\"\n        )\n\n        assertThat(createPropertiesTableViewModel(properties)).isEqualTo(\n            TableViewModel.create {\n                propertiesTableHeaderRow()\n                bodyRow(\n                    cell(\"a name\"),\n                    cell(\"value\"),\n                )\n                bodyRow(\n                    cell(\"b name\"),\n                    cell(\"value\"),\n                )\n            }\n        )\n    }\n\n    @Test\n    fun `properties with link`() {\n        val properties = mapOf(\n            \"url\" to \"https://tempuri.org/\",\n        )\n\n        assertThat(createPropertiesTableViewModel(properties)).isEqualTo(\n            TableViewModel.create {\n                propertiesTableHeaderRow()\n                bodyRow(\n                    cell(\"url\"),\n                    cellWithExternalLink(\"https://tempuri.org/\", \"https://tempuri.org/\"),\n                )\n            }\n        )\n    }\n\n    private fun TableViewModel.TableViewInitializerContext.propertiesTableHeaderRow() {\n        headerRow(headerCellMedium(\"Name\"), headerCell(\"Value\"))\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SearchViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.containsExactly\nimport assertk.assertions.hasSize\nimport assertk.assertions.isEmpty\nimport assertk.assertions.isEqualTo\nimport com.structurizr.documentation.Format\nimport com.structurizr.documentation.Section\nimport org.junit.jupiter.api.Test\n\nclass SearchViewModelTest : ViewModelTest() {\n    @Test\n    fun `no index language configured`() {\n        val viewModel = SearchViewModel(generatorContext())\n\n        assertThat(viewModel.language).isEmpty()\n    }\n\n    @Test\n    fun `index language configured`() {\n        val generatorContext = generatorContext().apply {\n            workspace.views.configuration.addProperty(\"generatr.search.language\", \"nl\")\n        }\n        val viewModel = SearchViewModel(generatorContext)\n\n        assertThat(viewModel.language).isEqualTo(\"nl\")\n    }\n\n    @Test\n    fun `nothing to index`() {\n        val viewModel = SearchViewModel(generatorContext())\n\n        assertThat(viewModel.documents).hasSize(0)\n    }\n\n    @Test\n    fun `indexes global pages`() {\n        val generatorContext = generatorContext()\n        generatorContext.workspace.apply {\n            documentation.addSection(createSection(\"# Home\\nSome info\"))\n            documentation.addSection(createSection(\"# Landscape\\nMore info\"))\n            documentation.addDecision(createDecision())\n        }\n        val viewModel = SearchViewModel(generatorContext)\n\n        assertThat(viewModel.documents.map { it.type })\n            .containsExactly(\"Home\", \"Workspace Decision\", \"Workspace Documentation\")\n    }\n\n    @Test\n    fun `indexes software system without additional information`() {\n        val generatorContext = generatorContext()\n        generatorContext.workspace.apply {\n            model.addSoftwareSystem(\"Software system\")\n        }\n        val viewModel = SearchViewModel(generatorContext)\n\n        assertThat(viewModel.documents.map { it.type })\n            .containsExactly(\"Context views\")\n    }\n\n    @Test\n    fun `indexes no external software system (declared external by tag)`() {\n        val generatorContext = generatorContext()\n        generatorContext.workspace.apply {\n            views.configuration.addProperty(\"generatr.site.externalTag\", \"External System\")\n            model.addSoftwareSystem(\"Software system\").apply { addTags(\"External System\") }\n        }\n        val viewModel = SearchViewModel(generatorContext)\n\n        assertThat(viewModel.documents.map { it.type })\n            .isEmpty()\n    }\n\n    @Test\n    fun `indexes all software system information`() {\n        val generatorContext = generatorContext()\n        generatorContext.workspace.apply {\n            model.addSoftwareSystem(\"Software system\").apply {\n                documentation.addSection(Section(Format.Markdown, \"# Introduction\\nSome info\"))\n                addContainer(\"Container 1\", \"a container\").apply {\n                    addComponent(\"Component 1\", \"a component\")\n                    documentation.addDecision(createDecision(\"1\"))\n                    documentation.addSection(Section(Format.Markdown, \"# Component Usage\\nThat's how it works\"))\n                }\n                documentation.addDecision(createDecision(\"1\"))\n                documentation.addSection(Section(Format.Markdown, \"# Usage\\nThat's how it works\"))\n            }\n        }\n        val viewModel = SearchViewModel(generatorContext)\n\n        assertThat(viewModel.documents.map { it.type })\n            .containsExactly(\n                \"Software System Info\",\n                \"Context views\",\n                \"Container views\",\n                \"Component views\",\n                \"Software System Decision\",\n                \"Software System Documentation\",\n                \"Container Documentation\",\n                \"Container Decision\"\n            )\n    }\n\n    @Test\n    fun `indexes software systems with relations`() {\n        val generatorContext = generatorContext()\n        generatorContext.workspace.apply {\n            val system1 = model.addSoftwareSystem(\"Software System 1\", \"One system to rule them all\")\n            val system2 = model.addSoftwareSystem(\"Software System 2\", \"One system to rule the others\")\n            system1.uses(system2, \"reads\")\n            system2.uses(system1, \"writes\", \"tech 1\")\n        }\n        val viewModel = SearchViewModel(generatorContext)\n\n        assertThat(viewModel.documents.map { it.type })\n            .containsExactly(\"Context views\", \"Dependencies\", \"Context views\", \"Dependencies\")\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentCodePageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.hasSize\nimport assertk.assertions.isEqualTo\nimport assertk.assertions.isFalse\nimport assertk.assertions.isTrue\nimport com.structurizr.model.Container\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.structurizr.site.generatr.normalize\nimport org.junit.jupiter.api.Test\n\nclass SoftwareSystemContainerComponentCodePageViewModelTest : ViewModelTest() {\n    private val generatorContext = generatorContext()\n    private val softwareSystem: SoftwareSystem = generatorContext.workspace.model\n        .addSoftwareSystem(\"Software system\").also {\n            val backend = it.addContainer(\"Backend\")\n            val frontend = it.addContainer(\"Frontend\")\n            generatorContext.workspace.views.createComponentView(backend, \"component-1-backend\", \"Component view 1 - Backend\")\n            generatorContext.workspace.views.createComponentView(backend, \"component-2-backend\", \"Component view 2 - Backend\")\n\n            generatorContext.workspace.views.createComponentView(frontend, \"component-1-frontend\", \"Component view 1 - Frontend\")\n            generatorContext.workspace.views.createComponentView(frontend, \"component-2-frontend\", \"Component view 2 - Frontend\")\n        }\n    private val backendContainer: Container = softwareSystem.containers.elementAt(0)\n    private val frontendContainer: Container = softwareSystem.containers.elementAt(1)\n    private val backendComponent = backendContainer.addComponent(\"Backend Component\")\n    private val backendComponent2 = backendContainer.addComponent(\"Java Application\")\n    private val frontendComponent = frontendContainer.addComponent(\"Frontend Component\")\n    private val backendImageView = createImageView(generatorContext.workspace, backendComponent)\n    private val backendImageView2 = createImageView(generatorContext.workspace, backendComponent2)\n    private val frontendImageView = createImageView(generatorContext.workspace, frontendComponent)\n\n    @Test\n    fun `active tab`() {\n        val viewModel = SoftwareSystemContainerComponentCodePageViewModel(generatorContext, backendContainer, backendComponent)\n\n        assertThat(viewModel.tabs.single { it.link.active }.tab)\n            .isEqualTo(SoftwareSystemPageViewModel.Tab.CODE)\n    }\n\n    @Test\n    fun `container tabs`() {\n        val viewModel = SoftwareSystemContainerComponentCodePageViewModel(generatorContext, backendContainer, backendComponent)\n        val containerTabList = listOf(\n            ContainerTabViewModel(viewModel, \"Backend\", \"/software-system/code/backend/backend-component\", Match.SIBLING),\n            ContainerTabViewModel(viewModel, \"Frontend\", \"/software-system/code/frontend/frontend-component\", Match.SIBLING)\n        )\n        assertThat(viewModel.containerTabs.elementAtOrNull(0)).isEqualTo(containerTabList.elementAt(0))\n        assertThat(viewModel.containerTabs.elementAtOrNull(1)).isEqualTo(containerTabList.elementAt(1))\n    }\n\n    @Test\n    fun `component tabs`() {\n        val viewModel = SoftwareSystemContainerComponentCodePageViewModel(generatorContext, backendContainer, backendComponent)\n        val componentTabList = listOf(\n            ComponentTabViewModel(viewModel, \"Backend Component\", \"/software-system/code/backend/backend-component\"),\n            ComponentTabViewModel(viewModel, \"Java Application\", \"/software-system/code/backend/java-application\")\n        )\n        assertThat(viewModel.componentTabs.elementAtOrNull(0)).isEqualTo(componentTabList.elementAt(0))\n        assertThat(viewModel.componentTabs.elementAtOrNull(1)).isEqualTo(componentTabList.elementAt(1))\n    }\n\n    @Test\n    fun url() {\n        var viewModel = SoftwareSystemContainerComponentCodePageViewModel(generatorContext, backendContainer, backendComponent)\n        assertThat(SoftwareSystemContainerComponentCodePageViewModel.url(backendContainer, backendComponent))\n            .isEqualTo(\"/${softwareSystem.name.normalize()}/code/${backendContainer.name.normalize()}/${backendComponent.name.normalize()}\")\n        assertThat(viewModel.url)\n            .isEqualTo(SoftwareSystemContainerComponentCodePageViewModel.url(backendContainer, backendComponent))\n\n        viewModel = SoftwareSystemContainerComponentCodePageViewModel(generatorContext, frontendContainer, frontendComponent)\n        assertThat(SoftwareSystemContainerComponentCodePageViewModel.url(frontendContainer, frontendComponent))\n            .isEqualTo(\"/${softwareSystem.name.normalize()}/code/${frontendContainer.name.normalize()}/${frontendComponent.name.normalize()}\")\n        assertThat(viewModel.url)\n            .isEqualTo(SoftwareSystemContainerComponentCodePageViewModel.url(frontendContainer, frontendComponent))\n    }\n\n    @Test\n    fun `has image`() {\n        var viewModel = SoftwareSystemContainerComponentCodePageViewModel(generatorContext, backendContainer, backendComponent)\n\n        assertThat(viewModel.visible).isTrue()\n        assertThat(viewModel.images).hasSize(1)\n        assertThat(viewModel.images.single().imageView).isEqualTo(backendImageView)\n\n        viewModel = SoftwareSystemContainerComponentCodePageViewModel(generatorContext, backendContainer, backendComponent2)\n\n        assertThat(viewModel.visible).isTrue()\n        assertThat(viewModel.images).hasSize(1)\n        assertThat(viewModel.images.single().imageView).isEqualTo(backendImageView2)\n\n        viewModel = SoftwareSystemContainerComponentCodePageViewModel(generatorContext, frontendContainer, frontendComponent)\n\n        assertThat(viewModel.visible).isTrue()\n        assertThat(viewModel.images).hasSize(1)\n        assertThat(viewModel.images.single().imageView).isEqualTo(frontendImageView)\n    }\n\n    @Test\n    fun `hidden view`() {\n        val viewModel = SoftwareSystemContainerComponentCodePageViewModel(\n            generatorContext,\n            softwareSystem.addContainer(\"Container\"),\n            backendContainer.addComponent(\"Component\")\n        )\n\n        assertThat(viewModel.visible).isFalse()\n    }\n\n    @Test\n    fun `no index`() {\n        val viewModel = SoftwareSystemContainerComponentCodePageViewModel(generatorContext, backendContainer, backendComponent)\n        assertThat(viewModel.diagramIndex.visible).isFalse()\n    }\n\n    @Test\n    fun `index visible`() {\n        createImageView(generatorContext.workspace, backendComponent, \"other-imageview\")\n        val viewModel = SoftwareSystemContainerComponentCodePageViewModel(generatorContext, backendContainer, backendComponent)\n\n        assertThat(viewModel.diagramIndex.visible).isTrue()\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentDecisionPageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport com.structurizr.documentation.Format\nimport kotlin.test.Test\n\nclass SoftwareSystemContainerComponentDecisionPageViewModelTest : ViewModelTest() {\n    private val generatorContext = generatorContext()\n    private val softwareSystem = generatorContext.workspace.model.addSoftwareSystem(\"Software System\")\n    private val container = softwareSystem.addContainer(\"API Application\")\n    private val component = container.addComponent(\"Email Component\")\n\n    @Test\n    fun url() {\n        val decision = createDecision()\n\n        val viewModel = SoftwareSystemContainerComponentDecisionPageViewModel(generatorContext, component, decision)\n\n        assertThat(viewModel.url)\n            .isEqualTo(\"/software-system/decisions/api-application/email-component/1\")\n    }\n\n    @Test\n    fun `active tab`() {\n        val viewModel = SoftwareSystemContainerComponentDecisionPageViewModel(generatorContext, component, createDecision())\n\n        assertThat(viewModel.tabs.single { it.link.active }.tab)\n            .isEqualTo(SoftwareSystemPageViewModel.Tab.DECISIONS)\n    }\n\n    @Test\n    fun content() {\n        val decision = createDecision()\n\n        val viewModel = SoftwareSystemContainerComponentDecisionPageViewModel(generatorContext, component, decision)\n\n        assertThat(viewModel.content).isEqualTo(toHtml(viewModel, decision.content, Format.Markdown, svgFactory))\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentDecisionsPageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport assertk.assertions.isFalse\nimport assertk.assertions.isTrue\nimport com.structurizr.model.Component\nimport com.structurizr.model.Container\nimport com.structurizr.model.SoftwareSystem\nimport kotlin.test.Test\n\nclass SoftwareSystemContainerComponentDecisionsPageViewModelTest : ViewModelTest() {\n    private val generatorContext = generatorContext()\n    private val softwareSystem: SoftwareSystem = generatorContext.workspace.model.addSoftwareSystem(\"Software system\")\n    private val container: Container = softwareSystem.addContainer(\"API Application\")\n    private val component: Component = container.addComponent(\"Email Component\")\n\n    @Test\n    fun `active tab`() {\n        val viewModel = SoftwareSystemContainerComponentDecisionsPageViewModel(generatorContext, component)\n\n        assertThat(viewModel.tabs.single { it.link.active }.tab)\n            .isEqualTo(SoftwareSystemPageViewModel.Tab.DECISIONS)\n    }\n\n    @Test\n    fun `decisions tabs`() {\n        softwareSystem.documentation.addDecision(createDecision(\"1\", \"Accepted\"))\n\n        container.documentation.addDecision(createDecision(\"1\", \"Accepted\"))\n\n        val viewModel = SoftwareSystemContainerComponentDecisionsPageViewModel(generatorContext, component)\n\n        assertThat(viewModel.decisionTabs)\n            .isEqualTo(listOf(\n                DecisionTabViewModel(\n                    viewModel,\n                    \"System\",\n                    \"/software-system/decisions\"\n                ),\n                DecisionTabViewModel(\n                    viewModel,\n                    \"API Application\",\n                    \"/software-system/decisions/api-application\",\n                    Match.CHILD\n                )\n            ))\n    }\n\n    @Test\n    fun `decisions tabs without container decisions`() {\n        softwareSystem.documentation.addDecision(createDecision(\"1\", \"Accepted\"))\n\n        val viewModel = SoftwareSystemContainerComponentDecisionsPageViewModel(generatorContext, component)\n\n        assertThat(viewModel.decisionTabs)\n            .isEqualTo(listOf(\n                DecisionTabViewModel(\n                    viewModel,\n                    \"System\",\n                    \"/software-system/decisions\"\n                )\n            ))\n    }\n\n    @Test\n    fun `decisions tabs without software system decisions`() {\n        container.documentation.addDecision(createDecision(\"1\", \"Accepted\"))\n\n        val viewModel = SoftwareSystemContainerComponentDecisionsPageViewModel(generatorContext, component)\n\n        assertThat(viewModel.decisionTabs)\n            .isEqualTo(listOf(\n                DecisionTabViewModel(\n                    viewModel,\n                    \"API Application\",\n                    \"/software-system/decisions/api-application\",\n                    Match.CHILD\n                )\n            ))\n    }\n\n    @Test\n    fun `component decisions tabs`() {\n        container.documentation.addDecision(createDecision(\"1\", \"Accepted\"))\n        createComponentDecisions()\n\n        val viewModel = SoftwareSystemContainerComponentDecisionsPageViewModel(generatorContext, component)\n\n        assertThat(viewModel.componentDecisionsTabs)\n            .isEqualTo(listOf(\n                DecisionTabViewModel(\n                    viewModel,\n                    \"Container\",\n                    \"/software-system/decisions/api-application\"\n                ),\n                DecisionTabViewModel(\n                    viewModel,\n                    \"Email Component\",\n                    \"/software-system/decisions/api-application/email-component\"\n                )\n            ))\n    }\n\n    @Test\n    fun `component decisions tabs without container decisions`() {\n        createComponentDecisions()\n\n        val viewModel = SoftwareSystemContainerComponentDecisionsPageViewModel(generatorContext, component)\n\n        assertThat(viewModel.componentDecisionsTabs)\n            .isEqualTo(listOf(\n                DecisionTabViewModel(\n                    viewModel,\n                    \"Email Component\",\n                    \"/software-system/decisions/api-application/email-component\"\n                )\n            ))\n    }\n\n    @Test\n    fun `component decisions tabs without component decisions`() {\n        container.documentation.addDecision(createDecision(\"1\", \"Accepted\"))\n\n        val viewModel = SoftwareSystemContainerComponentDecisionsPageViewModel(generatorContext, component)\n\n        assertThat(viewModel.componentDecisionsTabs)\n            .isEqualTo(listOf(\n                DecisionTabViewModel(\n                    viewModel,\n                    \"Container\",\n                    \"/software-system/decisions/api-application\"\n                )\n            ))\n    }\n\n    @Test\n    fun `decisions table`() {\n        createComponentDecisions()\n\n        val viewModel = SoftwareSystemContainerComponentDecisionsPageViewModel(generatorContext, component)\n\n        var counter = 1\n\n        assertThat(viewModel.decisionsTable)\n            .isEqualTo(\n                viewModel.createDecisionsTableViewModel(component.documentation.decisions) {\n                    \"/software-system/decisions/api-application/email-component/${counter++}\"\n                }\n            )\n    }\n\n    @Test\n    fun `is visible`() {\n        createComponentDecisions()\n\n        val viewModel = SoftwareSystemContainerComponentDecisionsPageViewModel(generatorContext, component)\n\n        assertThat(viewModel.visible).isTrue()\n    }\n\n    @Test\n    fun `hidden view`() {\n        val viewModel = SoftwareSystemContainerComponentDecisionsPageViewModel(generatorContext, component)\n\n        assertThat(viewModel.visible).isFalse()\n    }\n\n    private fun createComponentDecisions() {\n        component.documentation.addDecision(createDecision(\n            \"1\",\n            \"Accepted\"\n        ))\n        component.documentation.addDecision(createDecision(\n            \"2\",\n            \"Superseded by [3. Another Realisation of Feature 1](0003-another-realisation-of-feature-1.md)\"\n        ))\n        component.documentation.addDecision(createDecision(\n            \"3\",\n            \"Accepted\\n\\nSupersedes [2. Implement Feature 1](0002-implement-feature-1.md)\"\n        ))\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentSectionPageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport com.structurizr.documentation.Format\nimport nl.avisi.structurizr.site.generatr.normalize\nimport kotlin.test.Test\n\nclass SoftwareSystemContainerComponentSectionPageViewModelTest : ViewModelTest() {\n    private val generatorContext = generatorContext()\n    private val softwareSystem = generatorContext.workspace.model.addSoftwareSystem(\"Software system\").also {\n        it.addContainer(\"API Application\").apply {\n            documentation.addSection(createSection())\n\n            addComponent(\"Some Component\").apply {\n                documentation.addSection(createSection())\n            }\n        }\n    }\n\n    private val component = softwareSystem.containers.first().components.first()\n    private val section = component.documentation.sections.first()\n\n    @Test\n    fun url() {\n        val viewModel = SoftwareSystemContainerComponentSectionPageViewModel(generatorContext, component, section)\n\n        assertThat(SoftwareSystemContainerComponentSectionPageViewModel.url(component, section))\n            .isEqualTo(\"/software-system/sections/api-application/some-component/content\")\n        assertThat(viewModel.url)\n            .isEqualTo(SoftwareSystemContainerComponentSectionPageViewModel.url(component, section))\n    }\n\n    @Test\n    fun `active tab`() {\n        val viewModel = SoftwareSystemContainerComponentSectionPageViewModel(generatorContext, component, section)\n        assertThat(viewModel.tabs.single { it.link.active }.tab)\n            .isEqualTo(SoftwareSystemPageViewModel.Tab.SECTIONS)\n    }\n\n    @Test\n    fun content() {\n        val viewModel = SoftwareSystemContainerComponentSectionPageViewModel(generatorContext, component, section)\n\n        assertThat(viewModel.content).isEqualTo(toHtml(viewModel, section.content, Format.Markdown, svgFactory))\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentSectionsPageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.all\nimport assertk.assertThat\nimport assertk.assertions.*\nimport kotlin.test.BeforeTest\nimport kotlin.test.Test\n\nclass SoftwareSystemContainerComponentSectionsPageViewModelTest : ViewModelTest() {\n    private val generatorContext = generatorContext()\n    private val softwareSystem = generatorContext.workspace.model.addSoftwareSystem(\"Software system\").also {\n        it.documentation.addSection(createSection())\n        it.documentation.addSection(createSection())\n\n        it.addContainer(\"API Application\").apply {\n            documentation.addSection(createSection())\n\n            addComponent(\"Some Component\").apply {\n                documentation.addSection(createSection())\n            }\n        }\n    }\n\n    private val component = softwareSystem.containers.first().components.first()\n\n    @Test\n    fun `active tab documentation`() {\n        val viewModel = SoftwareSystemContainerComponentSectionsPageViewModel(generatorContext, component)\n        assertThat(viewModel.tabs.single { it.link.active }.tab).isEqualTo(SoftwareSystemPageViewModel.Tab.SECTIONS)\n    }\n\n    @Test\n    fun `active tab component`() {\n        val viewModel = SoftwareSystemContainerComponentSectionsPageViewModel(generatorContext, component)\n        assertThat(viewModel.sectionsTabs.single { it.link.active }.title).isEqualTo(\"API Application\")\n        assertThat(viewModel.sectionsTabs.single { it.title == \"System\" }.link.active).isFalse()\n    }\n\n    @Test\n    fun `active tab component section`() {\n        val viewModel = SoftwareSystemContainerComponentSectionsPageViewModel(generatorContext, component)\n        assertThat(viewModel.componentSectionsTabs.single { it.link.active }.title).isEqualTo(\"Some Component\")\n    }\n\n    @Test\n    fun `component section table`() {\n        val viewModel = SoftwareSystemContainerComponentSectionsPageViewModel(generatorContext, component)\n\n        assertThat(viewModel.componentSectionTable.bodyRows).all {\n            hasSize(1)\n            index(0).transform { (it.columns[1] as TableViewModel.LinkCellViewModel).link }\n                .isEqualTo(\n                    LinkViewModel(\n                        viewModel,\n                        \"Content\",\n                        \"/software-system/sections/api-application/some-component/content\"\n                    )\n                )\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentsPageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.*\nimport com.structurizr.model.Container\nimport com.structurizr.model.SoftwareSystem\nimport nl.avisi.structurizr.site.generatr.normalize\nimport kotlin.test.Test\nimport kotlin.test.assertTrue\n\nclass SoftwareSystemContainerComponentsPageViewModelTest : ViewModelTest() {\n    private val generatorContext = generatorContext()\n    private val softwareSystem: SoftwareSystem = generatorContext.workspace.model\n        .addSoftwareSystem(\"Software system\").also {\n            val backend = it.addContainer(\"Backend\")\n            val frontend = it.addContainer(\"Frontend\")\n            it.addContainer(\"Api\").also { c ->\n                c.addProperty(\"test\", \"value\")\n            }\n            generatorContext.workspace.views.createComponentView(backend, \"component-1-backend\", \"Component view 1 - Backend\")\n            generatorContext.workspace.views.createComponentView(backend, \"component-2-backend\", \"Component view 2 - Backend\")\n\n            generatorContext.workspace.views.createComponentView(frontend, \"component-1-frontend\", \"Component view 1 - Frontend\")\n            generatorContext.workspace.views.createComponentView(frontend, \"component-2-frontend\", \"Component view 2 - Frontend\")\n        }\n\n    private val backendContainer: Container = softwareSystem.containers.elementAt(0)\n    private val frontendContainer: Container = softwareSystem.containers.elementAt(1)\n    private val apiContainer: Container = softwareSystem.containers.elementAt(2)\n    private val backendImageView = createImageView(generatorContext.workspace, backendContainer)\n    private val frontendImageView = createImageView(generatorContext.workspace, frontendContainer)\n\n    @Test\n    fun `active tab`() {\n        val viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, backendContainer)\n        assertThat(viewModel.tabs.single { it.link.active }.tab)\n            .isEqualTo(SoftwareSystemPageViewModel.Tab.COMPONENT)\n    }\n\n    @Test\n    fun `container tabs`() {\n        val viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, backendContainer)\n        val componentTabList = listOf(\n            ContainerTabViewModel(viewModel, \"Api\", \"/software-system/component/api\"),\n            ContainerTabViewModel(viewModel, \"Backend\", \"/software-system/component/backend\"),\n            ContainerTabViewModel(viewModel, \"Frontend\", \"/software-system/component/frontend\"))\n        assertThat(viewModel.containerTabs.elementAtOrNull(0)).isEqualTo(componentTabList.elementAt(0))\n        assertThat(viewModel.containerTabs.elementAtOrNull(1)).isEqualTo(componentTabList.elementAt(1))\n        assertThat(viewModel.containerTabs.elementAtOrNull(2)).isEqualTo(componentTabList.elementAt(2))\n    }\n\n    @Test\n    fun url() {\n        var viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, backendContainer)\n        assertThat(SoftwareSystemContainerComponentsPageViewModel.url(backendContainer))\n            .isEqualTo(\"/${softwareSystem.name.normalize()}/component/${backendContainer.name.normalize()}\")\n        assertThat(viewModel.url)\n            .isEqualTo(SoftwareSystemContainerComponentsPageViewModel.url(backendContainer))\n\n        viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, frontendContainer)\n        assertThat(SoftwareSystemContainerComponentsPageViewModel.url(frontendContainer))\n            .isEqualTo(\"/${softwareSystem.name.normalize()}/component/${frontendContainer.name.normalize()}\")\n        assertThat(viewModel.url)\n            .isEqualTo(SoftwareSystemContainerComponentsPageViewModel.url(frontendContainer))\n    }\n\n    @Test\n    fun `diagrams correctly mapped to containers`() {\n        var viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, backendContainer)\n        assertThat(viewModel.visible).isTrue()\n        assertThat(viewModel.diagrams).containsExactly(\n            DiagramViewModel(\n                \"component-1-backend\",\n                \"Component View: Software system - Backend\",\n                \"Component view 1 - Backend\",\n                \"\"\"<svg viewBox=\"0 0 800 900\"></svg>\"\"\",\n                800,\n                ImageViewModel(viewModel, \"/svg/component-1-backend.svg\"),\n                ImageViewModel(viewModel, \"/png/component-1-backend.png\"),\n                ImageViewModel(viewModel, \"/puml/component-1-backend.puml\")\n            ),\n            DiagramViewModel(\n                \"component-2-backend\",\n                \"Component View: Software system - Backend\",\n                \"Component view 2 - Backend\",\n                \"\"\"<svg viewBox=\"0 0 800 900\"></svg>\"\"\",\n                800,\n                ImageViewModel(viewModel, \"/svg/component-2-backend.svg\"),\n                ImageViewModel(viewModel, \"/png/component-2-backend.png\"),\n                ImageViewModel(viewModel, \"/puml/component-2-backend.puml\")\n            )\n        )\n\n        viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, frontendContainer)\n        assertThat(viewModel.visible).isTrue()\n        assertThat(viewModel.diagrams).containsExactly(\n            DiagramViewModel(\n                \"component-1-frontend\",\n                \"Component View: Software system - Frontend\",\n                \"Component view 1 - Frontend\",\n                \"\"\"<svg viewBox=\"0 0 800 900\"></svg>\"\"\",\n                800,\n                ImageViewModel(viewModel, \"/svg/component-1-frontend.svg\"),\n                ImageViewModel(viewModel, \"/png/component-1-frontend.png\"),\n                ImageViewModel(viewModel, \"/puml/component-1-frontend.puml\")\n            ),\n            DiagramViewModel(\n                \"component-2-frontend\",\n                \"Component View: Software system - Frontend\",\n                \"Component view 2 - Frontend\",\n                \"\"\"<svg viewBox=\"0 0 800 900\"></svg>\"\"\",\n                800,\n                ImageViewModel(viewModel, \"/svg/component-2-frontend.svg\"),\n                ImageViewModel(viewModel, \"/png/component-2-frontend.png\"),\n                ImageViewModel(viewModel, \"/puml/component-2-frontend.puml\")\n            )\n        )\n    }\n\n    @Test\n    fun `has image`() {\n        var viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, backendContainer)\n        assertThat(viewModel.visible).isTrue()\n        assertThat(viewModel.images).hasSize(1)\n        assertThat(viewModel.images.single().imageView).isEqualTo(backendImageView)\n\n        viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, frontendContainer)\n        assertThat(viewModel.visible).isTrue()\n        assertThat(viewModel.images).hasSize(1)\n        assertThat(viewModel.images.single().imageView).isEqualTo(frontendImageView)\n    }\n\n    @Test\n    fun `has only properties`() {\n        val viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, apiContainer)\n        assertTrue(viewModel.visible)\n        assertThat(viewModel.propertiesTable.bodyRows).isNotEmpty()\n        assertThat(viewModel.images).isEmpty()\n        assertThat(viewModel.diagrams).isEmpty()\n    }\n\n    @Test\n    fun `hidden view`() {\n        val viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, softwareSystem.addContainer(\"Container\"))\n        assertThat(viewModel.visible).isFalse()\n    }\n\n    @Test\n    fun `no index`() {\n        val viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, apiContainer)\n        assertThat(viewModel.diagramIndex.visible).isFalse()\n    }\n\n    @Test\n    fun `has index`() {\n        val viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, backendContainer)\n        assertThat(viewModel.diagramIndex.visible).isTrue()\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerDecisionPageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport com.structurizr.documentation.Format\nimport nl.avisi.structurizr.site.generatr.normalize\nimport kotlin.test.Test\n\nclass SoftwareSystemContainerDecisionPageViewModelTest : ViewModelTest() {\n    private val generatorContext = generatorContext()\n    private val softwareSystem = generatorContext.workspace.model.addSoftwareSystem(\"Software System\").also {\n        it.addContainer(\"API Application\")\n    }\n    private val container = softwareSystem.containers.first()\n\n    @Test\n    fun url() {\n        val decision = createDecision()\n        val viewModel = SoftwareSystemContainerDecisionPageViewModel(generatorContext, container, decision)\n\n        assertThat(SoftwareSystemContainerDecisionPageViewModel.url(container, decision))\n            .isEqualTo(\"/${softwareSystem.name.normalize()}/decisions/${container.name.normalize()}/${decision.id}\")\n        assertThat(viewModel.url)\n            .isEqualTo(SoftwareSystemContainerDecisionPageViewModel.url(container, decision))\n    }\n\n    @Test\n    fun `active tab`() {\n        val viewModel = SoftwareSystemContainerDecisionPageViewModel(generatorContext, container, createDecision())\n\n        assertThat(viewModel.tabs.single { it.link.active }.tab)\n            .isEqualTo(SoftwareSystemPageViewModel.Tab.DECISIONS)\n    }\n\n    @Test\n    fun content() {\n        val decision = createDecision()\n        val viewModel = SoftwareSystemContainerDecisionPageViewModel(generatorContext, container, decision)\n\n        assertThat(viewModel.content).isEqualTo(toHtml(viewModel, decision.content, Format.Markdown, svgFactory))\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerDecisionsPageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport assertk.assertions.isFalse\nimport assertk.assertions.isTrue\nimport com.structurizr.model.Component\nimport com.structurizr.model.Container\nimport com.structurizr.model.SoftwareSystem\nimport kotlin.test.Test\n\nclass SoftwareSystemContainerDecisionsPageViewModelTest : ViewModelTest() {\n    private val generatorContext = generatorContext()\n    private val softwareSystem: SoftwareSystem = generatorContext.workspace.model.addSoftwareSystem(\"Software system\")\n    private val container: Container = softwareSystem.addContainer(\"API Application\")\n    private val component: Component = container.addComponent(\"Email Component\")\n\n    @Test\n    fun `active tab`() {\n        val viewModel = SoftwareSystemContainerDecisionsPageViewModel(generatorContext, container)\n\n        assertThat(viewModel.tabs.single { it.link.active }.tab)\n            .isEqualTo(SoftwareSystemPageViewModel.Tab.DECISIONS)\n    }\n\n    @Test\n    fun `decisions tabs`() {\n        softwareSystem.documentation.addDecision(createDecision(\"1\", \"Accepted\"))\n\n        container.documentation.addDecision(createDecision(\"1\", \"Accepted\"))\n\n        val viewModel = SoftwareSystemContainerDecisionsPageViewModel(generatorContext, container)\n\n        assertThat(viewModel.decisionTabs)\n            .isEqualTo(listOf(\n                DecisionTabViewModel(\n                    viewModel,\n                    \"System\",\n                    \"/software-system/decisions\"\n                ),\n                DecisionTabViewModel(\n                    viewModel,\n                    \"API Application\",\n                    \"/software-system/decisions/api-application\",\n                    Match.CHILD\n                )\n            ))\n    }\n\n    @Test\n    fun `decisions tabs without container decisions`() {\n        softwareSystem.documentation.addDecision(createDecision(\"1\", \"Accepted\"))\n\n        val viewModel = SoftwareSystemContainerDecisionsPageViewModel(generatorContext, container)\n\n        assertThat(viewModel.decisionTabs)\n            .isEqualTo(listOf(\n                DecisionTabViewModel(\n                    viewModel,\n                    \"System\",\n                    \"/software-system/decisions\"\n                )\n            ))\n    }\n\n    @Test\n    fun `decisions tabs without software system decisions`() {\n        container.documentation.addDecision(createDecision(\"1\", \"Accepted\"))\n\n        val viewModel = SoftwareSystemContainerDecisionsPageViewModel(generatorContext, container)\n\n        assertThat(viewModel.decisionTabs)\n            .isEqualTo(listOf(\n                DecisionTabViewModel(\n                    viewModel,\n                    \"API Application\",\n                    \"/software-system/decisions/api-application\",\n                    Match.CHILD\n                )\n            ))\n    }\n\n    @Test\n    fun `component decisions tabs`() {\n        container.documentation.addDecision(createDecision(\"1\", \"Accepted\"))\n        createComponentDecisions()\n\n        val viewModel = SoftwareSystemContainerDecisionsPageViewModel(generatorContext, container)\n\n        assertThat(viewModel.componentDecisionsTabs)\n            .isEqualTo(listOf(\n                DecisionTabViewModel(\n                    viewModel,\n                    \"Container\",\n                    \"/software-system/decisions/api-application\"\n                ),\n                DecisionTabViewModel(\n                    viewModel,\n                    \"Email Component\",\n                    \"/software-system/decisions/api-application/email-component\"\n                )\n            ))\n    }\n\n    @Test\n    fun `component decisions tabs without container decisions`() {\n        createComponentDecisions()\n\n        val viewModel = SoftwareSystemContainerDecisionsPageViewModel(generatorContext, container)\n\n        assertThat(viewModel.componentDecisionsTabs)\n            .isEqualTo(listOf(\n                DecisionTabViewModel(\n                    viewModel,\n                    \"Email Component\",\n                    \"/software-system/decisions/api-application/email-component\"\n                )\n            ))\n    }\n\n    @Test\n    fun `component decisions tabs without component decisions`() {\n        container.documentation.addDecision(createDecision(\"1\", \"Accepted\"))\n\n        val viewModel = SoftwareSystemContainerDecisionsPageViewModel(generatorContext, container)\n\n        assertThat(viewModel.componentDecisionsTabs)\n            .isEqualTo(listOf(\n                DecisionTabViewModel(\n                    viewModel,\n                    \"Container\",\n                    \"/software-system/decisions/api-application\"\n                )\n            ))\n    }\n\n    @Test\n    fun `decisions table`() {\n        container.documentation.addDecision(createDecision(\"1\", \"Accepted\"))\n\n        val viewModel = SoftwareSystemContainerDecisionsPageViewModel(generatorContext, container)\n\n        assertThat(viewModel.decisionsTable)\n            .isEqualTo(\n                viewModel.createDecisionsTableViewModel(container.documentation.decisions) {\n                    \"/software-system/decisions/api-application/1\"\n                }\n            )\n    }\n\n    @Test\n    fun `is visible`() {\n        container.documentation.addDecision(createDecision(\"1\", \"Accepted\"))\n\n        val viewModel = SoftwareSystemContainerDecisionsPageViewModel(generatorContext, container)\n\n        assertThat(viewModel.visible).isTrue()\n        assertThat(viewModel.onlyComponentDecisionsVisible).isFalse()\n    }\n\n    @Test\n    fun `only component decisions visible`() {\n        createComponentDecisions()\n\n        val viewModel = SoftwareSystemContainerDecisionsPageViewModel(generatorContext, container)\n\n        assertThat(viewModel.visible).isTrue()\n        assertThat(viewModel.onlyComponentDecisionsVisible).isTrue()\n    }\n\n    @Test\n    fun `hidden view`() {\n        val viewModel = SoftwareSystemContainerDecisionsPageViewModel(generatorContext, container)\n\n        assertThat(viewModel.visible).isFalse()\n        assertThat(viewModel.onlyComponentDecisionsVisible).isFalse()\n    }\n\n    private fun createComponentDecisions() {\n        component.documentation.addDecision(createDecision(\n            \"1\",\n            \"Accepted\"\n        ))\n        component.documentation.addDecision(createDecision(\n            \"2\",\n            \"Superseded by [3. Another Realisation of Feature 1](0003-another-realisation-of-feature-1.md)\"\n        ))\n        component.documentation.addDecision(createDecision(\n            \"3\",\n            \"Accepted\\n\\nSupersedes [2. Implement Feature 1](0002-implement-feature-1.md)\"\n        ))\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerPageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.containsExactly\nimport assertk.assertions.hasSize\nimport assertk.assertions.isEqualTo\nimport assertk.assertions.isFalse\nimport assertk.assertions.isTrue\nimport com.structurizr.model.SoftwareSystem\nimport kotlin.test.Test\n\nclass SoftwareSystemContainerPageViewModelTest : ViewModelTest() {\n    private val generatorContext = generatorContext()\n    private val softwareSystem: SoftwareSystem = generatorContext.workspace.model\n        .addSoftwareSystem(\"Software system\").also {\n            generatorContext.workspace.views.createContainerView(it, \"container-1\", \"Container view 1\")\n            generatorContext.workspace.views.createContainerView(it, \"container-2\", \"Container view 2\")\n        }\n    private val imageView = createImageView(generatorContext.workspace, softwareSystem)\n\n    @Test\n    fun `active tab`() {\n        val viewModel = SoftwareSystemContainerPageViewModel(generatorContext, softwareSystem)\n\n        assertThat(viewModel.tabs.single { it.link.active }.tab)\n            .isEqualTo(SoftwareSystemPageViewModel.Tab.CONTAINER)\n    }\n\n    @Test\n    fun `diagrams sorted by key`() {\n        val viewModel = SoftwareSystemContainerPageViewModel(generatorContext, softwareSystem)\n\n        assertThat(viewModel.visible).isTrue()\n        assertThat(viewModel.diagrams).containsExactly(\n            DiagramViewModel(\n                \"container-1\",\n                \"Container View: Software system\",\n                \"Container view 1\",\n                \"\"\"<svg viewBox=\"0 0 800 900\"></svg>\"\"\",\n                800,\n                ImageViewModel(viewModel, \"/svg/container-1.svg\"),\n                ImageViewModel(viewModel, \"/png/container-1.png\"),\n                ImageViewModel(viewModel, \"/puml/container-1.puml\")\n            ),\n            DiagramViewModel(\n                \"container-2\",\n                \"Container View: Software system\",\n                \"Container view 2\",\n                \"\"\"<svg viewBox=\"0 0 800 900\"></svg>\"\"\",\n                800,\n                ImageViewModel(viewModel, \"/svg/container-2.svg\"),\n                ImageViewModel(viewModel, \"/png/container-2.png\"),\n                ImageViewModel(viewModel, \"/puml/container-2.puml\")\n            )\n        )\n    }\n\n    @Test\n    fun `has image`() {\n        val viewModel = SoftwareSystemContainerPageViewModel(generatorContext, softwareSystem)\n\n        assertThat(viewModel.visible).isTrue()\n        assertThat(viewModel.images).hasSize(1)\n        assertThat(viewModel.images.single().imageView).isEqualTo(imageView)\n    }\n\n    @Test\n    fun `hidden view`() {\n        val viewModel = SoftwareSystemContainerPageViewModel(\n            generatorContext,\n            generatorContext.workspace.model.addSoftwareSystem(\"Software system 2\")\n        )\n\n        assertThat(viewModel.visible).isFalse()\n    }\n\n    @Test\n    fun `no index`() {\n        val viewModel = SoftwareSystemContainerPageViewModel(\n            generatorContext,\n            generatorContext.workspace.model.addSoftwareSystem(\"Software system 2\").also {\n                generatorContext.workspace.views.createContainerView(it, \"container-3\", \"Container view 3\")\n            }\n        )\n        assertThat(viewModel.diagramIndex.visible).isFalse()\n    }\n\n    @Test\n    fun `has index`() {\n        val viewModel = SoftwareSystemContainerPageViewModel(generatorContext, softwareSystem)\n        assertThat(viewModel.diagramIndex.visible).isTrue()\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerSectionPageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport com.structurizr.documentation.Format\nimport nl.avisi.structurizr.site.generatr.normalize\nimport kotlin.test.Test\n\nclass SoftwareSystemContainerSectionPageViewModelTest : ViewModelTest() {\n    private val generatorContext = generatorContext()\n    private val softwareSystem = generatorContext.workspace.model.addSoftwareSystem(\"Software System\").also {\n        it.addContainer(\"API Application\")\n    }\n    private val container = softwareSystem.containers.first()\n\n    @Test\n    fun url() {\n        val section = createSection()\n        val viewModel = SoftwareSystemContainerSectionPageViewModel(generatorContext, container, section)\n\n        assertThat(SoftwareSystemContainerSectionPageViewModel.url(container, section))\n            .isEqualTo(\"/${softwareSystem.name.normalize()}/sections/${container.name.normalize()}/${section.contentTitle().normalize()}\")\n        assertThat(viewModel.url)\n            .isEqualTo(SoftwareSystemContainerSectionPageViewModel.url(container, section))\n    }\n\n    @Test\n    fun `active tab`() {\n        val viewModel = SoftwareSystemContainerSectionPageViewModel(generatorContext, container, createSection())\n\n        assertThat(viewModel.tabs.single { it.link.active }.tab)\n            .isEqualTo(SoftwareSystemPageViewModel.Tab.SECTIONS)\n    }\n\n    @Test\n    fun content() {\n        val section = createSection()\n        val viewModel = SoftwareSystemContainerSectionPageViewModel(generatorContext, container, section)\n\n        assertThat(viewModel.content).isEqualTo(toHtml(viewModel, section.content, Format.Markdown, svgFactory))\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerSectionsPageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.all\nimport assertk.assertThat\nimport assertk.assertions.*\nimport kotlin.test.BeforeTest\nimport kotlin.test.Test\n\nclass SoftwareSystemContainerSectionsPageViewModelTest : ViewModelTest() {\n    private val generatorContext = generatorContext()\n    private val softwareSystem = generatorContext.workspace.model.addSoftwareSystem(\"Software System\").apply {\n        documentation.addSection(createSection())\n        documentation.addSection(createSection())\n    }\n    private val container = softwareSystem.addContainer(\"API Application\")\n\n    @Test\n    fun `active tab`() {\n        val viewModel = SoftwareSystemContainerSectionsPageViewModel(generatorContext, container)\n        assertThat(viewModel.tabs.single { it.link.active }.tab)\n            .isEqualTo(SoftwareSystemPageViewModel.Tab.SECTIONS)\n    }\n\n    @Test\n    fun `sections table`() {\n        container.documentation.addSection(createSection())\n\n        val viewModel = SoftwareSystemContainerSectionsPageViewModel(generatorContext, container)\n\n        assertThat(viewModel.sectionsTable.bodyRows).all {\n            hasSize(1)\n            index(0).transform { (it.columns[1] as TableViewModel.LinkCellViewModel).link }\n                .isEqualTo(\n                    LinkViewModel(\n                        viewModel,\n                        \"Content\",\n                        \"/software-system/sections/api-application/content\"\n                    )\n                )\n        }\n    }\n\n    @Test\n    fun `sections tabs`() {\n        container.documentation.addSection(createSection())\n\n        val viewModel = SoftwareSystemContainerSectionsPageViewModel(generatorContext, container)\n\n        assertThat(viewModel.sectionsTabs).all {\n            hasSize(2)\n            index(0).all {\n                transform { it.link.active }.isFalse()\n                transform { it.link.title }.isEqualTo(\"System\")\n            }\n            index(1).all {\n                transform { it.link.active }.isTrue()\n                transform { it.link }.isEqualTo(\n                    LinkViewModel(\n                        viewModel,\n                        \"API Application\",\n                        \"/software-system/sections/api-application\",\n                        Match.CHILD\n                    )\n                )\n            }\n        }\n    }\n\n    @Test\n    fun `component sections tabs`() {\n        container.documentation.addSection(createSection())\n        container.addComponent(\"Some Component\").apply {\n            documentation.addSection(createSection())\n        }\n\n        val viewModel = SoftwareSystemContainerSectionsPageViewModel(generatorContext, container)\n\n        assertThat(viewModel.componentSectionsTabs).all {\n            hasSize(2)\n            index(0).all {\n                transform { it.link.active }.isTrue()\n                transform { it.title }.isEqualTo(\"Container\")\n            }\n            index(1).all {\n                transform { it.link }.isEqualTo(\n                    LinkViewModel(\n                        viewModel,\n                        \"Some Component\",\n                        \"/software-system/sections/api-application/some-component\"\n                    )\n                )\n            }\n        }\n    }\n\n    @Test\n    fun `has sections`() {\n        container.documentation.addSection(createSection())\n\n        val viewModel = SoftwareSystemContainerSectionsPageViewModel(generatorContext, container)\n\n        assertThat(viewModel.visible).isTrue()\n        assertThat(viewModel.onlyComponentsDocumentationSectionsVisible).isFalse()\n    }\n\n    @Test\n    fun `no sections`() {\n        val viewModel = SoftwareSystemContainerSectionsPageViewModel(generatorContext, container)\n\n        assertThat(viewModel.visible).isFalse()\n        assertThat(viewModel.onlyComponentsDocumentationSectionsVisible).isFalse()\n    }\n\n    @Test\n    fun `child has section`() {\n        container.addComponent(\"Email Component\").documentation.addSection(createSection())\n\n        val viewModel = SoftwareSystemContainerSectionsPageViewModel(generatorContext, container)\n\n        assertThat(viewModel.visible).isTrue()\n        assertThat(viewModel.onlyComponentsDocumentationSectionsVisible).isTrue()\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContextPageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.containsExactly\nimport assertk.assertions.isFalse\nimport assertk.assertions.isTrue\nimport com.structurizr.model.SoftwareSystem\nimport kotlin.test.Test\n\nclass SoftwareSystemContextPageViewModelTest : ViewModelTest() {\n    private val generatorContext = generatorContext()\n    private val softwareSystem: SoftwareSystem = generatorContext.workspace.model\n        .addSoftwareSystem(\"Software system\").also {\n            generatorContext.workspace.views.createSystemContextView(it, \"context-1\", \"System context view 1\")\n            generatorContext.workspace.views.createSystemContextView(it, \"context-2\", \"System context view 2\")\n        }\n\n    @Test\n    fun `diagrams sorted by key`() {\n        val viewModel = SoftwareSystemContextPageViewModel(generatorContext, softwareSystem)\n\n        assertThat(viewModel.diagrams).containsExactly(\n            DiagramViewModel(\n                \"context-1\",\n                \"System Context View: Software system\",\n                \"System context view 1\",\n                \"\"\"<svg viewBox=\"0 0 800 900\"></svg>\"\"\",\n                800,\n                ImageViewModel(viewModel, \"/svg/context-1.svg\"),\n                ImageViewModel(viewModel, \"/png/context-1.png\"),\n                ImageViewModel(viewModel, \"/puml/context-1.puml\")\n            ),\n            DiagramViewModel(\n                \"context-2\",\n                \"System Context View: Software system\",\n                \"System context view 2\",\n                \"\"\"<svg viewBox=\"0 0 800 900\"></svg>\"\"\",\n                800,\n                ImageViewModel(viewModel, \"/svg/context-2.svg\"),\n                ImageViewModel(viewModel, \"/png/context-2.png\"),\n                ImageViewModel(viewModel, \"/puml/context-2.puml\")\n            )\n        )\n    }\n\n    @Test\n    fun `hidden view`() {\n        val viewModel = SoftwareSystemContextPageViewModel(\n            generatorContext,\n            generatorContext.workspace.model.addSoftwareSystem(\"Software system 2\")\n        )\n\n        assertThat(viewModel.visible).isFalse()\n    }\n\n    @Test\n    fun `no index`() {\n        val viewModel = SoftwareSystemContextPageViewModel(generatorContext, generatorContext.workspace.model\n            .addSoftwareSystem(\"Software system 2\").also {\n                generatorContext.workspace.views.createSystemContextView(it, \"context-3\", \"System context view 3\")\n            })\n        assertThat(viewModel.diagramIndex.visible).isFalse()\n    }\n\n    @Test\n    fun `has index`() {\n        val viewModel = SoftwareSystemContextPageViewModel(generatorContext, softwareSystem)\n        assertThat(viewModel.diagramIndex.visible).isTrue()\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDecisionPageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport com.structurizr.documentation.Format\nimport nl.avisi.structurizr.site.generatr.normalize\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemPageViewModel.Companion.url\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemPageViewModel.Tab\nimport kotlin.test.Test\n\nclass SoftwareSystemDecisionPageViewModelTest : ViewModelTest() {\n    private val generatorContext = generatorContext()\n    private val softwareSystem = generatorContext.workspace.model.addSoftwareSystem(\"Software System\")\n\n    @Test\n    fun url() {\n        val decision = createDecision()\n        val viewModel = SoftwareSystemDecisionPageViewModel(generatorContext, softwareSystem, decision)\n\n        assertThat(SoftwareSystemDecisionPageViewModel.url(softwareSystem, decision))\n            .isEqualTo(\"/${softwareSystem.name.normalize()}/decisions/${decision.id}\")\n        assertThat(viewModel.url)\n            .isEqualTo(SoftwareSystemDecisionPageViewModel.url(softwareSystem, decision))\n    }\n\n    @Test\n    fun `active tab`() {\n        val viewModel = SoftwareSystemDecisionPageViewModel(generatorContext, softwareSystem, createDecision())\n\n        assertThat(viewModel.tabs.single { it.link.active }.tab)\n            .isEqualTo(Tab.DECISIONS)\n    }\n\n    @Test\n    fun content() {\n        val decision = createDecision()\n        val viewModel = SoftwareSystemDecisionPageViewModel(generatorContext, softwareSystem, decision)\n\n        assertThat(viewModel.content).isEqualTo(toHtml(viewModel, decision.content, Format.Markdown, svgFactory))\n    }\n\n\n    @Test\n    fun `link to other ADR`() {\n        val decision = createDecision().apply {\n            content = \"\"\"\n                Decision with [link to other ADR](#2).\n                [Web link](https://google.com)\n                [Internal link](#other-section)\n            \"\"\".trimIndent()\n        }\n        val viewModel = SoftwareSystemDecisionPageViewModel(generatorContext, softwareSystem, decision)\n\n        assertThat(viewModel.content).isEqualTo(\n            toHtml(\n                viewModel, \"\"\"\n                    Decision with [link to other ADR](${url(softwareSystem, Tab.DECISIONS)}/2).\n                    [Web link](https://google.com)\n                    [Internal link](#other-section)\n                \"\"\".trimIndent(),\n                Format.Markdown,\n                svgFactory\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDecisionsPageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport assertk.assertions.isFalse\nimport assertk.assertions.isTrue\nimport com.structurizr.model.Container\nimport com.structurizr.model.SoftwareSystem\nimport kotlin.test.Test\n\nclass SoftwareSystemDecisionsPageViewModelTest : ViewModelTest() {\n    private val generatorContext = generatorContext()\n    private val softwareSystem: SoftwareSystem = generatorContext.workspace.model.addSoftwareSystem(\"Software system\")\n    private val container: Container = softwareSystem.addContainer(\"API Application\")\n\n    @Test\n    fun `active tab`() {\n        val viewModel = SoftwareSystemDecisionsPageViewModel(generatorContext, softwareSystem)\n\n        assertThat(viewModel.tabs.single { it.link.active }.tab)\n            .isEqualTo(SoftwareSystemPageViewModel.Tab.DECISIONS)\n    }\n\n    @Test\n    fun `decisions tabs`() {\n        softwareSystem.documentation.addDecision(createDecision(\"1\", \"Accepted\"))\n        container.documentation.addDecision(createDecision(\"1\", \"Accepted\"))\n\n        val viewModel = SoftwareSystemDecisionsPageViewModel(generatorContext, softwareSystem)\n\n        assertThat(viewModel.decisionTabs)\n            .isEqualTo(listOf(\n                DecisionTabViewModel(\n                    viewModel,\n                    \"System\",\n                    \"/software-system/decisions\"\n                ),\n                DecisionTabViewModel(\n                    viewModel,\n                    \"API Application\",\n                    \"/software-system/decisions/api-application\",\n                    Match.CHILD\n                )\n            ))\n    }\n\n    @Test\n    fun `decisions tabs without container decisions`() {\n        softwareSystem.documentation.addDecision(createDecision(\"1\", \"Accepted\"))\n\n        val viewModel = SoftwareSystemDecisionsPageViewModel(generatorContext, softwareSystem)\n\n        assertThat(viewModel.decisionTabs)\n            .isEqualTo(listOf(\n                DecisionTabViewModel(\n                    viewModel,\n                    \"System\",\n                    \"/software-system/decisions\"\n                )\n            ))\n    }\n\n    @Test\n    fun `decisions tabs without software system decisions`() {\n        container.documentation.addDecision(createDecision(\"1\", \"Accepted\"))\n\n        val viewModel = SoftwareSystemDecisionsPageViewModel(generatorContext, softwareSystem)\n\n        assertThat(viewModel.decisionTabs)\n            .isEqualTo(listOf(\n                DecisionTabViewModel(\n                    viewModel,\n                    \"API Application\",\n                    \"/software-system/decisions/api-application\",\n                    Match.CHILD\n                )\n            ))\n    }\n\n    @Test\n    fun `decisions table`() {\n        softwareSystem.documentation.addDecision(createDecision(\"1\", \"Accepted\"))\n\n        val viewModel = SoftwareSystemDecisionsPageViewModel(generatorContext, softwareSystem)\n\n        assertThat(viewModel.decisionsTable)\n            .isEqualTo(\n                viewModel.createDecisionsTableViewModel(softwareSystem.documentation.decisions) {\n                    \"/software-system/decisions/1\"\n                }\n            )\n    }\n\n    @Test\n    fun `is visible`() {\n        softwareSystem.documentation.addDecision(createDecision(\"1\", \"Accepted\"))\n\n        val viewModel = SoftwareSystemDecisionsPageViewModel(generatorContext, softwareSystem)\n\n        assertThat(viewModel.visible).isTrue()\n        assertThat(viewModel.onlyContainersDecisionsVisible).isFalse()\n    }\n\n    @Test\n    fun `only container decisions visible`() {\n        container.documentation.addDecision(createDecision(\"1\", \"Accepted\"))\n\n        val viewModel = SoftwareSystemDecisionsPageViewModel(generatorContext, softwareSystem)\n\n        assertThat(viewModel.visible).isTrue()\n        assertThat(viewModel.onlyContainersDecisionsVisible).isTrue()\n    }\n\n    @Test\n    fun `hidden view`() {\n        val viewModel = SoftwareSystemDecisionsPageViewModel(generatorContext, softwareSystem)\n\n        assertThat(viewModel.visible).isFalse()\n        assertThat(viewModel.onlyContainersDecisionsVisible).isFalse()\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDependenciesPageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.*\nimport kotlin.test.Test\n\nclass SoftwareSystemDependenciesPageViewModelTest : ViewModelTest() {\n    private val generatorContext = generatorContext()\n    private val softwareSystem1 = generatorContext.workspace.model.addSoftwareSystem(\"Software system 1\")\n    private val softwareSystem2 = generatorContext.workspace.model.addSoftwareSystem(\"Software system 2\")\n\n    @Test\n    fun `active tab`() {\n        val viewModel = SoftwareSystemDependenciesPageViewModel(generatorContext, softwareSystem1)\n\n        assertThat(viewModel.tabs.single { it.link.active }.tab)\n            .isEqualTo(SoftwareSystemPageViewModel.Tab.DEPENDENCIES)\n    }\n\n    @Test\n    fun `empty table when no dependencies are present`() {\n        val viewModel = SoftwareSystemDependenciesPageViewModel(generatorContext, softwareSystem1)\n\n        assertThat(viewModel.dependenciesInboundTable).isEqualTo(\n            TableViewModel.create {\n                dependenciesTableHeader()\n            }\n        )\n    }\n\n    @Test\n    fun `show outbound dependencies in table when present`() {\n        softwareSystem1.uses(softwareSystem2, \"Uses SOAP\", \"SOAP\")\n        val viewModel = SoftwareSystemDependenciesPageViewModel(generatorContext, softwareSystem1)\n\n        assertThat(viewModel.dependenciesOutboundTable).isEqualTo(\n            TableViewModel.create {\n                dependenciesTableHeader()\n                bodyRow(\n                    cellWithSoftwareSystemLink(\n                        viewModel, softwareSystem2.name,\n                        SoftwareSystemPageViewModel.url(softwareSystem2, SoftwareSystemPageViewModel.Tab.HOME)\n                    ),\n                    cell(\"Uses SOAP\"),\n                    cell(\"SOAP\"),\n                )\n            }\n        )\n    }\n\n    @Test\n    fun `show inbound dependencies in table when present`() {\n        softwareSystem2.uses(softwareSystem1, \"Uses REST\", \"REST\")\n        val viewModel = SoftwareSystemDependenciesPageViewModel(generatorContext, softwareSystem1)\n\n        assertThat(viewModel.dependenciesInboundTable).isEqualTo(\n            TableViewModel.create {\n                dependenciesTableHeader()\n                bodyRow(\n                    cellWithSoftwareSystemLink(\n                        viewModel, softwareSystem2.name,\n                        SoftwareSystemPageViewModel.url(softwareSystem2, SoftwareSystemPageViewModel.Tab.HOME)\n                    ),\n                    cell(\"Uses REST\"),\n                    cell(\"REST\"),\n                )\n            }\n        )\n    }\n\n    @Test\n    fun `only relationships from and to software systems`() {\n        val backend1 = softwareSystem1.addContainer(\"Backend 1\")\n        val backend2 = softwareSystem2.addContainer(\"Backend 2\")\n\n        softwareSystem1.uses(softwareSystem2, \"Uses REST\", \"REST\")\n        softwareSystem2.uses(softwareSystem1, \"Uses SOAP\", \"SOAP\")\n        backend1.uses(softwareSystem2, \"Uses from container 1 to system 2\")\n        backend2.uses(softwareSystem1, \"Uses from container 2 to system 1\", \"REST\")\n        softwareSystem1.uses(backend2, \"Uses from system 1 to container 2\", \"REST\")\n\n        val viewModel = SoftwareSystemDependenciesPageViewModel(generatorContext, softwareSystem1)\n\n        // Inbound Table\n        assertThat(viewModel.dependenciesInboundTable.bodyRows.extractTitle())\n            .containsExactly(\"Software system 2\")\n        // Outbound Table\n        assertThat(viewModel.dependenciesOutboundTable.bodyRows.extractTitle())\n            .containsExactly(\"Software system 2\")\n    }\n\n    @Test\n    fun `dependencies from and to external systems (declared external by tag)`() {\n        generatorContext.workspace.views.configuration.addProperty(\"generatr.site.externalTag\", \"External System\")\n        val externalSystem = generatorContext.workspace.model.addSoftwareSystem(\"External system\").apply { addTags(\"External System\") }\n        externalSystem.uses(softwareSystem1, \"Uses\", \"REST\")\n        softwareSystem1.uses(externalSystem, \"Uses\", \"REST\")\n\n        val viewModel = SoftwareSystemDependenciesPageViewModel(generatorContext, softwareSystem1)\n\n        assertThat(viewModel.dependenciesInboundTable.bodyRows[0].columns[0])\n            .isEqualTo(TableViewModel.TextCellViewModel(\"External system (External)\", isHeader = false, greyText = true, boldText = true))\n        assertThat(viewModel.dependenciesOutboundTable.bodyRows[0].columns[0])\n            .isEqualTo(TableViewModel.TextCellViewModel(\"External system (External)\", isHeader = false, greyText = true, boldText = true))\n    }\n\n    @Test\n    fun `sort by source system name case insensitive`() {\n        val system = generatorContext.workspace.model.addSoftwareSystem(\"Software system 3\")\n        system.uses(softwareSystem1, \"Uses\", \"REST\")\n        softwareSystem1.uses(softwareSystem2, \"Uses REST\", \"REST\")\n        softwareSystem2.uses(softwareSystem1, \"Uses SOAP\", \"SOAP\")\n        val viewModel = SoftwareSystemDependenciesPageViewModel(generatorContext, softwareSystem1)\n\n        // Inbound Table\n        assertThat(viewModel.dependenciesInboundTable.bodyRows.extractTitle())\n            .containsExactly(\"Software system 2\", \"Software system 3\")\n        // Outbound Table\n        assertThat(viewModel.dependenciesInboundTable.bodyRows.extractTitle())\n            .containsExactly(\"Software system 2\", \"Software system 3\")\n    }\n\n    private fun TableViewModel.TableViewInitializerContext.dependenciesTableHeader() {\n        headerRow(\n            headerCellMedium(\"System\"),\n            headerCellLarge(\"Description\"),\n            headerCell(\"Technology\"),\n        )\n    }\n\n    private fun List<TableViewModel.RowViewModel>.extractTitle() = map {\n        when (val source = it.columns[0]) {\n            is TableViewModel.TextCellViewModel -> source.title\n            is TableViewModel.LinkCellViewModel -> source.link.title\n            is TableViewModel.ExternalLinkCellViewModel -> source.link.title\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDeploymentPageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.*\nimport com.structurizr.model.SoftwareSystem\nimport kotlin.test.Test\n\nclass SoftwareSystemDeploymentPageViewModelTest : ViewModelTest() {\n    private val generatorContext = generatorContext()\n    private val softwareSystem: SoftwareSystem = generatorContext.workspace.model\n        .addSoftwareSystem(\"Software system\").also {\n            generatorContext.workspace.views.createDeploymentView(it, \"deployment-1\", \"Deployment view 1\")\n            generatorContext.workspace.views.createDeploymentView(it, \"deployment-2\", \"Deployment view 2\")\n        }\n\n    @Test\n    fun `active tab`() {\n        val viewModel = SoftwareSystemDeploymentPageViewModel(generatorContext, softwareSystem)\n\n        assertThat(viewModel.tabs.single { it.link.active }.tab)\n            .isEqualTo(SoftwareSystemPageViewModel.Tab.DEPLOYMENT)\n    }\n\n    @Test\n    fun `diagrams sorted by key`() {\n        val viewModel = SoftwareSystemDeploymentPageViewModel(generatorContext, softwareSystem)\n\n        assertThat(viewModel.diagrams).containsExactly(\n            DiagramViewModel(\n                \"deployment-1\",\n                \"Deployment View: Software system - Default\",\n                \"Deployment view 1\",\n                \"\"\"<svg viewBox=\"0 0 800 900\"></svg>\"\"\",\n                800,\n                ImageViewModel(viewModel, \"/svg/deployment-1.svg\"),\n                ImageViewModel(viewModel, \"/png/deployment-1.png\"),\n                ImageViewModel(viewModel, \"/puml/deployment-1.puml\")\n            ),\n            DiagramViewModel(\n                \"deployment-2\",\n                \"Deployment View: Software system - Default\",\n                \"Deployment view 2\",\n                \"\"\"<svg viewBox=\"0 0 800 900\"></svg>\"\"\",\n                800,\n                ImageViewModel(viewModel, \"/svg/deployment-2.svg\"),\n                ImageViewModel(viewModel, \"/png/deployment-2.png\"),\n                ImageViewModel(viewModel, \"/puml/deployment-2.puml\")\n            )\n        )\n    }\n\n    @Test\n    fun `hidden view`() {\n        val viewModel = SoftwareSystemDeploymentPageViewModel(\n            generatorContext,\n            generatorContext.workspace.model.addSoftwareSystem(\"Software system 2\")\n        )\n\n        assertThat(viewModel.visible).isFalse()\n    }\n\n    @Test\n    fun `no index`() {\n        val viewModel = SoftwareSystemDeploymentPageViewModel(generatorContext, generatorContext.workspace.model\n            .addSoftwareSystem(\"Software system 2\").also {\n                generatorContext.workspace.views.createDeploymentView(it, \"deployment-3\", \"Deployment view 3\")\n            })\n        assertThat(viewModel.diagramIndex.visible).isFalse()\n    }\n\n    @Test\n    fun `has index`() {\n        val viewModel = SoftwareSystemDeploymentPageViewModel(generatorContext, softwareSystem)\n        assertThat(viewModel.diagramIndex.visible).isTrue()\n    }\n\n    @Test\n    fun `includes star-scoped deployment view when belongsTo in properties`() {\n        val viewModel = SoftwareSystemDeploymentPageViewModel(\n            generatorContext, generatorContext.workspace.model.addSoftwareSystem(\"Software system 3\").also {\n                generatorContext.workspace.views.createDeploymentView(\"star-scoped\", \"Star Scoped Deployment View\").apply {\n                    addProperty(\"generatr.view.deployment.belongsTo\", it.name)\n                }\n            })\n\n        assertThat(viewModel.visible, \"is visible\").isTrue()\n\n        assertThat(viewModel.diagrams).exactly(1) { assert ->\n            assert.transform { it.key }.isEqualTo(\"star-scoped\")\n        }\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDynamicPageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.containsExactly\nimport assertk.assertions.isEqualTo\nimport assertk.assertions.isFalse\nimport assertk.assertions.isTrue\nimport com.structurizr.model.SoftwareSystem\nimport kotlin.test.Test\n\nclass SoftwareSystemDynamicPageViewModelTest : ViewModelTest() {\n    private val generatorContext = generatorContext()\n    private val softwareSystem: SoftwareSystem = generatorContext.workspace.model\n        .addSoftwareSystem(\"Software system\").also {\n            val backend = it.addContainer(\"Backend\")\n            val frontend = it.addContainer(\"Frontend\")\n            generatorContext.workspace.views.createDynamicView(backend, \"backend-dynamic\", \"Dynamic view 1\")\n            generatorContext.workspace.views.createDynamicView(frontend, \"frontend-dynamic\", \"Dynamic view 2\")\n        }\n\n    @Test\n    fun `active tab`() {\n        val viewModel = SoftwareSystemDynamicPageViewModel(generatorContext, softwareSystem)\n\n        assertThat(viewModel.tabs.single { it.link.active }.tab)\n            .isEqualTo(SoftwareSystemPageViewModel.Tab.DYNAMIC)\n    }\n\n    @Test\n    fun `diagrams sorted by key`() {\n        val viewModel = SoftwareSystemDynamicPageViewModel(generatorContext, softwareSystem)\n\n        assertThat(viewModel.diagrams).containsExactly(\n            DiagramViewModel(\n                \"backend-dynamic\",\n                \"Dynamic View: Software system - Backend\",\n                \"Dynamic view 1\",\n                \"\"\"<svg viewBox=\"0 0 800 900\"></svg>\"\"\",\n                800,\n                ImageViewModel(viewModel, \"/svg/backend-dynamic.svg\"),\n                ImageViewModel(viewModel, \"/png/backend-dynamic.png\"),\n                ImageViewModel(viewModel, \"/puml/backend-dynamic.puml\")\n            ),\n            DiagramViewModel(\n                \"frontend-dynamic\",\n                \"Dynamic View: Software system - Frontend\",\n                \"Dynamic view 2\",\n                \"\"\"<svg viewBox=\"0 0 800 900\"></svg>\"\"\",\n                800,\n                ImageViewModel(viewModel, \"/svg/frontend-dynamic.svg\"),\n                ImageViewModel(viewModel, \"/png/frontend-dynamic.png\"),\n                ImageViewModel(viewModel, \"/puml/frontend-dynamic.puml\")\n            )\n        )\n    }\n\n    @Test\n    fun `hidden view`() {\n        val viewModel = SoftwareSystemDynamicPageViewModel(\n            generatorContext,\n            generatorContext.workspace.model.addSoftwareSystem(\"Software system 2\")\n        )\n\n        assertThat(viewModel.visible).isFalse()\n    }\n\n    @Test\n    fun `no index`() {\n        val viewModel = SoftwareSystemDynamicPageViewModel(generatorContext, generatorContext.workspace.model\n            .addSoftwareSystem(\"Software system 2\").also {\n                val backend = it.addContainer(\"Api\")\n                generatorContext.workspace.views.createDynamicView(backend, \"api-dynamic\", \"Dynamic view 3\")\n            })\n        assertThat(viewModel.diagramIndex.visible).isFalse()\n    }\n\n    @Test\n    fun `has index`() {\n        val viewModel = SoftwareSystemDynamicPageViewModel(generatorContext, softwareSystem)\n        assertThat(viewModel.diagramIndex.visible).isTrue()\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemHomePageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.all\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport assertk.assertions.prop\nimport com.structurizr.documentation.Format\nimport com.structurizr.model.SoftwareSystem\nimport kotlin.test.Test\n\nclass SoftwareSystemHomePageViewModelTest : ViewModelTest() {\n    @Test\n    fun `active tab`() {\n        val generatorContext = generatorContext()\n        val softwareSystem: SoftwareSystem = generatorContext.workspace.model.addSoftwareSystem(\"Software system\")\n        val viewModel = SoftwareSystemHomePageViewModel(generatorContext, softwareSystem)\n\n        assertThat(viewModel.tabs.single { it.link.active }.tab)\n            .isEqualTo(SoftwareSystemPageViewModel.Tab.HOME)\n    }\n\n    @Test\n    fun `no section present`() {\n        val generatorContext = generatorContext()\n        val softwareSystem: SoftwareSystem = generatorContext.workspace.model.addSoftwareSystem(\"Software system\")\n            .apply { description = \"It's a system.\" }\n        val viewModel = SoftwareSystemHomePageViewModel(generatorContext, softwareSystem)\n\n        assertThat(viewModel.content)\n            .isEqualTo(\n                toHtml(\n                    viewModel,\n                    \"# Description${System.lineSeparator()}${softwareSystem.description}\",\n                    Format.Markdown,\n                    svgFactory\n                )\n            )\n    }\n\n    @Test\n    fun `section present`() {\n        val generatorContext = generatorContext()\n        val softwareSystem: SoftwareSystem = generatorContext.workspace.model.addSoftwareSystem(\"Software system\")\n            .apply {\n                documentation.addSection(createSection(\"# Title\"))\n            }\n        val viewModel = SoftwareSystemHomePageViewModel(generatorContext, softwareSystem)\n\n        assertThat(viewModel.content)\n            .isEqualTo(\n                toHtml(viewModel, softwareSystem.documentation.sections.single().content, Format.Markdown, svgFactory)\n            )\n    }\n\n    @Test\n    fun `no properties present`() {\n        val generatorContext = generatorContext()\n        val softwareSystem: SoftwareSystem = generatorContext.workspace.model.addSoftwareSystem(\"Software system\")\n            .apply { description = \"It's a system.\" }\n        val viewModel = SoftwareSystemHomePageViewModel(generatorContext, softwareSystem)\n\n        assertThat(viewModel)\n            .all {\n                prop(SoftwareSystemHomePageViewModel::hasProperties).isEqualTo(false)\n                prop(SoftwareSystemHomePageViewModel::propertiesTable)\n                    .isEqualTo(createPropertiesTableViewModel(mapOf()))\n            }\n    }\n\n    @Test\n    fun `properties present`() {\n        val generatorContext = generatorContext()\n        val softwareSystem: SoftwareSystem = generatorContext.workspace.model.addSoftwareSystem(\"Software system\")\n            .apply {\n                description = \"It's a system.\"\n                addProperty(\"Url\", \"https://tempuri.org/\")\n            }\n        val viewModel = SoftwareSystemHomePageViewModel(generatorContext, softwareSystem)\n\n        assertThat(viewModel)\n            .all {\n                prop(SoftwareSystemHomePageViewModel::hasProperties).isEqualTo(true)\n                prop(SoftwareSystemHomePageViewModel::propertiesTable)\n                    .isEqualTo(createPropertiesTableViewModel(softwareSystem.properties))\n            }\n    }\n\n    @Test\n    fun `internal properties present`() {\n        val generatorContext = generatorContext()\n        val softwareSystem: SoftwareSystem = generatorContext.workspace.model.addSoftwareSystem(\"Software system\")\n            .apply {\n                description = \"It's a system.\"\n                addProperty(\"structurizr.dsl.identifier\", \"id\")\n            }\n        val viewModel = SoftwareSystemHomePageViewModel(generatorContext, softwareSystem)\n\n        assertThat(viewModel)\n            .all {\n                prop(SoftwareSystemHomePageViewModel::hasProperties).isEqualTo(false)\n                prop(SoftwareSystemHomePageViewModel::propertiesTable)\n                    .isEqualTo(createPropertiesTableViewModel(mapOf()))\n            }\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemPageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.*\nimport nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemPageViewModel.Tab\nimport org.junit.jupiter.api.DynamicTest\nimport org.junit.jupiter.api.TestFactory\nimport kotlin.test.Test\n\nclass SoftwareSystemPageViewModelTest : ViewModelTest() {\n    @TestFactory\n    fun url() = listOf(\n        Tab.HOME to \"/software-system-with-1-name\",\n        Tab.SYSTEM_CONTEXT to \"/software-system-with-1-name/context\",\n        Tab.CONTAINER to \"/software-system-with-1-name/container\",\n        Tab.COMPONENT to \"/software-system-with-1-name/component\",\n        Tab.DEPLOYMENT to \"/software-system-with-1-name/deployment\",\n        Tab.DEPENDENCIES to \"/software-system-with-1-name/dependencies\",\n        Tab.DECISIONS to \"/software-system-with-1-name/decisions\",\n    ).map { (tab, expectedUrl) ->\n        DynamicTest.dynamicTest(\"url - $tab\") {\n            val generatorContext = generatorContext()\n            val softwareSystem = generatorContext.workspace.model.addSoftwareSystem(\"Software system With 1 name\")\n\n            assertThat(SoftwareSystemPageViewModel.url(softwareSystem, tab))\n                .isEqualTo(expectedUrl)\n            assertThat(SoftwareSystemPageViewModel(generatorContext, softwareSystem, tab).url)\n                .isEqualTo(SoftwareSystemPageViewModel.url(softwareSystem, tab))\n        }\n    }\n\n    @TestFactory\n    fun subtitle() = Tab.entries.map { tab ->\n        DynamicTest.dynamicTest(\"subtitle - $tab\") {\n            val generatorContext = generatorContext()\n            val softwareSystem = generatorContext.workspace.model.addSoftwareSystem(\"Some software system\")\n\n            assertThat(SoftwareSystemPageViewModel(generatorContext, softwareSystem, tab).pageSubTitle)\n                .isEqualTo(\"Some software system\")\n        }\n    }\n\n    @TestFactory\n    fun `active tab`() = Tab.entries\n        .filter { it != Tab.COMPONENT && it != Tab.CODE } // Component & code links are dynamic\n        .map { tab ->\n            DynamicTest.dynamicTest(\"active tab - $tab\") {\n                val generatorContext = generatorContext()\n                val softwareSystem = generatorContext.workspace.model.addSoftwareSystem(\"Some software system\")\n                val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, tab)\n\n                assertThat(\n                    viewModel.tabs\n                        .single { it.link.active }\n                        .tab\n                ).isEqualTo(tab)\n            }\n        }\n\n    private val generatorContext = generatorContext()\n    private val softwareSystem = generatorContext.workspace.model.addSoftwareSystem(\"Some Software system\", \"Description\")\n\n    @Test\n    fun `software system description`() {\n        val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME)\n\n        assertThat(viewModel.description).isEqualTo(\"Description\")\n    }\n\n    @Test\n    fun tabs() {\n        val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME)\n\n        assertThat(viewModel.tabs.map { it.tab })\n            .containsExactly(\n                Tab.HOME,\n                Tab.SYSTEM_CONTEXT,\n                Tab.CONTAINER,\n                Tab.COMPONENT,\n                Tab.CODE,\n                Tab.DYNAMIC,\n                Tab.DEPLOYMENT,\n                Tab.DEPENDENCIES,\n                Tab.DECISIONS,\n                Tab.SECTIONS\n            )\n        assertThat(viewModel.tabs.map { it.link.title })\n            .containsExactly(\n                \"Info\",\n                \"Context views\",\n                \"Container views\",\n                \"Component views\",\n                \"Code views\",\n                \"Dynamic views\",\n                \"Deployment views\",\n                \"Dependencies\",\n                \"Decisions\",\n                \"Documentation\"\n            )\n    }\n\n    @Test\n    fun `home tab is visible`() {\n        val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME)\n\n        assertThat(getTab(viewModel, Tab.HOME).visible).isTrue()\n    }\n\n    @Test\n    fun `dependencies tab is visible`() {\n        val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME)\n\n        assertThat(getTab(viewModel, Tab.DEPENDENCIES).visible).isTrue()\n    }\n\n    @Test\n    fun `context views tab only visible when context diagrams available`() {\n        val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME)\n\n        assertThat(getTab(viewModel, Tab.SYSTEM_CONTEXT).visible).isFalse()\n        generatorContext.workspace.views.createSystemContextView(softwareSystem, \"context\", \"description\")\n        assertThat(getTab(viewModel, Tab.SYSTEM_CONTEXT).visible).isTrue()\n    }\n\n    @Test\n    fun `container views tab only visible when container diagrams available`() {\n        val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME)\n\n        assertThat(getTab(viewModel, Tab.CONTAINER).visible).isFalse()\n        generatorContext.workspace.views.createContainerView(softwareSystem, \"container\", \"description\")\n        assertThat(getTab(viewModel, Tab.CONTAINER).visible).isTrue()\n    }\n\n    @Test\n    fun `component views tab only visible when component diagrams available`() {\n        val container = softwareSystem.addContainer(\"Backend\")\n        val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME)\n\n        assertThat(getTab(viewModel, Tab.COMPONENT).visible).isFalse()\n        generatorContext.workspace.views.createComponentView(container, \"component\", \"description\")\n        assertThat(getTab(viewModel, Tab.COMPONENT).visible).isTrue()\n    }\n\n    @Test\n    fun `code views tab only visible when component diagrams available and component diagram has image view`() {\n        val container = softwareSystem.addContainer(\"Backend\")\n        val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME)\n\n        assertThat(getTab(viewModel, Tab.CODE).visible).isFalse()\n        val component = container.addComponent(\"Backend Component\")\n        generatorContext.workspace.views.createComponentView(container, \"component\", \"description\")\n        createImageView(generatorContext.workspace, component)\n        assertThat(getTab(viewModel, Tab.CODE).visible).isTrue()\n    }\n\n    @Test\n    fun `dynamic views tab only visible when dynamic diagrams available`() {\n        val container = softwareSystem.addContainer(\"Backend\")\n        val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME)\n\n        assertThat(getTab(viewModel, Tab.DYNAMIC).visible).isFalse()\n        generatorContext.workspace.views.createDynamicView(container, \"component\", \"description\")\n        assertThat(getTab(viewModel, Tab.DYNAMIC).visible).isTrue()\n    }\n\n    @Test\n    fun `deployment views tab only visible when deployment diagrams available`() {\n        val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME)\n\n        assertThat(getTab(viewModel, Tab.DEPLOYMENT).visible).isFalse()\n        generatorContext.workspace.views.createDeploymentView(softwareSystem, \"deployment\", \"description\")\n        assertThat(getTab(viewModel, Tab.DEPLOYMENT).visible).isTrue()\n    }\n\n    @Test\n    fun `decisions views tab visible when software system decisions available`() {\n        val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME)\n\n        assertThat(getTab(viewModel, Tab.DECISIONS).visible).isFalse()\n        softwareSystem.documentation.addDecision(createDecision(\"1\", \"Proposed\"))\n        assertThat(getTab(viewModel, Tab.DECISIONS).visible).isTrue()\n    }\n\n    @Test\n    fun `decisions views tab visible when component decisions available in software system`() {\n        val container = softwareSystem.addContainer(\"Some Container\")\n\n        container.addComponent(\"Some component\").documentation.addDecision(createDecision(\"2\", \"Proposed\"))\n\n        val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME)\n\n        assertThat(getTab(viewModel, Tab.DECISIONS).visible).isTrue()\n    }\n\n    @Test\n    fun `decisions views tab visible when container decisions available in software system`() {\n        val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME)\n\n        assertThat(getTab(viewModel, Tab.DECISIONS).visible).isFalse()\n        softwareSystem.addContainer(\"Some Container\").documentation.addDecision(createDecision(\"2\", \"Proposed\"))\n        assertThat(getTab(viewModel, Tab.DECISIONS).visible).isTrue()\n    }\n\n    @Test\n    fun `decisions views tab visible when container & software system decisions are available`() {\n        val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME)\n\n        assertThat(getTab(viewModel, Tab.DECISIONS).visible).isFalse()\n        softwareSystem.addContainer(\"Some Container\").documentation.addDecision(createDecision(\"2\", \"Proposed\"))\n        softwareSystem.documentation.addDecision(createDecision(\"1\", \"Proposed\"))\n        assertThat(getTab(viewModel, Tab.DECISIONS).visible).isTrue()\n    }\n\n    @Test\n    fun `sections views tab only visible when two or more sections available`() {\n        val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME)\n\n        assertThat(getTab(viewModel, Tab.SECTIONS).visible).isFalse()\n        softwareSystem.documentation.addSection(createSection(\"# Section 0000\"))\n        assertThat(getTab(viewModel, Tab.SECTIONS).visible).isFalse()\n        softwareSystem.documentation.addSection(createSection(\"# Section 0001\"))\n        assertThat(getTab(viewModel, Tab.SECTIONS).visible).isTrue()\n    }\n\n    @Test\n    fun `sections views tab visible when container sections available in software system`() {\n        val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME)\n\n        assertThat(getTab(viewModel, Tab.SECTIONS).visible).isFalse()\n        softwareSystem.addContainer(\"Some Container\").documentation.addSection(createSection())\n        assertThat(getTab(viewModel, Tab.SECTIONS).visible).isTrue()\n    }\n\n    @Test\n    fun `sections views tab visible when container & software system sections are available`() {\n        val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.HOME)\n\n        assertThat(getTab(viewModel, Tab.SECTIONS).visible).isFalse()\n        softwareSystem.addContainer(\"Some Container\").documentation.addSection(createSection())\n        repeat(2) { // the first section is shown in the software system home\n            softwareSystem.documentation.addSection(createSection())\n        }\n        assertThat(getTab(viewModel, Tab.SECTIONS).visible).isTrue()\n    }\n\n    private fun getTab(viewModel: SoftwareSystemPageViewModel, tab: Tab) =\n        viewModel.tabs.single { it.tab == tab }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemSectionPageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport com.structurizr.documentation.Format\nimport nl.avisi.structurizr.site.generatr.normalize\nimport kotlin.test.Test\n\nclass SoftwareSystemSectionPageViewModelTest : ViewModelTest() {\n    private val generatorContext = generatorContext()\n    private val softwareSystem = generatorContext.workspace.model.addSoftwareSystem(\"Software System\")\n\n    @Test\n    fun url() {\n        val section = createSection()\n        val viewModel = SoftwareSystemSectionPageViewModel(generatorContext, softwareSystem, section)\n\n        assertThat(SoftwareSystemSectionPageViewModel.url(softwareSystem, section))\n            .isEqualTo(\"/${softwareSystem.name.normalize()}/sections/${section.contentTitle().normalize()}\")\n        assertThat(viewModel.url)\n            .isEqualTo(SoftwareSystemSectionPageViewModel.url(softwareSystem, section))\n    }\n\n    @Test\n    fun `active tab`() {\n        val viewModel = SoftwareSystemSectionPageViewModel(generatorContext, softwareSystem, createSection())\n\n        assertThat(viewModel.tabs.single { it.link.active }.tab)\n            .isEqualTo(SoftwareSystemPageViewModel.Tab.SECTIONS)\n    }\n\n    @Test\n    fun content() {\n        val section = createSection()\n        val viewModel = SoftwareSystemSectionPageViewModel(generatorContext, softwareSystem, section)\n\n        assertThat(viewModel.content).isEqualTo(toHtml(viewModel, section.content, Format.Markdown, svgFactory))\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemSectionsPageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.all\nimport assertk.assertThat\nimport assertk.assertions.hasSize\nimport assertk.assertions.index\nimport assertk.assertions.isEqualTo\nimport assertk.assertions.isFalse\nimport assertk.assertions.isTrue\nimport nl.avisi.structurizr.site.generatr.normalize\nimport kotlin.test.Test\n\nclass SoftwareSystemSectionsPageViewModelTest : ViewModelTest() {\n    private val generatorContext = generatorContext()\n    private val softwareSystem = generatorContext.workspace.model.addSoftwareSystem(\"Software system\")\n\n    @Test\n    fun `active tab`() {\n        val viewModel = SoftwareSystemSectionsPageViewModel(generatorContext, softwareSystem)\n\n        assertThat(viewModel.tabs.single { it.link.active }.tab)\n            .isEqualTo(SoftwareSystemPageViewModel.Tab.SECTIONS)\n    }\n\n    @Test\n    fun `sections table`() {\n        listOf(\"# Section 0000\", \"# Section 0001\")\n            .forEach { softwareSystem.documentation.addSection(createSection(it)) }\n\n        val viewModel = SoftwareSystemSectionsPageViewModel(generatorContext, softwareSystem)\n\n        assertThat(viewModel.sectionsTable.bodyRows).all {\n            hasSize(1)\n            index(0).transform { (it.columns[1] as TableViewModel.LinkCellViewModel).link }\n                .isEqualTo(\n                    LinkViewModel(\n                        viewModel,\n                        \"Section 0001\",\n                        \"/software-system/sections/section-0001\"\n                    )\n                )\n        }\n    }\n\n    @Test\n    fun `has sections`() {\n        listOf(\"# Section 0000\", \"# Section 0001\")\n            .forEach { softwareSystem.documentation.addSection(createSection(it)) }\n\n        val viewModel = SoftwareSystemSectionsPageViewModel(generatorContext, softwareSystem)\n\n        assertThat(viewModel.visible).isTrue()\n        assertThat(viewModel.onlyContainersDocumentationSectionsVisible).isFalse()\n    }\n\n    @Test\n    fun `no sections`() {\n        val viewModel = SoftwareSystemSectionsPageViewModel(\n            generatorContext,\n            generatorContext.workspace.model.addSoftwareSystem(\"Software system 2\")\n        )\n\n        assertThat(viewModel.visible).isFalse()\n        assertThat(viewModel.onlyContainersDocumentationSectionsVisible).isFalse()\n    }\n\n    @Test\n    fun `child has section`() {\n        val container = softwareSystem.addContainer(\"API Application\")\n        container.documentation.addSection(createSection())\n\n        val viewModel = SoftwareSystemSectionsPageViewModel(generatorContext, softwareSystem)\n\n        assertThat(viewModel.visible).isTrue()\n        assertThat(viewModel.onlyContainersDocumentationSectionsVisible).isTrue()\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemsPageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.containsExactly\nimport assertk.assertions.isEqualTo\nimport kotlin.test.Test\n\nclass SoftwareSystemsPageViewModelTest : ViewModelTest() {\n    @Test\n    fun url() {\n        assertThat(SoftwareSystemsPageViewModel.url())\n            .isEqualTo(\"/software-systems\")\n        assertThat(SoftwareSystemsPageViewModel(generatorContext()).url)\n            .isEqualTo(SoftwareSystemsPageViewModel.url())\n    }\n\n    @Test\n    fun subtitle() {\n        assertThat(SoftwareSystemsPageViewModel(generatorContext()).pageSubTitle)\n            .isEqualTo(\"Software Systems\")\n    }\n\n    @Test\n    fun `software systems table`() {\n        val generatorContext = generatorContext()\n        val system1 = generatorContext.workspace.model.addSoftwareSystem(\"System 1\", \"System 1 description\")\n        val system2 = generatorContext.workspace.model.addSoftwareSystem(\"System 2\", \"System 2 description\")\n        val viewModel = SoftwareSystemsPageViewModel(generatorContext)\n\n        assertThat(viewModel.softwareSystemsTable).isEqualTo(\n            TableViewModel.create {\n                headerRow(headerCellMedium(\"Name\"), headerCell(\"Description\"))\n                bodyRow(\n                    cellWithSoftwareSystemLink(\n                        viewModel, system1.name, SoftwareSystemPageViewModel.url(\n                            system1,\n                            SoftwareSystemPageViewModel.Tab.HOME\n                        )\n                    ),\n                    cell(system1.description)\n                )\n                bodyRow(\n                    cellWithSoftwareSystemLink(\n                        viewModel, system2.name, SoftwareSystemPageViewModel.url(\n                            system2,\n                            SoftwareSystemPageViewModel.Tab.HOME\n                        )\n                    ),\n                    cell(system2.description)\n                )\n            }\n        )\n    }\n\n    @Test\n    fun `software systems table sorted by name - case insensitive`() {\n        val generatorContext = generatorContext()\n        val system1 = generatorContext.workspace.model.addSoftwareSystem(\"system 1\", \"System 1 description\")\n        val system2 = generatorContext.workspace.model.addSoftwareSystem(\"System 2\", \"System 2 description\")\n        val viewModel = SoftwareSystemsPageViewModel(generatorContext)\n\n        val names = viewModel.softwareSystemsTable.bodyRows\n            .map { it.columns[0] }\n            .filterIsInstance<TableViewModel.LinkCellViewModel>()\n            .map { it.link.title }\n\n        assertThat(names).containsExactly(system1.name, system2.name)\n    }\n\n    @Test\n    fun `external systems have grey text (declared external by tag)`() {\n        val generatorContext = generatorContext()\n        generatorContext.workspace.views.configuration.addProperty(\"generatr.site.externalTag\", \"External System\")\n        generatorContext.workspace.model.addSoftwareSystem(\"system 1\", \"System 1 description\")\n        generatorContext.workspace.model.addSoftwareSystem(\"system 2\", \"System 2 description\").apply { addTags(\"External System\") }\n\n        val viewModel = SoftwareSystemsPageViewModel(generatorContext)\n\n        assertThat(viewModel.softwareSystemsTable.bodyRows[1].columns[0])\n            .isEqualTo(TableViewModel.TextCellViewModel(\"system 2 (External)\", isHeader = false, greyText = true, boldText = true))\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/TableViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.containsAtLeast\nimport kotlin.test.Test\n\nclass TableViewModelTest : ViewModelTest() {\n    private val pageViewModel = object : PageViewModel(generatorContext()) {\n        override val url = \"/test-page\"\n        override val pageSubTitle = \"Test page\"\n    }\n\n    @Test\n    fun `header rows`() {\n        val viewModel = TableViewModel.create {\n            headerRow(headerCell(\"1\"), headerCell(\"2\"), headerCell(\"3\"))\n        }\n\n        assertThat(viewModel.headerRows).containsAtLeast(\n            TableViewModel.RowViewModel(\n                listOf(\n                    TableViewModel.TextCellViewModel(\"1\", isHeader = true),\n                    TableViewModel.TextCellViewModel(\"2\", isHeader = true),\n                    TableViewModel.TextCellViewModel(\"3\", isHeader = true),\n                )\n            )\n        )\n    }\n\n    @Test\n    fun `header row with index`() {\n        val viewModel = TableViewModel.create {\n            headerRow(headerCellSmall(\"1\"))\n        }\n\n        assertThat(viewModel.headerRows).containsAtLeast(\n            TableViewModel.RowViewModel(\n                listOf(\n                    TableViewModel.TextCellViewModel(\"1\", isHeader = true, width = CellWidth.ONE_TENTH)\n                )\n            )\n        )\n    }\n\n    @Test\n    fun `header row with name`() {\n        val viewModel = TableViewModel.create {\n            headerRow(headerCellMedium(\"Name\"))\n        }\n\n        assertThat(viewModel.headerRows).containsAtLeast(\n            TableViewModel.RowViewModel(\n                listOf(\n                    TableViewModel.TextCellViewModel(\"Name\", isHeader = true, width = CellWidth.ONE_FOURTH)\n                )\n            )\n        )\n    }\n\n    @Test\n    fun `header row with description`() {\n        val viewModel = TableViewModel.create {\n            headerRow(headerCellLarge(\"Description\"))\n        }\n\n        assertThat(viewModel.headerRows).containsAtLeast(\n            TableViewModel.RowViewModel(\n                listOf(\n                    TableViewModel.TextCellViewModel(\"Description\", isHeader = true, width = CellWidth.TWO_FOURTH)\n                )\n            )\n        )\n    }\n\n    @Test\n    fun `body rows`() {\n        val viewModel = TableViewModel.create {\n            bodyRow(cell(\"1\"), cell(\"2\"), cell(\"3\"))\n        }\n\n        assertThat(viewModel.bodyRows).containsAtLeast(\n            TableViewModel.RowViewModel(\n                listOf(\n                    TableViewModel.TextCellViewModel(\"1\", isHeader = false),\n                    TableViewModel.TextCellViewModel(\"2\", isHeader = false),\n                    TableViewModel.TextCellViewModel(\"3\", isHeader = false),\n                )\n            )\n        )\n    }\n\n    @Test\n    fun `cell with link`() {\n        val viewModel = TableViewModel.create {\n            bodyRow(cellWithLink(pageViewModel, \"click me\", \"/decisions\"))\n        }\n\n        assertThat(viewModel.bodyRows).containsAtLeast(\n            TableViewModel.RowViewModel(\n                listOf(\n                    TableViewModel.LinkCellViewModel(\n                        LinkViewModel(pageViewModel, \"click me\", \"/decisions\"),\n                        isHeader = false\n                    )\n                )\n            )\n        )\n    }\n\n    @Test\n    fun `cell with external link`() {\n        val viewModel = TableViewModel.create {\n            bodyRow(cellWithExternalLink(\"Temporary URI\", \"https://tempuri.org/\"))\n        }\n\n        assertThat(viewModel.bodyRows).containsAtLeast(\n            TableViewModel.RowViewModel(\n                listOf(\n                    TableViewModel.ExternalLinkCellViewModel(\n                        ExternalLinkViewModel(\"Temporary URI\", \"https://tempuri.org/\"),\n                        isHeader = false\n                    )\n                )\n            )\n        )\n    }\n\n    @Test\n    fun `cell with index`() {\n        val viewModel = TableViewModel.create {\n            bodyRow(cellWithIndex(\"1\"))\n        }\n\n        assertThat(viewModel.bodyRows).containsAtLeast(\n            TableViewModel.RowViewModel(\n                listOf(\n                    TableViewModel.TextCellViewModel(\n                        \"1\",\n                        isHeader = false,\n                        greyText = false,\n                        boldText = true\n                    )\n                )\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/ViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport com.structurizr.Workspace\nimport com.structurizr.documentation.Decision\nimport com.structurizr.documentation.Format\nimport com.structurizr.documentation.Section\nimport com.structurizr.model.Element\nimport com.structurizr.view.ImageView\nimport nl.avisi.structurizr.site.generatr.site.GeneratorContext\nimport java.time.LocalDate\nimport java.time.ZoneId\nimport java.util.*\n\nabstract class ViewModelTest {\n    protected val svgFactory = { _: String, _: String -> \"\"\"<svg viewBox=\"0 0 800 900\"></svg>\"\"\" }\n\n    protected fun generatorContext(\n        workspaceName: String = \"Workspace name\",\n        branches: List<String> = listOf(\"main\"),\n        currentBranch: String = \"main\",\n        version: String = \"1.0.0\"\n    ) = GeneratorContext(version, Workspace(workspaceName, \"\"), branches, currentBranch, false, svgFactory)\n\n    protected fun pageViewModel(pageHref: String = \"/some-page\") = object : PageViewModel(generatorContext()) {\n        override val url = pageHref\n        override val pageSubTitle = \"Some page\"\n    }\n\n    protected fun createDecision(\n        id: String = \"1\",\n        decisionStatus: String = \"Accepted\",\n        decisionDate: LocalDate = LocalDate.now()\n    ) =\n        Decision(id).apply {\n            title = \"Decision $id\"\n            status = decisionStatus\n            date = Date.from(decisionDate.atStartOfDay(ZoneId.systemDefault()).toInstant())\n            format = Format.Markdown\n            content = \"Decision $id content\"\n        }\n\n    protected fun createSection(content: String = \"# Content\") = Section(Format.Markdown, content)\n\n    protected fun createImageView(workspace: Workspace, element: Element, key: String = \"imageview-${element.id}\", title: String? = \"Image View Title\"): ImageView = workspace.views.createImageView(element, key).also {\n        it.description = \"Image View Description\"\n        it.title = title\n        it.contentType = \"image/png\"\n        it.content = \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=\"\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/WorkspaceDecisionPageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport com.structurizr.documentation.Format\nimport kotlin.test.Test\n\nclass WorkspaceDecisionPageViewModelTest : ViewModelTest() {\n    @Test\n    fun url() {\n        val decision = createDecision()\n\n        assertThat(WorkspaceDecisionPageViewModel.url(decision))\n            .isEqualTo(\"/decisions/${decision.id}\")\n        assertThat(WorkspaceDecisionPageViewModel(generatorContext(), decision).url)\n            .isEqualTo(WorkspaceDecisionPageViewModel.url(decision))\n    }\n\n    @Test\n    fun subtitle() {\n        val decision = createDecision()\n        val viewModel = WorkspaceDecisionPageViewModel(generatorContext(), decision)\n\n        assertThat(viewModel.pageSubTitle).isEqualTo(decision.title)\n    }\n\n    @Test\n    fun content() {\n        val decision = createDecision()\n        val viewModel = WorkspaceDecisionPageViewModel(generatorContext(), decision)\n\n        assertThat(viewModel.content).isEqualTo(toHtml(viewModel, decision.content, Format.Markdown, svgFactory))\n    }\n\n    @Test\n    fun `link to other ADR`() {\n        val decision = createDecision().apply {\n            content = \"\"\"\n                Decision with [link to other ADR](#2).\n                [Web link](https://google.com)\n                [Section link](#other-section)\n                [Internal link](../embedding-diagrams-and-images/)\n            \"\"\".trimIndent()\n        }\n        val viewModel = WorkspaceDecisionPageViewModel(generatorContext(), decision)\n\n        assertThat(viewModel.content).isEqualTo(\n            toHtml(\n                viewModel, \"\"\"\n                    Decision with [link to other ADR](decisions/2/).\n                    [Web link](https://google.com)\n                    [Section link](#other-section)\n                    [Internal link](../embedding-diagrams-and-images/)\n                \"\"\".trimIndent(),\n                Format.Markdown,\n                svgFactory\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/WorkspaceDecisionsPageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport java.time.LocalDate\nimport java.time.Month\nimport kotlin.test.Test\n\nclass WorkspaceDecisionsPageViewModelTest : ViewModelTest() {\n    @Test\n    fun url() {\n        assertThat(WorkspaceDecisionsPageViewModel.url())\n            .isEqualTo(\"/decisions\")\n    }\n\n    @Test\n    fun `decisions table`() {\n        val generatorContext = generatorContext()\n        val decision1 = createDecision(\"1\", \"Accepted\", LocalDate.of(2022, Month.JANUARY, 1))\n        val decision2 = createDecision(\"2\", \"Proposed\", LocalDate.of(2022, Month.JANUARY, 2))\n        generatorContext.workspace.documentation.addDecision(decision1)\n        generatorContext.workspace.documentation.addDecision(decision2)\n\n        val viewModel = WorkspaceDecisionsPageViewModel(generatorContext)\n\n        assertThat(viewModel.decisionsTable).isEqualTo(\n            viewModel.createDecisionsTableViewModel(generatorContext.workspace.documentation.decisions) {\n                WorkspaceDecisionPageViewModel.url(it)\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/WorkspaceDocumentationSectionPageViewModelTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.model\n\nimport assertk.assertThat\nimport assertk.assertions.isEqualTo\nimport com.structurizr.documentation.Format\nimport kotlin.test.Test\n\nclass WorkspaceDocumentationSectionPageViewModelTest : ViewModelTest() {\n    @Test\n    fun url() {\n        val section = createSection(\"# Some section With words and 1 number\")\n\n        assertThat(WorkspaceDocumentationSectionPageViewModel.url(section))\n            .isEqualTo(\"/some-section-with-words-and-1-number\")\n    }\n\n    @Test\n    fun `normalized url`() {\n        val generatorContext = generatorContext()\n        val viewModel = WorkspaceDocumentationSectionPageViewModel(\n            generatorContext, createSection(\"# Some section With words and 1 number\")\n        )\n\n        assertThat(viewModel.url).isEqualTo(\"/some-section-with-words-and-1-number\")\n    }\n\n    @Test\n    fun subtitle() {\n        val generatorContext = generatorContext()\n        val section = createSection()\n        val viewModel = WorkspaceDocumentationSectionPageViewModel(generatorContext, section)\n\n        assertThat(viewModel.pageSubTitle).isEqualTo(section.contentTitle())\n    }\n\n    @Test\n    fun content() {\n        val generatorContext = generatorContext()\n        val section = createSection()\n        val viewModel = WorkspaceDocumentationSectionPageViewModel(generatorContext, section)\n\n        assertThat(viewModel.content).isEqualTo(toHtml(viewModel, section.content, Format.Markdown, svgFactory))\n    }\n}\n"
  },
  {
    "path": "src/test/kotlin/nl/avisi/structurizr/site/generatr/site/views/CDNTest.kt",
    "content": "package nl.avisi.structurizr.site.generatr.site.views\n\nimport assertk.all\nimport assertk.assertThat\nimport assertk.assertions.*\nimport com.structurizr.Workspace\nimport org.junit.jupiter.api.DynamicTest\nimport org.junit.jupiter.api.Test\nimport org.junit.jupiter.api.TestFactory\n\nclass CDNTest {\n    @TestFactory\n    fun `cdn locations`() : List<DynamicTest> {\n        val workspace = Workspace(\"workspace name\", \"\")\n        val cdn = CDN(workspace)\n\n        return listOf(\n            cdn.bulmaCss() to \"/css/bulma.min.css\",\n            cdn.katexJs() to \"/dist/katex.min.js\",\n            cdn.katexCss() to \"/dist/katex.min.css\",\n            cdn.lunrJs() to \"/lunr.min.js\",\n            cdn.lunrLanguagesStemmerJs() to \"/min/lunr.stemmer.support.min.js\",\n            cdn.lunrLanguagesJs(\"en\") to \"/min/lunr.en.min.js\",\n            cdn.mermaidJs() to \"/dist/mermaid.esm.min.mjs\",\n            cdn.svgpanzoomJs() to \"/dist/svg-pan-zoom.min.js\",\n            cdn.webfontloaderJs() to \"/webfontloader.js\"\n        ).map { (url, suffix) ->\n            DynamicTest.dynamicTest(url) {\n                assertThat(url).all {\n                    startsWith(\"https://\")\n                    endsWith(suffix)\n                    doesNotContain(\"~\", \"^\", \">\", \"<\", \"=\", \":=\")\n                }\n            }\n        }\n    }\n\n    @Test\n    fun `usage of default cdn url`() {\n        val workspace = Workspace(\"workspace\", \"\")\n        val cdn = CDN(workspace)\n\n        assertThat(cdn.workspace.views.configuration.properties[\"generatr.site.cdn\"]).isNull()\n        assertThat(cdn.getCdnBaseUrl()).isEqualTo(\"https://cdn.jsdelivr.net/npm\")\n    }\n\n    @Test\n    fun `usage of view configuration with ending slash`() {\n        val workspace = Workspace(\"workspace name\", \"\").apply {\n            views.configuration.addProperty(\"generatr.site.cdn\", \"https://cdn.companyname.net/npm/\")\n        }\n        val cdn = CDN(workspace)\n\n        assertThat(cdn.getCdnBaseUrl()).isEqualTo(\"https://cdn.companyname.net/npm\")\n    }\n\n    @Test\n    fun `usage of view configuration without ending slash`() {\n        val workspace = Workspace(\"workspace name\", \"\").apply {\n            views.configuration.addProperty(\"generatr.site.cdn\", \"https://cdn.companyname.net/npm\")\n        }\n        val cdn = CDN(workspace)\n\n        assertThat(cdn.getCdnBaseUrl()).isEqualTo(\"https://cdn.companyname.net/npm\")\n    }\n}\n"
  }
]