├── .github ├── dependabot.yml ├── dockerhub.png ├── logo.png ├── preview.png └── workflows │ ├── codecov.yml │ ├── release.yaml │ └── scan.yml ├── .gitignore ├── .goreleaser.yaml ├── LICENSE ├── README.md ├── README_CN.md ├── SECURITY.md ├── docker ├── Dockerfile └── Dockerfile.goreleaser ├── go.mod ├── go.sum ├── internal ├── checker │ └── checker.go ├── cmd │ └── cmd.go ├── define │ └── define.go ├── formatter │ ├── beautifier.go │ ├── beautifier.js │ ├── formatter.go │ └── formatter_test.go ├── server │ ├── assets.go │ ├── assets │ │ ├── base.css │ │ ├── base.js │ │ └── index.html │ └── server.go ├── updater │ ├── update_test.go │ └── updater.go └── version │ └── version.go └── main.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" -------------------------------------------------------------------------------- /.github/dockerhub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulteary/nginx-formatter/8d01de13b95db0c83e1cb94526a742f06b707a42/.github/dockerhub.png -------------------------------------------------------------------------------- /.github/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulteary/nginx-formatter/8d01de13b95db0c83e1cb94526a742f06b707a42/.github/logo.png -------------------------------------------------------------------------------- /.github/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soulteary/nginx-formatter/8d01de13b95db0c83e1cb94526a742f06b707a42/.github/preview.png -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Codecov 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | 7 | jobs: 8 | CodeCov: 9 | runs-on: ubuntu-latest 10 | 11 | env: 12 | GO111MODULE: on 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v3 21 | with: 22 | go-version: ${{ env.GO_VERSION }} 23 | 24 | - name: Cache Go modules 25 | uses: actions/cache@v2 26 | with: 27 | path: ~/go/pkg/mod 28 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 29 | restore-keys: | 30 | ${{ runner.os }}-go- 31 | 32 | - name: Run coverage 33 | run: go test ./... -coverprofile=coverage.out -covermode=atomic 34 | - name: Upload coverage to Codecov 35 | uses: codecov/codecov-action@v3 36 | with: 37 | token: ${{ secrets.CODECOV_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - 'main' 8 | tags: 9 | - 'v*' 10 | env: 11 | GO_VERSION: "1.20" 12 | 13 | permissions: 14 | contents: write 15 | id-token: write 16 | packages: write 17 | 18 | jobs: 19 | build: 20 | runs-on: ubuntu-latest 21 | env: 22 | GO111MODULE: on 23 | DOCKER_CLI_EXPERIMENTAL: "enabled" 24 | steps: 25 | - 26 | name: Checkout 27 | uses: actions/checkout@v3 28 | with: 29 | fetch-depth: 0 30 | - 31 | name: Set up Go 32 | uses: actions/setup-go@v3 33 | with: 34 | go-version: ${{ env.GO_VERSION }} 35 | - 36 | name: Set up QEMU 37 | uses: docker/setup-qemu-action@v1 38 | - 39 | name: Cache Go modules 40 | uses: actions/cache@v1 41 | with: 42 | path: ~/go/pkg/mod 43 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 44 | restore-keys: | 45 | ${{ runner.os }}-go- 46 | - 47 | name: Tests 48 | run: | 49 | go mod tidy 50 | go test -v ./... 51 | - name: Login to Docker Hub 52 | if: github.event_name != 'pull_request' 53 | uses: docker/login-action@v2 54 | with: 55 | username: ${{ secrets.DOCKERHUB_USERNAME }} 56 | password: ${{ secrets.DOCKERHUB_TOKEN }} 57 | - 58 | name: Run GoReleaser 59 | uses: goreleaser/goreleaser-action@v3 60 | if: success() && startsWith(github.ref, 'refs/tags/') 61 | with: 62 | version: latest 63 | args: release --clean 64 | env: 65 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/scan.yml: -------------------------------------------------------------------------------- 1 | name: "Security Scan" 2 | 3 | # Run workflow each time code is pushed to your repository and on a schedule. 4 | # The scheduled workflow runs every at 00:00 on Sunday UTC time. 5 | on: 6 | workflow_dispatch: 7 | push: 8 | branches: 9 | - main 10 | paths: 11 | - '*.go' 12 | pull_request: 13 | branches: 14 | - main 15 | schedule: 16 | - cron: '0 0 * * 0' 17 | 18 | jobs: 19 | scan: 20 | permissions: write-all 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Check out code into the Go module directory 24 | uses: actions/checkout@v2 25 | - name: Security Scan 26 | uses: securego/gosec@master 27 | with: 28 | # we let the report trigger content trigger a failure using the GitHub Security features. 29 | args: '-no-fail -fmt sarif -out results.sarif ./...' 30 | - name: Upload SARIF file 31 | uses: github/codeql-action/upload-sarif@v2 32 | with: 33 | # Path to SARIF file relative to the root of the repository 34 | sarif_file: results.sarif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | *.conf 18 | .DS_Store -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: nginx-formatter 2 | 3 | builds: 4 | - <<: &build_defaults 5 | env: 6 | - CGO_ENABLED=0 7 | ldflags: 8 | - -X "github.com/soulteary/nginx-formatter/internal/version.Version={{ .Tag }}" 9 | id: macos 10 | goos: [ darwin ] 11 | goarch: [ amd64, arm64 ] 12 | 13 | - <<: *build_defaults 14 | id: linux 15 | goos: [linux] 16 | goarch: ["386", arm, amd64, arm64] 17 | goarm: 18 | - "7" 19 | - "6" 20 | 21 | dockers: 22 | 23 | - image_templates: 24 | - "soulteary/nginx-formatter:linux-amd64-{{ .Tag }}" 25 | - "soulteary/nginx-formatter:linux-amd64" 26 | dockerfile: docker/Dockerfile.goreleaser 27 | use: buildx 28 | goarch: amd64 29 | build_flag_templates: 30 | - "--pull" 31 | - "--platform=linux/amd64" 32 | - "--label=org.opencontainers.image.title={{ .ProjectName }}" 33 | - "--label=org.opencontainers.image.description={{ .ProjectName }}" 34 | - "--label=org.opencontainers.image.url=https://github.com/soulteary/nginx-formatter" 35 | - "--label=org.opencontainers.image.source=https://github.com/soulteary/nginx-formatter" 36 | - "--label=org.opencontainers.image.version={{ .Version }}" 37 | - "--label=org.opencontainers.image.created={{ .Date }}" 38 | - "--label=org.opencontainers.image.revision={{ .FullCommit }}" 39 | - "--label=org.opencontainers.image.licenses=Apache-v2" 40 | 41 | - image_templates: 42 | - "soulteary/nginx-formatter:linux-arm64-{{ .Tag }}" 43 | - "soulteary/nginx-formatter:linux-arm64" 44 | dockerfile: docker/Dockerfile.goreleaser 45 | use: buildx 46 | goos: linux 47 | goarch: arm64 48 | goarm: '' 49 | build_flag_templates: 50 | - "--pull" 51 | - "--platform=linux/arm64" 52 | - "--label=org.opencontainers.image.title={{ .ProjectName }}" 53 | - "--label=org.opencontainers.image.description={{ .ProjectName }}" 54 | - "--label=org.opencontainers.image.url=https://github.com/soulteary/nginx-formatter" 55 | - "--label=org.opencontainers.image.source=https://github.com/soulteary/nginx-formatter" 56 | - "--label=org.opencontainers.image.version={{ .Version }}" 57 | - "--label=org.opencontainers.image.created={{ .Date }}" 58 | - "--label=org.opencontainers.image.revision={{ .FullCommit }}" 59 | - "--label=org.opencontainers.image.licenses=Apache-v2" 60 | 61 | - image_templates: 62 | - "soulteary/nginx-formatter:linux-armv7-{{ .Tag }}" 63 | - "soulteary/nginx-formatter:linux-armv7" 64 | dockerfile: docker/Dockerfile.goreleaser 65 | use: buildx 66 | goos: linux 67 | goarch: arm 68 | goarm: "7" 69 | build_flag_templates: 70 | - "--pull" 71 | - "--platform=linux/arm/v7" 72 | - "--label=org.opencontainers.image.title={{ .ProjectName }}" 73 | - "--label=org.opencontainers.image.description={{ .ProjectName }}" 74 | - "--label=org.opencontainers.image.url=https://github.com/soulteary/nginx-formatter" 75 | - "--label=org.opencontainers.image.source=https://github.com/soulteary/nginx-formatter" 76 | - "--label=org.opencontainers.image.version={{ .Version }}" 77 | - "--label=org.opencontainers.image.created={{ .Date }}" 78 | - "--label=org.opencontainers.image.revision={{ .FullCommit }}" 79 | - "--label=org.opencontainers.image.licenses=Apache-v2" 80 | 81 | - image_templates: 82 | - "soulteary/nginx-formatter:linux-armv6-{{ .Tag }}" 83 | - "soulteary/nginx-formatter:linux-armv6" 84 | dockerfile: docker/Dockerfile.goreleaser 85 | use: buildx 86 | goos: linux 87 | goarch: arm 88 | goarm: "6" 89 | build_flag_templates: 90 | - "--pull" 91 | - "--platform=linux/arm/v6" 92 | - "--label=org.opencontainers.image.title={{ .ProjectName }}" 93 | - "--label=org.opencontainers.image.description={{ .ProjectName }}" 94 | - "--label=org.opencontainers.image.url=https://github.com/soulteary/nginx-formatter" 95 | - "--label=org.opencontainers.image.source=https://github.com/soulteary/nginx-formatter" 96 | - "--label=org.opencontainers.image.version={{ .Version }}" 97 | - "--label=org.opencontainers.image.created={{ .Date }}" 98 | - "--label=org.opencontainers.image.revision={{ .FullCommit }}" 99 | - "--label=org.opencontainers.image.licenses=Apache-v2" 100 | 101 | 102 | docker_manifests: 103 | - name_template: "soulteary/nginx-formatter:{{ .Tag }}" 104 | image_templates: 105 | - "soulteary/nginx-formatter:linux-amd64-{{ .Tag }}" 106 | - "soulteary/nginx-formatter:linux-arm64-{{ .Tag }}" 107 | - "soulteary/nginx-formatter:linux-armv7-{{ .Tag }}" 108 | - "soulteary/nginx-formatter:linux-armv6-{{ .Tag }}" 109 | skip_push: "false" 110 | 111 | - name_template: "soulteary/nginx-formatter:latest" 112 | image_templates: 113 | - "soulteary/nginx-formatter:linux-amd64-{{ .Tag }}" 114 | - "soulteary/nginx-formatter:linux-arm64-{{ .Tag }}" 115 | - "soulteary/nginx-formatter:linux-armv7-{{ .Tag }}" 116 | - "soulteary/nginx-formatter:linux-armv6-{{ .Tag }}" 117 | skip_push: "false" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nginx Formatter 2 | 3 | [![CodeQL](https://github.com/soulteary/nginx-formatter/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/soulteary/nginx-formatter/actions/workflows/github-code-scanning/codeql) [![Codecov](https://github.com/soulteary/nginx-formatter/actions/workflows/codecov.yml/badge.svg)](https://github.com/soulteary/nginx-formatter/actions/workflows/codecov.yml) [![Security Scan](https://github.com/soulteary/nginx-formatter/actions/workflows/scan.yml/badge.svg)](https://github.com/soulteary/nginx-formatter/actions/workflows/scan.yml) [![Release](https://github.com/soulteary/nginx-formatter/actions/workflows/release.yaml/badge.svg)](https://github.com/soulteary/nginx-formatter/actions/workflows/release.yaml) ![Go Report Card](https://goreportcard.com/badge/github.com/soulteary/nginx-formatter) [![Docker Image](https://img.shields.io/docker/pulls/soulteary/nginx-formatter.svg)](https://hub.docker.com/r/soulteary/nginx-formatter) 4 | 5 |

