├── .dockerignore ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ ├── documentation.yml │ └── feature_request.yml ├── contributing.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── codeql.yml │ ├── docker.yml │ └── go.yml ├── .gitignore ├── .golangci.yml ├── CODEOWNERS ├── Dockerfile ├── LICENSE ├── README.md ├── Taskfile.yml ├── conf ├── app.ini ├── embed.go └── locale │ ├── embed.go │ ├── locale_en-US.ini │ └── locale_zh-CN.ini ├── docs ├── assets │ └── github-webhook.jpg ├── dev │ ├── local_development.md │ └── new_release.md ├── en-US │ ├── howto │ │ ├── README.md │ │ ├── configure-reverse-proxy.md │ │ ├── customize-templates.md │ │ ├── run-through-docker.md │ │ ├── set-up-documentation.md │ │ ├── sync-through-webhook.md │ │ ├── use-extensions.md │ │ └── write-document.md │ └── introduction │ │ ├── README.md │ │ ├── installation.md │ │ └── quick-start.md ├── toc.ini └── zh-CN │ ├── howto │ ├── README.md │ ├── configure-reverse-proxy.md │ ├── customize-templates.md │ ├── run-through-docker.md │ ├── set-up-documentation.md │ ├── sync-through-webhook.md │ ├── use-extensions.md │ └── write-document.md │ └── introduction │ ├── README.md │ ├── installation.md │ └── quick-start.md ├── go.mod ├── go.sum ├── internal ├── cmd │ ├── cmd.go │ └── web.go ├── conf │ ├── conf.go │ └── static.go ├── osutil │ ├── osutil.go │ └── osutil_test.go └── store │ ├── markdown.go │ ├── markdown_test.go │ ├── store.go │ └── toc.go ├── main.go ├── public ├── css │ └── main.css ├── embed.go └── img │ ├── asouldocs-dark.png │ ├── asouldocs-light.png │ └── favicon.png ├── tailwind.config.js └── templates ├── 404.html ├── common ├── head.html └── navbar.html ├── docs ├── navbar.html └── page.html ├── embed.go └── home.html /.dockerignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | .dockerignore 3 | *.yml 4 | !Taskfile.yml 5 | *.md 6 | .editorconfig 7 | .gitignore 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{html, css, yml}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.js] 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: File a bug report to help us improve 3 | labels: ["\U0001F48A bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to fill out this bug report! 9 | 10 | - Please use English :) 11 | - For questions, ask in [Discussions](https://github.com/asoul-sig/asouldocs/discussions). 12 | - Before you file an issue read the [Contributing guide](https://github.com/asoul-sig/asouldocs/blob/main/.github/contributing.md). 13 | - Check to make sure someone hasn't already opened a similar [issue](https://github.com/asoul-sig/asouldocs/issues). 14 | - type: input 15 | attributes: 16 | label: Version 17 | description: | 18 | Please specify the exact version you're reporting for, e.g. "1.0.0". 19 | 20 | _Note that "unknwon/asouldocs:latest" is not a valid version, it does not mean anything._ 21 | validations: 22 | required: true 23 | - type: input 24 | attributes: 25 | label: Operating system 26 | description: | 27 | Please specify the exact operating system name and version you're reporting for, e.g. "Windows 10", "CentOS 7", "Ubuntu 20.04". 28 | validations: 29 | required: true 30 | - type: textarea 31 | attributes: 32 | label: Describe the bug 33 | description: A clear and concise description of what the bug is. 34 | validations: 35 | required: true 36 | - type: textarea 37 | attributes: 38 | label: To reproduce 39 | description: The steps to reproduce the problem described above. 40 | validations: 41 | required: true 42 | - type: textarea 43 | attributes: 44 | label: Expected behavior 45 | description: A clear and concise description of what you expected to happen. 46 | validations: 47 | required: true 48 | - type: textarea 49 | attributes: 50 | label: Additional context 51 | description: | 52 | Links? References? Suggestions? Anything that will give us more context about the issue you are encountering! 53 | 54 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. 55 | validations: 56 | required: false 57 | - type: checkboxes 58 | attributes: 59 | label: Code of Conduct 60 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://go.dev/conduct) 61 | options: 62 | - label: I agree to follow this project's Code of Conduct 63 | required: true 64 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask questions 4 | url: https://github.com/asoul-sig/asouldocs/discussions 5 | about: Please ask questions in Discussions. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Improve documentation 2 | description: Suggest an idea or a patch for documentation 3 | labels: ["📖 documentation"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to fill out this form! 9 | 10 | - Please use English :) 11 | - For questions, ask in [Discussions](https://github.com/asoul-sig/asouldocs/discussions). 12 | - Before you file an issue read the [Contributing guide](https://github.com/asoul-sig/asouldocs/blob/main/.github/contributing.md). 13 | - Check to make sure someone hasn't already opened a similar [issue](https://github.com/asoul-sig/asouldocs/issues). 14 | - type: textarea 15 | attributes: 16 | label: What needs to be improved? Please describe 17 | description: A clear and concise description of what is wrong or missing. 18 | validations: 19 | required: true 20 | - type: textarea 21 | attributes: 22 | label: Why do you think it is important? 23 | description: A clear and concise explanation of the rationale. 24 | validations: 25 | required: true 26 | - type: checkboxes 27 | attributes: 28 | label: Code of Conduct 29 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://go.dev/conduct) 30 | options: 31 | - label: I agree to follow this project's Code of Conduct 32 | required: true 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this project 3 | labels: ["\U0001F3AF feature"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to fill out this form! 9 | 10 | - Please use English :) 11 | - For questions, ask in [Discussions](https://github.com/asoul-sig/asouldocs/discussions). 12 | - Before you file an issue read the [Contributing guide](https://github.com/asoul-sig/asouldocs/blob/main/.github/contributing.md). 13 | - Check to make sure someone hasn't already opened a similar [issue](https://github.com/asoul-sig/asouldocs/issues). 14 | - type: textarea 15 | attributes: 16 | label: Describe the feature 17 | description: A clear and concise description of what the feature is, e.g. I think it is reasonable to have [...] 18 | validations: 19 | required: true 20 | - type: textarea 21 | attributes: 22 | label: Describe the solution you'd like 23 | description: A clear and concise description of what you want to happen. 24 | validations: 25 | required: true 26 | - type: textarea 27 | attributes: 28 | label: Describe alternatives you've considered 29 | description: A clear and concise description of any alternative solutions or features you've considered. 30 | validations: 31 | required: true 32 | - type: textarea 33 | attributes: 34 | label: Additional context 35 | description: | 36 | Links? References? Suggestions? Anything that will give us more context about the feature you are requesting! 37 | 38 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. 39 | validations: 40 | required: false 41 | - type: checkboxes 42 | attributes: 43 | label: Code of Conduct 44 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://go.dev/conduct) 45 | options: 46 | - label: I agree to follow this project's Code of Conduct 47 | required: true 48 | -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | # Welcome to _**ASoulDocs**_ contributing guide 2 | 3 | Thank you for investing your time in contributing to our projects! 4 | 5 | Read our [Code of Conduct](https://go.dev/conduct) to keep our community approachable and respectable. 6 | 7 | In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR. 8 | 9 | Use the table of contents icon on the top left corner of this document to get to a specific section of this guide quickly. 10 | 11 | ## New contributor guide 12 | 13 | To get an overview of the project, read the [README](/README.md). Here are some resources to help you get started with open source contributions: 14 | 15 | - [Finding ways to contribute to open source on GitHub](https://docs.github.com/en/get-started/exploring-projects-on-github/finding-ways-to-contribute-to-open-source-on-github) 16 | - [Set up Git](https://docs.github.com/en/get-started/quickstart/set-up-git) 17 | - [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow) 18 | - [Collaborating with pull requests](https://docs.github.com/en/github/collaborating-with-pull-requests) 19 | 20 | In addition to the general guides with open source contributions, you would also need to: 21 | 22 | - Have basic knowledge about HTTP, web applications development and programming in [Go](https://go.dev/). 23 | - Have a working local development setup with a reasonable good IDE or editor like [Visual Studio Code](https://code.visualstudio.com/docs/languages/go), [GoLand](https://www.jetbrains.com/go/) or [Vim](https://github.com/fatih/vim-go). 24 | 25 | ## Philosophy and methodology 26 | 27 | - [Talk, then code](https://www.craft.do/s/kyHVs6OoE4Dj5V) 28 | 29 | ## Issues 30 | 31 | ### Create a new issue 32 | 33 | - For questions, ask in [Discussions](https://github.com/asoul-sig/asouldocs/discussions). 34 | - [Check to make sure](https://docs.github.com/en/github/searching-for-information-on-github/searching-on-github/searching-issues-and-pull-requests#search-by-the-title-body-or-comments) someone hasn't already opened a similar [issue](https://github.com/asoul-sig/asouldocs/issues). 35 | - If a similar issue doesn't exist, open a new issue using a relevant [issue form](https://github.com/asoul-sig/asouldocs/issues/new/choose). 36 | 37 | ### Pick up an issue to solve 38 | 39 | - Scan through our [existing issues](https://github.com/asoul-sig/asouldocs/issues) to find one that interests you. 40 | - The [👋 good first issue](https://github.com/asoul-sig/asouldocs/issues?q=is%3Aissue+is%3Aopen+label%3A%22%F0%9F%91%8B+good+first+issue%22) is a good place to start exploring issues that are well-groomed for newcomers. 41 | - Do not hesitate to ask for more details or clarifying questions on the issue! 42 | - Communicate on the issue you are intended to pick up _before_ starting working on it. 43 | - Every issue that gets picked up will have an expected timeline for the implementation, the issue may be reassigned after the expected timeline. Please be responsible and proactive on the communication 🙇‍♂️ 44 | 45 | ## Pull requests 46 | 47 | When you're finished with the changes, create a pull request, or a series of pull requests if necessary. 48 | 49 | Contributing to another codebase is not as simple as code changes, it is also about contributing influence to the design. Therefore, we kindly ask you that: 50 | 51 | - Please acknowledge that no pull request is guaranteed to be merged. 52 | - Please always do a self-review before requesting reviews from others. 53 | - Please expect code review to be strict and may have multiple rounds. 54 | - Please make self-contained incremental changes, pull requests with huge diff may be rejected for review. 55 | - Please use English in code comments and docstring. 56 | - Please do not force push unless absolutely necessary. Force pushes make review much harder in multiple rounds, and we use [Squash and merge](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-pull-request-commits) so you don't need to worry about messy commits and just focus on the changes. 57 | 58 | ## Your PR is merged! 59 | 60 | Congratulations 🎉🎉 Thanks again for taking the effort to have this journey with us 🌟 61 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Docs: https://git.io/JCUAY 2 | version: 2 3 | updates: 4 | - package-ecosystem: "gomod" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | commit-message: 9 | prefix: "mod:" 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "monthly" 14 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Describe the pull request 2 | 3 | A clear and concise description of what the pull request is about, i.e. what problem should be fixed? 4 | 5 | Link to the issue: 6 | 7 | ### Checklist 8 | 9 | - [ ] I agree to follow the [Code of Conduct](https://go.dev/conduct) by submitting this pull request. 10 | - [ ] I have read and acknowledge the [Contributing guide](https://github.com/asoul-sig/asouldocs/blob/main/.github/contributing.md). 11 | - [ ] I have added test cases to cover the new code. 12 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | paths: 19 | - '.github/workflows/codeql.yml' 20 | schedule: 21 | - cron: '0 19 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'go' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v4 39 | with: 40 | # We must fetch at least the immediate parents so that if this is 41 | # a pull request then we can checkout the head. 42 | fetch-depth: 2 43 | 44 | # If this run was triggered by a pull request event, then checkout 45 | # the head of the pull request instead of the merge commit. 46 | - run: git checkout HEAD^2 47 | if: ${{ github.event_name == 'pull_request' }} 48 | 49 | # Initializes the CodeQL tools for scanning. 50 | - name: Initialize CodeQL 51 | uses: github/codeql-action/init@v3 52 | with: 53 | languages: ${{ matrix.language }} 54 | # If you wish to specify custom queries, you can do so here or in a config file. 55 | # By default, queries listed here will override any specified in a config file. 56 | # Prefix the list here with "+" to use these queries and those in the config file. 57 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 58 | 59 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 60 | # If this step fails, then you should remove it and run the build manually (see below) 61 | - name: Autobuild 62 | uses: github/codeql-action/autobuild@v3 63 | 64 | # ℹ️ Command-line programs to run using the OS shell. 65 | # 📚 https://git.io/JvXDl 66 | 67 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 68 | # and modify them (or add more) to build your code if your project 69 | # uses a compiled language 70 | 71 | #- run: | 72 | # make bootstrap 73 | # make release 74 | 75 | - name: Perform CodeQL Analysis 76 | uses: github/codeql-action/analyze@v3 77 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | paths: 8 | - 'Dockerfile' 9 | - '.github/workflows/docker.yml' 10 | release: 11 | types: [ published ] 12 | 13 | jobs: 14 | buildx: 15 | if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} 16 | runs-on: ubuntu-latest 17 | permissions: 18 | actions: write 19 | contents: read 20 | packages: write 21 | steps: 22 | - name: Canel previous runs 23 | uses: styfle/cancel-workflow-action@0.12.1 24 | with: 25 | all_but_latest: true 26 | access_token: ${{ secrets.GITHUB_TOKEN }} 27 | - name: Checkout code 28 | uses: actions/checkout@v4 29 | - name: Set up QEMU 30 | uses: docker/setup-qemu-action@v3 31 | - name: Set up Docker Buildx 32 | id: buildx 33 | uses: docker/setup-buildx-action@v3 34 | with: 35 | config-inline: | 36 | [worker.oci] 37 | max-parallelism = 2 38 | - name: Inspect builder 39 | run: | 40 | echo "Name: ${{ steps.buildx.outputs.name }}" 41 | echo "Endpoint: ${{ steps.buildx.outputs.endpoint }}" 42 | echo "Status: ${{ steps.buildx.outputs.status }}" 43 | echo "Flags: ${{ steps.buildx.outputs.flags }}" 44 | echo "Platforms: ${{ steps.buildx.outputs.platforms }}" 45 | - name: Login to Docker Hub 46 | uses: docker/login-action@v3 47 | with: 48 | username: ${{ secrets.DOCKERHUB_USERNAME }} 49 | password: ${{ secrets.DOCKERHUB_TOKEN }} 50 | - name: Login to GitHub Container registry 51 | uses: docker/login-action@v3 52 | with: 53 | registry: ghcr.io 54 | username: ${{ github.repository_owner }} 55 | password: ${{ secrets.GITHUB_TOKEN }} 56 | - name: Build and push images 57 | uses: docker/build-push-action@v6 58 | with: 59 | context: . 60 | platforms: linux/amd64,linux/arm64,linux/arm/v7 61 | push: true 62 | tags: | 63 | unknwon/asouldocs:latest 64 | ghcr.io/asoul-sig/asouldocs:latest 65 | - name: Send email on failure 66 | uses: dawidd6/action-send-mail@v5 67 | if: ${{ failure() }} 68 | with: 69 | server_address: smtp.mailgun.org 70 | server_port: 465 71 | username: ${{ secrets.SMTP_USERNAME }} 72 | password: ${{ secrets.SMTP_PASSWORD }} 73 | subject: GitHub Actions (${{ github.repository }}) job result 74 | to: github-actions-8ce6454@unknwon.io 75 | from: GitHub Actions (${{ github.repository }}) 76 | reply_to: noreply@unknwon.io 77 | body: | 78 | The job "${{ github.job }}" of ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }} completed with "${{ job.status }}". 79 | 80 | View the job run at: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 81 | 82 | buildx-pull-request: 83 | if: ${{ github.event_name == 'pull_request'}} 84 | runs-on: ubuntu-latest 85 | permissions: 86 | contents: read 87 | steps: 88 | - name: Checkout code 89 | uses: actions/checkout@v4 90 | - name: Set up Docker Buildx 91 | id: buildx 92 | uses: docker/setup-buildx-action@v3 93 | with: 94 | config-inline: | 95 | [worker.oci] 96 | max-parallelism = 2 97 | - name: Inspect builder 98 | run: | 99 | echo "Name: ${{ steps.buildx.outputs.name }}" 100 | echo "Endpoint: ${{ steps.buildx.outputs.endpoint }}" 101 | echo "Status: ${{ steps.buildx.outputs.status }}" 102 | echo "Flags: ${{ steps.buildx.outputs.flags }}" 103 | echo "Platforms: ${{ steps.buildx.outputs.platforms }}" 104 | - name: Compute short commit SHA 105 | uses: benjlevesque/short-sha@v3.0 106 | - name: Build and push images 107 | uses: docker/build-push-action@v6 108 | with: 109 | context: . 110 | platforms: linux/amd64 111 | push: true 112 | tags: | 113 | ttl.sh/asoul-sig/asouldocs-${{ env.SHA }}:1d 114 | 115 | # Updates to the following section needs to be synced to all release branches within their lifecycles. 116 | buildx-release: 117 | if: ${{ github.event_name == 'release' }} 118 | runs-on: ubuntu-latest 119 | permissions: 120 | actions: write 121 | contents: read 122 | packages: write 123 | steps: 124 | - name: Compute image tag name 125 | run: echo "IMAGE_TAG=$(echo $GITHUB_REF_NAME | cut -c 2-)" >> $GITHUB_ENV 126 | - name: Checkout code 127 | uses: actions/checkout@v4 128 | - name: Set up QEMU 129 | uses: docker/setup-qemu-action@v3 130 | - name: Set up Docker Buildx 131 | id: buildx 132 | uses: docker/setup-buildx-action@v3 133 | with: 134 | config-inline: | 135 | [worker.oci] 136 | max-parallelism = 2 137 | - name: Inspect builder 138 | run: | 139 | echo "Name: ${{ steps.buildx.outputs.name }}" 140 | echo "Endpoint: ${{ steps.buildx.outputs.endpoint }}" 141 | echo "Status: ${{ steps.buildx.outputs.status }}" 142 | echo "Flags: ${{ steps.buildx.outputs.flags }}" 143 | echo "Platforms: ${{ steps.buildx.outputs.platforms }}" 144 | - name: Login to Docker Hub 145 | uses: docker/login-action@v3 146 | with: 147 | username: ${{ secrets.DOCKERHUB_USERNAME }} 148 | password: ${{ secrets.DOCKERHUB_TOKEN }} 149 | - name: Login to GitHub Container registry 150 | uses: docker/login-action@v3 151 | with: 152 | registry: ghcr.io 153 | username: ${{ github.repository_owner }} 154 | password: ${{ secrets.GITHUB_TOKEN }} 155 | - name: Build and push images 156 | uses: docker/build-push-action@v6 157 | with: 158 | context: . 159 | platforms: linux/amd64,linux/arm64,linux/arm/v7 160 | push: true 161 | tags: | 162 | unknwon/asouldocs:${{ env.IMAGE_TAG }} 163 | ghcr.io/asoul-sig/asouldocs:${{ env.IMAGE_TAG }} 164 | - name: Send email on failure 165 | uses: dawidd6/action-send-mail@v5 166 | if: ${{ failure() }} 167 | with: 168 | server_address: smtp.mailgun.org 169 | server_port: 465 170 | username: ${{ secrets.SMTP_USERNAME }} 171 | password: ${{ secrets.SMTP_PASSWORD }} 172 | subject: GitHub Actions (${{ github.repository }}) job result 173 | to: github-actions-8ce6454@unknwon.io 174 | from: GitHub Actions (${{ github.repository }}) 175 | reply_to: noreply@unknwon.io 176 | body: | 177 | The job "${{ github.job }}" of ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }} completed with "${{ job.status }}". 178 | 179 | View the job run at: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 180 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: 3 | push: 4 | branches: [ main ] 5 | paths: 6 | - '**.go' 7 | - 'go.mod' 8 | - '.golangci.yml' 9 | - '.github/workflows/go.yml' 10 | pull_request: 11 | paths: 12 | - '**.go' 13 | - 'go.mod' 14 | - '.golangci.yml' 15 | - '.github/workflows/go.yml' 16 | env: 17 | GOPROXY: "https://proxy.golang.org" 18 | 19 | jobs: 20 | lint: 21 | name: Lint 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@v4 26 | - name: Run golangci-lint 27 | uses: golangci/golangci-lint-action@v8 28 | with: 29 | version: latest 30 | args: --timeout=30m 31 | - name: Check Go module tidiness 32 | shell: bash 33 | run: | 34 | go mod tidy 35 | STATUS=$(git status --porcelain go.mod go.sum) 36 | if [ ! -z "$STATUS" ]; then 37 | echo "Running go mod tidy modified go.mod and/or go.sum" 38 | exit 1 39 | fi 40 | 41 | test: 42 | name: Test 43 | strategy: 44 | matrix: 45 | go-version: [ 1.21.x, 1.22.x ] 46 | platform: [ ubuntu-latest, macos-latest, windows-latest ] 47 | runs-on: ${{ matrix.platform }} 48 | steps: 49 | - name: Install Go 50 | uses: actions/setup-go@v5 51 | with: 52 | go-version: ${{ matrix.go-version }} 53 | - name: Checkout code 54 | uses: actions/checkout@v4 55 | - name: Run tests 56 | run: go test -v -race ./... 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | custom/ 26 | data 27 | /.idea 28 | /asouldocs 29 | .DS_Store 30 | .task 31 | node_modules 32 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable: 4 | - nakedret 5 | - rowserrcheck 6 | - unconvert 7 | - unparam 8 | settings: 9 | nakedret: 10 | max-func-lines: 0 # Disallow any unnamed return statement 11 | exclusions: 12 | generated: lax 13 | presets: 14 | - comments 15 | - common-false-positives 16 | - legacy 17 | - std-error-handling 18 | paths: 19 | - third_party$ 20 | - builtin$ 21 | - examples$ 22 | formatters: 23 | enable: 24 | - gofmt 25 | - goimports 26 | exclusions: 27 | generated: lax 28 | paths: 29 | - third_party$ 30 | - builtin$ 31 | - examples$ 32 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Default 2 | * @asoul-sig/core 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine3.15 AS binarybuilder 2 | RUN apk --no-cache --no-progress add --virtual \ 3 | build-deps \ 4 | build-base \ 5 | git 6 | 7 | # Install Task 8 | RUN wget --quiet https://github.com/go-task/task/releases/download/v3.12.0/task_linux_amd64.tar.gz -O task_linux_amd64.tar.gz \ 9 | && sh -c 'echo "803d3c1752da31486cbfb4ddf7d8ba5e0fa8c8ebba8acf227a9cd76ff9901343 task_linux_amd64.tar.gz" | sha256sum -c' \ 10 | && tar -xzf task_linux_amd64.tar.gz \ 11 | && mv task /usr/local/bin/task 12 | 13 | WORKDIR /dist 14 | COPY . . 15 | RUN task build 16 | 17 | FROM alpine:3.15 18 | RUN echo https://dl-cdn.alpinelinux.org/alpine/edge/community/ >> /etc/apk/repositories \ 19 | && apk --no-cache --no-progress add \ 20 | ca-certificates \ 21 | git 22 | 23 | # Install gosu 24 | RUN export url="https://github.com/tianon/gosu/releases/download/1.14/gosu-"; \ 25 | if [ `uname -m` == "aarch64" ]; then \ 26 | wget --quiet ${url}arm64 -O /usr/sbin/gosu \ 27 | && sh -c 'echo "73244a858f5514a927a0f2510d533b4b57169b64d2aa3f9d98d92a7a7df80cea /usr/sbin/gosu" | sha256sum -c'; \ 28 | elif [ `uname -m` == "armv7l" ]; then \ 29 | wget --quiet ${url}armhf -O /usr/sbin/gosu \ 30 | && sh -c 'echo "abb1489357358b443789571d52b5410258ddaca525ee7ac3ba0dd91d34484589 /usr/sbin/gosu" | sha256sum -c'; \ 31 | else \ 32 | wget --quiet ${url}amd64 -O /usr/sbin/gosu \ 33 | && sh -c 'echo "bd8be776e97ec2b911190a82d9ab3fa6c013ae6d3121eea3d0bfd5c82a0eaf8c /usr/sbin/gosu" | sha256sum -c'; \ 34 | fi \ 35 | && chmod +x /usr/sbin/gosu 36 | 37 | WORKDIR /app/asouldocs/ 38 | COPY --from=binarybuilder /dist/asouldocs . 39 | 40 | VOLUME ["/app/asouldocs/custom"] 41 | EXPOSE 5555 42 | CMD ["/app/asouldocs/asouldocs", "web"] 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015 ASoulDocs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![ASoulDocs](https://user-images.githubusercontent.com/2946214/159929056-183eb412-1317-4b14-9e24-c3265d599aed.png#gh-light-mode-only) 2 | ![ASoulDocs](https://user-images.githubusercontent.com/2946214/159929046-6f1eb4c1-53b5-40d5-b5a2-e7d805566e73.png#gh-dark-mode-only) 3 | 4 |
5 | Sourcegraph 6 | 7 | _**ASoulDocs**_ is a stupid web server for multilingual documentation. 8 |
9 | 10 | ## Features 11 | 12 | - Multilingual documentation with [language fallback](https://asouldocs.dev/docs/howto/set-up-documentation#Localization%20configuration) 13 | - Builtin [push-to-sync through webhook](https://asouldocs.dev/docs/howto/sync-through-webhook) 14 | - Integrated [commenting systems and analytics platforms](https://asouldocs.dev/docs/howto/use-extensions) 15 | - Automatic dark mode 16 | - Fully [customizable look and feel](https://asouldocs.dev/docs/howto/customize-templates) 17 | 18 | ## Getting help 19 | 20 | - Please [file an issue](https://github.com/asoul-sig/asouldocs/issues) or [start a discussion](https://github.com/asoul-sig/asouldocs/discussions) if you want to reach out. 21 | 22 | ## License 23 | 24 | This project is under the MIT License. See the [LICENSE](LICENSE) file for the full license text. 25 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | vars: 4 | PKG_PATH: github.com/asoul-sig/asouldocs/internal/conf 5 | BUILD_TIME: 6 | sh: date -u '+%Y-%m-%d %I:%M:%S %Z' 7 | BUILD_COMMIT: 8 | sh: git rev-parse HEAD 9 | 10 | tasks: 11 | web: 12 | desc: Build the binary and start the web server 13 | deps: [build] 14 | cmds: 15 | - ./asouldocs web 16 | 17 | build: 18 | desc: Build the binary 19 | cmds: 20 | - go build -v 21 | -ldflags ' 22 | -X "{{.PKG_PATH}}.BuildTime={{.BUILD_TIME}}" 23 | -X "{{.PKG_PATH}}.BuildCommit={{.BUILD_COMMIT}}"' 24 | -trimpath -o asouldocs 25 | sources: 26 | - main.go 27 | - internal/**/*.go 28 | - conf/**/* 29 | - public/embed.go 30 | - templates/embed.go 31 | - docs/toc.ini 32 | method: timestamp 33 | 34 | clean: 35 | desc: Cleans up system meta files 36 | cmds: 37 | - find . -name "*.DS_Store" -type f -delete 38 | - rm asouldocs_* checksum_sha256.txt 39 | 40 | release: 41 | desc: Build the binaries and pack resources to ZIP archives 42 | cmds: 43 | - env GOOS=darwin GOARCH=amd64 go build -ldflags '-X "{{.PKG_PATH}}.BuildTime={{.BUILD_TIME}}" -X "{{.PKG_PATH}}.BuildCommit={{.BUILD_COMMIT}}" -X "{{.PKG_PATH}}.BuildVersion={{.BUILD_VERSION}}"' -trimpath -o asouldocs; tar czf asouldocs_{{.BUILD_VERSION}}_darwin_amd64.tar.gz asouldocs 44 | - env GOOS=darwin GOARCH=arm64 go build -ldflags '-X "{{.PKG_PATH}}.BuildTime={{.BUILD_TIME}}" -X "{{.PKG_PATH}}.BuildCommit={{.BUILD_COMMIT}}" -X "{{.PKG_PATH}}.BuildVersion={{.BUILD_VERSION}}"' -trimpath -o asouldocs; tar czf asouldocs_{{.BUILD_VERSION}}_darwin_arm64.tar.gz asouldocs 45 | - env GOOS=linux GOARCH=amd64 go build -ldflags '-X "{{.PKG_PATH}}.BuildTime={{.BUILD_TIME}}" -X "{{.PKG_PATH}}.BuildCommit={{.BUILD_COMMIT}}" -X "{{.PKG_PATH}}.BuildVersion={{.BUILD_VERSION}}"' -trimpath -o asouldocs; tar czf asouldocs_{{.BUILD_VERSION}}_linux_amd64.tar.gz asouldocs 46 | - env GOOS=linux GOARCH=386 go build -ldflags '-X "{{.PKG_PATH}}.BuildTime={{.BUILD_TIME}}" -X "{{.PKG_PATH}}.BuildCommit={{.BUILD_COMMIT}}" -X "{{.PKG_PATH}}.BuildVersion={{.BUILD_VERSION}}"' -trimpath -o asouldocs; tar czf asouldocs_{{.BUILD_VERSION}}_linux_386.tar.gz asouldocs 47 | - env GOOS=linux GOARCH=arm go build -ldflags '-X "{{.PKG_PATH}}.BuildTime={{.BUILD_TIME}}" -X "{{.PKG_PATH}}.BuildCommit={{.BUILD_COMMIT}}" -X "{{.PKG_PATH}}.BuildVersion={{.BUILD_VERSION}}"' -trimpath -o asouldocs; tar czf asouldocs_{{.BUILD_VERSION}}_linux_arm.tar.gz asouldocs 48 | - env GOOS=windows GOARCH=amd64 go build -ldflags '-X "{{.PKG_PATH}}.BuildTime={{.BUILD_TIME}}" -X "{{.PKG_PATH}}.BuildCommit={{.BUILD_COMMIT}}" -X "{{.PKG_PATH}}.BuildVersion={{.BUILD_VERSION}}"' -trimpath -o asouldocs.exe; tar czf asouldocs_{{.BUILD_VERSION}}_windows_amd64.tar.gz asouldocs 49 | - env GOOS=windows GOARCH=386 go build -ldflags '-X "{{.PKG_PATH}}.BuildTime={{.BUILD_TIME}}" -X "{{.PKG_PATH}}.BuildCommit={{.BUILD_COMMIT}}" -X "{{.PKG_PATH}}.BuildVersion={{.BUILD_VERSION}}"' -trimpath -o asouldocs.exe; tar czf asouldocs_{{.BUILD_VERSION}}_windows_386.tar.gz asouldocs 50 | - shasum -a 256 asouldocs_* >> checksum_sha256.txt 51 | -------------------------------------------------------------------------------- /conf/app.ini: -------------------------------------------------------------------------------- 1 | ; The running environment, could be "dev" or "prod" 2 | ENV = dev 3 | ; The local address to listen on, use "0.0.0.0" to listen on all network interfaces 4 | HTTP_ADDR = localhost 5 | ; The local port to listen on 6 | HTTP_PORT = 5555 7 | 8 | [site] 9 | ; The description of the site 10 | DESCRIPTION = ASoulDocs is a stupid web server for multilingual documentation. 11 | ; The external-facing URL of the site 12 | EXTERNAL_URL = http://localhost:5555 13 | 14 | [asset] 15 | ; The local directory that contains custom assets (e.g. images, CSS, JavaScript) 16 | CUSTOM_DIRECTORY = custom/public 17 | 18 | [page] 19 | ; Whether the site has a landing page, set to "false" to redirect users for documentation directly 20 | HAS_LANDING_PAGE = true 21 | ; The base path for documentation 22 | DOCS_BASE_PATH = /docs 23 | ; The local directory that contains custom templates 24 | CUSTOM_DIRECTORY = custom/templates 25 | 26 | [i18n] 27 | ; The list of languages that is supported 28 | LANGUAGES = en-US,zh-CN 29 | ; The list of user-friendly names of languages 30 | NAMES = English,简体中文 31 | ; The local directory that contains custom locale files 32 | CUSTOM_DIRECTORY = custom/locale 33 | 34 | [docs] 35 | ; The type of the documentation, could be "local" or "remote" 36 | TYPE = local 37 | ; The local path or remote Git address 38 | TARGET = ./docs 39 | ; The relative directory where the root of documentation resides within the target 40 | TARGET_DIR = 41 | ; The format to construct a edit page link, leave it empty to disable, e.g. 42 | ; https://github.com/asoul-sig/asouldocs/blob/main/docs/{blob} 43 | EDIT_PAGE_LINK_FORMAT = 44 | 45 | ; https://plausible.io/ 46 | [extension.plausible] 47 | ; Whether to enable this extension 48 | ENABLED = false 49 | ; The optional value to be specified for the "data-domain" attribute 50 | DOMAIN = 51 | 52 | ; https://developers.google.com/analytics/devguides/collection/ga4 53 | [extension.google_analytics] 54 | ; Whether to enable this extension 55 | ENABLED = false 56 | ; The measurement ID of your property 57 | MEASUREMENT_ID = 58 | 59 | ; https://disqus.com/ 60 | [extension.disqus] 61 | ; Whether to enable this extension 62 | ENABLED = false 63 | ; The shortname of your site 64 | SHORTNAME = 65 | 66 | ; https://utteranc.es/ 67 | [extension.utterances] 68 | ; Whether to enable this extension 69 | ENABLED = false 70 | ; The GitHub repository 71 | REPO = 72 | ; The issue mapping pattern 73 | ISSUE_TERM = pathname 74 | ; The issue label for comments 75 | LABEL = utterances 76 | ; The theme of the component 77 | THEME = github-light 78 | -------------------------------------------------------------------------------- /conf/embed.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 ASoulDocs. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package conf 6 | 7 | import ( 8 | "embed" 9 | ) 10 | 11 | //go:embed app.ini 12 | var Files embed.FS 13 | -------------------------------------------------------------------------------- /conf/locale/embed.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 ASoulDocs. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package locale 6 | 7 | import ( 8 | "embed" 9 | ) 10 | 11 | //go:embed *.ini 12 | var Files embed.FS 13 | -------------------------------------------------------------------------------- /conf/locale/locale_en-US.ini: -------------------------------------------------------------------------------- 1 | name = ASoulDocs 2 | tag_line = A stupid web server for multilingual documentation 3 | 4 | [navbar] 5 | docs = Docs 6 | 7 | [home] 8 | title = ASoulDocs 9 | tag_line = A stupid web server for multilingual documentation. 10 | get_started = Get started 11 | 12 | multilingual = Multilingual 13 | multilingual_desc = Give your users the best documentation experience ever with the ability to instantly change between multiple languages, and remember their preference! 14 | real_time_sync = Real-time synchronization 15 | real_time_sync_desc = Stop wasting time on worthless waiting! Keep in sync of your documentation in real-time from any Git hosting sources. 16 | searchable = Full-text search 17 | searchable_desc = The best way to navigate users through your documentation is being able to search, seamlessly integrated with Algolia. 18 | markdown = Markdown 19 | markdown_desc = Markdown is getting its dominance as the language of documentation for 2022. 20 | customizable = Customizable 21 | customizable_desc = It is your ultimate right to present the site to your users that is unique to your project. 22 | comment = Commenting 23 | comment_desc = Integrate with popular commenting systems like Disqus and utterances. Let users give you feedback directly without a hitch. 24 | 25 | [docs] 26 | pages = Pages 27 | showing_default = The document you're looking for is not available in current language, and we're showing the version of default language to you. 28 | on_this_page = On this page 29 | edit_this_page = Edit this page 30 | 31 | [status] 32 | 404 = Page Not Found 33 | 404_desc = Something unexpected happened, but who cares I'm 404. 34 | 35 | [footer] 36 | getting_started = Getting started 37 | installation = Installation 38 | documentation = Documentation 39 | extensions = Extensions 40 | 41 | powered_by = Powered by 42 | languages = Languages 43 | copyright = A-SOUL SIG. All rights reserved. 44 | 45 | [alert] 46 | warning = Attention needed 47 | -------------------------------------------------------------------------------- /conf/locale/locale_zh-CN.ini: -------------------------------------------------------------------------------- 1 | name = 一魂文档 2 | tag_line = 一款支持多语言的 Web 文档服务器 3 | 4 | [navbar] 5 | docs = 查看文档 6 | 7 | [home] 8 | title = 一魂文档 9 | tag_line = 一款支持多语言的 Web 文档服务器 10 | get_started = 开始使用 11 | 12 | multilingual = 多语言支持 13 | multilingual_desc = 可即时切换多语言来为您的用户提供最佳的文档浏览体验,并可在下次访问时自动展示保存的语言偏好 14 | real_time_sync = 实时更新同步 15 | real_time_sync_desc = 告别无谓的等待,从任意的 Git 托管源实时同步您的文档,一年可以省下好多烟钱! 16 | searchable = 支持全文搜索 17 | searchable_desc = 内置集成文档搜索引擎 Algolia,为您的用户提供世界一流的查询体验,快速定位感兴趣的内容 18 | markdown = Markdown 语法 19 | markdown_desc = Markdown 语法凭借着自身简单易懂的特性,广受社区好评,并已然成为编写文档的首选语言 20 | customizable = 高度可定制 21 | customizable_desc = 可高度自定义化的架构设计,除了核心渲染模块全部可以换掉,为每一个产品展现不一样的自己 22 | comment = 评论系统集成 23 | comment_desc = 内置集成 Disqus 和 utterances 评论系统,让产品与用户零距离,收集反馈不再困难 24 | 25 | [docs] 26 | pages = 页面 27 | showing_default = 您所查看的文档没有与当前语言匹配的版本,因此我们将显示默认语言的版本。 28 | on_this_page = 页内导航 29 | edit_this_page = 编辑本页内容 30 | 31 | [status] 32 | 404 = 页面未找到 33 | 404_desc = 没想到我藏得这么深都能被你发现,然而并没有什么卵用。 34 | 35 | [footer] 36 | getting_started = 开始使用 37 | installation = 下载安装 38 | documentation = 创建文档 39 | extensions = 使用扩展 40 | 41 | powered_by = 网站基于 42 | languages = 语言选项 43 | copyright = A-SOUL 特别兴趣小组 - 版权所有 44 | 45 | [alert] 46 | warning = 温馨提示 47 | -------------------------------------------------------------------------------- /docs/assets/github-webhook.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asoul-sig/asouldocs/031839cf765ec8e8de2c0cf5e5b10af96cb9b4c3/docs/assets/github-webhook.jpg -------------------------------------------------------------------------------- /docs/dev/local_development.md: -------------------------------------------------------------------------------- 1 | # Set up your development environment 2 | 3 | _**ASoulDocs**_ is written in [Go](https://golang.org/), please take [A Tour of Go](https://tour.golang.org/) if you haven't done so! 4 | 5 | ## Outline 6 | 7 | - [Environment](#environment) 8 | - [Step 1: Install dependencies](#step-1-install-dependencies) 9 | - [Step 2: Get the code](#step-2-get-the-code) 10 | - [Step 3: Start the server](#step-3-start-the-server) 11 | - [Other nice things](#other-nice-things) 12 | 13 | ## Environment 14 | 15 | _**ASoulDocs**_ is built and runs as a single binary and meant to be cross platform. Therefore, you should be able to develop _**ASoulDocs**_ in any major platforms you prefer. However, this guide will focus on macOS only. 16 | 17 | ## Step 1: Install dependencies 18 | 19 | _**ASoulDocs**_ has the following dependencies: 20 | 21 | - [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) (v2 or higher) 22 | - [Go](https://go.dev/doc/install) (v1.17 or higher) 23 | - [Task](https://taskfile.dev/) (v3) 24 | 25 | 1. Install [Homebrew](https://brew.sh/). 26 | 1. Install dependencies: 27 | 28 | ```bash 29 | brew install go git go-task/tap/go-task 30 | ``` 31 | 32 | ## Step 2: Get the code 33 | 34 | Generally, you don't need a full clone, so set `--depth` to `10`: 35 | 36 | ```bash 37 | git clone --depth 10 https://github.com/asoul-sig/asouldocs.git 38 | 39 | # or 40 | 41 | git clone --depth 10 git@github.com:asoul-sig/asouldocs.git 42 | ``` 43 | 44 | **NOTE** The repository has Go modules enabled, please clone to somewhere outside of your `$GOPATH`. 45 | 46 | ## Step 3: Start the server 47 | 48 | The following command will start the web server and automatically recompile and restart the server if any watched files changed: 49 | 50 | ```bash 51 | task web --watch 52 | ``` 53 | 54 | ## Other nice things 55 | 56 | ### Load HTML templates and static files from disk 57 | 58 | When you are actively working on HTML templates and static files during development, you would want to ensure the following configuration to avoid recompiling and restarting _**ASoulDocs**_ every time you make a change to files under `templates/` and `public/` directories: 59 | 60 | ```ini 61 | ENV = dev 62 | ``` 63 | -------------------------------------------------------------------------------- /docs/dev/new_release.md: -------------------------------------------------------------------------------- 1 | # Release a new version 2 | 3 | ## Playbook 4 | 5 | ### Build and pack binaries 6 | 7 | ```bash 8 | $ BUILD_VERSION="1.0.0-beta.1" task release 9 | 10 | # Upload binaries to the GitHub release 11 | 12 | $ task clean # to clean up 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/en-US/howto/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: How to ... 3 | --- 4 | -------------------------------------------------------------------------------- /docs/en-US/howto/configure-reverse-proxy.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Configure reverse proxy 3 | --- 4 | 5 | ## Caddy 2 6 | 7 | You get HTTPS for free and forever just by using [Caddy](https://caddyserver.com/): 8 | 9 | ```caddyfile 10 | { 11 | http_port 80 12 | https_port 443 13 | } 14 | 15 | asouldocs.dev { 16 | reverse_proxy * localhost:5555 17 | } 18 | ``` 19 | 20 | ## NGINX 21 | 22 | Here is an example of NGINX config section, but values can be different based on your situation: 23 | 24 | ```nginx 25 | server { 26 | listen 80; 27 | server_name asouldocs.dev; 28 | 29 | location / { 30 | proxy_pass http://localhost:5555; 31 | } 32 | } 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/en-US/howto/customize-templates.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Customize templates 3 | --- 4 | 5 | Every [template file](set-up-documentation.md#template-files) can be customized through overwrite, and custom template files should be placed under the `custom/templates` directory (the path of directory can be changed via `[page] CUSTOM_DIRECTORY`). 6 | 7 | It is only recommended to customize the `home.html` and `common/navbar.html` files to maintain the maximum backward compatibility. 8 | 9 | ## UI framework 10 | 11 | By default, the JIT of [Tailwind CSS](https://tailwindcss.com/) is included so you do not need any build step for using any of its classes in your custom templates. 12 | 13 | However, it is not required to use Tailwind CSS as you have all the freedom to use any UI framework in your `custom/common/head.html` file. 14 | 15 | ## Localization 16 | 17 | The Flamego's [i18n](https://flamego.dev/middleware/i18n.html) middleware is used to handle localization, create locale files under the `custom/locale` directory (the path of directory can be changed via `[i18n] CUSTOM_DIRECTORY`) to customize [localization configuration](set-up-documentation.md#localization-configuration). 18 | 19 | The syntax for invoking localization function in template files looks like `{{call .Tr "footer::copyright"}}`, where `footer` is the section name and `copyright` is the key name. 20 | 21 | ## Static assets 22 | 23 | Custom static assets should be placed under the `custom/public` directory (the path of directory can be changed via `[asset] CUSTOM_DIRECTORY`), then include them in your template file. 24 | 25 | For example, suppose you have a custom static asset in the path `custom/public/css/my.css`, then add the following line in your `custom/common/head.tmpl` file: 26 | 27 | ```go-html-template 28 | 29 | ``` 30 | 31 | Notice there is no `public` prefix in the `href` attribute. 32 | -------------------------------------------------------------------------------- /docs/en-US/howto/run-through-docker.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Run through Docker 3 | --- 4 | 5 | Docker images for the _**ASoulDocs**_ server are available both on [Docker Hub](https://hub.docker.com/r/unknwon/asouldocs) and [GitHub Container Registry](https://github.com/asoul-sig/asouldocs/pkgs/container/asouldocs). 6 | 7 | The `latest` tag represents the latest build from the [`main` branch](https://github.com/asoul-sig/asouldocs). 8 | 9 | ## Caveats 10 | 11 | The `HTTP_ADDR` should be changed to listen on the Docker network or all network addresses: 12 | 13 | ```ini 14 | HTTP_ADDR = 0.0.0.0 15 | ``` 16 | 17 | ## Start the container 18 | 19 | You need to volume the `custom` directory into the Docker container for it being able to start (`/app/asouldocs/custom` is the path inside the container): 20 | 21 | ```bash 22 | $ docker run \ 23 | --name=asouldocs \ 24 | -p 15555:5555 \ 25 | -v $(pwd)/custom:/app/asouldocs/custom \ 26 | unknwon/asouldocs 27 | ``` 28 | 29 | You should also volumn the `docs` directory into the Docker container directly if you are not using a [remote Git address](set-up-documentation.md#target) (`/app/asouldocs/docs` is the path inside the container): 30 | 31 | ```bash 32 | $ docker run \ 33 | --name=asouldocs \ 34 | -p 15555:5555 \ 35 | -v $(pwd)/custom:/app/asouldocs/custom \ 36 | -v $(pwd)/docs:/app/asouldocs/docs \ 37 | unknwon/asouldocs 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/en-US/howto/set-up-documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Set up documentation 3 | --- 4 | 5 | There are four essential components of a _**ASoulDocs**_ documentation project: 6 | 7 | 1. Configuration file 8 | 1. Template files 9 | 1. Locale files 10 | 1. Actual documents 11 | 12 | ## Configuration file 13 | 14 | The configuration file is required for every documentation project. By default, it is expected be available at `custom/app.ini`. 15 | 16 | Every server comes with a [builtin configuration file](https://github.com/asoul-sig/asouldocs/blob/main/conf/app.ini) as default options. Therefore, you only need to overwrite few options in your own configuration file. 17 | 18 | The `--config` (or `-c`) CLI flag can be used to specify a configuration file that is not located in the default location. 19 | 20 | ## Template files 21 | 22 | Template files are [Go HTML templates](https://pkg.go.dev/html/template) used to render different pages served by the server. 23 | 24 | Every server comes with a set of [builtin template files](https://github.com/asoul-sig/asouldocs/tree/main/templates) that works out-of-the-box. However, the content of builtin template files are probably not what you would want for your documentation in most cases, at least for `home.html` and `common/navbar.html`. 25 | 26 | Luckily, you can overwrite these template files by placing your template files with same file name under the `custom/templates` directory. 27 | 28 | For example, you can replace the "GitHub" in navbar to be a happy face by overwriting the `common/navbar.html`: 29 | 30 | ```go-html-template {hl_lines=["7-10"]} 31 | 44 | ``` 45 | 46 | You can read more about how to [customize templates](customize-templates.md). 47 | 48 | ## Document hierarchy 49 | 50 | Whether you want to serve your documentation in multiple languages or just one language, the hierarchy is the same. In the root directory of your documentation, you need a `toc.ini` file and subdirectories using [IETF BCP 47 language tags](https://en.wikipedia.org/wiki/IETF_language_tag), e.g. "en-US", "zh-CN". 51 | 52 | Here is an example hierarchy: 53 | 54 | ``` 55 | ├── en-US 56 | │   ├── howto 57 | │   │   ├── README.md 58 | │   │   └── set_up_documentation.md 59 | │   └── introduction 60 | │   ├── README.md 61 | │   ├── installation.md 62 | │   └── quick_start.md 63 | ├── zh-CN 64 | │   ├── howto 65 | │   │   ├── README.md 66 | │   │   └── set_up_documentation.md 67 | │   └── introduction 68 | │   ├── README.md 69 | │   ├── installation.md 70 | │   └── quick_start.md 71 | └── toc.ini 72 | ``` 73 | 74 | The `toc.ini` is used to define how exactly these documents should look like on the site (e.g. how they are ordered). The following example is a corresponding `toc.ini` to the above hierarchy: 75 | 76 | ```ini 77 | -: introduction 78 | -: howto 79 | 80 | [introduction] 81 | -: README 82 | -: installation 83 | -: quick_start 84 | 85 | [howto] 86 | -: README 87 | -: set_up_documentation 88 | ``` 89 | 90 | In the default section, document directories are defined in the exact order, and these names are corresponding to the directories' title: 91 | 92 | ```ini 93 | -: introduction 94 | -: howto 95 | ``` 96 | 97 | Then there are sections for each directory: 98 | 99 | ```ini 100 | [introduction] 101 | ... 102 | 103 | [howto] 104 | ... 105 | ``` 106 | 107 | Within each section, files are defined in the exact order, and these names are corresponding to the files' name in the document directory (but without the `.md` extension): 108 | 109 | ```ini 110 | [introduction] 111 | -: README 112 | -: installation 113 | -: quick_start 114 | ``` 115 | 116 | Other notes: 117 | 118 | 1. Only single-level directories are supported. 119 | 1. Every document directory must have a `README.md` file, to at least define its name through the [frontmatter](write-document.md#frontmatter). 120 | 121 | ### Localization configuration 122 | 123 | By default, the server assumes to have documentation both in English (`en-US`) and Simplified Chinese (`zh-CN`), as in the [default configuration](https://github.com/asoul-sig/asouldocs/blob/39b59c4159e4a2b0e0a290c79f85c46a3e1faf0b/conf/app.ini#L26-L30): 124 | 125 | ```ini 126 | [i18n] 127 | ; The list of languages that is supported 128 | LANGUAGES = en-US,zh-CN 129 | ; The list of user-friendly names of languages 130 | NAMES = English,简体中文 131 | ``` 132 | 133 | The first language in the `LANGUAGES` is considered as the default language, and the server shows its content if the preferred language (from browser's `Accept-Language` request header) does not exists, or the particular document is not available in the preferred language (but available in the default language). 134 | 135 | If you are just writing documentation in English, you would need to overwrite the configuration as follows: 136 | 137 | ```ini 138 | [i18n] 139 | ; The list of languages that is supported 140 | LANGUAGES = en-US 141 | ; The list of user-friendly names of languages 142 | NAMES = English 143 | ``` 144 | 145 | ## Target 146 | 147 | The target of documents can be either a local directory or a remote Git address. 148 | 149 | To use a local directory: 150 | 151 | ```ini 152 | [docs] 153 | TYPE = local 154 | TARGET = ./docs 155 | ``` 156 | 157 | To use a remote Git address: 158 | 159 | ```ini 160 | [docs] 161 | TYPE = remote 162 | TARGET = https://github.com/asoul-sig/asouldocs.git 163 | ``` 164 | 165 | If documents are residing in a subdirectory of the target, use `TARGET_DIR` as follows: 166 | 167 | ```ini 168 | [docs] 169 | TYPE = remote 170 | TARGET = https://github.com/asoul-sig/asouldocs.git 171 | TARGET_DIR = docs 172 | ``` 173 | 174 | ## Link to edit page 175 | 176 | You probably want to welcome small contributions from the community if your documentation repository is open sourced. You can navigate users directly to edit the page on the code host: 177 | 178 | ```ini 179 | [docs] 180 | ; The format to construct a edit page link, leave it empty to disable 181 | EDIT_PAGE_LINK_FORMAT = https://github.com/asoul-sig/asouldocs/blob/main/docs/{blob} 182 | ``` 183 | -------------------------------------------------------------------------------- /docs/en-US/howto/sync-through-webhook.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Sync through webhook 3 | --- 4 | 5 | Obviously, you don't want to login to your server and update documentation manually every time you make a change, especially when changes are small and frequent. 6 | 7 | This is a solved problem for the _**ASoulDocs**_ server. 8 | 9 | Just two simple steps: 10 | 11 | 1. [Set up your documentation from a remote Git address](set-up-documentation.md#target) 12 | 1. Fire a webhook whenever there is a `push` event. 13 | 14 | The URL path for receiving the webhook is `/webhook`, and it accepts any kind of HTTP method, GET, POST or HEAD? Cool with that. 15 | 16 | Therefore, a simple `curl` can do the trick: 17 | 18 | ```bash 19 | $ curl http://localhost:5555/webhook 20 | ``` 21 | 22 | Almost all code hosts provide builtin webhook, to configurate the webhook on GitHub, navigate to your repository **Settings > Webhooks**, then click on the **Add webhook** button: 23 | 24 | ![GitHub webhook](../../assets/github-webhook.jpg) 25 | -------------------------------------------------------------------------------- /docs/en-US/howto/use-extensions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Use extensions 3 | --- 4 | 5 | Every _**ASoulDocs**_ server comes with some builtin extensions, it's just few edits away to use them! 6 | 7 | ## Plausible 8 | 9 | The Plausible extension integrates with https://plausible.io/, it is disabled by default. Use the following configuration to enable it: 10 | 11 | ```ini 12 | [extension.plausible] 13 | ENABLED = true 14 | ; The optional value to be specified for the "data-domain" attribute 15 | DOMAIN = 16 | ``` 17 | 18 | ## Google Analytics 19 | 20 | The Google Analytics extension integrates with [Google Analytics 4](https://developers.google.com/analytics/devguides/collection/ga4), it is disabled by default. Use the following configuration to enable it: 21 | 22 | ```ini 23 | [extension.google_analytics] 24 | ENABLED = true 25 | ; The measurement ID of your property 26 | MEASUREMENT_ID = G-VXXXYYYYZZ 27 | ``` 28 | 29 | ## Disqus 30 | 31 | The Disqus extension integrates with [Disqus](https://disqus.com/), it is disabled by default. Use the following configuration to enable it: 32 | 33 | ```ini 34 | [extension.disqus] 35 | ENABLED = true 36 | ; The shortname of your site 37 | SHORTNAME = ellien 38 | ``` 39 | 40 | ## utterances 41 | 42 | The utterances extension integrates with [utterances](https://utteranc.es/), it is disabled by default. Use the following configuration to enable it: 43 | 44 | ```ini 45 | [extension.utterances] 46 | ENABLED = true 47 | ; The GitHub repository 48 | REPO = owner/repo 49 | ; The issue mapping pattern 50 | ISSUE_TERM = pathname 51 | ; The issue label for comments 52 | LABEL = utterances 53 | ; The theme of the component 54 | THEME = github-light 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/en-US/howto/write-document.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Write document 3 | --- 4 | 5 | Every document is a [Markdown](https://www.markdownguide.org/) file, most of features from [GitHub Flavored Markdown](https://github.github.com/gfm/) are supported through [yuin/goldmark](https://github.com/yuin/goldmark). 6 | 7 | Here is an simple example: 8 | 9 | ```markdown 10 | --- 11 | title: Introduction 12 | --- 13 | 14 | _**ASoulDocs**_ is a stupid web server for multilingual documentation. 15 | ``` 16 | 17 | ### Frontmatter 18 | 19 | The frontmatter is a block of YAML snippet, `---` are used to both indicate the start and the end of the snippet. 20 | 21 | Here is an full example of supported fields: 22 | 23 | ```yaml 24 | title: The title of the document 25 | previous: 26 | title: The title of the previous page 27 | link: the relative path to the page 28 | next: 29 | title: The title of the next page 30 | link: the relative path to the page 31 | ``` 32 | 33 | - Only the `title` field is required, others all have reasonable default. 34 | - The `link` syntax of both `previous` and `next` sections is exactly same as described in [Links and images](#links-and-images). 35 | 36 | ### Links and images 37 | 38 | Links to other documents or images just works like you would do in any editor (e.g. VSCode): 39 | 40 | - Link to a document under the same directory: `[Customize templates](customize-templates.md)` 41 | - Link to the directory page: `[How to](README.md)` 42 | - Link to a document in another directory: `[Quick start](../introduction/quick-start.md)` 43 | - Link to an image: `![](../../assets/workflow.png)` 44 | 45 | ### Code blocks 46 | 47 | The [alecthomas/chroma](https://github.com/alecthomas/chroma) is used to syntax highlighting your code blocks. 48 | 49 | Use name from its [supported languages](https://github.com/alecthomas/chroma#supported-languages) to specify the language of the code block, be sure to replace whitespaces with hyphens (`-`) in the language name, e.g. use `go-html-template` not `go html template` (names are case insensitive). 50 | 51 | To enable line numbers and highlighting lines: 52 | 53 | ```markdown 54 | ...go-html-template {linenos=true, hl_lines=["7-10", 12]} 55 | ``` 56 | 57 | ### Render caching 58 | 59 | Each documents is reloaded and re-rendered for every request if the server is running with `dev` environment, as defined in the [configuration file](set-up-documentation.md#configuration-file): 60 | 61 | ```ini 62 | ENV = dev 63 | ``` 64 | 65 | Set `ENV = prod` to enable render caching when deploy to production. 66 | -------------------------------------------------------------------------------- /docs/en-US/introduction/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | previous: 4 | title: Home 5 | path: ../../ 6 | --- 7 | 8 | _**ASoulDocs**_ is a stupid web server for multilingual documentation. 9 | 10 | ## Motivation 11 | 12 | Project documentation tools is an already-crowded place yet not being both mature and affordable for individual especially OSS developers. Countless static site generators, documentation servers, SaaS products, unfortunately, we are not happy with any of them. More importantly, we love and are capable of hacking on this area. 13 | 14 | It has been years we struggled with picking one of them, that's basically why _**ASoulDocs**_ was created (previously "Peach" pre-1.0). 15 | 16 | The following table illustrates the features (that we care) comparisons between _**ASoulDocs**_ and other existing tools (that we investigated, concepts may be different from what we understand): 17 | 18 | |Name/Feature |_**ASoulDocs**_|[Mkdocs](https://www.mkdocs.org/)|[Hugo](https://gohugo.io/)|[VuePress](https://v2.vuepress.vuejs.org/)/[VitePress](https://vitepress.vuejs.org/)|[GitBook](https://www.gitbook.com/)| 19 | |:---------------------------:|:-------------:|:----:|:--:|:----------------:|:----:| 20 | |Self-hosted | ✅ | ✅ | ✅ | ✅ | ❌ | 21 | |Multilingual1 | ✅ | ✅ | ✅ | ✅ | ❌ | 22 | |Builtin push-to-sync | ✅ | ❌ | ❌ | ❌ | ✅ | 23 | |DocSearch | 🎯 | ❌ | ✅ | ✅ | ❌ | 24 | |Builtin search | 🎯 | ✅ | ❌ | ✅ | ✅ | 25 | |Commenting system | ✅ | ❌ | ✅ | ❌ | ❌ | 26 | |Versionable | 🎯 | ❌ | ❌ | ❌ | ❌ | 27 | |Protected resources | 🎯 | ❌ | ❌ | ❌ | ❌ | 28 | |Dark mode | ✅ | ❌ | ✅ | ✅ | ❌ | 29 | |Customizable2 | ✅ | ❌ | ✅ | ❌ | ❌ | 30 | |Language fallback3| ✅ | ❌ | ❌ | ❌ | ❌ | 31 | 32 | - 1: None of others support multilingual without changing the URL, which to us is a bizarre because we have to share different URLs for different groups of users. 33 | - 2: In such way that visitors couldn't recognize what is powering the site behind the scene. 34 | - 3: When a page does not exist in the preferred language, fallback to show the version from the default language. 35 | - 🎯: Features that are on the roadmap. 36 | 37 | ## History 38 | 39 | The project was initially named as "Peach Docs" in pre-1.0 releases, which is now branded as _**ASoulDocs**_ starting from the 1.0 release. 40 | 41 | The tech stack has evolved since 2015, [Macaron](https://go-macaron.com) and [Semantic UI](https://semantic-ui.com/) was the new and hot things, and the latest golden partners are [Flamego](https://flamego.dev) and [Tailwind CSS](https://tailwindcss.com/). 42 | 43 | The project is now part of [A-SOUL SIG](https://github.com/asoul-sig) (previously "github.com/peachdocs"), consists a group of A-SOUL fans. 44 | 45 | ## OK, then what? 46 | 47 | [Install the server](installation.md) or go ahead to [Quick start](quick-start.md)! 48 | -------------------------------------------------------------------------------- /docs/en-US/introduction/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | --- 4 | 5 | ## From binary 6 | 7 | Release binaries are available on [GitHub releases](https://github.com/asoul-sig/asouldocs/releases). 8 | 9 | ## From source code 10 | 11 | Install from source code requires you having a working local environment of [Go](https://go.dev/). 12 | 13 | Use the following command to check: 14 | 15 | ```bash 16 | $ go version 17 | ``` 18 | 19 | The minimum requirement version of Go is **1.19**. 20 | 21 | Then build the binary: 22 | 23 | ```bash 24 | $ go build 25 | ``` 26 | 27 | Finally, start the server: 28 | 29 | ```bash 30 | $ ./asouldocs web 31 | ``` 32 | 33 | Please refer to [Set up your development environment](https://github.com/asoul-sig/asouldocs/blob/main/docs/dev/local_development.md) for local development guide. 34 | -------------------------------------------------------------------------------- /docs/en-US/introduction/quick-start.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Quick start 3 | --- 4 | 5 | Let's boot up a server serving the documentation of _**ASoulDocs**_ its own! 6 | 7 | At bare minimum, the server requires a `custom/app.ini` file and a local directory where source files of documentation are located. 8 | 9 | 1. Clone the `asoul-sig/asouldocs` repository locally: 10 | 11 | ```bash 12 | $ git clone --depth 1 https://github.com/asoul-sig/asouldocs.git 13 | ``` 14 | 15 | 1. Create a `custom/app.ini` file: 16 | 17 | ```bash 18 | $ mkdir custom 19 | $ touch custom/app.ini 20 | ``` 21 | 22 | 1. Edit the `custom/app.ini` to tell the server where "docs" directory is located: 23 | 24 | ```ini 25 | [docs] 26 | TARGET = ./asouldocs/docs 27 | ``` 28 | 29 | 1. Start the server and visit [http://localhost:5555](http://localhost:5555): 30 | 31 | ```bash 32 | $ asouldocs web 33 | YYYY/MM/DD 00:00:00 [ INFO] ASoulDocs 1.0.0 34 | YYYY/MM/DD 00:00:00 [ INFO] Listen on http://localhost:5555 35 | ``` 36 | 37 | Great! Let’s move on to [how to set up documentation](../howto/set-up-documentation.md). 38 | -------------------------------------------------------------------------------- /docs/toc.ini: -------------------------------------------------------------------------------- 1 | -: introduction 2 | -: howto 3 | -: faqs 4 | 5 | [introduction] 6 | -: README 7 | -: installation 8 | -: quick-start 9 | 10 | [howto] 11 | -: README 12 | -: set-up-documentation 13 | -: write-document 14 | -: sync-through-webhook 15 | -: use-extensions 16 | -: customize-templates 17 | -: run-through-docker 18 | -: configure-reverse-proxy 19 | -------------------------------------------------------------------------------- /docs/zh-CN/howto/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 如何 ... 3 | --- 4 | -------------------------------------------------------------------------------- /docs/zh-CN/howto/configure-reverse-proxy.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 配置反向代理 3 | --- 4 | 5 | 6 | ## Caddy 2 7 | 8 | 通过 [Caddy](https://caddyserver.com/) 可以获得永久免费的 HTTPS: 9 | 10 | ```caddyfile 11 | { 12 | http_port 80 13 | https_port 443 14 | } 15 | 16 | asouldocs.dev { 17 | reverse_proxy * localhost:5555 18 | } 19 | ``` 20 | 21 | ## NGINX 22 | 23 | 如下展示了 NGINX 的配置,但具体值需要根据实际情况修改: 24 | 25 | ```nginx 26 | server { 27 | listen 80; 28 | server_name asouldocs.dev; 29 | 30 | location / { 31 | proxy_pass http://localhost:5555; 32 | } 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/zh-CN/howto/customize-templates.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 自定义模板 3 | --- 4 | 5 | 每个[模板文件](set-up-documentation.md#模板文件)都可以通过覆写实现自定义,自定义模板文件需要被放置在 `custom/templates` 目录下(可以通过配置选项 `[page] CUSTOM_DIRECTORY` 指定其它目录)。 6 | 7 | 一般只建议自定义 `home.html` 和 `common/navbar.html` 这两个模板文件来实现最大程度上的向后兼容。 8 | 9 | ## UI 框架 10 | 11 | 页面样式模式使用 [Tailwind CSS](https://tailwindcss.com/) 的 JIT 编译器进行渲染,因此你可以在自定义模板直接使用该框架的所有样式。 12 | 13 | 当然了,并不强求所有服务器都使用 Tailwind CSS 作为 UI 框架,只要在 `custom/common/head.html` 中导入你的自选资源即可。 14 | 15 | ## 本地化 16 | 17 | 服务器使用 Flamego 的 [i18n](https://flamego.cn/middleware/i18n.html) 中间件实现本地化,进行[本地化配置](set-up-documentation.md#本地化配置)的本地化文件需要被放置在 `custom/locale` 目录下(可以通过配置选项 `[i18n] CUSTOM_DIRECTORY` 指定其它目录)。 18 | 19 | 在模板文件中调用本地化函数的语法为 `{{call .Tr "footer::copyright"}}`,其中, `footer` 为分区名,`copyright` 为键名。 20 | 21 | ## 静态资源 22 | 23 | 自定义静态资源需要被放置在 `custom/public` 目录下(可以通过配置选项 `[asset] CUSTOM_DIRECTORY` 指定其它目录)并在模板中导入。 24 | 25 | 假设你有一个路径为 `custom/public/css/my.css` 的自定义静态资源,并在 `custom/common/head.tmpl` 文件中添加如下内容: 26 | 27 | ```go-html-template 28 | 29 | ``` 30 | 31 | 注意 `href` 属性并不包含 `public` 前缀。 32 | -------------------------------------------------------------------------------- /docs/zh-CN/howto/run-through-docker.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 通过 Docker 运行 3 | --- 4 | 5 | _**一魂文档**_ 的 Docker 镜像可以通过 [Docker Hub](https://hub.docker.com/r/unknwon/asouldocs) 或 [GitHub Container Registry](https://github.com/asoul-sig/asouldocs/pkgs/container/asouldocs) 获取。 6 | 7 | `latest` 标签指向 [`main` 分支](https://github.com/asoul-sig/asouldocs)上的最新构建版本。 8 | 9 | ## 注意事项 10 | 11 | 配置选项 `HTTP_ADDR` 需要被修改为监听 Docker 容器中的网络地址: 12 | 13 | ```ini 14 | HTTP_ADDR = 0.0.0.0 15 | ``` 16 | 17 | ## 启动容器 18 | 19 | 你需要挂在 `custom` 目录才能使 Docker 容器成功启动(`/app/asouldocs/custom` 为容器内的对应路径): 20 | 21 | ```bash 22 | $ docker run \ 23 | --name=asouldocs \ 24 | -p 15555:5555 \ 25 | -v $(pwd)/custom:/app/asouldocs/custom \ 26 | unknwon/asouldocs 27 | ``` 28 | 29 | 如果你的文档目标并不是[远程 Git 地址](set-up-documentation.md#文档目标),则还需要挂载 `docs` 目录(`/app/asouldocs/docs` 为容器内的对应路径): 30 | 31 | ```bash 32 | $ docker run \ 33 | --name=asouldocs \ 34 | -p 15555:5555 \ 35 | -v $(pwd)/custom:/app/asouldocs/custom \ 36 | -v $(pwd)/docs:/app/asouldocs/docs \ 37 | unknwon/asouldocs 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/zh-CN/howto/set-up-documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 创建文档仓库 3 | --- 4 | 5 | _**一魂文档**_ 的文档仓库包含以下四个要素: 6 | 7 | 1. 配置文件 8 | 1. 模板文件 9 | 1. 本地化文件 10 | 1. 用户文档 11 | 12 | ## 配置文件 13 | 14 | 每一个文档项目都需要一个配置文件,默认路径为 `custom/app.ini`。 15 | 16 | 每个服务器都会内置一个[默认配置文件](https://github.com/asoul-sig/asouldocs/blob/main/conf/app.ini)为各项配置提供了默认选项,从而使你的文档项目只需要进行几项必要的修改。 17 | 18 | 命令行参数 `--config`(或 `-c`)可以用于指定飞默认路径的配置文件。 19 | 20 | ## 模板文件 21 | 22 | 模板文件使用 [Go HTML 模板](https://pkg.go.dev/html/template)语法渲染各个页面。 23 | 24 | 每个服务器都会内置一批可直接使用的[默认模板文件](https://github.com/asoul-sig/asouldocs/tree/main/templates),但大多数情况下你可能都需要修改 `home.html` 和 `common/navbar.html` 这两个模板文件用于展示与你项目相关的内容。 25 | 26 | 你可以通过在 `custom/templates` 目录下放置对应名称的模板文件来覆写内置模板文件。 27 | 28 | 例如,下面展示了如何将 `common/navbar.html` 中的 GitHub 字样替换为一个笑脸的图案: 29 | 30 | ```go-html-template {hl_lines=["7-10"]} 31 | 44 | ``` 45 | 46 | 如需了解更多,请阅读[如何自定义模板](customize-templates.md)。 47 | 48 | ## 文档结构 49 | 50 | 单语言和多语言的文档结构是一致的,在文档的根目录中需要放置一个 `toc.ini` 文件,其子目录的命名也需要遵循 [IETF BCP 47 language tags](https://en.wikipedia.org/wiki/IETF_language_tag) 规范,如 en-US、zh-CN。 51 | 52 | 如下所示: 53 | 54 | ``` 55 | ├── en-US 56 | │   ├── howto 57 | │   │   ├── README.md 58 | │   │   └── set_up_documentation.md 59 | │   └── introduction 60 | │   ├── README.md 61 | │   ├── installation.md 62 | │   └── quick_start.md 63 | ├── zh-CN 64 | │   ├── howto 65 | │   │   ├── README.md 66 | │   │   └── set_up_documentation.md 67 | │   └── introduction 68 | │   ├── README.md 69 | │   ├── installation.md 70 | │   └── quick_start.md 71 | └── toc.ini 72 | ``` 73 | 74 | `toc.ini` 文件用于描述各个文档将以如何顺序和组织结构展现在站点上,下面展示了与上例所匹配的 `toc.ini` 文件内容: 75 | 76 | ```ini 77 | -: introduction 78 | -: howto 79 | 80 | [introduction] 81 | -: README 82 | -: installation 83 | -: quick_start 84 | 85 | [howto] 86 | -: README 87 | -: set_up_documentation 88 | ``` 89 | 90 | 默认分区用于定义文档目录的展现顺序,各个值与目录名称一一对应: 91 | 92 | ```ini 93 | -: introduction 94 | -: howto 95 | ``` 96 | 97 | 各个目录也分别需要一个同名的分区: 98 | 99 | ```ini 100 | [introduction] 101 | ... 102 | 103 | [howto] 104 | ... 105 | ``` 106 | 107 | 与目录同名的分区用于定义文件的展现顺序,各个值与文件名称一一对应(但不包括 `.md` 后缀名): 108 | 109 | ```ini 110 | [introduction] 111 | -: README 112 | -: installation 113 | -: quick_start 114 | ``` 115 | 116 | 其它事项: 117 | 118 | 1. 目前仅支持单层目录结构 119 | 1. 每个目录都必须包含一个 `README.md` 文件,并且该文件至少需要包含[前置配置](write-document.md#前置配置)部分的内容。 120 | 121 | ### 本地化配置 122 | 123 | 每个服务器都会根据[默认配置](https://github.com/asoul-sig/asouldocs/blob/39b59c4159e4a2b0e0a290c79f85c46a3e1faf0b/conf/app.ini#L26-L30)认为需要为用户提供英语 (en-US) 和简体中文 (zh-CN) 两种语言版本的文档: 124 | 125 | ```ini 126 | [i18n] 127 | ; 支持的文档语言列表 128 | LANGUAGES = en-US,zh-CN 129 | ; 各个语言用户友好的名称 130 | NAMES = English,简体中文 131 | ``` 132 | 133 | `LANGUAGES` 值中的第一个语言会被作为默认语言,当服务器无法找到(根据浏览器的 `Accept-Language` 请求头识别)偏好语言的文档时则会显示默认语言的内容。 134 | 135 | 如果你的文档仅包含简体中文,则需要进行以下配置的修改: 136 | 137 | ```ini 138 | [i18n] 139 | ; 支持的文档语言列表 140 | LANGUAGES = zh-CN 141 | ; 各个语言用户友好的名称 142 | NAMES = 简体中文 143 | ``` 144 | 145 | ## 文档目标 146 | 147 | 文档目标可以是本地目录的路径或远程 Git 地址。 148 | 149 | 以下配置用于本地目录的文档: 150 | 151 | ```ini 152 | [docs] 153 | TYPE = local 154 | TARGET = ./docs 155 | ``` 156 | 157 | 以下配置用于远程 Git 地址的文档: 158 | 159 | ```ini 160 | [docs] 161 | TYPE = remote 162 | TARGET = https://github.com/asoul-sig/asouldocs.git 163 | ``` 164 | 165 | 如果文档目录存在于子目录,则可以通过 `TARGET_DIR` 选项指明: 166 | 167 | ```ini 168 | [docs] 169 | TYPE = remote 170 | TARGET = https://github.com/asoul-sig/asouldocs.git 171 | TARGET_DIR = docs 172 | ``` 173 | 174 | ## 编辑本页内容 175 | 176 | 如果你希望用户帮助改进文档,则可以提供一个快捷链接方便用户快速定位到当前所浏览文档的源文件: 177 | 178 | ```ini 179 | [docs] 180 | ; 编辑链接的格式化字符串,留空表示禁用该功能 181 | EDIT_PAGE_LINK_FORMAT = https://github.com/asoul-sig/asouldocs/blob/main/docs/{blob} 182 | ``` 183 | -------------------------------------------------------------------------------- /docs/zh-CN/howto/sync-through-webhook.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 通过 Webhook 同步 3 | --- 4 | 5 | 每次更新文档都要登录服务器拉取和重启总是令人不爽,而 _**一魂文档**_ 仅需通过以下两步就能实现文档的自动更新: 6 | 7 | 1. [使用远程 Git 地址作为文档仓库](set-up-documentation.md#文档目标) 8 | 1. 配置在每次推送后都发送 Webhook 到服务器 9 | 10 | Webhook 的请求路径为 `/webhook` 且接受任何 HTTP 方法,因此一个简单的 `curl` 命令就可以实现: 11 | 12 | ```bash 13 | $ curl http://localhost:5555/webhook 14 | ``` 15 | 16 | 几乎所有的代码平台都会提供内置的 Webhook 功能,如可以通过 **Settings > Webhooks** 页面中的 **Add webhook** 按钮为 GitHub 仓库配置 Webhook: 17 | 18 | ![GitHub webhook](../../assets/github-webhook.jpg) 19 | -------------------------------------------------------------------------------- /docs/zh-CN/howto/use-extensions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 使用扩展 3 | --- 4 | 5 | 每个 _**一魂文档**_ 服务器都提供了一些极其便捷的内置扩展。 6 | 7 | ## Plausible 8 | 9 | Plausible 扩展用于集成 https://plausible.io/,可以通过如下配置启用: 10 | 11 | ```ini 12 | [extension.plausible] 13 | ENABLED = true 14 | ; 用于指定 data-domain 属性的可选值 15 | DOMAIN = 16 | ``` 17 | 18 | ## Google Analytics 19 | 20 | Google Analytics 扩展用于集成 [Google Analytics 4](https://developers.google.com/analytics/devguides/collection/ga4),可以通过如下配置启用: 21 | 22 | ```ini 23 | [extension.google_analytics] 24 | ENABLED = true 25 | ; 资产所对应的 Measurement ID 26 | MEASUREMENT_ID = G-VXXXYYYYZZ 27 | ``` 28 | 29 | ## Disqus 30 | 31 | Disqus 扩展用于集成 [Disqus](https://disqus.com/),可以通过如下配置启用: 32 | 33 | ```ini 34 | [extension.disqus] 35 | ENABLED = true 36 | ; 站点的 shortname 37 | SHORTNAME = ellien 38 | ``` 39 | 40 | ## utterances 41 | 42 | utterances 扩展用于集成 [utterances](https://utteranc.es/),可以通过如下配置启用: 43 | 44 | ```ini 45 | [extension.utterances] 46 | ENABLED = true 47 | ; GitHub 仓库名称 48 | REPO = owner/repo 49 | ; Issue 映射模式 50 | ISSUE_TERM = pathname 51 | ; Issue 标签 52 | LABEL = utterances 53 | ; 组件主题 54 | THEME = github-light 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/zh-CN/howto/write-document.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 编写文档 3 | --- 4 | 5 | 每个文档都需要使用 [Markdown](https://www.markdownguide.org/) 语法编写,[GitHub Flavored Markdown](https://github.github.com/gfm/) 规范当中的大部分功能也已通过 [yuin/goldmark](https://github.com/yuin/goldmark) 实现。 6 | 7 | 如下所示: 8 | 9 | ```markdown 10 | --- 11 | title: 项目介绍 12 | --- 13 | 14 | _**一魂文档**_ 是一款支持多语言的 Web 文档服务器。 15 | ``` 16 | 17 | ### 前置配置 18 | 19 | 前置配置是指每个文档的开头部分使用 `---` 包括的 YAML 代码块。 20 | 21 | 下面展示了目前支持的所有前置配置字段: 22 | 23 | ```yaml 24 | title: 文档标题 25 | previous: 26 | title: 前个页面的标题 27 | link: 前个页面的相对路径 28 | next: 29 | title: 后个页面的标题 30 | link: 后个页面的相对路径 31 | ``` 32 | 33 | - 除了 `title` 以为均为可选字段 34 | - `previous` 和 `next` 下 `link` 字段的语法和[链接与图片](#链接与图片)一致。 35 | 36 | ### 链接与图片 37 | 38 | 指向其它文档或图片的链接与任何编辑器中的语法并无二致(如 VSCode): 39 | 40 | - 指向同个目录下的文档:`[Customize templates](customize-templates.md)` 41 | - 指向其它目录:`[How to](README.md)` 42 | - 指向不同目录下的文档:`[Quick start](../introduction/quick-start.md)` 43 | - 指向图片:`![](../../assets/workflow.png)` 44 | 45 | ### 代码块 46 | 47 | 代码块的高亮使用 [alecthomas/chroma](https://github.com/alecthomas/chroma) 实现,因此需要使用其[支持语言](https://github.com/alecthomas/chroma#supported-languages)(英文)列表中的名称来指定代码块的语言,名称中的空格需要使用横线 (`-`) 替代,如使用 `go-html-template` 而不是 `go html template`(大小写不敏感)。 48 | 49 | 代码块还支持行数显示和多行高亮: 50 | 51 | ```markdown 52 | ...go-html-template {linenos=true, hl_lines=["7-10", 12]} 53 | ``` 54 | 55 | ### 渲染缓存 56 | 57 | 当服务器使用[默认配置](set-up-documentation.md#configuration-file)运行时(`dev` 环境),每个请求都会重新加载并渲染文档页面: 58 | 59 | ```ini 60 | ENV = dev 61 | ``` 62 | 63 | 在生产环境需要通过配置 `ENV = prod` 启用渲染缓存。 64 | -------------------------------------------------------------------------------- /docs/zh-CN/introduction/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 项目介绍 3 | --- 4 | 5 | _**一魂文档**_ 是一款支持多语言的 Web 文档服务器。 6 | 7 | ## 项目初衷 8 | 9 | 项目文档工具已经是一个非常拥挤的领域,各种工具层出不穷,但却没有一个真正专注于个人尤其是开源开发者的成熟方案。无数的静态生成器、文档服务器、SaaS 产品,却没有一个能满足我们自己的实际需求,幸好我们是一群热爱编程的人,可以自己动手实现自己的需求。 10 | 11 | 由于经过多年的痛苦探索都无法在市面上找到一个合适的产品,促使了 _**一魂文档**_ 的诞生(1.0 之前的版本名为 Peach)。 12 | 13 | 下表展示了我们所关注的功能在几个主要产品之间的对比(功能点的理解上可能会出现差异): 14 | 15 | |产品/功能 |_**一魂文档**_|[Mkdocs](https://www.mkdocs.org/)|[Hugo](https://gohugo.io/)|[VuePress](https://v2.vuepress.vuejs.org/)/[VitePress](https://vitepress.vuejs.org/)|[GitBook](https://www.gitbook.com/)| 16 | |:---------------------------:|:-------------:|:----:|:--:|:----------------:|:----:| 17 | |自托管 | ✅ | ✅ | ✅ | ✅ | ❌ | 18 | |多语言文档1 | ✅ | ✅ | ✅ | ✅ | ❌ | 19 | |内置更新同步 | ✅ | ❌ | ❌ | ❌ | ✅ | 20 | |DocSearch | 🎯 | ❌ | ✅ | ✅ | ❌ | 21 | |内置搜索功能 | 🎯 | ✅ | ❌ | ✅ | ✅ | 22 | |评论系统集成 | ✅ | ❌ | ✅ | ❌ | ❌ | 23 | |多版本 | 🎯 | ❌ | ❌ | ❌ | ❌ | 24 | |保护资源 | 🎯 | ❌ | ❌ | ❌ | ❌ | 25 | |深色模式 | ✅ | ❌ | ✅ | ✅ | ❌ | 26 | |可定制化2 | ✅ | ❌ | ✅ | ❌ | ❌ | 27 | |语言回退3 | ✅ | ❌ | ❌ | ❌ | ❌ | 28 | 29 | - 1:目前市面上没有任何一个产品支持在不变更 URL 的情况下支持展现多语言的文档,这导致面向不同用户分享文档时需要使用不同的链接 30 | - 2:指可定制化的程度让用户无法识别时后端所使用的产品 31 | - 3:当某个文档在偏好语言中不存在时,回退显示默认语言版本的文档 32 | - 🎯:在产品路线图中的计划功能 33 | 34 | ## 项目历史 35 | 36 | 本项目在 1.0 之前的版本名称为 Peach Docs,自 1.0 版本起已更名为 _**一魂文档**_。 37 | 38 | 项目的技术栈也从 2015 年的热门组合 [Macaron](https://go-macaron.com) 和 [Semantic UI](https://semantic-ui.com/) 升级成为最新的黄金拍档 [Flamego](https://flamego.dev) 和 [Tailwind CSS](https://tailwindcss.com/)。 39 | 40 | 项目目前也已成为 [A-SOUL 特别兴趣小组](https://github.com/asoul-sig)的一部分(之前所属于 github.com/peachdocs)。 41 | 42 | ## 开始使用 43 | 44 | [下载安装](installation.md)或直接阅读[快速开始](quick-start.md)吧! 45 | -------------------------------------------------------------------------------- /docs/zh-CN/introduction/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 下载安装 3 | --- 4 | 5 | ## 二进制安装 6 | 7 | 请通过 [GitHub releases](https://github.com/asoul-sig/asouldocs/releases) 页面获取各个版本的二进制文件。 8 | 9 | ## 源码安装 10 | 11 | 源码安装要求具有本地的 [Go 语言](https://go.dev/)开发环境,可以通过以下命令检查: 12 | 13 | ```bash 14 | $ go version 15 | ``` 16 | 17 | 最低的 Go 语言版本要求为 **1.19**。 18 | 19 | 然后通过以下命令构建二进制: 20 | 21 | ```bash 22 | $ go build 23 | ``` 24 | 25 | 最后启动服务器: 26 | 27 | ```bash 28 | $ ./asouldocs web 29 | ``` 30 | 31 | 如需进行本地开发,请阅读[搭建本地开发环境](https://github.com/asoul-sig/asouldocs/blob/main/docs/dev/local_development.md)(英文)。 32 | -------------------------------------------------------------------------------- /docs/zh-CN/introduction/quick-start.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 快速开始 3 | --- 4 | 5 | 我们将会通过渲染 _**一魂文档**_ 自身的用户文档来对服务器的用法进行了解。 6 | 7 | 每个服务器都要求一个路径为 `custom/app.ini` 的配置文件和一个包含用户文档的本地目录。 8 | 9 | 1. 克隆 `asoul-sig/asouldocs` 仓库到本地: 10 | 11 | ```bash 12 | $ git clone --depth 1 https://github.com/asoul-sig/asouldocs.git 13 | ``` 14 | 15 | 1. 创建配置文件 `custom/app.ini`: 16 | 17 | ```bash 18 | $ mkdir custom 19 | $ touch custom/app.ini 20 | ``` 21 | 22 | 1. 编辑配置文件 `custom/app.ini` 并指明文档目录 (docs) 的路径: 23 | 24 | ```ini 25 | [docs] 26 | TARGET = ./asouldocs/docs 27 | ``` 28 | 29 | 1. 启动服务器并访问 [http://localhost:5555](http://localhost:5555): 30 | 31 | ```bash 32 | $ asouldocs web 33 | YYYY/MM/DD 00:00:00 [ INFO] ASoulDocs 1.0.0 34 | YYYY/MM/DD 00:00:00 [ INFO] Listen on http://localhost:5555 35 | ``` 36 | 37 | 完美!下一步,让我们学习[如何创建文档仓库](../howto/set-up-documentation.md)。 38 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/asoul-sig/asouldocs 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/abhinav/goldmark-toc v0.2.1 7 | github.com/flamego/flamego v1.9.5 8 | github.com/flamego/i18n v1.1.0 9 | github.com/flamego/template v1.2.2 10 | github.com/gogs/git-module v1.8.4 11 | github.com/pkg/errors v0.9.1 12 | github.com/stretchr/testify v1.10.0 13 | github.com/urfave/cli v1.22.16 14 | github.com/yuin/goldmark v1.7.12 15 | github.com/yuin/goldmark-emoji v1.0.5 16 | github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594 17 | github.com/yuin/goldmark-meta v1.1.0 18 | gopkg.in/ini.v1 v1.67.0 19 | unknwon.dev/clog/v2 v2.2.0 20 | ) 21 | 22 | require ( 23 | github.com/alecthomas/chroma v0.10.0 // indirect 24 | github.com/alecthomas/participle/v2 v2.1.1 // indirect 25 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 26 | github.com/charmbracelet/lipgloss v0.10.0 // indirect 27 | github.com/charmbracelet/log v0.4.0 // indirect 28 | github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect 29 | github.com/davecgh/go-spew v1.1.1 // indirect 30 | github.com/dlclark/regexp2 v1.4.0 // indirect 31 | github.com/fatih/color v1.13.0 // indirect 32 | github.com/go-logfmt/logfmt v0.6.0 // indirect 33 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 34 | github.com/mattn/go-colorable v0.1.9 // indirect 35 | github.com/mattn/go-isatty v0.0.18 // indirect 36 | github.com/mattn/go-runewidth v0.0.15 // indirect 37 | github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75 // indirect 38 | github.com/muesli/reflow v0.3.0 // indirect 39 | github.com/muesli/termenv v0.15.2 // indirect 40 | github.com/pmezard/go-difflib v1.0.0 // indirect 41 | github.com/rivo/uniseg v0.4.7 // indirect 42 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 43 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect 44 | golang.org/x/sys v0.13.0 // indirect 45 | golang.org/x/text v0.4.0 // indirect 46 | gopkg.in/yaml.v2 v2.4.0 // indirect 47 | gopkg.in/yaml.v3 v3.0.1 // indirect 48 | unknwon.dev/i18n v1.0.1 // indirect 49 | ) 50 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 2 | github.com/abhinav/goldmark-toc v0.2.1 h1:QJsKKGbdVeCWYMB11hSkNuZLuIzls7Y4KBZfwTkBB90= 3 | github.com/abhinav/goldmark-toc v0.2.1/go.mod h1:aq1IZ9qN85uFYpowec98iJrFkEHYT4oeFD1SC0qd8d0= 4 | github.com/alecthomas/assert/v2 v2.3.0 h1:mAsH2wmvjsuvyBvAmCtm7zFsBlb8mIHx5ySLVdDZXL0= 5 | github.com/alecthomas/assert/v2 v2.3.0/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= 6 | github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= 7 | github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= 8 | github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= 9 | github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= 10 | github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= 11 | github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 12 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 13 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 14 | github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= 15 | github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= 16 | github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= 17 | github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= 18 | github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= 19 | github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 20 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 22 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= 24 | github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= 25 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 26 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 27 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 28 | github.com/flamego/flamego v1.9.5 h1:GbUHZ58bEaI6MfiC8SAaRR96VEHDGjA1dZVWN3qtmEQ= 29 | github.com/flamego/flamego v1.9.5/go.mod h1:n1CMZUtcP30xeJJ+di9E+wrfWWzptAxjkKabIV806to= 30 | github.com/flamego/i18n v1.1.0 h1:C6Dns0oq9QJeLV2QwRi1GiEsPf+Gq6oiykk5kwPS5JQ= 31 | github.com/flamego/i18n v1.1.0/go.mod h1:qzMYCxfenGkfFyM3ubygk8lpI3Pmv0A4JLIcQl1GXtY= 32 | github.com/flamego/template v1.2.2 h1:aMpt8RzXBb2ZGuABf9p/q8oBBpXrurUV8rgBbz7mj2o= 33 | github.com/flamego/template v1.2.2/go.mod h1:xTAmwCCPaOuxN5t4CpzOP7WZN5WkLRiJfJCpsiB0aUg= 34 | github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= 35 | github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 36 | github.com/gogs/git-module v1.8.4 h1:oSt8sOL4NWOGrSo/CwbS+C4YXtk76QvxyPofem/ViTU= 37 | github.com/gogs/git-module v1.8.4/go.mod h1:bQY0aoMK5Q5+NKgy4jXe3K1GFW+GnsSk0SJK0jh6yD0= 38 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 39 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 40 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 41 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 42 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 43 | github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= 44 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 45 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 46 | github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= 47 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 48 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 49 | github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= 50 | github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 51 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 52 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 53 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 54 | github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75 h1:Pijfgr7ZuvX7QIQiEwLdRVr3RoMG+i0SbBO1Qu+7yVk= 55 | github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo= 56 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 57 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 58 | github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= 59 | github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= 60 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 61 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 62 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 63 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 64 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 65 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 66 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 67 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 68 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 69 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 70 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 71 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 72 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 73 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 74 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 75 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 76 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 77 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 78 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 79 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 80 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 81 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 82 | github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= 83 | github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= 84 | github.com/yuin/goldmark v1.3.3/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 85 | github.com/yuin/goldmark v1.4.5/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg= 86 | github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= 87 | github.com/yuin/goldmark v1.7.12 h1:YwGP/rrea2/CnCtUHgjuolG/PnMxdQtPMO5PvaE2/nY= 88 | github.com/yuin/goldmark v1.7.12/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= 89 | github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk= 90 | github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= 91 | github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594 h1:yHfZyN55+5dp1wG7wDKv8HQ044moxkyGq12KFFMFDxg= 92 | github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594/go.mod h1:U9ihbh+1ZN7fR5Se3daSPoz1CGF9IYtSvWwVQtnzGHU= 93 | github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc= 94 | github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= 95 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= 96 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= 97 | golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= 98 | golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 99 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 100 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 101 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 102 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 103 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 104 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 105 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= 106 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 107 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 108 | golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= 109 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 110 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 111 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 112 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 113 | gopkg.in/ini.v1 v1.64.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 114 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 115 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 116 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 117 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 118 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 119 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 120 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 121 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 122 | unknwon.dev/clog/v2 v2.2.0 h1:jkPdsxux0MC04BT/9NHbT75z4prK92SH10VBNmIpVCc= 123 | unknwon.dev/clog/v2 v2.2.0/go.mod h1:zvUlyibDHI4mykYdWyWje2G9nF/nBzfDOqRo2my4mWc= 124 | unknwon.dev/i18n v1.0.1 h1:u3lR67ur4bsM5lucFO5LTHCwAUqGbQ4Gk+1Oe3J8U1M= 125 | unknwon.dev/i18n v1.0.1/go.mod h1:3dj1tQFJQE+HA5/iwBXVkZbWgMwdoRQZ9X2O90ZixBc= 126 | -------------------------------------------------------------------------------- /internal/cmd/cmd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 ASoulDocs. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package cmd 6 | 7 | import ( 8 | "github.com/urfave/cli" 9 | ) 10 | 11 | func stringFlag(name, value, usage string) cli.StringFlag { 12 | return cli.StringFlag{ 13 | Name: name, 14 | Value: value, 15 | Usage: usage, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /internal/cmd/web.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 ASoulDocs. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package cmd 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | gotemplate "html/template" 11 | "net/http" 12 | "strings" 13 | "time" 14 | 15 | "github.com/flamego/flamego" 16 | "github.com/flamego/i18n" 17 | "github.com/flamego/template" 18 | "github.com/urfave/cli" 19 | log "unknwon.dev/clog/v2" 20 | 21 | "github.com/asoul-sig/asouldocs/conf/locale" 22 | "github.com/asoul-sig/asouldocs/internal/conf" 23 | "github.com/asoul-sig/asouldocs/internal/osutil" 24 | "github.com/asoul-sig/asouldocs/internal/store" 25 | "github.com/asoul-sig/asouldocs/public" 26 | "github.com/asoul-sig/asouldocs/templates" 27 | ) 28 | 29 | var Web = cli.Command{ 30 | Name: "web", 31 | Usage: "Start the web server", 32 | Action: runWeb, 33 | Flags: []cli.Flag{ 34 | stringFlag("config, c", "custom/app.ini", "Custom configuration file path"), 35 | }, 36 | } 37 | 38 | func runWeb(ctx *cli.Context) { 39 | err := conf.Init(ctx.String("config")) 40 | if err != nil { 41 | log.Fatal("Failed to init configuration: %v", err) 42 | } 43 | log.Info("ASoulDocs %s", conf.App.Version) 44 | 45 | docstore, err := store.Init(conf.Docs.Type, conf.Docs.Target, conf.Docs.TargetDir, conf.I18n.Languages, conf.Page.DocsBasePath) 46 | if err != nil { 47 | log.Fatal("Failed to init store: %v", err) 48 | } 49 | 50 | f := flamego.New() 51 | f.Use(flamego.Recovery()) 52 | 53 | // Custom assets should be served first to support overwrite 54 | f.Use(flamego.Static( 55 | flamego.StaticOptions{ 56 | Directory: conf.Asset.CustomDirectory, 57 | SetETag: true, 58 | }, 59 | )) 60 | 61 | // Serve assets of the documentation 62 | f.Use(flamego.Static( 63 | flamego.StaticOptions{ 64 | Directory: docstore.RootDir(), 65 | }, 66 | )) 67 | 68 | // Load assets from disk if in development and the local directory exists 69 | if flamego.Env() == flamego.EnvTypeDev && 70 | osutil.IsDir("public") { 71 | f.Use(flamego.Static()) 72 | } else { 73 | f.Use(flamego.Static( 74 | flamego.StaticOptions{ 75 | FileSystem: http.FS(public.Files), 76 | SetETag: true, 77 | }, 78 | )) 79 | } 80 | 81 | // Load templates from disk if in development and the local directory exists 82 | funcMaps := []gotemplate.FuncMap{{ 83 | "Year": func() int { return time.Now().Year() }, 84 | "Safe": func(p []byte) gotemplate.HTML { return gotemplate.HTML(p) }, 85 | }} 86 | if flamego.Env() == flamego.EnvTypeDev && 87 | osutil.IsDir("templates") { 88 | f.Use(template.Templater( 89 | template.Options{ 90 | AppendDirectories: []string{conf.Page.CustomDirectory}, 91 | FuncMaps: funcMaps, 92 | }, 93 | )) 94 | } else { 95 | fs, err := template.EmbedFS(templates.Files, ".", []string{".html"}) 96 | if err != nil { 97 | log.Fatal("Failed to convert template files to embed.FS: %v", err) 98 | } 99 | f.Use(template.Templater( 100 | template.Options{ 101 | FileSystem: fs, 102 | AppendDirectories: []string{conf.Page.CustomDirectory}, 103 | FuncMaps: funcMaps, 104 | }, 105 | )) 106 | } 107 | 108 | languages := make([]i18n.Language, len(conf.I18n.Languages)) 109 | for i := range conf.I18n.Languages { 110 | languages[i] = i18n.Language{ 111 | Name: conf.I18n.Languages[i], 112 | Description: conf.I18n.Names[i], 113 | } 114 | } 115 | f.Use(i18n.I18n( 116 | i18n.Options{ 117 | FileSystem: http.FS(locale.Files), 118 | AppendDirectories: []string{conf.I18n.CustomDirectory}, 119 | Languages: languages, 120 | }, 121 | )) 122 | 123 | f.Use(func(r *http.Request, data template.Data, l i18n.Locale) { 124 | data["BuildCommit"] = conf.BuildCommit 125 | data["Site"] = conf.Site 126 | data["Page"] = conf.Page 127 | data["Extension"] = conf.Extension 128 | 129 | data["Tr"] = l.Translate 130 | data["Lang"] = l.Lang() 131 | data["Languages"] = languages 132 | 133 | data["URL"] = r.URL.Path 134 | }) 135 | 136 | notFound := func(t template.Template, data template.Data, l i18n.Locale) { 137 | data["Title"] = l.Translate("status::404") 138 | t.HTML(http.StatusNotFound, "404") 139 | } 140 | 141 | f.Get("/", 142 | func(c flamego.Context, t template.Template, data template.Data, l i18n.Locale) { 143 | if !conf.Page.HasLandingPage { 144 | c.Redirect(conf.Page.DocsBasePath) 145 | return 146 | } 147 | 148 | data["Title"] = l.Translate("name") + " - " + l.Translate("tag_line") 149 | t.HTML(http.StatusOK, "home") 150 | }, 151 | ) 152 | f.Get(conf.Page.DocsBasePath+"/?{**}", 153 | func(c flamego.Context, t template.Template, data template.Data, l i18n.Locale) { 154 | current := c.Param("**") 155 | if current == "" || current == "/" { 156 | c.Redirect(conf.Page.DocsBasePath + "/" + docstore.FirstDocPath()) 157 | return 158 | } 159 | 160 | if flamego.Env() == flamego.EnvTypeDev { 161 | err = docstore.Reload() 162 | if err != nil { 163 | panic("reload store: " + err.Error()) 164 | } 165 | } 166 | 167 | data["Current"] = current 168 | data["TOC"] = docstore.TOC(l.Lang()) 169 | 170 | node, fallback, err := docstore.Match(l.Lang(), current) 171 | if err != nil { 172 | notFound(t, data, l) 173 | return 174 | } 175 | 176 | data["Fallback"] = fallback 177 | data["Category"] = node.Category 178 | data["Title"] = node.Title + " - " + l.Translate("name") 179 | data["Node"] = node 180 | 181 | if conf.Docs.EditPageLinkFormat != "" { 182 | blob := strings.TrimPrefix(node.LocalPath, docstore.RootDir()+"/") 183 | data["EditLink"] = strings.Replace(conf.Docs.EditPageLinkFormat, "{blob}", blob, 1) 184 | } 185 | t.HTML(http.StatusOK, "docs/page") 186 | }, 187 | ) 188 | f.Any("/webhook", func(w http.ResponseWriter) { 189 | err := docstore.Reload() 190 | if err != nil { 191 | log.Error("Failed to reload store triggered by webhook: %v", err) 192 | 193 | w.WriteHeader(http.StatusInternalServerError) 194 | _ = json.NewEncoder(w).Encode(map[string]any{ 195 | "error": err.Error(), 196 | }) 197 | return 198 | } 199 | w.WriteHeader(http.StatusNoContent) 200 | }) 201 | 202 | f.NotFound(notFound) 203 | 204 | listenAddr := fmt.Sprintf("%s:%d", conf.App.HTTPHost, conf.App.HTTPPort) 205 | log.Info("Listen on http://%s", listenAddr) 206 | if err := http.ListenAndServe(listenAddr, f); err != nil { 207 | log.Fatal("Failed to start server: %v", err) 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /internal/conf/conf.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 ASoulDocs. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package conf 6 | 7 | import ( 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/flamego/flamego" 12 | "github.com/pkg/errors" 13 | "gopkg.in/ini.v1" 14 | log "unknwon.dev/clog/v2" 15 | 16 | "github.com/asoul-sig/asouldocs/conf" 17 | "github.com/asoul-sig/asouldocs/internal/osutil" 18 | ) 19 | 20 | func init() { 21 | // Initialize the primary logger until logging service is up. 22 | err := log.NewConsole() 23 | if err != nil { 24 | panic("init console logger: " + err.Error()) 25 | } 26 | } 27 | 28 | // File is the configuration object. 29 | var File *ini.File 30 | 31 | // Init initializes configuration from conf assets and given custom 32 | // configuration file. If `customConf` is empty, it falls back to default 33 | // location, i.e. "/custom". 34 | // 35 | // It is safe to call this function multiple times with desired `customConf`, 36 | // but it is not concurrent safe. 37 | // 38 | // NOTE: The order of loading configuration sections matters as one may depend 39 | // on another. 40 | func Init(customConf string) (err error) { 41 | if customConf == "" { 42 | customConf = filepath.Join("custom", "conf", "app.ini") 43 | } else { 44 | customConf, err = filepath.Abs(customConf) 45 | if err != nil { 46 | return errors.Wrap(err, "get absolute path") 47 | } 48 | } 49 | 50 | if !osutil.IsFile(customConf) { 51 | return errors.Errorf("no custom configuration found at %q", customConf) 52 | } 53 | 54 | data, err := conf.Files.ReadFile("app.ini") 55 | if err != nil { 56 | return errors.Wrap(err, `read default "app.ini"`) 57 | } 58 | 59 | File, err = ini.LoadSources( 60 | ini.LoadOptions{ 61 | IgnoreInlineComment: true, 62 | }, 63 | data, customConf, 64 | ) 65 | if err != nil { 66 | return errors.Wrap(err, "load configuration sources") 67 | } 68 | File.NameMapper = ini.SnackCase 69 | 70 | if err = File.Section(ini.DefaultSection).MapTo(&App); err != nil { 71 | return errors.Wrap(err, "mapping default section") 72 | } else if err = File.Section("site").MapTo(&Site); err != nil { 73 | return errors.Wrap(err, "mapping [site] section") 74 | } else if err = File.Section("asset").MapTo(&Asset); err != nil { 75 | return errors.Wrap(err, "mapping [asset] section") 76 | } else if err = File.Section("page").MapTo(&Page); err != nil { 77 | return errors.Wrap(err, "mapping [page] section") 78 | } else if err = File.Section("i18n").MapTo(&I18n); err != nil { 79 | return errors.Wrap(err, "mapping [i18n] section") 80 | } else if err = File.Section("docs").MapTo(&Docs); err != nil { 81 | return errors.Wrap(err, "mapping [docs] section") 82 | } 83 | 84 | if err = File.Section("extension.plausible").MapTo(&Extension.Plausible); err != nil { 85 | return errors.Wrap(err, "mapping [extension.plausible] section") 86 | } else if err = File.Section("extension.google_analytics").MapTo(&Extension.GoogleAnalytics); err != nil { 87 | return errors.Wrap(err, "mapping [extension.google_analytics] section") 88 | } else if err = File.Section("extension.disqus").MapTo(&Extension.Disqus); err != nil { 89 | return errors.Wrap(err, "mapping [extension.disqus] section") 90 | } else if err = File.Section("extension.utterances").MapTo(&Extension.Utterances); err != nil { 91 | return errors.Wrap(err, "mapping [extension.utterances] section") 92 | } 93 | 94 | Page.DocsBasePath = strings.TrimRight(Page.DocsBasePath, "/") 95 | 96 | if App.Env == "prod" { 97 | flamego.SetEnv(flamego.EnvTypeProd) 98 | } 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /internal/conf/static.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 ASoulDocs. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package conf 6 | 7 | // Build time and commit information. 8 | // 9 | // ⚠️ WARNING: should only be set by "-ldflags". 10 | var ( 11 | BuildVersion string 12 | BuildTime string 13 | BuildCommit string 14 | ) 15 | 16 | var ( 17 | // Application settings 18 | App struct { 19 | // ⚠️ WARNING: Should only be set by the main package (i.e. "main.go"). 20 | Version string `ini:"-"` 21 | 22 | Env string 23 | HTTPHost string `ini:"HTTP_ADDR"` 24 | HTTPPort int `ini:"HTTP_PORT"` 25 | } 26 | 27 | // Site settings 28 | Site struct { 29 | Description string 30 | ExternalURL string `ini:"EXTERNAL_URL"` 31 | } 32 | 33 | // Asset settings 34 | Asset struct { 35 | CustomDirectory string 36 | } 37 | 38 | // Page settings 39 | Page struct { 40 | HasLandingPage bool 41 | DocsBasePath string 42 | CustomDirectory string 43 | } 44 | 45 | // I18n settings 46 | I18n struct { 47 | Languages []string 48 | Names []string 49 | CustomDirectory string 50 | } 51 | 52 | // Documentation settings 53 | Docs struct { 54 | Type DocType 55 | Target string 56 | TargetDir string 57 | EditPageLinkFormat string 58 | } 59 | 60 | // Extension settings 61 | Extension struct { 62 | Plausible struct { 63 | Enabled bool 64 | Domain string 65 | } 66 | GoogleAnalytics struct { 67 | Enabled bool 68 | MeasurementID string `ini:"MEASUREMENT_ID"` 69 | } 70 | Disqus struct { 71 | Enabled bool 72 | Shortname string 73 | } 74 | Utterances struct { 75 | Enabled bool 76 | Repo string 77 | IssueTerm string 78 | Label string 79 | Theme string 80 | } 81 | } 82 | ) 83 | 84 | type DocType string 85 | 86 | const ( 87 | DocTypeLocal DocType = "local" 88 | DocTypeRemote DocType = "remote" 89 | ) 90 | -------------------------------------------------------------------------------- /internal/osutil/osutil.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Gogs Authors. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package osutil 6 | 7 | import ( 8 | "os" 9 | ) 10 | 11 | // IsFile returns true if given path exists as a file (i.e. not a directory). 12 | func IsFile(path string) bool { 13 | f, e := os.Stat(path) 14 | if e != nil { 15 | return false 16 | } 17 | return !f.IsDir() 18 | } 19 | 20 | // IsDir returns true if given path is a directory, and returns false when it's 21 | // a file or does not exist. 22 | func IsDir(dir string) bool { 23 | f, e := os.Stat(dir) 24 | if e != nil { 25 | return false 26 | } 27 | return f.IsDir() 28 | } 29 | 30 | // IsExist returns true if a file or directory exists. 31 | func IsExist(path string) bool { 32 | _, err := os.Stat(path) 33 | return err == nil || os.IsExist(err) 34 | } 35 | -------------------------------------------------------------------------------- /internal/osutil/osutil_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Gogs Authors. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package osutil 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestIsFile(t *testing.T) { 14 | tests := []struct { 15 | path string 16 | expVal bool 17 | }{ 18 | { 19 | path: "osutil.go", 20 | expVal: true, 21 | }, { 22 | path: "../osutil", 23 | expVal: false, 24 | }, { 25 | path: "not_found", 26 | expVal: false, 27 | }, 28 | } 29 | for _, test := range tests { 30 | t.Run("", func(t *testing.T) { 31 | assert.Equal(t, test.expVal, IsFile(test.path)) 32 | }) 33 | } 34 | } 35 | 36 | func TestIsDir(t *testing.T) { 37 | tests := []struct { 38 | path string 39 | expVal bool 40 | }{ 41 | { 42 | path: "osutil.go", 43 | expVal: false, 44 | }, { 45 | path: "../osutil", 46 | expVal: true, 47 | }, { 48 | path: "not_found", 49 | expVal: false, 50 | }, 51 | } 52 | for _, test := range tests { 53 | t.Run("", func(t *testing.T) { 54 | assert.Equal(t, test.expVal, IsDir(test.path)) 55 | }) 56 | } 57 | } 58 | 59 | func TestIsExist(t *testing.T) { 60 | tests := []struct { 61 | path string 62 | expVal bool 63 | }{ 64 | { 65 | path: "osutil.go", 66 | expVal: true, 67 | }, { 68 | path: "../osutil", 69 | expVal: true, 70 | }, { 71 | path: "not_found", 72 | expVal: false, 73 | }, 74 | } 75 | for _, test := range tests { 76 | t.Run("", func(t *testing.T) { 77 | assert.Equal(t, test.expVal, IsExist(test.path)) 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /internal/store/markdown.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 ASoulDocs. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package store 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "net/url" 11 | "os" 12 | "path" 13 | 14 | goldmarktoc "github.com/abhinav/goldmark-toc" 15 | "github.com/pkg/errors" 16 | "github.com/yuin/goldmark" 17 | emoji "github.com/yuin/goldmark-emoji" 18 | highlighting "github.com/yuin/goldmark-highlighting" 19 | goldmarkmeta "github.com/yuin/goldmark-meta" 20 | "github.com/yuin/goldmark/ast" 21 | "github.com/yuin/goldmark/extension" 22 | "github.com/yuin/goldmark/parser" 23 | goldmarkhtml "github.com/yuin/goldmark/renderer/html" 24 | "github.com/yuin/goldmark/text" 25 | "github.com/yuin/goldmark/util" 26 | ) 27 | 28 | func convertFile(pathPrefix, file string) (content []byte, meta map[string]any, headings goldmarktoc.Items, err error) { 29 | body, err := os.ReadFile(file) 30 | if err != nil { 31 | return nil, nil, nil, errors.Wrap(err, "read") 32 | } 33 | 34 | md := goldmark.New( 35 | goldmark.WithParserOptions( 36 | parser.WithAutoHeadingID(), 37 | ), 38 | goldmark.WithRendererOptions( 39 | goldmarkhtml.WithHardWraps(), 40 | goldmarkhtml.WithXHTML(), 41 | goldmarkhtml.WithUnsafe(), 42 | ), 43 | goldmark.WithExtensions( 44 | extension.GFM, 45 | goldmarkmeta.Meta, 46 | emoji.Emoji, 47 | highlighting.NewHighlighting( 48 | highlighting.WithStyle("base16-snazzy"), 49 | highlighting.WithGuessLanguage(true), 50 | ), 51 | extension.NewFootnote(), 52 | ), 53 | ) 54 | 55 | ctx := parser.NewContext( 56 | func(cfg *parser.ContextConfig) { 57 | cfg.IDs = newIDs() 58 | }, 59 | ) 60 | doc := md.Parser().Parse(text.NewReader(body), parser.WithContext(ctx)) 61 | 62 | // Headings 63 | tree, err := goldmarktoc.Inspect(doc, body) 64 | if err != nil { 65 | return nil, nil, nil, errors.Wrap(err, "inspect headings") 66 | } 67 | headings = tree.Items 68 | if len(headings) > 0 { 69 | headings = headings[0].Items 70 | } 71 | 72 | // Links 73 | err = inspectLinks(pathPrefix, doc) 74 | if err != nil { 75 | return nil, nil, nil, errors.Wrap(err, "inspect links") 76 | } 77 | 78 | var buf bytes.Buffer 79 | err = md.Renderer().Render(&buf, body, doc) 80 | if err != nil { 81 | return nil, nil, nil, errors.Wrap(err, "render") 82 | } 83 | 84 | return buf.Bytes(), goldmarkmeta.Get(ctx), headings, nil 85 | } 86 | 87 | func convertRelativeLink(pathPrefix string, link []byte) []byte { 88 | var anchor []byte 89 | if i := bytes.IndexByte(link, '#'); i > -1 { 90 | if i == 0 { 91 | return link 92 | } 93 | 94 | anchor = link[i:] 95 | link = link[:i] 96 | } 97 | 98 | // Example: README.md => /docs/introduction 99 | if bytes.EqualFold(link, []byte(readme+".md")) { 100 | link = append([]byte(pathPrefix), anchor...) 101 | return link 102 | } 103 | 104 | // Example: "installation.md" => "installation" 105 | link = bytes.TrimSuffix(link, []byte(".md")) 106 | 107 | // Example: "../howto/README" => "../howto/" 108 | link = bytes.TrimSuffix(link, []byte(readme)) 109 | 110 | // Example: ("/docs", "../howto/") => "/docs/howto" 111 | link = []byte(path.Join(pathPrefix, string(link))) 112 | 113 | link = append(link, anchor...) 114 | return link 115 | } 116 | 117 | func inspectLinks(pathPrefix string, doc ast.Node) error { 118 | return ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) { 119 | if !entering { 120 | return ast.WalkContinue, nil 121 | } 122 | 123 | link, ok := n.(*ast.Link) 124 | if !ok { 125 | return ast.WalkContinue, nil 126 | } 127 | 128 | dest, err := url.Parse(string(link.Destination)) 129 | if err != nil { 130 | return ast.WalkContinue, nil 131 | } 132 | 133 | if dest.Scheme == "http" || dest.Scheme == "https" { 134 | // TODO: external links adds an SVG 135 | return ast.WalkSkipChildren, nil 136 | } else if dest.Scheme != "" { 137 | return ast.WalkContinue, nil 138 | } 139 | 140 | link.Destination = convertRelativeLink(pathPrefix, link.Destination) 141 | return ast.WalkSkipChildren, nil 142 | }) 143 | } 144 | 145 | // ids is a modified version to allow any non-whitespace characters instead of 146 | // just alphabets or numerics from 147 | // https://github.com/yuin/goldmark/blob/113ae87dd9e662b54012a596671cb38f311a8e9c/parser/parser.go#L65. 148 | type ids struct { 149 | values map[string]bool 150 | } 151 | 152 | func newIDs() parser.IDs { 153 | return &ids{ 154 | values: map[string]bool{}, 155 | } 156 | } 157 | 158 | func (s *ids) Generate(value []byte, kind ast.NodeKind) []byte { 159 | value = util.TrimLeftSpace(value) 160 | value = util.TrimRightSpace(value) 161 | if len(value) == 0 { 162 | if kind == ast.KindHeading { 163 | value = []byte("heading") 164 | } else { 165 | value = []byte("id") 166 | } 167 | } 168 | if _, ok := s.values[util.BytesToReadOnlyString(value)]; !ok { 169 | s.values[util.BytesToReadOnlyString(value)] = true 170 | return value 171 | } 172 | for i := 1; ; i++ { 173 | newResult := fmt.Sprintf("%s-%d", value, i) 174 | if _, ok := s.values[newResult]; !ok { 175 | s.values[newResult] = true 176 | return []byte(newResult) 177 | } 178 | } 179 | } 180 | 181 | func (s *ids) Put(value []byte) { 182 | s.values[util.BytesToReadOnlyString(value)] = true 183 | } 184 | -------------------------------------------------------------------------------- /internal/store/markdown_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 ASoulDocs. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package store 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/yuin/goldmark/ast" 12 | ) 13 | 14 | func TestIDs_Generate(t *testing.T) { 15 | ids := newIDs() 16 | 17 | tests := []struct { 18 | name string 19 | value string 20 | kind ast.NodeKind 21 | want string 22 | }{ 23 | { 24 | name: "normal", 25 | value: "Hello, 世界", 26 | kind: ast.KindHeading, 27 | want: "Hello, 世界", 28 | }, 29 | { 30 | name: "empty heading", 31 | value: "", 32 | kind: ast.KindHeading, 33 | want: "heading", 34 | }, 35 | { 36 | name: "empty id", 37 | value: "", 38 | kind: ast.KindImage, 39 | want: "id", 40 | }, 41 | 42 | { 43 | name: "duplicated heading", 44 | value: "", 45 | kind: ast.KindHeading, 46 | want: "heading-1", 47 | }, 48 | } 49 | for _, test := range tests { 50 | t.Run(test.name, func(t *testing.T) { 51 | got := ids.Generate([]byte(test.value), test.kind) 52 | assert.Equal(t, test.want, string(got)) 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /internal/store/store.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 ASoulDocs. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package store 6 | 7 | import ( 8 | "path/filepath" 9 | "sync" 10 | "sync/atomic" 11 | 12 | "github.com/gogs/git-module" 13 | "github.com/pkg/errors" 14 | log "unknwon.dev/clog/v2" 15 | 16 | "github.com/asoul-sig/asouldocs/internal/conf" 17 | "github.com/asoul-sig/asouldocs/internal/osutil" 18 | ) 19 | 20 | // Store is a store maintaining documentation hierarchies for multiple 21 | // languages. 22 | type Store struct { 23 | // The list of config values 24 | typ conf.DocType 25 | target string 26 | targetDir string 27 | languages []string 28 | baseURLPath string 29 | 30 | // The list of inferred values 31 | rootDir string 32 | defaultLanguage string 33 | tocs atomic.Value 34 | reloadLock sync.Mutex 35 | } 36 | 37 | // RootDir returns the root directory of documentation hierarchies. 38 | func (s *Store) RootDir() string { 39 | return s.rootDir 40 | } 41 | 42 | func (s *Store) getTOCs() map[string]*TOC { 43 | return s.tocs.Load().(map[string]*TOC) 44 | } 45 | 46 | func (s *Store) setTOCs(tocs map[string]*TOC) { 47 | s.tocs.Store(tocs) 48 | } 49 | 50 | // FirstDocPath returns the URL path of the first doc that has content in the 51 | // default language. 52 | func (s *Store) FirstDocPath() string { 53 | for _, dir := range s.getTOCs()[s.defaultLanguage].Nodes { 54 | if len(dir.Content) > 0 { 55 | return dir.Path 56 | } 57 | 58 | for _, file := range dir.Nodes { 59 | return file.Path 60 | } 61 | } 62 | return "404" 63 | } 64 | 65 | // TOC returns the TOC of the given language. It returns the TOC of the default 66 | // language if the given language is not found. 67 | func (s *Store) TOC(language string) *TOC { 68 | toc, ok := s.getTOCs()[language] 69 | if !ok { 70 | return s.getTOCs()[s.defaultLanguage] 71 | } 72 | return toc 73 | } 74 | 75 | var ErrNoMatch = errors.New("no match for the path") 76 | 77 | // Match matches a node with given path in given language. If the no such node 78 | // exists or the node content is empty, it fallbacks to use the node with same 79 | // path in default language. 80 | func (s *Store) Match(language, path string) (n *Node, fallback bool, err error) { 81 | toc := s.TOC(language) 82 | n, ok := toc.nodes[path] 83 | if ok && len(n.Content) > 0 { 84 | return n, false, nil 85 | } 86 | 87 | if toc.Language == s.defaultLanguage { 88 | return nil, false, ErrNoMatch 89 | } 90 | 91 | n, ok = s.getTOCs()[s.defaultLanguage].nodes[path] 92 | if ok && len(n.Content) > 0 { 93 | return n, true, nil 94 | } 95 | return nil, false, ErrNoMatch 96 | } 97 | 98 | // Reload re-initializes the documentation store. 99 | func (s *Store) Reload() error { 100 | s.reloadLock.Lock() 101 | defer s.reloadLock.Unlock() 102 | 103 | log.Trace("Reloading %s...", s.target) 104 | 105 | root := filepath.Join(s.target, s.targetDir) 106 | if s.typ == conf.DocTypeRemote { 107 | localCache := filepath.Join("data", "docs") 108 | if !osutil.IsExist(localCache) { 109 | log.Trace("Cloning %s...", s.target) 110 | err := git.Clone(s.target, localCache, git.CloneOptions{Depth: 1}) 111 | if err != nil { 112 | return errors.Wrapf(err, "clone %q", s.target) 113 | } 114 | } else { 115 | repo, err := git.Open(localCache) 116 | if err != nil { 117 | return errors.Wrapf(err, "open %q", localCache) 118 | } 119 | 120 | log.Trace("Pulling %s...", s.target) 121 | err = repo.Pull() 122 | if err != nil { 123 | return errors.Wrapf(err, "pull %q", s.target) 124 | } 125 | } 126 | 127 | root = filepath.Join(localCache, s.targetDir) 128 | } 129 | 130 | if !osutil.IsDir(root) { 131 | return errors.Errorf("directory root %q does not exist", root) 132 | } 133 | 134 | tocs, err := initTocs(root, s.languages, s.baseURLPath) 135 | if err != nil { 136 | return errors.Wrap(err, "init toc") 137 | } 138 | 139 | s.rootDir = root 140 | s.setTOCs(tocs) 141 | return nil 142 | } 143 | 144 | // Init initializes the documentation store from given type and target. 145 | func Init(typ conf.DocType, target, targetDir string, languages []string, baseURLPath string) (*Store, error) { 146 | if len(languages) < 1 { 147 | return nil, errors.New("no languages") 148 | } 149 | 150 | s := &Store{ 151 | typ: typ, 152 | target: target, 153 | targetDir: targetDir, 154 | languages: languages, 155 | baseURLPath: baseURLPath, 156 | defaultLanguage: languages[0], 157 | } 158 | err := s.Reload() 159 | if err != nil { 160 | return nil, errors.Wrap(err, "reload") 161 | } 162 | return s, nil 163 | } 164 | -------------------------------------------------------------------------------- /internal/store/toc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 ASoulDocs. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package store 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "path" 11 | "path/filepath" 12 | "strings" 13 | 14 | goldmarktoc "github.com/abhinav/goldmark-toc" 15 | "github.com/pkg/errors" 16 | "gopkg.in/ini.v1" 17 | 18 | "github.com/asoul-sig/asouldocs/internal/osutil" 19 | ) 20 | 21 | // TOC represents documentation hierarchy for a specific language. 22 | type TOC struct { 23 | Language string // The language of the documentation 24 | Nodes []*Node // Directories of the documentation 25 | Pages []*Node // Individuals pages of the documentation 26 | 27 | nodes map[string]*Node // Key is the Node.Path 28 | } 29 | 30 | // Node is a node in the documentation hierarchy. 31 | type Node struct { 32 | Category string // The category (name) of the node, for directories and single pages, categories are empty 33 | Path string // The URL path 34 | LocalPath string // Full path with .md extension 35 | 36 | Content []byte // The content of the node 37 | Title string // The title of the document in the given language 38 | Headings goldmarktoc.Items // Headings in the node 39 | 40 | Nodes []*Node // The list of sub-nodes 41 | Previous *PageLink // The previous page 42 | Next *PageLink // The next page 43 | } 44 | 45 | // PageLink is a link to another page. 46 | type PageLink struct { 47 | Title string // The title of the page 48 | Path string // the path to the page 49 | } 50 | 51 | // Reload reloads and converts the content from local disk. 52 | func (n *Node) Reload(baseURLPath string) error { 53 | pathPrefix := path.Join(baseURLPath, strings.SplitN(n.Path, "/", 2)[0]) 54 | content, meta, headings, err := convertFile(pathPrefix, n.LocalPath) 55 | if err != nil { 56 | return err 57 | } 58 | n.Content = content 59 | n.Title = fmt.Sprintf("%v", meta["title"]) 60 | n.Headings = headings 61 | 62 | previous, ok := meta["previous"].(map[any]any) 63 | if ok { 64 | n.Previous = &PageLink{ 65 | Title: fmt.Sprintf("%v", previous["title"]), 66 | Path: string(convertRelativeLink(pathPrefix, []byte(fmt.Sprintf("%v", previous["path"])))), 67 | } 68 | } 69 | next, ok := meta["next"].(map[any]any) 70 | if ok { 71 | n.Next = &PageLink{ 72 | Title: fmt.Sprintf("%v", next["title"]), 73 | Path: string(convertRelativeLink(pathPrefix, []byte(fmt.Sprintf("%v", next["path"])))), 74 | } 75 | } 76 | return nil 77 | } 78 | 79 | const readme = "README" 80 | 81 | // initTocs initializes documentation hierarchy for given languages in the given 82 | // root directory. The language is the key in the returned map. 83 | func initTocs(root string, languages []string, baseURLPath string) (map[string]*TOC, error) { 84 | tocPath := filepath.Join(root, "toc.ini") 85 | tocCfg, err := ini.Load(tocPath) 86 | if err != nil { 87 | return nil, errors.Wrapf(err, "load %q", tocPath) 88 | } 89 | 90 | var tocprint bytes.Buffer 91 | tocs := make(map[string]*TOC) 92 | for i, lang := range languages { 93 | tocprint.WriteString(lang) 94 | tocprint.WriteString(":\n") 95 | 96 | toc := &TOC{ 97 | Language: lang, 98 | nodes: make(map[string]*Node), 99 | } 100 | 101 | var previous *Node 102 | setPrevious := func(n *Node) { 103 | defer func() { 104 | previous = n 105 | }() 106 | if previous == nil { 107 | return 108 | } 109 | 110 | if n.Previous == nil { 111 | n.Previous = &PageLink{ 112 | Title: previous.Title, 113 | Path: string(convertRelativeLink(baseURLPath, []byte(previous.Path))), 114 | } 115 | } 116 | if previous.Next == nil { 117 | previous.Next = &PageLink{ 118 | Title: n.Title, 119 | Path: string(convertRelativeLink(baseURLPath, []byte(n.Path))), 120 | } 121 | } 122 | } 123 | 124 | dirs := tocCfg.Section("").KeyStrings() 125 | toc.Nodes = make([]*Node, 0, len(dirs)) 126 | for _, dir := range dirs { 127 | dirname := tocCfg.Section("").Key(dir).String() 128 | files := tocCfg.Section(dirname).KeyStrings() 129 | // Skip empty directory 130 | if len(files) == 0 { 131 | continue 132 | } 133 | tocprint.WriteString(dirname) 134 | tocprint.WriteString("/\n") 135 | 136 | dirNode := &Node{ 137 | Path: dirname, 138 | Nodes: make([]*Node, 0, len(files)-1), 139 | } 140 | toc.Nodes = append(toc.Nodes, dirNode) 141 | toc.nodes[dirNode.Path] = dirNode 142 | 143 | if tocCfg.Section(dirname).HasValue(readme) { 144 | localpath := filepath.Join(root, lang, dirNode.Path, readme+".md") 145 | if i > 0 && !osutil.IsFile(localpath) { 146 | continue // It is OK to have missing file for non-default language 147 | } 148 | 149 | dirNode.LocalPath = localpath 150 | err = dirNode.Reload(baseURLPath) 151 | if err != nil { 152 | return nil, errors.Wrapf(err, "reload node from %q", dirNode.LocalPath) 153 | } 154 | 155 | if len(dirNode.Content) > 0 { 156 | setPrevious(dirNode) 157 | } 158 | } 159 | 160 | for _, file := range files { 161 | filename := tocCfg.Section(dirname).Key(file).String() 162 | if filename == readme { 163 | continue 164 | } 165 | 166 | localpath := filepath.Join(root, lang, dirname, filename) + ".md" 167 | if i > 0 && !osutil.IsFile(localpath) { 168 | continue // It is OK to have missing file for non-default language 169 | } 170 | 171 | node := &Node{ 172 | Category: dirNode.Title, 173 | Path: path.Join(dirname, filename), 174 | LocalPath: localpath, 175 | } 176 | dirNode.Nodes = append(dirNode.Nodes, node) 177 | toc.nodes[node.Path] = node 178 | 179 | err = node.Reload(baseURLPath) 180 | if err != nil { 181 | return nil, errors.Wrapf(err, "reload node from %q", node.LocalPath) 182 | } 183 | 184 | setPrevious(node) 185 | tocprint.WriteString(strings.Repeat(" ", len(dirname))) 186 | tocprint.WriteString("|__") 187 | tocprint.WriteString(filename) 188 | tocprint.WriteString("\n") 189 | } 190 | } 191 | 192 | // Single pages 193 | pages := tocCfg.Section("pages").KeysHash() 194 | toc.Pages = make([]*Node, 0, len(pages)) 195 | for _, page := range pages { 196 | tocprint.WriteString(page) 197 | tocprint.WriteString("\n") 198 | 199 | node := &Node{ 200 | Path: page, 201 | LocalPath: filepath.Join(root, lang, page) + ".md", 202 | } 203 | toc.Pages = append(toc.Pages, node) 204 | toc.nodes[node.Path] = node 205 | 206 | err = node.Reload("") 207 | if err != nil { 208 | return nil, errors.Wrapf(err, "reload node from %q", node.LocalPath) 209 | } 210 | } 211 | 212 | tocs[lang] = toc 213 | } 214 | 215 | fmt.Print(tocprint.String()) 216 | return tocs, nil 217 | } 218 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 ASoulDocs. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // ASoulDocs is a stupid web server for multilingual documentation. 6 | package main 7 | 8 | import ( 9 | "os" 10 | 11 | "github.com/urfave/cli" 12 | log "unknwon.dev/clog/v2" 13 | 14 | "github.com/asoul-sig/asouldocs/internal/cmd" 15 | "github.com/asoul-sig/asouldocs/internal/conf" 16 | ) 17 | 18 | func init() { 19 | conf.App.Version = "1.0.0+dev" 20 | } 21 | 22 | func main() { 23 | version := conf.App.Version 24 | if conf.BuildVersion != "" { 25 | version = conf.BuildVersion 26 | } 27 | if conf.BuildCommit != "" { 28 | version += "-" + conf.BuildCommit 29 | } 30 | if conf.BuildTime != "" { 31 | version += "@" + conf.BuildTime 32 | } 33 | 34 | app := cli.NewApp() 35 | app.Name = "ASoulDocs" 36 | app.Usage = "Ellien's documentation server" 37 | app.Version = version 38 | app.Commands = []cli.Command{ 39 | cmd.Web, 40 | } 41 | if err := app.Run(os.Args); err != nil { 42 | log.Fatal("Failed to start application: %v", err) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /public/css/main.css: -------------------------------------------------------------------------------- 1 | .prose :where(a):not(:where([class~="not-prose"] *)) { 2 | color: var(--tw-prose-links); 3 | text-decoration: none !important; 4 | font-weight: 600 !important; 5 | border-bottom: 1px solid #7dd3fc; 6 | } 7 | 8 | .prose :where(a:hover):not(:where([class~="not-prose"] *)) { 9 | border-bottom-width: 2px; 10 | } 11 | 12 | .max-w-8xl { 13 | max-width: 90rem; 14 | } 15 | 16 | /* Syntax highlighting */ 17 | #content-wrapper pre { 18 | padding: 15px 0; 19 | } 20 | 21 | #content-wrapper pre code > span { 22 | line-height: 25px; 23 | padding: 0 20px; 24 | } 25 | 26 | /* https://stackoverflow.com/a/24298427/1875015 */ 27 | [id]::before { 28 | content: ""; 29 | display: block; 30 | height: 75px; 31 | margin-top: -75px; 32 | visibility: hidden; 33 | } 34 | -------------------------------------------------------------------------------- /public/embed.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 ASoulDocs. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package public 6 | 7 | import ( 8 | "embed" 9 | ) 10 | 11 | //go:embed **/* 12 | var Files embed.FS 13 | -------------------------------------------------------------------------------- /public/img/asouldocs-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asoul-sig/asouldocs/031839cf765ec8e8de2c0cf5e5b10af96cb9b4c3/public/img/asouldocs-dark.png -------------------------------------------------------------------------------- /public/img/asouldocs-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asoul-sig/asouldocs/031839cf765ec8e8de2c0cf5e5b10af96cb9b4c3/public/img/asouldocs-light.png -------------------------------------------------------------------------------- /public/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asoul-sig/asouldocs/031839cf765ec8e8de2c0cf5e5b10af96cb9b4c3/public/img/favicon.png -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{template "common/head" .}} 4 | 5 |
6 |
7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | 30 |

31 | 404 32 |

33 |
34 |

35 | {{call .Tr "status::404_desc"}} 36 |

37 |
38 |
39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /templates/common/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {{if .Extension.Plausible.Enabled}} 14 | 20 | {{end}} 21 | 22 | {{if .Extension.GoogleAnalytics.Enabled}} 23 | 24 | 31 | {{end}} 32 | 33 | {{.Title}} 34 | 35 | -------------------------------------------------------------------------------- /templates/common/navbar.html: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /templates/docs/navbar.html: -------------------------------------------------------------------------------- 1 | 31 | -------------------------------------------------------------------------------- /templates/docs/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{template "common/head" .}} 4 | 5 |
6 |
7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 20 |
21 |
22 |
23 |
24 | 30 |
    31 | {{if .Node.Category}} 32 |
  1. 33 | {{.Node.Category}} 34 | 35 |
  2. 36 | {{end}} 37 |
  3. {{.Node.Title}}
  4. 38 |