6 | ENGLISH | 中文文档 7 |

8 | 9 | 10 | 11 | Nginx configuration formatter ~10MB size, support CLI, WebUI, x86, ARM, Linux, macOS. 12 | 13 | 14 | 15 | ## Download 16 | 17 | Download the binaries for your system and architecture from the [releases page](https://github.com/soulteary/nginx-formatter/releases). 18 | 19 | 20 | 21 | If you use docker, you can use the following command ([DockerHub](https://hub.docker.com/r/soulteary/nginx-formatter)): 22 | 23 | ```bash 24 | docker pull soulteary/nginx-formatter:latest 25 | docker pull soulteary/nginx-formatter:v1.1.1 26 | ``` 27 | 28 | ## Usage 29 | 30 | Use default parameters to format all configuration files in the current directory: 31 | 32 | ```bash 33 | ./nginx-formatter 34 | ``` 35 | 36 | ### Common Usage (CLI & WebUI) 37 | 38 | Use different indentation symbols (You can use spaces, tabs, ` `, `\s`, `\t`) and indentation amounts: 39 | 40 | ```bash 41 | ./nginx-formatter -indent=4 -char=" " 42 | ``` 43 | 44 | ### CLI Usage 45 | 46 | Format the configuration file in the specified directory: 47 | 48 | ```bash 49 | ./nginx-formatter -input=./your-dir-path 50 | ``` 51 | 52 | Format a file somewhere and save it in a new directory: 53 | 54 | ```bash 55 | ./nginx-formatter -input=./your-dir-path -output=./your-output-dir 56 | ``` 57 | 58 | ### WebUI Usage 59 | 60 | Start the web interface: 61 | 62 | ```bash 63 | ./nginx-formatter -web 64 | ``` 65 | 66 | specified the port: 67 | 68 | ```bash 69 | ./nginx-formatter -web -port=8123 70 | ``` 71 | 72 | ### Docker Usage 73 | 74 | There is no difference between using parameters in Docker and the above, for example, we start a Web UI formatting tool service in Docker: 75 | 76 | ```bash 77 | docker run --rm -it -p 8080:8080 soulteary/nginx-formatter:v1.1.1 -web 78 | ``` 79 | 80 | If you want to format the configuration of the current directory, you can use the program in Docker with a command similar to the following: 81 | 82 | ```bash 83 | docker run --rm -it -v `pwd`:/app soulteary/nginx-formatter:v1.1.1 -input=/app 84 | ``` 85 | 86 | ## Full parameters supported 87 | 88 | List of parameters supported: 89 | 90 | ```bash 91 | Nginx Formatter 92 | 93 | Usage of ./nginx-formatter: 94 | -char 95 | Indent char, defualt: (default " ") 96 | -indent int 97 | Indent size, defualt: 2 (default 2) 98 | -input string 99 | Input directory 100 | -output string 101 | Output directory 102 | -port 8080 103 | WebUI Port, defualt: 8080 (default 8080) 104 | -web false 105 | Enable WebUI, defualt: false 106 | ``` 107 | 108 | ## Credits 109 | 110 | Formatter Components 111 | 112 | - Slomkowski Created a beautifier for nginx config files with Python under [Apache-2.0 license], 24/06/2016 113 | - https://github.com/1connect/nginx-config-formatter (https://github.com/slomkowski/nginx-config-formatter) 114 | - Yosef Ported the JavaScript beautifier under [Apache-2.0 license], 24/08/2016 115 | - https://github.com/vasilevich/nginxbeautifier 116 | - soulteary Modify the JavaScript version for golang execution, under [Apache-2.0 license], 18/04/2023: 117 | - simplify the program, fix bugs, improve running speed, and allow running in golang 118 | - https://github.com/soulteary/nginx-formatter 119 | 120 | Runtime dependent Components 121 | 122 | - ECMAScript 5.1(+) implementation in Go, under [MIT license]. 123 | - https://github.com/dop251/goja 124 | 125 | Web Components 126 | 127 | - Gin is a HTTP web framework written in Go (Golang), under [MIT license]. 128 | - https://github.com/gin-gonic/gin 129 | - Code Mirror, in-browser code editor, under [MIT license]. 130 | - https://github.com/codemirror/codemirror5 131 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # Nginx Formatter / Nginx 格式化工具 2 | 3 | [![CodeQL](https://github.com/soulteary/nginx-formatter/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/soulteary/nginx-formatter/actions/workflows/github-code-scanning/codeql) [![Codecov](https://github.com/soulteary/nginx-formatter/actions/workflows/codecov.yml/badge.svg)](https://github.com/soulteary/nginx-formatter/actions/workflows/codecov.yml) [![Security Scan](https://github.com/soulteary/nginx-formatter/actions/workflows/scan.yml/badge.svg)](https://github.com/soulteary/nginx-formatter/actions/workflows/scan.yml) [![Release](https://github.com/soulteary/nginx-formatter/actions/workflows/release.yaml/badge.svg)](https://github.com/soulteary/nginx-formatter/actions/workflows/release.yaml) ![Go Report Card](https://goreportcard.com/badge/github.com/soulteary/nginx-formatter) [![Docker Image](https://img.shields.io/docker/pulls/soulteary/nginx-formatter.svg)](https://hub.docker.com/r/soulteary/nginx-formatter) 4 | 5 |

6 | ENGLISH | 中文文档 7 |

8 | 9 | 10 | 11 | 一款 10MB 左右的,小巧、简洁的 Nginx 格式化工具,支持命令行、WebUI、Docker、x86、ARM、macOS、Linux。 12 | 13 | 14 | 15 | ## 程序下载 16 | 17 | 从[发布页面](https://github.com/soulteary/nginx-formatter/releases)下载适用于您系统和架构的二进制文件和压缩包。 18 | 19 | 20 | 21 | 如果使用 Docker,可以使用以下命令([DockerHub](https://hub.docker.com/r/soulteary/nginx-formatter)): 22 | 23 | ```bash 24 | docker pull soulteary/nginx-formatter:latest 25 | docker pull soulteary/nginx-formatter:v1.1.1 26 | ``` 27 | 28 | ## 程序使用 29 | 30 | 使用默认参数格式化当前目录中的所有的 Nginx 配置文件: 31 | 32 | ```bash 33 | ./nginx-formatter 34 | ``` 35 | 36 | ### 通用玩法 (CLI & WebUI) 37 | 38 | 使用不同的缩进符号(可以使用空格、制表符、`\s`、`\t`、` `)和缩进量: 39 | 40 | ```bash 41 | ./nginx-formatter -indent=4 -char=" " 42 | ``` 43 | 44 | ### 命令行用法(CLI) 45 | 46 | 格式化指定目录中的配置文件: 47 | 48 | ```bash 49 | ./nginx-formatter -input=./your-dir-path 50 | ``` 51 | 52 | 在新目录中保存格式化后的配置文件: 53 | 54 | ```bash 55 | ./nginx-formatter -input=./your-dir-path -output=./your-output-dir 56 | ``` 57 | 58 | ### WebUI 用法 59 | 60 | 启动 WebUI 界面: 61 | 62 | ```bash 63 | ./nginx-formatter -web 64 | ``` 65 | 66 | 指定服务端口: 67 | 68 | ```bash 69 | ./nginx-formatter -web -port=8123 70 | ``` 71 | 72 | ### Docker 用法 73 | 74 | 在 Docker 中使用和上面没有什么区别,比如我们启动一个在 Docker 中的 Web UI 格式化工具服务: 75 | 76 | ```bash 77 | docker run --rm -it -p 8080:8080 soulteary/nginx-formatter:v1.1.1 -web 78 | ``` 79 | 80 | 81 | 如果你希望格式化当前目录的配置,可以通过类似下面的命令,来使用 Docker 中的程序: 82 | 83 | ```bash 84 | docker run --rm -it -v `pwd`:/app soulteary/nginx-formatter:v1.1.1 -input=/app 85 | ``` 86 | 87 | 88 | ## 支持的完整参数列表 89 | 90 | ```bash 91 | Nginx Formatter 92 | 93 | Usage of ./nginx-formatter: 94 | -char 95 | Indent char, defualt: (default " ") 96 | -indent int 97 | Indent size, defualt: 2 (default 2) 98 | -input string 99 | Input directory 100 | -output string 101 | Output directory 102 | -port 8080 103 | WebUI Port, defualt: 8080 (default 8080) 104 | -web false 105 | Enable WebUI, defualt: false 106 | ``` 107 | 108 | ## 鸣谢 109 | 110 | 格式化组件 111 | 112 | - 2016/06/24 Slomkowski 使用 Python 创建了一个 nginx 配置文件美化器,在 [Apache-2.0 许可] 下发布。 113 | - https://github.com/1connect/nginx-config-formatter (https://github.com/slomkowski/nginx-config-formatter) 114 | - 2016/08/24 Yosef 在 [Apache-2.0 许可] 下移植了 JavaScript beautifier。 115 | - https://github.com/vasilevich/nginxbeautifier 116 | - 2023/04/18,soulteary 根据 [Apache-2.0 许可] 简化程序,修复错误,提高运行速度,并允许在 Golang 中运行。 117 | - https://github.com/soulteary/nginx-formatter 118 | 119 | JavaScript 运行时组件: 120 | 121 | - Go 中的 ECMAScript 5.1(+) 实现,在 [MIT 许可]下发布。 122 | - https://github.com/dop251/goja 123 | 124 | 网络组件 125 | 126 | - Gin,HTTP Web 框架,在 [MIT 许可]下发布。 127 | - https://github.com/gin-gonic/gin 128 | - Code Mirror, 浏览器内的编辑器,在 [MIT 许可]下发布。 129 | - https://github.com/codemirror/codemirror5 130 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | All releases accept security reports, updates. 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | * | :white_check_mark: | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | If you find any security issues, welcome to file an issue or submit a PR directly. 14 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20.0-alpine3.16 as builder 2 | RUN echo '' > /etc/apk/repositories && \ 3 | echo "https://mirror.tuna.tsinghua.edu.cn/alpine/v3.16/main" >> /etc/apk/repositories && \ 4 | echo "https://mirror.tuna.tsinghua.edu.cn/alpine/v3.16/community" >> /etc/apk/repositories && \ 5 | echo "Asia/Shanghai" > /etc/timezone 6 | RUN apk add upx 7 | WORKDIR /build 8 | ENV CGO_ENABLED=0 9 | COPY ./go.mod . 10 | COPY ./go.sum . 11 | RUN go mod tidy && go mod download 12 | COPY ./ . 13 | RUN go build -ldflags "-w -s" 14 | RUN upx -9 -o nginx-formatter.minify nginx-formatter && mv nginx-formatter.minify nginx-formatter 15 | 16 | FROM alpine:3.16 17 | RUN echo '' > /etc/apk/repositories && \ 18 | echo "https://mirror.tuna.tsinghua.edu.cn/alpine/v3.16/main" >> /etc/apk/repositories && \ 19 | echo "https://mirror.tuna.tsinghua.edu.cn/alpine/v3.16/community" >> /etc/apk/repositories && \ 20 | echo "Asia/Shanghai" > /etc/timezone 21 | RUN apk add openssl && rm -rf /var/cache/apk/* 22 | WORKDIR / 23 | COPY --from=Builder /build/nginx-formatter /bin/nginx-formatter 24 | ENTRYPOINT ["nginx-formatter"] -------------------------------------------------------------------------------- /docker/Dockerfile.goreleaser: -------------------------------------------------------------------------------- 1 | FROM alpine:3.16 2 | RUN apk add openssl && rm -rf /var/cache/apk/* 3 | WORKDIR / 4 | COPY nginx-formatter /bin/nginx-formatter 5 | ENTRYPOINT ["nginx-formatter"] -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/soulteary/nginx-formatter 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/dop251/goja v0.0.0-20230427124612-428fc442ff5f 7 | github.com/gin-gonic/gin v1.9.0 8 | ) 9 | 10 | require ( 11 | github.com/bytedance/sonic v1.8.8 // indirect 12 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 13 | github.com/dlclark/regexp2 v1.9.0 // indirect 14 | github.com/gin-contrib/sse v0.1.0 // indirect 15 | github.com/go-playground/locales v0.14.1 // indirect 16 | github.com/go-playground/universal-translator v0.18.1 // indirect 17 | github.com/go-playground/validator/v10 v10.13.0 // indirect 18 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect 19 | github.com/goccy/go-json v0.10.2 // indirect 20 | github.com/google/pprof v0.0.0-20230502171905-255e3b9b56de // indirect 21 | github.com/json-iterator/go v1.1.12 // indirect 22 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 23 | github.com/leodido/go-urn v1.2.4 // indirect 24 | github.com/mattn/go-isatty v0.0.18 // indirect 25 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 26 | github.com/modern-go/reflect2 v1.0.2 // indirect 27 | github.com/pelletier/go-toml/v2 v2.0.7 // indirect 28 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 29 | github.com/ugorji/go/codec v1.2.11 // indirect 30 | golang.org/x/arch v0.3.0 // indirect 31 | golang.org/x/crypto v0.8.0 // indirect 32 | golang.org/x/net v0.9.0 // indirect 33 | golang.org/x/sys v0.8.0 // indirect 34 | golang.org/x/text v0.9.0 // indirect 35 | google.golang.org/protobuf v1.30.0 // indirect 36 | gopkg.in/yaml.v3 v3.0.1 // indirect 37 | ) 38 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 2 | github.com/bytedance/sonic v1.8.8 h1:Kj4AYbZSeENfyXicsYppYKO0K2YWab+i2UTSY7Ukz9Q= 3 | github.com/bytedance/sonic v1.8.8/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= 4 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 5 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= 6 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 7 | github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= 8 | github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= 9 | github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 10 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= 15 | github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 16 | github.com/dlclark/regexp2 v1.9.0 h1:pTK/l/3qYIKaRXuHnEnIf7Y5NxfRPfpb7dis6/gdlVI= 17 | github.com/dlclark/regexp2 v1.9.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 18 | github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= 19 | github.com/dop251/goja v0.0.0-20230427124612-428fc442ff5f h1:3Z9NjtffvA8Qoh8xjgUpPmyKawJw/mDRcJlR9oPCvqI= 20 | github.com/dop251/goja v0.0.0-20230427124612-428fc442ff5f/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= 21 | github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= 22 | github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= 23 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 24 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 25 | github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8= 26 | github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k= 27 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 28 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 29 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 30 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 31 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 32 | github.com/go-playground/validator/v10 v10.13.0 h1:cFRQdfaSMCOSfGCCLB20MHvuoHb/s5G8L5pu2ppK5AQ= 33 | github.com/go-playground/validator/v10 v10.13.0/go.mod h1:dwu7+CG8/CtBiJFZDz4e+5Upb6OLw04gtBYw0mcG/z4= 34 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= 35 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= 36 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 37 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 38 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 39 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 40 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 41 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 42 | github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= 43 | github.com/google/pprof v0.0.0-20230502171905-255e3b9b56de h1:6bMcLOeKoNo0+mTOb1ee3McF6CCKGixjLR3EDQY1Jik= 44 | github.com/google/pprof v0.0.0-20230502171905-255e3b9b56de/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= 45 | github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= 46 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 47 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 48 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 49 | github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= 50 | github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= 51 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 52 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 53 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 54 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 55 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 56 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 57 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 58 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 59 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= 60 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 61 | github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= 62 | github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 63 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 64 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 65 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 66 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 67 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 68 | github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us= 69 | github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= 70 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 71 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 72 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= 73 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 74 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 75 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 76 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 77 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 78 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 79 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 80 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 81 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 82 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 83 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 84 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 85 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 86 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= 87 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 88 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 89 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 90 | golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= 91 | golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 92 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 93 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 94 | golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= 95 | golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= 96 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 97 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 98 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 99 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 100 | golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= 101 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= 102 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 103 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 104 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 105 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 106 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 107 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 108 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 109 | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 110 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 111 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 112 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= 113 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 114 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 115 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 116 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 117 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 118 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 119 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 120 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 121 | golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= 122 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 123 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 124 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 125 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 126 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 127 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 128 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 129 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 130 | google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= 131 | google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 132 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 133 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 134 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 135 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 136 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 137 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 138 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 139 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 140 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 141 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 142 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 143 | -------------------------------------------------------------------------------- /internal/checker/checker.go: -------------------------------------------------------------------------------- 1 | package checker 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | ) 8 | 9 | func InDockerAndWorkDirIsRoot(src string) { 10 | if _, err := os.Stat("/.dockerenv"); err == nil && src == "/" { 11 | fmt.Println("To run using the Docker model, you need to specify a run directory other than the root directory.") 12 | fmt.Println("example:") 13 | fmt.Println(" docker run --rm -it -v `pwd`:/app soulteary/nginx-formatter -input=/app") 14 | os.Exit(0) 15 | } 16 | } 17 | 18 | func InputDirExist(src string) { 19 | if _, err := os.Stat(src); err != nil { 20 | fmt.Println("The directory you specified does not exist, please check the path parameters and try again.") 21 | fmt.Println("Input directory:", src) 22 | os.Exit(0) 23 | } 24 | } 25 | 26 | func FailToRun(err error) { 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/soulteary/nginx-formatter/internal/checker" 9 | "github.com/soulteary/nginx-formatter/internal/define" 10 | ) 11 | 12 | func InitArgv() (argvSrc string, argvDest string, argvIndent int, argvIndentChar string, argvWeb bool, argvPort int) { 13 | var inputDir string 14 | flag.StringVar(&inputDir, define.APP_ARGV_INPUT, define.DEFAULT_WORKDIR, "Input directory") 15 | var outputDir string 16 | flag.StringVar(&outputDir, define.APP_ARGV_OUTPUT, define.DEFAULT_WORKDIR, "Output directory") 17 | var indent int 18 | flag.IntVar(&indent, define.APP_ARGV_INDENT, define.DEFAULT_INDENT_SIZE, fmt.Sprintf("Indent size, defualt: %d", define.DEFAULT_INDENT_SIZE)) 19 | var indentChar string 20 | flag.StringVar(&indentChar, define.APP_ARGV_CHAR, define.DEFAULT_INDENT_CHAR, fmt.Sprintf("Indent char, defualt: `%s`", define.DEFAULT_INDENT_CHAR)) 21 | 22 | var web bool 23 | flag.BoolVar(&web, define.APP_ARGV_WEB, define.DEFAULT_WEB, fmt.Sprintf("Enable WebUI, defualt: `%v`", define.DEFAULT_WEB)) 24 | var port int 25 | flag.IntVar(&port, define.APP_ARGV_PORT, define.DEFAULT_PORT, fmt.Sprintf("WebUI Port, defualt: `%d`", define.DEFAULT_PORT)) 26 | 27 | flag.Parse() 28 | 29 | if inputDir == "" { 30 | dir, err := os.Getwd() 31 | checker.FailToRun(err) 32 | fmt.Println("No input directory specified, use the current working directory:", dir) 33 | argvSrc = dir 34 | } else { 35 | fmt.Println("Specify the working directory as:", inputDir) 36 | argvSrc = inputDir 37 | } 38 | 39 | if outputDir == "" { 40 | dir, err := os.Getwd() 41 | checker.FailToRun(err) 42 | fmt.Println("No output directory specified, use the current working directory:", dir) 43 | argvDest = dir 44 | } else { 45 | err := os.MkdirAll(outputDir, 0750) 46 | if err != nil { 47 | fmt.Println(err) 48 | os.Exit(1) 49 | } 50 | fmt.Println("Specify the output directory as:", inputDir) 51 | argvDest = outputDir 52 | } 53 | 54 | if indent <= 0 { 55 | fmt.Println("No output indent size specified, use the default value:", define.DEFAULT_INDENT_SIZE) 56 | argvIndent = define.DEFAULT_INDENT_SIZE 57 | } else { 58 | fmt.Println("Specify the indent size as:", indent) 59 | argvIndent = indent 60 | } 61 | 62 | if indentChar == "" { 63 | argvIndentChar = define.DEFAULT_INDENT_CHAR 64 | fmt.Printf("No output indent char specified, use the default value: `%s`\n", define.DISPLAY_INDENT_CHARS[define.DEFAULT_INDENT_CHAR]) 65 | } else { 66 | if !(indentChar == "\t" || indentChar == " " || indentChar == "\\s") { 67 | indentChar = define.DEFAULT_INDENT_CHAR 68 | fmt.Printf("Specify the indent char not support, use the default value: `%s`\n", define.DISPLAY_INDENT_CHARS[define.DEFAULT_INDENT_CHAR]) 69 | } 70 | argvIndentChar = indentChar 71 | display, ok := define.DISPLAY_INDENT_CHARS[indentChar] 72 | if ok { 73 | fmt.Printf("Specify the indent char as: `%s`\n", display) 74 | } else { 75 | fmt.Printf("Specify the indent char as: `%s`\n", indentChar) 76 | } 77 | } 78 | 79 | if web { 80 | argvWeb = true 81 | if port <= 1024 || port >= 65535 { 82 | fmt.Println("Please set the port above 1024 and the port within 65535") 83 | fmt.Printf("use the default value: `%d`\n", define.DEFAULT_PORT) 84 | argvPort = define.DEFAULT_PORT 85 | } else { 86 | argvPort = port 87 | fmt.Printf("Specify the indent char as: `%d`\n", port) 88 | } 89 | fmt.Printf("Enable WebUI, please visit http://localhost:%d\n", port) 90 | } else { 91 | argvWeb = false 92 | } 93 | 94 | fmt.Println() 95 | return argvSrc, argvDest, argvIndent, argvIndentChar, argvWeb, argvPort 96 | } 97 | -------------------------------------------------------------------------------- /internal/define/define.go: -------------------------------------------------------------------------------- 1 | package define 2 | 3 | // default config 4 | const ( 5 | // common config 6 | DEFAULT_INDENT_SIZE = 2 7 | DEFAULT_INDENT_CHAR = " " 8 | DEFAULT_WORKDIR = "" 9 | // web config 10 | DEFAULT_PORT = 8080 11 | DEFAULT_WEB = false 12 | ) 13 | 14 | var DISPLAY_INDENT_CHARS = map[string]string{ 15 | " ": "[SPACE]", 16 | "\\s": "[SPACE]", 17 | "\t": "[TAB]", 18 | } 19 | 20 | const ( 21 | APP_ARGV_INPUT = "input" 22 | APP_ARGV_OUTPUT = "output" 23 | APP_ARGV_INDENT = "indent" 24 | APP_ARGV_CHAR = "char" 25 | // web flags 26 | APP_ARGV_PORT = "port" 27 | APP_ARGV_WEB = "web" 28 | ) 29 | -------------------------------------------------------------------------------- /internal/formatter/beautifier.go: -------------------------------------------------------------------------------- 1 | package formatter 2 | 3 | import ( 4 | _ "embed" 5 | ) 6 | 7 | //go:embed beautifier.js 8 | var JS_FORMATTER string 9 | -------------------------------------------------------------------------------- /internal/formatter/beautifier.js: -------------------------------------------------------------------------------- 1 | /** 2 | * - Soulteary Modify the JavaScript version for golang execution, under [Apache-2.0 license], 18/04/2023: 3 | * - simplify the program, fix bugs, improve running speed, and allow running in golang 4 | * - https://github.com/soulteary/nginx-formatter 5 | * 6 | * History: 7 | * - Yosef Ported the JavaScript beautifier under [Apache-2.0 license], 24/08/2016 8 | * - https://github.com/vasilevich/nginxbeautifier 9 | * - Slomkowski Created a beautifier for nginx config files with Python under [Apache-2.0 license], 24/06/2016 10 | * - https://github.com/1connect/nginx-config-formatter (https://github.com/slomkowski/nginx-config-formatter) 11 | */ 12 | 13 | /** 14 | * Grabs text in between two seperators seperator1 thetextIwant seperator2 15 | * @param {string} input String to seperate 16 | * @param {string} seperator1 The first seperator to use 17 | * @param {string} seperator2 The second seperator to use 18 | * @return {string} 19 | */ 20 | function extractTextBySeperator(input, seperator1, seperator2) { 21 | if (seperator2 == undefined) seperator2 = seperator1; 22 | var seperator1Regex = new RegExp(seperator1); 23 | var seperator2Regex = new RegExp(seperator2); 24 | var catchRegex = new RegExp(seperator1 + "(.*?)" + seperator2); 25 | if (seperator1Regex.test(input) && seperator2Regex.test(input)) { 26 | return input.match(catchRegex)[1]; 27 | } else { 28 | return ""; 29 | } 30 | } 31 | 32 | /** 33 | * Grabs text in between two seperators seperator1 thetextIwant seperator2 34 | * @param {string} input String to seperate 35 | * @param {string} seperator1 The first seperator to use 36 | * @param {string} seperator2 The second seperator to use 37 | * @return {object} 38 | */ 39 | function extractAllPossibleText(input, seperator1, seperator2) { 40 | if (seperator2 == undefined) seperator2 = seperator1; 41 | var extracted = {}; 42 | var textInBetween; 43 | var cnt = 0; 44 | var seperator1CharCode = seperator1.length > 0 ? seperator1.charCodeAt(0) : ""; 45 | var seperator2CharCode = seperator2.length > 0 ? seperator2.charCodeAt(0) : ""; 46 | while ((textInBetween = extractTextBySeperator(input, seperator1, seperator2)) != "") { 47 | var placeHolder = "#$#%#$#placeholder" + cnt + "" + seperator1CharCode + "" + seperator2CharCode + "#$#%#$#"; 48 | extracted[placeHolder] = seperator1 + textInBetween + seperator2; 49 | input = input.replace(extracted[placeHolder], placeHolder); 50 | cnt++; 51 | } 52 | return { 53 | filteredInput: input, 54 | extracted: extracted, 55 | getRestored: function () { 56 | var textToFix = this.filteredInput; 57 | for (var key in extracted) { 58 | textToFix = textToFix.replace(key, extracted[key]); 59 | } 60 | return textToFix; 61 | }, 62 | }; 63 | } 64 | 65 | /** 66 | * @param {string} single_line the whole nginx config 67 | * @return {string} stripped out string without multi spaces 68 | */ 69 | function strip_line(single_line) { 70 | //"""Strips the line and replaces neighbouring whitespaces with single space (except when within quotation marks).""" 71 | //trim the line before and after 72 | var trimmed = single_line.trim(); 73 | //get text without any quatation marks(text foudn with quatation marks is replaced with a placeholder) 74 | var removedDoubleQuatations = extractAllPossibleText(trimmed, '"', '"'); 75 | //replace multi spaces with single spaces, but skip in sub_filter directive 76 | if (!removedDoubleQuatations.filteredInput.includes("sub_filter")) { 77 | removedDoubleQuatations.filteredInput = removedDoubleQuatations.filteredInput.replace(/\s\s+/g, " "); 78 | } 79 | //restore anything of quatation marks 80 | return removedDoubleQuatations.getRestored(); 81 | } 82 | 83 | /** 84 | * @param {string} configContents the whole nginx config 85 | */ 86 | function clean_lines(configContents) { 87 | var splittedByLines = configContents.split(/\r\n|\r|\n/g); 88 | //put { } on their own seperate lines 89 | //trim the spaces before and after each line 90 | //trim multi spaces into single spaces 91 | //trim multi lines into two 92 | 93 | for (var index = 0, newline = 0; index < splittedByLines.length; index++) { 94 | splittedByLines[index] = splittedByLines[index].trim(); 95 | if (splittedByLines[index] != "") { 96 | splittedByLines[index] = splittedByLines[index].replace(/\{\}/g, `{ }`); 97 | } 98 | 99 | if (!splittedByLines[index].startsWith("#") && splittedByLines[index] != "") { 100 | newline = 0; 101 | var line = (splittedByLines[index] = strip_line(splittedByLines[index])); 102 | if (line != "}" && line != "{" && !(line.includes("('{") || line.includes("}')") || line.includes("'{'") || line.includes("'}'"))) { 103 | var startOfComment = line.indexOf("#"); 104 | var code = startOfComment >= 0 ? line.slice(0, startOfComment) : line; 105 | 106 | var removedDoubleQuatations = extractAllPossibleText(code, '"', '"'); 107 | code = removedDoubleQuatations.filteredInput; 108 | 109 | var startOfParanthesis = code.indexOf("}"); 110 | if (startOfParanthesis >= 0) { 111 | if (startOfParanthesis > 0) { 112 | splittedByLines[index] = strip_line(code.slice(0, startOfParanthesis - 1)); 113 | splittedByLines.splice(index + 1, 0, "}"); 114 | } 115 | var l2 = strip_line(code.slice(startOfParanthesis + 1)); 116 | if (l2 != "") splittedByLines.splice(index + 2, 0, l2); 117 | code = splittedByLines[index]; 118 | } 119 | var endOfParanthesis = code.indexOf("{"); 120 | if (endOfParanthesis >= 0) { 121 | splittedByLines[index] = strip_line(code.slice(0, endOfParanthesis)); 122 | splittedByLines.splice(index + 1, 0, "{"); 123 | var l2 = strip_line(code.slice(endOfParanthesis + 1)); 124 | if (l2 != "") splittedByLines.splice(index + 2, 0, l2); 125 | } 126 | 127 | removedDoubleQuatations.filteredInput = splittedByLines[index]; 128 | line = removedDoubleQuatations.getRestored(); 129 | splittedByLines[index] = line; 130 | } 131 | } 132 | //remove more than two newlines 133 | else if (splittedByLines[index] == "") { 134 | if (newline++ >= 2) { 135 | splittedByLines.splice(index, 1); 136 | index--; 137 | } 138 | } 139 | } 140 | return splittedByLines; 141 | } 142 | 143 | function join_opening_bracket(lines) { 144 | for (var i = 0; i < lines.length; i++) { 145 | var line = lines[i]; 146 | if (line == "{") { 147 | //just make sure we don't put anything before 0 148 | if (i >= 1) { 149 | lines[i] = lines[i - 1] + " {"; 150 | lines.splice(i - 1, 1); 151 | } 152 | } 153 | } 154 | return lines; 155 | } 156 | 157 | function fold_empty_brackets(lines) { 158 | return lines 159 | .join("\n") 160 | .replace(new RegExp(`\\s+{[\\s\\n\\r]*?}`, "gm"), " { }") 161 | .replace(/\n{3,}/gm, "\n\n"); 162 | } 163 | 164 | function add_empty_line_after_nginx_directives(lines) { 165 | let clone = lines.reverse(); 166 | let output = []; 167 | for (let i = 0, j = clone.length; i < j; i++) { 168 | let current = (clone[i] + "").trim(); 169 | let next = (clone[i + 1] + "").trim(); 170 | if (next && !next.startsWith("}") && current.startsWith("}")) output.push(""); 171 | output.push(clone[i]); 172 | } 173 | return output.reverse(); 174 | } 175 | 176 | function fixDollarVar(lines) { 177 | const placeHolder = `[dollar]`; 178 | return lines.map((line) => { 179 | while (line.indexOf(placeHolder) !== -1) { 180 | line = line.replace(placeHolder, "$"); 181 | } 182 | return line; 183 | }); 184 | } 185 | 186 | var options = { INDENTATION: "\t" }; 187 | 188 | function perform_indentation(lines) { 189 | var indented_lines, current_indent, line; 190 | ("Indents the lines according to their nesting level determined by curly brackets."); 191 | indented_lines = []; 192 | current_indent = 0; 193 | var iterator1 = lines; 194 | for (var index1 = 0; index1 < iterator1.length; index1++) { 195 | line = iterator1[index1]; 196 | if (!line.startsWith("#") && /.*?\}(\s*#.*)?$/.test(line) && current_indent > 0) { 197 | current_indent -= 1; 198 | } 199 | if (line !== "") { 200 | indented_lines.push(options.INDENTATION.repeat(current_indent) + line); 201 | } else { 202 | indented_lines.push(""); 203 | } 204 | if (!line.startsWith("#") && /.*?\{(\s*#.*)?$/.test(line)) { 205 | current_indent += 1; 206 | } 207 | } 208 | return indented_lines; 209 | } 210 | 211 | function FormatNginxConf(text, indentSize = 2, indentChar = " ") { 212 | options["INDENTATION"] = indentChar.repeat(indentSize); 213 | 214 | let lines = clean_lines(text); 215 | lines = join_opening_bracket(lines); 216 | lines = perform_indentation(lines); 217 | lines = add_empty_line_after_nginx_directives(lines); 218 | lines = fixDollarVar(lines); 219 | return fold_empty_brackets(lines); 220 | } 221 | -------------------------------------------------------------------------------- /internal/formatter/formatter.go: -------------------------------------------------------------------------------- 1 | package formatter 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/dop251/goja" 7 | ) 8 | 9 | func Formatter(s string, indent int, char string) (string, error) { 10 | if s == "" { 11 | return "", nil 12 | } 13 | vm := goja.New() 14 | v, err := vm.RunString(fmt.Sprintf("%s;FormatNginxConf(`%s`, %d, `%s`)", JS_FORMATTER, s, indent, char)) 15 | if err != nil { 16 | return "", err 17 | } 18 | return v.String(), nil 19 | } 20 | -------------------------------------------------------------------------------- /internal/formatter/formatter_test.go: -------------------------------------------------------------------------------- 1 | package formatter_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/soulteary/nginx-formatter/internal/formatter" 7 | ) 8 | 9 | func TestFormatter(t *testing.T) { 10 | 11 | const TestData = ` 12 | load_module modules/ngx_http_js_module.so; 13 | 14 | events { } 15 | 16 | http { 17 | js_path "/etc/nginx/njs/"; 18 | 19 | js_import main from http/api/set_keyval.js; 20 | 21 | keyval_zone zone=foo:10m; 22 | 23 | server { 24 | listen 80; 25 | 26 | location /keyval { 27 | js_content main.set_keyval; 28 | } 29 | location /api { 30 | internal; 31 | api write=on; 32 | } 33 | location /api/ro { 34 | api; 35 | } 36 | } 37 | }` 38 | 39 | const TestExpected = ` 40 | load_module modules/ngx_http_js_module.so; 41 | 42 | events { } 43 | 44 | http { 45 | js_path "/etc/nginx/njs/"; 46 | 47 | js_import main from http/api/set_keyval.js; 48 | 49 | keyval_zone zone=foo:10m; 50 | 51 | server { 52 | listen 80; 53 | 54 | location /keyval { 55 | js_content main.set_keyval; 56 | } 57 | 58 | location /api { 59 | internal; 60 | api write=on; 61 | } 62 | 63 | location /api/ro { 64 | api; 65 | } 66 | 67 | } 68 | }` 69 | 70 | result, err := formatter.Formatter(TestData, 4, " ") 71 | if err != nil { 72 | t.Errorf("formatter error: %v\n", err) 73 | } 74 | 75 | if result != TestExpected { 76 | t.Error("formatter result not expected", result, TestExpected) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /internal/server/assets.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | _ "embed" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | //go:embed assets/base.css 10 | var PAGE_STYLESTHEET string 11 | var CACHE_STYLESHEET = []byte(PAGE_STYLESTHEET) 12 | 13 | //go:embed assets/base.js 14 | var PAGE_SCRIPT string 15 | var CACHE_SCRIPT = []byte(PAGE_SCRIPT) 16 | 17 | //go:embed assets/index.html 18 | var PAGE_DOCUMENT string 19 | var CACHE_DOCUMENT string 20 | 21 | var REGEXP_UPDATE_CODE = regexp.MustCompile(`(?m)`) 37 | } 38 | -------------------------------------------------------------------------------- /internal/server/assets/base.css: -------------------------------------------------------------------------------- 1 | .CodeMirror{font-family:monospace;height:300px;color:#000;direction:ltr}.CodeMirror-lines{padding:4px 0}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{padding:0 4px}.CodeMirror-scrollbar-filler,.CodeMirror-gutter-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid #000;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0!important;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-fat-cursor .CodeMirror-line::selection,.cm-fat-cursor .CodeMirror-line>span::selection,.cm-fat-cursor .CodeMirror-line>span>span::selection{background:transparent}.cm-fat-cursor .CodeMirror-line::-moz-selection,.cm-fat-cursor .CodeMirror-line>span::-moz-selection,.cm-fat-cursor .CodeMirror-line>span>span::-moz-selection{background:transparent}.cm-fat-cursor{caret-color:transparent}@-moz-keyframes blink{0%{}50%{background-color:transparent}}@-webkit-keyframes blink{0%{}50%{background-color:transparent}}@keyframes blink{0%{}50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-rulers{position:absolute;left:0;right:0;top:-50px;bottom:0;overflow:hidden}.CodeMirror-ruler{border-left:1px solid #ccc;top:0;bottom:0;position:absolute}.cm-s-default .cm-header{color:blue}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-variable-3,.cm-s-default .cm-type{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta{color:#555}.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-s-default .cm-error{color:red}.cm-invalidchar{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0b0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#a22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-50px;margin-right:-50px;padding-bottom:50px;height:100%;outline:none;position:relative;z-index:0}.CodeMirror-sizer{position:relative;border-right:50px solid transparent}.CodeMirror-vscrollbar,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-gutter-filler{position:absolute;z-index:6;display:none;outline:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-50px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:none!important;border:none!important}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-gutter-wrapper ::selection{background-color:transparent}.CodeMirror-gutter-wrapper ::-moz-selection{background-color:transparent}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:transparent;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:contextual;font-variant-ligatures:contextual}.CodeMirror-wrap pre.CodeMirror-line,.CodeMirror-wrap pre.CodeMirror-line-like{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;padding:.1px}.CodeMirror-rtl pre{direction:rtl}.CodeMirror-code{outline:none}.CodeMirror-scroll,.CodeMirror-sizer,.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute;pointer-events:none}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-focused div.CodeMirror-cursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background-color:#ffa;background-color:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:''}span.CodeMirror-selectedtext{background:none} 2 | .cm-s-material-darker.CodeMirror{background-color:#212121;color:#EFF}.cm-s-material-darker .CodeMirror-gutters{background:#212121;color:#545454;border:none}.cm-s-material-darker .CodeMirror-guttermarker,.cm-s-material-darker .CodeMirror-guttermarker-subtle,.cm-s-material-darker .CodeMirror-linenumber{color:#545454}.cm-s-material-darker .CodeMirror-cursor{border-left:1px solid #FC0}.cm-s-material-darker div.CodeMirror-selected{background:rgba(97,97,97,.2)}.cm-s-material-darker.CodeMirror-focused div.CodeMirror-selected{background:rgba(97,97,97,.2)}.cm-s-material-darker .CodeMirror-line::selection,.cm-s-material-darker .CodeMirror-line>span::selection,.cm-s-material-darker .CodeMirror-line>span>span::selection{background:rgba(128,203,196,.2)}.cm-s-material-darker .CodeMirror-line::-moz-selection,.cm-s-material-darker .CodeMirror-line>span::-moz-selection,.cm-s-material-darker .CodeMirror-line>span>span::-moz-selection{background:rgba(128,203,196,.2)}.cm-s-material-darker .CodeMirror-activeline-background{background:rgba(0,0,0,.5)}.cm-s-material-darker .cm-keyword{color:#C792EA}.cm-s-material-darker .cm-operator{color:#89DDFF}.cm-s-material-darker .cm-variable-2{color:#EFF}.cm-s-material-darker .cm-variable-3,.cm-s-material-darker .cm-type{color:#f07178}.cm-s-material-darker .cm-builtin{color:#FFCB6B}.cm-s-material-darker .cm-atom{color:#F78C6C}.cm-s-material-darker .cm-number{color:#FF5370}.cm-s-material-darker .cm-def{color:#82AAFF}.cm-s-material-darker .cm-string{color:#C3E88D}.cm-s-material-darker .cm-string-2{color:#f07178}.cm-s-material-darker .cm-comment{color:#545454}.cm-s-material-darker .cm-variable{color:#f07178}.cm-s-material-darker .cm-tag{color:#FF5370}.cm-s-material-darker .cm-meta{color:#FFCB6B}.cm-s-material-darker .cm-attribute{color:#C792EA}.cm-s-material-darker .cm-property{color:#C792EA}.cm-s-material-darker .cm-qualifier{color:#DECB6B}.cm-s-material-darker .cm-variable-3,.cm-s-material-darker .cm-type{color:#DECB6B}.cm-s-material-darker .cm-error{color:rgba(255,255,255,1);background-color:#FF5370}.cm-s-material-darker .CodeMirror-matchingbracket{text-decoration:underline;color:white!important} -------------------------------------------------------------------------------- /internal/server/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Nginx Formatter 9 | 10 | 11 | 19 | 79 | 80 | 81 | 82 |
83 |