39 |
40 |
41 |
42 |
43 |
44 | 61 |
62 |
63 | 97 |
98 | {{Safe .Node.Content}} 99 |
100 |
101 | {{if or .Node.Previous .Node.Next}} 102 |
103 | {{if .Node.Previous}} 104 | 105 | 106 | 107 | 108 | 109 | {{.Node.Previous.Title}} 110 | 111 | {{end}} 112 | {{if .Node.Next}} 113 | 114 | {{.Node.Next.Title}} 115 | 116 | 117 | 118 | 119 | 120 | {{end}} 121 |
122 | {{end}} 123 | 124 | {{if .Extension.Disqus.Enabled}} 125 |
126 | 138 | {{end}} 139 | 140 | {{if .Extension.Utterances.Enabled}} 141 | 149 | {{end}} 150 | 151 |
152 |
153 |

154 | © {{Year}} {{call .Tr "footer::copyright"}} 155 |

156 |
157 |
158 |
159 | 160 | 213 |
214 |
215 |
216 |
217 | 218 | 230 | 231 | 242 | 243 | 244 | -------------------------------------------------------------------------------- /templates/embed.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 ASoulDocs. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package templates 6 | 7 | import ( 8 | "embed" 9 | ) 10 | 11 | //go:embed *.html **/*.html 12 | var Files embed.FS 13 | -------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{template "common/head" .}} 4 | 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 24 |
25 | 28 |
29 |
30 |
31 |
32 |

33 | {{call .Tr "home::title"}} 34 |

35 |

36 | {{call .Tr "home::tag_line"}} 37 |

38 | 64 |
65 |
66 |
67 |
68 | 69 |
70 |
71 |

72 | 73 | 74 | 75 | {{call .Tr "home::multilingual"}} 76 |

77 |

78 | {{call .Tr "home::multilingual_desc"}} 79 |

80 |
81 |
82 |

83 | 84 | 85 | 86 | {{call .Tr "home::real_time_sync"}} 87 |

88 |

89 | {{call .Tr "home::real_time_sync_desc"}} 90 |

91 |
92 | 103 | 114 |
115 |

116 | 117 | 118 | 119 | 120 | {{call .Tr "home::customizable"}} 121 |

122 |

123 | {{call .Tr "home::customizable_desc"}} 124 |

125 |
126 |
127 |

128 | 129 | 130 | 131 | {{call .Tr "home::comment"}} 132 |

133 |

134 | {{call .Tr "home::comment_desc"}} 135 |

136 |
137 |
138 |
139 | 140 | 190 |
191 | 192 | 193 | --------------------------------------------------------------------------------