Nginx Formatter

84 | 116 | 117 |
118 | 121 |
122 |
123 | 124 | 133 | 134 | -------------------------------------------------------------------------------- /internal/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func Launch(port int, indent int, char string, fn func(s string, indent int, char string) (string, error)) error { 11 | gin.SetMode(gin.ReleaseMode) 12 | 13 | router := gin.Default() 14 | router.GET("/", func(c *gin.Context) { 15 | c.Data(http.StatusOK, "text/html", []byte(getDocCache())) 16 | }) 17 | 18 | router.POST("/format", func(c *gin.Context) { 19 | code := c.PostForm("code") 20 | updateDocCache(code, indent, char, fn) 21 | c.Redirect(http.StatusFound, "/") 22 | }) 23 | 24 | router.GET("/base.css", func(c *gin.Context) { 25 | c.Data(http.StatusOK, "text/css", CACHE_STYLESHEET) 26 | }) 27 | 28 | router.GET("/base.js", func(c *gin.Context) { 29 | c.Data(http.StatusOK, "text/javascript", CACHE_SCRIPT) 30 | }) 31 | 32 | return router.Run(fmt.Sprintf(":%d", port)) 33 | } 34 | -------------------------------------------------------------------------------- /internal/updater/update_test.go: -------------------------------------------------------------------------------- 1 | package updater_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/soulteary/nginx-formatter/internal/updater" 7 | ) 8 | 9 | func TestEncodeEscapeChars(t *testing.T) { 10 | testCases := []struct { 11 | input string 12 | expected string 13 | }{ 14 | { 15 | input: "", 16 | expected: "", 17 | }, 18 | { 19 | input: `Hello\tworld`, 20 | expected: `Hello【&】tworld`, 21 | }, 22 | { 23 | input: `This is a test\nwith new line`, 24 | expected: `This is a test【&】nwith new line`, 25 | }, 26 | { 27 | input: `Some\rtext`, 28 | expected: `Some【&】rtext`, 29 | }, 30 | { 31 | input: `\t\s\r\n`, 32 | expected: `【&】t【&】s【&】r【&】n`, 33 | }, 34 | } 35 | 36 | for _, tc := range testCases { 37 | output := updater.EncodeEscapeChars(tc.input) 38 | if output != tc.expected { 39 | t.Errorf("Unexpected output. Input: %s, Expected: %s, Output: %s", tc.input, tc.expected, output) 40 | } 41 | } 42 | } 43 | 44 | func TestDecodeEscapeChars(t *testing.T) { 45 | testCases := []struct { 46 | input string 47 | expected string 48 | }{ 49 | { 50 | input: "", 51 | expected: "", 52 | }, 53 | { 54 | input: `Hello【&】tworld`, 55 | expected: `Hello\tworld`, 56 | }, 57 | { 58 | input: `This is a test【&】nwith new line`, 59 | expected: `This is a test\nwith new line`, 60 | }, 61 | { 62 | input: `Some【&】rtext`, 63 | expected: `Some\rtext`, 64 | }, 65 | { 66 | input: `【&】t【&】r【&】n【&】s`, 67 | expected: `\t\r\n\s`, 68 | }, 69 | } 70 | 71 | for _, tc := range testCases { 72 | output := updater.DecodeEscapeChars(tc.input) 73 | if output != tc.expected { 74 | t.Errorf("Unexpected output. Input: %s, Expected: %s, Output: %s", tc.input, tc.expected, output) 75 | } 76 | } 77 | } 78 | 79 | func TestFixReturn(t *testing.T) { 80 | testCases := []struct { 81 | input string 82 | expected string 83 | }{ 84 | { // empty 85 | input: "", 86 | expected: "", 87 | }, 88 | { // add quotes to strings 89 | input: "return 200 ok;", 90 | expected: "return 200 \"ok\";", 91 | }, 92 | { // add quotes to strings 93 | input: "return 200 $content;", 94 | expected: "return 200 \"$content\";", 95 | }, 96 | { // keep it as it is 97 | input: "return BACKEND\n;", 98 | expected: "return BACKEND;", 99 | }, 100 | { // keep it as it is 101 | input: "return 200 \"BACKEND B:$uri\n\";", 102 | expected: "return 200 \"BACKEND B:$uri\n\";", 103 | }, 104 | { // keep it as it is 105 | input: "return 200;", 106 | expected: "return 200;", 107 | }, 108 | { // trim 109 | input: "return 200 ;", 110 | expected: "return 200;", 111 | }, 112 | { // trim 113 | input: "return \"ok\" ; ", 114 | expected: "return \"ok\";", 115 | }, 116 | { // trim 117 | input: "return 200 \"ok\" ; ", 118 | expected: "return 200 \"ok\";", 119 | }, 120 | { 121 | input: "return 200 \"1 1 1\" ;", 122 | expected: "return 200 \"1 1 1\";", 123 | }, 124 | { 125 | input: "return \"1\n\" ;", 126 | expected: "return \"1\n\";", 127 | }, 128 | { 129 | input: "return \"1\n 11\" ;", 130 | expected: "return \"1\n 11\";", 131 | }, 132 | { 133 | input: "return \"1\n\t\r 11\n\" ;", 134 | expected: "return \"1\n\t\r 11\n\";", 135 | }, 136 | } 137 | 138 | for _, tc := range testCases { 139 | output := updater.FixReturn(updater.FixVars(updater.EncodeEscapeChars(tc.input))) 140 | if output != tc.expected { 141 | t.Errorf("Unexpected output. Input: %s, Expected: %s, Output: %s", tc.input, tc.expected, (updater.DecodeEscapeChars(output))) 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /internal/updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "regexp" 8 | "strings" 9 | ) 10 | 11 | func EncodeEscapeChars(s string) string { 12 | return strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(s, `\t`, `【&】t`), `\s`, `【&】s`), `\r`, `【&】r`), `\n`, `【&】n`) 13 | } 14 | 15 | func DecodeEscapeChars(s string) string { 16 | return strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(s, `【&】t`, `\t`), `【&】s`, `\s`), `【&】r`, `\r`), `【&】n`, `\n`) 17 | } 18 | 19 | func FixReturn(s string) string { 20 | 21 | var scene1 = regexp.MustCompile(`return\s+(\d+)\s+(\S+)\s*;`) 22 | var scene2 = regexp.MustCompile(`return\s+(\d+)\s+"(\S+)"\s*;`) 23 | var scene3 = regexp.MustCompile(`return\s+(\S+)\s*;`) 24 | var scene4 = regexp.MustCompile(`return\s+"(\S+)"\s*;`) 25 | var scene5 = regexp.MustCompile(`return\s+(\d+)\s*;`) 26 | var scene6 = regexp.MustCompile(`return\s+"(.+)"\s*;`) 27 | var scene7 = regexp.MustCompile(`return\s+(\d+)\s+"(.+)"\s*;`) 28 | var scene8 = regexp.MustCompile(`return\s+(\d+)\s+"([\s|\S|\n|\r|\t]+)"\s*;`) 29 | var scene9 = regexp.MustCompile(`return\s+"([\s|\S|\n|\r|\t]+)"\s*;`) 30 | 31 | if scene1.MatchString(s) { 32 | if scene2.MatchString(s) { // eg: `return 200 "ok";` 33 | return strings.TrimSpace(scene2.ReplaceAllString(s, "return $1 \"$2\";")) 34 | } else { // eg: `return 200 $content;` 35 | return strings.TrimSpace(scene1.ReplaceAllString(s, "return $1 \"$2\";")) 36 | } 37 | } else if scene3.MatchString(s) { 38 | if scene5.MatchString(s) { // eg: `return 200;` 39 | return strings.TrimSpace(scene5.ReplaceAllString(s, "return $1;")) 40 | } else if scene6.MatchString(s) { // eg: `return "ok";` 41 | if scene4.MatchString(s) { 42 | return strings.TrimSpace(scene4.ReplaceAllString(s, "return \"$1\";")) 43 | } else { 44 | return strings.TrimSpace(scene6.ReplaceAllString(s, "return \"$1\";")) 45 | } 46 | } else { // eg: `return BACKEND\n;` 47 | found := scene3.FindString(s) 48 | if !(strings.HasPrefix(found, `"`) && strings.HasSuffix(found, `"`)) { 49 | return strings.TrimSpace(scene3.ReplaceAllString(s, "return $1;")) 50 | } else { 51 | return strings.TrimSpace(scene3.ReplaceAllString(s, "return \"$1\";")) 52 | } 53 | } 54 | } else { 55 | if scene7.MatchString(s) { 56 | return strings.TrimSpace(scene7.ReplaceAllString(s, "return $1 \"$2\";")) 57 | } else if scene8.MatchString(s) { 58 | return strings.TrimSpace(scene8.ReplaceAllString(s, "return $1 \"$2\";")) 59 | } else if scene9.MatchString(s) { 60 | return strings.TrimSpace(scene9.ReplaceAllString(s, "return \"$1\";")) 61 | } 62 | } 63 | return s 64 | } 65 | 66 | func FixVars(s string) string { 67 | s = regexp.MustCompile(`(\$)(\{\S+?\})`).ReplaceAllString(s, "[dollar]$2") 68 | return s 69 | } 70 | 71 | func ScanFiles(rootDir string) ([]string, error) { 72 | if rootDir == "" { 73 | return nil, fmt.Errorf("scandir is empty") 74 | } 75 | var files []string 76 | err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { 77 | if info.IsDir() { 78 | return nil 79 | } 80 | 81 | file := filepath.Clean(path) 82 | if strings.HasPrefix(file, rootDir) && strings.HasSuffix(file, ".conf") { 83 | _, err := os.ReadFile(path) 84 | if err != nil { 85 | return err 86 | } 87 | files = append(files, path) 88 | } 89 | return nil 90 | }) 91 | if err != nil { 92 | return nil, err 93 | } 94 | return files, nil 95 | } 96 | 97 | func UpdateConfInDir(rootDir string, outputDir string, indent int, indentChar string, fn func(s string, indent int, char string) (string, error)) error { 98 | files, err := ScanFiles(rootDir) 99 | if err != nil { 100 | return err 101 | } 102 | for _, src := range files { 103 | file := filepath.Clean(src) 104 | if !strings.HasPrefix(file, rootDir) { 105 | continue 106 | } 107 | 108 | buf, err := os.ReadFile(file) 109 | if err != nil { 110 | fmt.Printf("Formatter Nginx Conf %s failed, can not open the file\n", err) 111 | return err 112 | } 113 | 114 | modifiedData, err := fn(FixVars(FixReturn(EncodeEscapeChars(string(buf)))), indent, indentChar) 115 | if err != nil { 116 | fmt.Printf("Formatter Nginx Conf %s failed, can not format the file\n", err) 117 | return err 118 | } 119 | 120 | output := "" 121 | relPath, err := filepath.Rel(rootDir, file) 122 | if err != nil { 123 | output = filepath.Join(outputDir, file) 124 | } else { 125 | output = filepath.Join(outputDir, relPath) 126 | } 127 | 128 | err = os.MkdirAll(filepath.Dir(output), 0700) 129 | if err != nil { 130 | fmt.Printf("Formatter Nginx Conf %s failed, can not prepare the save dir\n", err) 131 | return err 132 | } 133 | 134 | err = os.WriteFile(output, []byte(DecodeEscapeChars(modifiedData)), 0600) 135 | if err != nil { 136 | fmt.Printf("Formatter Nginx Conf %s failed, can not save the file\n", err) 137 | return err 138 | } 139 | 140 | relPath, err = filepath.Rel(rootDir, output) 141 | if err != nil { 142 | fmt.Printf("Formatter Nginx Conf %s Successed\n", output) 143 | } else { 144 | fmt.Printf("Formatter Nginx Conf %s Successed\n", relPath) 145 | } 146 | } 147 | return nil 148 | } 149 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | var Version = "Dev" 4 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/soulteary/nginx-formatter/internal/checker" 7 | "github.com/soulteary/nginx-formatter/internal/cmd" 8 | "github.com/soulteary/nginx-formatter/internal/formatter" 9 | "github.com/soulteary/nginx-formatter/internal/server" 10 | "github.com/soulteary/nginx-formatter/internal/updater" 11 | "github.com/soulteary/nginx-formatter/internal/version" 12 | ) 13 | 14 | func main() { 15 | fmt.Printf("Nginx Formatter %s\n\n", version.Version) 16 | 17 | src, dest, indent, char, web, port := cmd.InitArgv() 18 | if web { 19 | err := server.Launch(port, indent, char, formatter.Formatter) 20 | checker.FailToRun(err) 21 | } else { 22 | checker.InDockerAndWorkDirIsRoot(src) 23 | checker.InputDirExist(src) 24 | 25 | err := updater.UpdateConfInDir(src, dest, indent, char, formatter.Formatter) 26 | checker.FailToRun(err) 27 | } 28 | } 29 | --------------------------------------------------------------------------------