├── .editorconfig ├── .github └── workflows │ ├── codeql-analysis.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .goreleaser.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── apidoc.go ├── apidoc_test.go ├── build ├── build.go ├── build_test.go ├── config.go ├── config_test.go ├── detect.go ├── detect_test.go ├── input.go ├── input_test.go ├── output.go ├── output_test.go ├── path.go ├── path_test.go ├── path_windows_test.go └── testdata │ ├── failed.yaml │ ├── gbk.php │ ├── no-extension │ ├── testdir1 │ ├── testfile.1 │ └── testfile.2 │ ├── testdir2 │ └── testfile.1 │ ├── testfile.1 │ ├── testfile.c │ └── testfile.h ├── cmd └── apidoc │ ├── .gitignore │ └── main.go ├── core ├── core.go ├── core_test.go ├── errors.go ├── errors_test.go ├── gbk.php ├── message.go ├── message_test.go ├── messagetest │ ├── messagetest.go │ └── messagetest_test.go ├── uri.go ├── uri_test.go ├── version.go └── version_test.go ├── docs ├── CNAME ├── README.md ├── docs.go ├── example │ ├── .apidoc.yaml │ ├── .gitignore │ ├── apis.cpp │ ├── apis.rs │ ├── doc.cpp │ └── index.xml ├── icon.svg ├── index.cmn-Hant.xml ├── index.css ├── index.js ├── index.xml ├── index.xsl ├── locale.cmn-Hans.xml ├── locale.cmn-Hant.xml ├── site.xml ├── v5 │ ├── apidoc.css │ ├── apidoc.js │ ├── apidoc.xsl │ ├── locales.xsl │ └── view.html └── v6 │ ├── apidoc.css │ ├── apidoc.js │ ├── apidoc.xsl │ ├── locales.xsl │ └── view.html ├── go.mod ├── go.sum ├── internal ├── ast │ ├── ast.go │ ├── ast_test.go │ ├── asttest │ │ ├── asttest.go │ │ ├── asttest_test.go │ │ ├── gen.go │ │ ├── index.xml │ │ └── make.go │ ├── attributes.go │ ├── attributes_test.go │ ├── elements.go │ ├── elements_test.go │ ├── parse.go │ ├── parse_test.go │ ├── sanitize.go │ ├── sanitize_test.go │ ├── search.go │ ├── search_test.go │ └── testdata │ │ ├── all.xml │ │ ├── api.xml │ │ └── doc.xml ├── cmd │ ├── build.go │ ├── cmd.go │ ├── cmd_test.go │ ├── detect.go │ ├── detect_test.go │ ├── lang.go │ ├── lang_test.go │ ├── locale.go │ ├── locale_test.go │ ├── lsp.go │ ├── mock.go │ ├── mock_test.go │ ├── static.go │ ├── syntax.go │ ├── syntax_test.go │ ├── version.go │ └── version_test.go ├── docs │ ├── docs.go │ ├── docs_test.go │ ├── gen.go │ ├── make_site.go │ └── site │ │ ├── command.go │ │ ├── config.go │ │ ├── config_test.go │ │ ├── site.go │ │ ├── site_test.go │ │ ├── spec.go │ │ └── spec_test.go ├── lang │ ├── block.go │ ├── block_test.go │ ├── lang.go │ ├── lang_test.go │ ├── nim.go │ ├── nim_test.go │ ├── parse.go │ ├── parse_test.go │ ├── pascal.go │ ├── pascal_test.go │ ├── php.go │ ├── php_test.go │ ├── ruby.go │ ├── ruby_test.go │ ├── swift.go │ ├── swift_test.go │ ├── testdata │ │ ├── c# │ │ │ └── test.cs │ │ ├── c++ │ │ │ └── test.cpp │ │ ├── d │ │ │ └── test.d │ │ ├── dart │ │ │ └── test.dart │ │ ├── erlang │ │ │ └── test.erl │ │ ├── go │ │ │ └── test.go │ │ ├── groovy │ │ │ └── test.groovy │ │ ├── java │ │ │ └── test.java │ │ ├── javascript │ │ │ └── test.js │ │ ├── julia │ │ │ └── test.jl │ │ ├── kotlin │ │ │ └── test.kt │ │ ├── lisp │ │ │ └── test.clj │ │ ├── lua │ │ │ └── test.lua │ │ ├── nim │ │ │ └── test.nim │ │ ├── pascal │ │ │ └── test.pas │ │ ├── perl │ │ │ └── test.perl │ │ ├── php │ │ │ └── test.php │ │ ├── python │ │ │ └── test.py │ │ ├── ruby │ │ │ └── test.rb │ │ ├── rust │ │ │ └── test.rs │ │ ├── scala │ │ │ └── test.scala │ │ ├── swift │ │ │ └── test.swift │ │ ├── typescript │ │ │ └── test.ts │ │ └── zig │ │ │ └── test.zig │ └── testdata_test.go ├── lexer │ ├── lexer.go │ ├── lexer_test.go │ ├── position.go │ └── position_test.go ├── locale │ ├── locale.go │ ├── locale_test.go │ ├── message.go │ ├── message_cmn-Hans.go │ └── message_cmn-Hant.go ├── lsp │ ├── apidoc.go │ ├── errors.go │ ├── folder.go │ ├── folder_test.go │ ├── hover.go │ ├── hover_test.go │ ├── initialize.go │ ├── initialize_test.go │ ├── lsp.go │ ├── lsp_test.go │ ├── protocol │ │ ├── apidoc.go │ │ ├── apidoc_test.go │ │ ├── completion.go │ │ ├── completion_test.go │ │ ├── diagnostic.go │ │ ├── diagnostic_test.go │ │ ├── folding.go │ │ ├── folding_test.go │ │ ├── hover.go │ │ ├── hover_test.go │ │ ├── initialize.go │ │ ├── initialize_test.go │ │ ├── message.go │ │ ├── message_test.go │ │ ├── protocol.go │ │ ├── reference.go │ │ ├── semantic.go │ │ ├── server.go │ │ ├── textdocument.go │ │ ├── textdocument_test.go │ │ ├── workspace.go │ │ └── workspace_test.go │ ├── reference.go │ ├── reference_test.go │ ├── semantic.go │ ├── semantic_test.go │ ├── server.go │ ├── server_test.go │ ├── textdocument.go │ ├── textdocument_test.go │ ├── workspace.go │ └── workspace_test.go ├── mock │ ├── api.go │ ├── api_test.go │ ├── json.go │ ├── json_test.go │ ├── mock.go │ ├── mock_test.go │ ├── options.go │ ├── options_test.go │ ├── xml.go │ └── xml_test.go ├── node │ ├── node.go │ ├── node_test.go │ ├── value.go │ └── value_test.go ├── openapi │ ├── errors.go │ ├── info.go │ ├── info_test.go │ ├── openapi.go │ ├── openapi_test.go │ ├── parameter.go │ ├── parameter_test.go │ ├── parse.go │ ├── parse_test.go │ ├── path.go │ ├── schema.go │ ├── schema_test.go │ ├── security.go │ ├── server.go │ ├── server_test.go │ ├── style.go │ └── style_test.go └── xmlenc │ ├── decode.go │ ├── decode_test.go │ ├── encode.go │ ├── encode_test.go │ ├── parser.go │ ├── parser_test.go │ ├── token.go │ ├── token_test.go │ ├── xmlenc.go │ └── xmlenc_test.go ├── mock.go ├── mock_test.go └── scripts ├── build-env.sh ├── build.cmd ├── build.sh └── install.sh /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | charset = utf-8 11 | 12 | # html 13 | [*.{htm,html,js,css}] 14 | indent_style = space 15 | indent_size = 4 16 | 17 | # yaml 18 | [*.{yaml,yml}] 19 | indent_style = space 20 | indent_size = 4 21 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.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 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 5 * * 6' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['go'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v2 34 | with: 35 | # We must fetch at least the immediate parents so that if this is 36 | # a pull request then we can checkout the head. 37 | fetch-depth: 2 38 | 39 | # Initializes the CodeQL tools for scanning. 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v2 42 | with: 43 | languages: ${{ matrix.language }} 44 | # If you wish to specify custom queries, you can do so here or in a config file. 45 | # By default, queries listed here will override any specified in a config file. 46 | # Prefix the list here with "+" to use these queries and those in the config file. 47 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 48 | 49 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 50 | # If this step fails, then you should remove it and run the build manually (see below) 51 | - name: Autobuild 52 | uses: github/codeql-action/autobuild@v2 53 | 54 | # ℹ️ Command-line programs to run using the OS shell. 55 | # 📚 https://git.io/JvXDl 56 | 57 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 58 | # and modify them (or add more) to build your code if your project 59 | # uses a compiled language 60 | 61 | #- run: | 62 | # make bootstrap 63 | # make release 64 | 65 | - name: Perform CodeQL Analysis 66 | uses: github/codeql-action/analyze@v2 67 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | create: 4 | tags: 5 | - 'v*' 6 | - '!v*-alpha' 7 | - '!v*-beta' 8 | 9 | jobs: 10 | 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | 17 | - name: setup Go 18 | uses: actions/setup-go@v2 19 | with: 20 | go-version: 1.19.x 21 | id: go 22 | 23 | - name: Check out code into the Go module directory 24 | uses: actions/checkout@v2 25 | 26 | - name: Login to Docker Hub 27 | uses: docker/login-action@v1 28 | with: 29 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 30 | password: ${{ secrets.DOCKER_HUB_TOKEN }} 31 | 32 | - name: Login to Github Container Registry 33 | uses: docker/login-action@v1 34 | with: 35 | registry: ghcr.io 36 | username: ${{ github.repository_owner }} 37 | password: ${{ secrets.GHCR_TOKEN }} 38 | 39 | - name: release 40 | uses: goreleaser/goreleaser-action@v2 41 | with: 42 | version: latest 43 | args: release 44 | env: 45 | # 如果需要操作其它仓库,比如将 brew 写入其它仓库中, 46 | # 则不能使用默认的 GITHUB_TOKEN,需要自行创建。 47 | GITHUB_TOKEN: ${{ secrets.HOMEBREW }} 48 | 49 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | 6 | test: 7 | name: Test 8 | runs-on: ${{ matrix.os }} 9 | 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest, macOS-latest, windows-latest] 13 | go: ['1.18.x', '1.19.x'] 14 | 15 | steps: 16 | 17 | - name: Set git to use LF 18 | run: | 19 | git config --global core.autocrlf false 20 | git config --global core.eol lf 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v2 24 | 25 | - name: Set up Go ${{ matrix.go }} 26 | uses: actions/setup-go@v2 27 | with: 28 | stable: '!contains(${{ matrix.go }}, "beta") && !contains(${{ matrix.go }}, "rc")' 29 | go-version: ${{ matrix.go }} 30 | id: go 31 | 32 | - name: generate 33 | run: go generate -v ./... 34 | 35 | - name: Vet 36 | run: go vet -v ./... 37 | 38 | - name: Test 39 | env: 40 | LANG: en # 必须指定语言,否则 internal/locale 测试不通过 41 | run: go test -v -coverprofile='coverage.txt' -covermode=atomic ./... 42 | 43 | - name: Upload Coverage report 44 | uses: codecov/codecov-action@v1 45 | with: 46 | token: ${{secrets.CODECOV_TOKEN}} 47 | file: ./coverage.txt 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | *.exe 7 | *.test 8 | *.prof 9 | 10 | # vim 11 | *.swp 12 | 13 | # macOS 14 | .DS_Store 15 | 16 | # goreleaser 17 | dist 18 | 19 | # project 20 | .apidoc.yaml 21 | .apidoc.yml 22 | apidoc.json 23 | apidoc.yaml 24 | apidoc.xml 25 | openapi.json 26 | openapi.yaml 27 | 28 | .vscode 29 | .idea 30 | 31 | .testdata 32 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: apidoc 2 | 3 | release: 4 | github: 5 | owner: caixw 6 | name: apidoc 7 | name_template: '{{.Tag}}' 8 | 9 | builds: 10 | - id: apidoc 11 | 12 | goos: 13 | - darwin 14 | - linux 15 | - windows 16 | 17 | goarch: 18 | - amd64 19 | - arm64 20 | 21 | ignore: 22 | - goos: windows 23 | goarch: arm64 24 | 25 | main: ./cmd/apidoc 26 | 27 | binary: apidoc 28 | 29 | flags: 30 | - -trimpath 31 | 32 | ldflags: 33 | - -s -w -X github.com/caixw/apidoc/v7/core.metadata={{time "20060102"}}.{{.Commit}} -X github.com/caixw/apidoc/v7/core.mainVersion={{.Tag}} 34 | 35 | env: 36 | - CGO_ENABLED=0 37 | 38 | brews: 39 | - tap: 40 | owner: caixw 41 | name: homebrew-brew 42 | url_template: "https://github.com/caixw/apidoc/releases/download/{{ .Tag }}/{{ .ArtifactName }}" 43 | 44 | commit_author: 45 | name: goreleaserbot 46 | email: goreleaser@carlosbecker.com 47 | folder: Formula 48 | homepage: "https://apidoc.tools" 49 | description: RESTful API 文档生成工具 50 | license: MIT 51 | 52 | 53 | dockers: 54 | - 55 | ids: ['apidoc'] 56 | goos: linux 57 | goarch: amd64 58 | dockerfile: Dockerfile 59 | 60 | image_templates: 61 | - "docker.io/caixw/apidoc:{{ .Tag }}" 62 | - "docker.io/caixw/apidoc:v{{ .Major }}" 63 | - "docker.io/caixw/apidoc:v{{ .Major }}.{{ .Minor }}" 64 | - "docker.io/caixw/apidoc:latest" 65 | 66 | - "ghcr.io/caixw/apidoc:{{ .Tag }}" 67 | - "ghcr.io/caixw/apidoc:v{{ .Major }}" 68 | - "ghcr.io/caixw/apidoc:v{{ .Major }}.{{ .Minor }}" 69 | - "ghcr.io/caixw/apidoc:latest" 70 | 71 | 72 | archives: 73 | - builds: 74 | - apidoc 75 | replacements: 76 | darwin: macOS 77 | format_overrides: 78 | - goos: windows 79 | format: zip 80 | files: 81 | - licence* 82 | - LICENCE* 83 | - license* 84 | - LICENSE* 85 | - readme* 86 | - README* 87 | - changelog* 88 | - CHANGELOG* 89 | 90 | checksum: 91 | name_template: checksums.txt 92 | algorithm: sha256 93 | 94 | changelog: 95 | skip: true 96 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 如何为 apidoc 贡献代码 2 | 3 | apidoc 是一个基于 [MIT](https://opensource.org/licenses/MIT) 的开源软件。 4 | 欢迎大家共同参与开发。**若需要新功能,请先开 issue 讨论。** 5 | 6 | ## 本地化 7 | 8 | 本地化包含以下几个部分: 9 | 10 | - `internal/locale` 主要包含了程序内各种语法错误以及命令行的提示信息; 11 | - `docs/vx/locales.xsl` 包含展示界面中的本化元素;`vx` 表示版本信息,比如 `v5`、`v6` 等; 12 | - `docs/index.*.xml` 网站的内容,* 表示语言 ID; 13 | 14 | ## 文档 15 | 16 | 可以通过 `apidoc static -docs=xxx` 将 docs 作为一个本地的 web 服务,方便 XSL 相关功能的调试; 17 | 18 | 文档应该尽可能的保证在非 Javascript 环境下,也有基本的功能。 19 | 20 | ## 添加新编程语言支持 21 | 22 | `internal/lang/lang.go` 文件中有所有语言模型的定义,在该文件中有详细的文档说明如何定义语言模型。若需要添加对新语言的支持,提交并更新 [#11](https://github.com/caixw/apidoc/issues/11) 23 | 24 | ## Git 25 | 26 | Git 的编写规则参照 , 27 | scope 值可直接写包名,或是文件名,比如 `internal/mock`、`README.md`。 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # apidoc dockerfile 2 | 3 | FROM scratch 4 | 5 | MAINTAINER caixw 6 | 7 | COPY ./apidoc / 8 | 9 | ENTRYPOINT ["/apidoc"] 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 caixw 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 | 23 | -------------------------------------------------------------------------------- /build/build.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | // Package build 提供构建文档的相关功能 4 | package build 5 | 6 | import ( 7 | "bytes" 8 | 9 | "github.com/caixw/apidoc/v7/core" 10 | "github.com/caixw/apidoc/v7/internal/ast" 11 | ) 12 | 13 | // Build 解析文档并输出文档内容 14 | // 15 | // 如果是配置文件有问题,则直接返回错误信息,文档错误则输出至 h 对象。 16 | func Build(h *core.MessageHandler, o *Output, i ...*Input) error { 17 | d, err := parse(h, i...) 18 | if err != nil { 19 | return err 20 | } 21 | if err = o.sanitize(); err != nil { 22 | return err 23 | } 24 | 25 | buf, err := o.buffer(d) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | return o.Path.WriteAll(buf.Bytes()) 31 | } 32 | 33 | // Buffer 生成文档内容并返回 34 | // 35 | // 如果是配置文件有问题,则直接返回错误信息,文档错误则输出至 h 对象。 36 | func Buffer(h *core.MessageHandler, o *Output, i ...*Input) (*bytes.Buffer, error) { 37 | d, err := parse(h, i...) 38 | if err != nil { 39 | return nil, err 40 | } 41 | if err = o.sanitize(); err != nil { 42 | return nil, err 43 | } 44 | 45 | return o.buffer(d) 46 | } 47 | 48 | // CheckSyntax 测试文档语法 49 | // 50 | // 如果是配置文件有问题,则直接返回错误信息,文档错误则输出至 h 对象。 51 | func CheckSyntax(h *core.MessageHandler, i ...*Input) error { 52 | _, err := parse(h, i...) 53 | return err 54 | } 55 | 56 | func parse(h *core.MessageHandler, i ...*Input) (*ast.APIDoc, error) { 57 | for _, item := range i { 58 | if err := item.sanitize(); err != nil { 59 | return nil, err 60 | } 61 | } 62 | 63 | d := &ast.APIDoc{} 64 | d.ParseBlocks(h, func(blocks chan core.Block) { 65 | ParseInputs(blocks, h, i...) 66 | }) 67 | 68 | return d, nil 69 | } 70 | -------------------------------------------------------------------------------- /build/build_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package build 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/issue9/assert/v3" 9 | 10 | "github.com/caixw/apidoc/v7/core/messagetest" 11 | ) 12 | 13 | func TestParse(t *testing.T) { 14 | a := assert.New(t, false) 15 | 16 | php := &Input{ 17 | Lang: "php", 18 | Dir: "./testdata", 19 | Recursive: true, 20 | Encoding: "gbk", 21 | } 22 | 23 | c := &Input{ 24 | Lang: "c++", 25 | Dir: "./testdata", 26 | Recursive: true, 27 | } 28 | 29 | rslt := messagetest.NewMessageHandler() 30 | doc, err := parse(rslt.Handler, php, c) 31 | a.NotError(err).NotNil(doc) 32 | rslt.Handler.Stop() 33 | a.Empty(rslt.Errors) 34 | 35 | a.Equal(2, len(doc.APIs)). 36 | Equal(doc.Version.V(), "1.1.1") 37 | api := doc.APIs[0] 38 | a.Equal(api.Method.V(), "GET") 39 | } 40 | -------------------------------------------------------------------------------- /build/detect.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package build 4 | 5 | import ( 6 | "os" 7 | "path/filepath" 8 | "sort" 9 | "strings" 10 | 11 | "github.com/caixw/apidoc/v7/core" 12 | "github.com/caixw/apidoc/v7/internal/ast" 13 | "github.com/caixw/apidoc/v7/internal/lang" 14 | "github.com/caixw/apidoc/v7/internal/locale" 15 | ) 16 | 17 | // DetectConfig 检测 wd 内容并生成 Config 实例 18 | // 19 | // wd 只能为本地文件系统; 20 | // recursive 是否检测子目录; 21 | func DetectConfig(wd core.URI, recursive bool) (*Config, error) { 22 | scheme, path := wd.Parse() 23 | if scheme != "" && scheme != core.SchemeFile { 24 | panic("参数 wd 只能为本地文件") 25 | } 26 | 27 | inputs, err := detectInput(path, recursive) 28 | if err != nil { 29 | return nil, err 30 | } 31 | if len(inputs) == 0 { 32 | return nil, core.NewError(locale.ErrNotFoundSupportedLang) 33 | } 34 | 35 | cfg := &Config{ 36 | Version: ast.Version, 37 | Inputs: inputs, 38 | Output: &Output{ 39 | Path: "./apidoc.xml", 40 | }, 41 | } 42 | 43 | if err = cfg.sanitize(wd); err != nil { 44 | return nil, err 45 | } 46 | return cfg, nil 47 | } 48 | 49 | // 检测指定目录下的内容,并为其生成一个合适的 Input 实例。 50 | // 51 | // 检测依据为根据扩展名来做统计,数量最大且被支持的获胜。 52 | func detectInput(dir string, recursive bool) ([]*Input, error) { 53 | exts, err := detectExts(dir, recursive) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | langs := detectLanguage(exts) 59 | 60 | opts := make([]*Input, 0, len(langs)) 61 | for _, l := range langs { 62 | opts = append(opts, &Input{ 63 | Lang: l.ID, 64 | Dir: "./", 65 | Exts: l.Exts, 66 | Recursive: recursive, 67 | }) 68 | } 69 | 70 | return opts, nil 71 | } 72 | 73 | type language struct { 74 | lang.Language 75 | count int 76 | } 77 | 78 | // 根据 exts 计算每个语言对应的文件数量,并按倒序返回 79 | // 80 | // exts 参数为从 detectExts 中获取的返回值 81 | func detectLanguage(exts map[string]int) []*language { 82 | langs := make([]*language, 0, len(exts)) 83 | 84 | for ext, count := range exts { 85 | l := lang.GetByExt(ext) 86 | if l == nil { 87 | continue 88 | } 89 | 90 | found := false 91 | for _, item := range langs { 92 | if item.ID == l.ID { 93 | item.count += count 94 | found = true 95 | break 96 | } 97 | } 98 | if !found { 99 | langs = append(langs, &language{ 100 | count: count, 101 | Language: *l, 102 | }) 103 | } 104 | } // end for 105 | 106 | sort.SliceStable(langs, func(i, j int) bool { 107 | return langs[i].count > langs[j].count 108 | }) 109 | 110 | return langs 111 | } 112 | 113 | // 返回 dir 目录下文件类型及对应的文件数量的一个集合。 114 | // recursive 表示是否查找子目录。 115 | func detectExts(dir string, recursive bool) (map[string]int, error) { 116 | exts := map[string]int{} 117 | 118 | walk := func(path string, fi os.FileInfo, err error) error { 119 | if err != nil { 120 | return err 121 | } 122 | 123 | if fi.IsDir() { 124 | if !recursive && dir != path { 125 | return filepath.SkipDir 126 | } 127 | } else { 128 | ext := strings.ToLower(filepath.Ext(path)) 129 | if len(ext) > 0 { 130 | exts[ext]++ 131 | } 132 | } 133 | 134 | return nil 135 | } 136 | 137 | if err := filepath.Walk(dir, walk); err != nil { 138 | return nil, err 139 | } 140 | 141 | return exts, nil 142 | } 143 | -------------------------------------------------------------------------------- /build/detect_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package build 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/issue9/assert/v3" 9 | ) 10 | 11 | func TestDetectInput(t *testing.T) { 12 | a := assert.New(t, false) 13 | 14 | o, err := detectInput("./testdata", true) 15 | a.NotError(err).NotEmpty(o) 16 | a.Equal(len(o), 2). // c and php 17 | Equal(o[0].Lang, "c++"). 18 | Equal(o[1].Lang, "php") 19 | } 20 | 21 | func TestDetectLanguage(t *testing.T) { 22 | a := assert.New(t, false) 23 | exts := map[string]int{ 24 | ".h": 2, 25 | ".c": 3, 26 | ".swift": 1, 27 | ".php": 2, 28 | } 29 | 30 | langs := detectLanguage(exts) 31 | a.Equal(len(langs), 3) // c++,php,swift 32 | a.Equal(langs[0].ID, "c++"). 33 | Equal(langs[0].count, 5) 34 | a.Equal(langs[1].ID, "php"). 35 | Equal(langs[1].count, 2) 36 | a.Equal(langs[2].ID, "swift"). 37 | Equal(langs[2].count, 1) 38 | } 39 | 40 | func TestDetectExts(t *testing.T) { 41 | a := assert.New(t, false) 42 | 43 | files, err := detectExts("./testdata", false) 44 | a.NotError(err) 45 | a.Equal(len(files), 5) 46 | a.Equal(files[".php"], 1).Equal(files[".c"], 1) 47 | 48 | files, err = detectExts("./testdata", true) 49 | a.NotError(err) 50 | a.Equal(len(files), 6) 51 | a.Equal(files[".php"], 1).Equal(files[".1"], 3) 52 | } 53 | -------------------------------------------------------------------------------- /build/output_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package build 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/issue9/assert/v3" 9 | 10 | "github.com/caixw/apidoc/v7/core" 11 | "github.com/caixw/apidoc/v7/internal/ast/asttest" 12 | "github.com/caixw/apidoc/v7/internal/docs" 13 | ) 14 | 15 | func TestOptions_contains(t *testing.T) { 16 | a := assert.New(t, false) 17 | 18 | o := &Output{} 19 | a.True(o.contains("tag")) 20 | a.True(o.contains("")) 21 | 22 | o.Tags = []string{"t1", "t2"} 23 | a.True(o.contains("t1")) 24 | a.False(o.contains("not-exists")) 25 | a.False(o.contains("")) 26 | } 27 | 28 | func TestOutput_Sanitize(t *testing.T) { 29 | a := assert.New(t, false) 30 | 31 | // 默认的 Type 32 | o := &Output{} 33 | a.NotError(o.sanitize()) 34 | a.Equal(o.Type, APIDocXML).NotNil(o.marshal) 35 | 36 | o = &Output{Type: "invalid-type"} 37 | a.Error(o.sanitize()) 38 | 39 | o = &Output{Type: APIDocXML} 40 | o.Path = "./testdir/apidoc.json" 41 | a.NotError(o.sanitize()) 42 | a.Equal(o.Style, docs.StylesheetURL(core.OfficialURL)). 43 | Equal(2, len(o.procInst)). 44 | Contains(o.procInst[1], docs.StylesheetURL(core.OfficialURL)) 45 | 46 | o.Version = "1.0.0" 47 | a.NotError(o.sanitize()) 48 | o.Version = "1" 49 | a.Error(o.sanitize()) 50 | } 51 | 52 | func TestOptions_buffer(t *testing.T) { 53 | a := assert.New(t, false) 54 | 55 | doc := asttest.Get() 56 | o := &Output{ 57 | Type: OpenapiJSON, 58 | Path: "./openapi.json", 59 | } 60 | a.NotError(o.sanitize()) 61 | _, err := o.buffer(doc) 62 | a.NotError(err) 63 | 64 | doc = asttest.Get() 65 | o = &Output{} 66 | a.NotError(o.sanitize()) 67 | buf, err := o.buffer(doc) 68 | a.NotError(err).NotNil(buf) 69 | } 70 | 71 | func TestFilterDoc(t *testing.T) { 72 | a := assert.New(t, false) 73 | 74 | d := asttest.Get() 75 | o := &Output{} 76 | a.NotError(o.sanitize()) 77 | filterDoc(d, o) 78 | a.Equal(3, len(d.Tags)) 79 | 80 | d = asttest.Get() 81 | o = &Output{ 82 | Tags: []string{"t1"}, 83 | } 84 | a.NotError(o.sanitize()) 85 | filterDoc(d, o) 86 | a.Equal(1, len(d.Tags)). 87 | Equal(2, len(d.APIs)) 88 | 89 | d = asttest.Get() 90 | o = &Output{ 91 | Tags: []string{"t1", "t2"}, 92 | } 93 | a.NotError(o.sanitize()) 94 | filterDoc(d, o) 95 | a.Equal(2, len(d.Tags)). 96 | Equal(2, len(d.APIs)) 97 | 98 | d = asttest.Get() 99 | o = &Output{ 100 | Tags: []string{"tag1"}, 101 | } 102 | a.NotError(o.sanitize()) 103 | filterDoc(d, o) 104 | a.Equal(1, len(d.Tags)). 105 | Equal(1, len(d.APIs)) 106 | 107 | d = asttest.Get() 108 | o = &Output{ 109 | Tags: []string{"not-exists"}, 110 | } 111 | a.NotError(o.sanitize()) 112 | filterDoc(d, o) 113 | a.Equal(0, len(d.Tags)). 114 | Equal(0, len(d.APIs)) 115 | } 116 | -------------------------------------------------------------------------------- /build/path.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package build 4 | 5 | import ( 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/caixw/apidoc/v7/core" 11 | "github.com/caixw/apidoc/v7/internal/locale" 12 | ) 13 | 14 | // 获取 path 的绝对路径 15 | // 16 | // 如果 path 是相对路径的,则将其设置为相对于 wd 的路径。 17 | func abs(path, wd core.URI) (uri core.URI, err error) { 18 | scheme, p := path.Parse() 19 | if scheme != "" && scheme != core.SchemeFile { 20 | return "", locale.NewError(locale.ErrInvalidURIScheme, scheme) 21 | } 22 | 23 | scheme, dir := wd.Parse() 24 | if scheme != "" && scheme != core.SchemeFile { 25 | return "", locale.NewError(locale.ErrInvalidURIScheme, scheme) 26 | } 27 | if !filepath.IsAbs(dir) { 28 | if dir, err = filepath.Abs(dir); err != nil { 29 | return "", err 30 | } 31 | } 32 | 33 | ps := string(p) 34 | switch { 35 | case ps == "": 36 | return core.FileURI(dir), nil 37 | case ps[0] == '~': 38 | dir, err := os.UserHomeDir() 39 | if err != nil { 40 | return "", err 41 | } 42 | 43 | return core.FileURI(dir).Append(ps[1:]), nil 44 | case filepath.IsAbs(ps) || ps[0] == '/' || ps[0] == os.PathSeparator: 45 | return core.FileURI(ps), nil 46 | default: // 相对路径 47 | return core.FileURI(filepath.Clean(filepath.Join(dir, ps))), nil 48 | } 49 | } 50 | 51 | // 获取 path 相对于 wd 的路径 52 | // 53 | // 如果两者不存在关联性,则返回 path 的原始值。 54 | // 返回值仅为普通的路径表示,不会带 scheme 内容。 55 | func rel(path, wd core.URI) (uri core.URI, err error) { 56 | scheme, p := path.Parse() 57 | if scheme != "" && scheme != core.SchemeFile { 58 | return "", locale.NewError(locale.ErrInvalidURIScheme, scheme) 59 | } 60 | if !filepath.IsAbs(p) { 61 | if p, err = filepath.Abs(p); err != nil { 62 | return "", err 63 | } 64 | } 65 | 66 | scheme, dir := wd.Parse() 67 | if scheme != "" && scheme != core.SchemeFile { 68 | return "", locale.NewError(locale.ErrInvalidURIScheme, scheme) 69 | } 70 | if !filepath.IsAbs(dir) { 71 | if dir, err = filepath.Abs(dir); err != nil { 72 | return "", err 73 | } 74 | } 75 | 76 | pp, err := filepath.Rel(dir, p) 77 | if err != nil || strings.HasPrefix(pp, "../") || strings.HasPrefix(pp, "..\\") { 78 | return path, nil 79 | } 80 | return core.URI(pp), nil 81 | } 82 | -------------------------------------------------------------------------------- /build/path_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package build 4 | 5 | import ( 6 | "os" 7 | "runtime" 8 | "testing" 9 | 10 | "github.com/issue9/assert/v3" 11 | 12 | "github.com/caixw/apidoc/v7/core" 13 | ) 14 | 15 | type pathTester struct { 16 | path, wd, result string 17 | } 18 | 19 | func TestAbs(t *testing.T) { 20 | if runtime.GOOS == "windows" { // windows 由 path_windows_test.go 程序测试 21 | return 22 | } 23 | 24 | a := assert.New(t, false) 25 | hd, err := os.UserHomeDir() 26 | a.NotError(err).NotNil(hd) 27 | hdURI := core.FileURI(hd) 28 | a.NotEmpty(hdURI) 29 | 30 | data := []*pathTester{ 31 | { 32 | path: "", 33 | wd: "file:///wd/", 34 | result: "file:///wd/", 35 | }, 36 | { 37 | path: "~/path", 38 | wd: "file:///wd/", 39 | result: hdURI.Append("/path").String(), 40 | }, 41 | { 42 | path: "/path", 43 | wd: "file:///wd/", 44 | result: "file:///path", 45 | }, 46 | { 47 | path: "file:///path", 48 | wd: "file:///wd/", 49 | result: "file:///path", 50 | }, 51 | { 52 | path: "path", 53 | wd: "file:///wd/", 54 | result: "file:///wd/path", 55 | }, 56 | { 57 | path: "../path", 58 | wd: "file:///wd/", 59 | result: "file:///path", 60 | }, 61 | { 62 | path: "../../path", 63 | wd: "file:///wd/dir", 64 | result: "file:///path", 65 | }, 66 | { 67 | path: "./path", 68 | wd: "file:///wd/", 69 | result: "file:///wd/path", 70 | }, 71 | { 72 | path: "path", 73 | wd: "/wd/", 74 | result: "file:///wd/path", 75 | }, 76 | { 77 | path: "./path", 78 | wd: "/wd/", 79 | result: "file:///wd/path", 80 | }, 81 | } 82 | 83 | for index, item := range data { 84 | result, err := abs(core.URI(item.path), core.URI(item.wd)) 85 | 86 | a.NotError(err, "err @%d,%s", index, err). 87 | Equal(result, item.result, "not equal @%d,v1=%s,v2=%s", index, result, item.result) 88 | } 89 | } 90 | 91 | func TestRel(t *testing.T) { 92 | if runtime.GOOS == "windows" { // windows 由 path_windows_test.go 程序测试 93 | return 94 | } 95 | 96 | a := assert.New(t, false) 97 | hd, err := os.UserHomeDir() 98 | a.NotError(err).NotNil(hd) 99 | hdURI := core.FileURI(hd) 100 | a.NotEmpty(hdURI) 101 | 102 | data := []*pathTester{ 103 | { 104 | path: "", 105 | wd: "file:///wd/", 106 | result: "", 107 | }, 108 | { 109 | path: "/wd/path", 110 | wd: "file:///wd/", 111 | result: "path", 112 | }, 113 | { 114 | path: "/wd/path", 115 | wd: "file:///wd1/", 116 | result: "/wd/path", 117 | }, 118 | { 119 | path: "wd/path", 120 | wd: "file:///wd/", 121 | result: "wd/path", 122 | }, 123 | { 124 | path: "path", 125 | wd: "file:///wd/", 126 | result: "path", 127 | }, 128 | { 129 | path: "path", 130 | wd: "/wd/", 131 | result: "path", 132 | }, 133 | { 134 | path: "file:///wd/path", 135 | wd: "/wd/", 136 | result: "path", 137 | }, 138 | } 139 | 140 | for index, item := range data { 141 | result, err := rel(core.URI(item.path), core.URI(item.wd)) 142 | 143 | a.NotError(err, "err @%d,%s", index, err). 144 | Equal(result, item.result, "not equal @%d,v1=%s,v2=%s", index, result, item.result) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /build/testdata/failed.yaml: -------------------------------------------------------------------------------- 1 | # 这是一个无效的 yaml 文件,用于测试 2 | 3 | version: 1.1.x 4 | : value 5 | -------------------------------------------------------------------------------- /build/testdata/gbk.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caixw/apidoc/2f6aabc8047abc18722af23e931df0580951f6d3/build/testdata/gbk.php -------------------------------------------------------------------------------- /build/testdata/no-extension: -------------------------------------------------------------------------------- 1 | 表示没有扩展名的文件 2 | -------------------------------------------------------------------------------- /build/testdata/testdir1/testfile.1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caixw/apidoc/2f6aabc8047abc18722af23e931df0580951f6d3/build/testdata/testdir1/testfile.1 -------------------------------------------------------------------------------- /build/testdata/testdir1/testfile.2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caixw/apidoc/2f6aabc8047abc18722af23e931df0580951f6d3/build/testdata/testdir1/testfile.2 -------------------------------------------------------------------------------- /build/testdata/testdir2/testfile.1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caixw/apidoc/2f6aabc8047abc18722af23e931df0580951f6d3/build/testdata/testdir2/testfile.1 -------------------------------------------------------------------------------- /build/testdata/testfile.1: -------------------------------------------------------------------------------- 1 | // 这里展示的是一段没有结尾符号的 C 风格注释代码 2 | 3 | /* 4 | * 5 | * desc 6 | * test 7 | * 8 | -------------------------------------------------------------------------------- /build/testdata/testfile.c: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | // 4 | // 5 | // 6 | // test 7 | // 8 | void api() { 9 | // api 10 | } 11 | -------------------------------------------------------------------------------- /build/testdata/testfile.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | // 4 | // title 5 | // application/json 6 | // 7 | // 8 | -------------------------------------------------------------------------------- /cmd/apidoc/.gitignore: -------------------------------------------------------------------------------- 1 | apidoc 2 | -------------------------------------------------------------------------------- /cmd/apidoc/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | // apidoc 是一个 RESTful API 文档生成工具 4 | // 5 | // 大致的使用方法为: 6 | // 7 | // apidoc cmd [args] 8 | // 9 | // 其中的 cmd 为子命令,args 代码传递给该子命令的参数。 10 | // 可以使用 help 查看每个子命令的具体说明: 11 | // 12 | // apidoc help [cmd] 13 | package main 14 | 15 | import ( 16 | "fmt" 17 | "os" 18 | 19 | "github.com/issue9/localeutil" 20 | "golang.org/x/text/language" 21 | 22 | "github.com/caixw/apidoc/v7" 23 | "github.com/caixw/apidoc/v7/internal/cmd" 24 | "github.com/caixw/apidoc/v7/internal/locale" 25 | ) 26 | 27 | func main() { 28 | tag, err := localeutil.DetectUserLanguageTag() 29 | if err != nil { // 无法获取系统语言,则采用默认值 30 | fmt.Fprintln(os.Stderr, err, tag) 31 | tag = language.MustParse(locale.DefaultLocaleID) 32 | } 33 | apidoc.SetLocale(tag) 34 | 35 | if err := cmd.Init(os.Stdout).Exec(os.Args[1:]); err != nil { 36 | if _, err := fmt.Fprintln(os.Stderr, err); err != nil { 37 | panic(err) 38 | } 39 | os.Exit(2) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /core/core.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | // Package core 提供基础的核心功能 4 | package core 5 | 6 | import "fmt" 7 | 8 | const ( 9 | // Name 程序的正式名称 10 | Name = "apidoc" 11 | 12 | // RepoURL 源码仓库地址 13 | RepoURL = "https://github.com/caixw/apidoc" 14 | 15 | // OfficialURL 官网 16 | OfficialURL = "https://apidoc.tools" 17 | 18 | // XMLNamespace 定义 xml 命名空间的 URI 19 | XMLNamespace = "https://apidoc.tools/v6/XMLSchema" 20 | ) 21 | 22 | // Searcher 实现了搜索的基本方法集合 23 | // 24 | // 所有内嵌 [Location] 的对象都可以使用此接口判断是否内嵌 [Location]。 25 | type Searcher interface { 26 | Contains(URI, Position) bool 27 | Loc() Location 28 | } 29 | 30 | // Block 最基本的代码单位 31 | // 32 | // 一般从注释提取的一个完整注释作为 Block 实例。 33 | type Block struct { 34 | Location Location 35 | Data []byte 36 | } 37 | 38 | // Position 用于描述字符在文件中的定位 39 | // 40 | // 兼容 LSP https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position 41 | type Position struct { 42 | Line int `json:"line" apidoc:"-"` 43 | Character int `json:"character" apidoc:"-"` 44 | } 45 | 46 | // Range 用于描述文档中的一段范围 47 | // 48 | // 兼容 LSP https://microsoft.github.io/language-server-protocol/specifications/specification-current/#range 49 | type Range struct { 50 | Start Position `json:"start" apidoc:"-"` 51 | End Position `json:"end" apidoc:"-"` 52 | } 53 | 54 | // Location 用于描述一段内容的定位 55 | // 56 | // 兼容 LSP https://microsoft.github.io/language-server-protocol/specifications/specification-current/#location 57 | type Location struct { 58 | URI URI `json:"uri" apidoc:"-"` 59 | Range Range `json:"range" apidoc:"-"` 60 | } 61 | 62 | // Equal 判断与 v 是否相同 63 | func (p Position) Equal(v Position) bool { 64 | return p.Line == v.Line && p.Character == v.Character 65 | } 66 | 67 | // Equal 判断与 v 是否相同 68 | func (r Range) Equal(v Range) bool { 69 | return r.Start.Equal(v.Start) && r.End.Equal(v.End) 70 | } 71 | 72 | // IsEmpty 表示 Range 表示的范围长度为空 73 | func (r Range) IsEmpty() bool { return r.End == r.Start } 74 | 75 | // Contains 是否包含了 p 这个点 76 | func (r Range) Contains(p Position) bool { 77 | s := r.Start 78 | e := r.End 79 | return (s.Line < p.Line || (s.Line == p.Line && s.Character <= p.Character)) && 80 | (e.Line > p.Line || (e.Line == p.Line && e.Character >= p.Character)) 81 | } 82 | 83 | // Loc 返回当前的范围 84 | func (l Location) Loc() Location { return l } 85 | 86 | // Contains l 是否包含 pos 这个点 87 | func (l Location) Contains(uri URI, pos Position) bool { 88 | return l.URI == uri && l.Range.Contains(pos) 89 | } 90 | 91 | func (l Location) String() string { 92 | if l.IsEmpty() { 93 | return "" 94 | } 95 | 96 | if l.Range.IsEmpty() { 97 | return l.URI.String() 98 | } 99 | 100 | s := l.Range.Start 101 | e := l.Range.End 102 | return fmt.Sprintf("%s[%d:%d,%d:%d]", l.URI, s.Line, s.Character, e.Line, e.Character) 103 | } 104 | 105 | // Equal 判断与 v 是否相等 106 | // 107 | // 所有字段都相同即返回 true。 108 | func (l Location) Equal(v Location) bool { 109 | return l.Range.Equal(v.Range) && l.URI == v.URI 110 | } 111 | 112 | // IsEmpty 表示 Location 未指向任何位置 113 | func (l Location) IsEmpty() bool { 114 | return l.URI == "" && l.Range.IsEmpty() 115 | } 116 | -------------------------------------------------------------------------------- /core/errors_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package core 4 | 5 | import ( 6 | "errors" 7 | "os" 8 | "testing" 9 | 10 | "github.com/issue9/assert/v3" 11 | 12 | "github.com/caixw/apidoc/v7/internal/locale" 13 | ) 14 | 15 | var _ error = &Error{} 16 | 17 | func TestError(t *testing.T) { 18 | a := assert.New(t, false) 19 | 20 | err1 := NewError("msg") 21 | err2 := NewError("msg").WithField("field") 22 | a.NotEqual(err1.Error(), err2.Error()) 23 | } 24 | 25 | func TestWithError(t *testing.T) { 26 | a := assert.New(t, false) 27 | 28 | err := errors.New("test") 29 | serr := WithError(err).WithField("field") 30 | a.Equal(serr.Err, err) 31 | 32 | serr2 := WithError(serr).WithLocation(Location{URI: "uri"}) 33 | a.Equal(serr2.Err, err) 34 | } 35 | 36 | func TestError_AddTypes(t *testing.T) { 37 | a := assert.New(t, false) 38 | loc := Location{} 39 | 40 | err := loc.WithError(errors.New("err1")) 41 | err.AddTypes(ErrorTypeDeprecated) 42 | a.Equal(err.Types, []ErrorType{ErrorTypeDeprecated}) 43 | err.AddTypes(ErrorTypeDeprecated) 44 | a.Equal(err.Types, []ErrorType{ErrorTypeDeprecated}) 45 | 46 | err.AddTypes(ErrorTypeUnused) 47 | a.Equal(err.Types, []ErrorType{ErrorTypeDeprecated, ErrorTypeUnused}) 48 | } 49 | 50 | func TestError_Is_Unwrap(t *testing.T) { 51 | a := assert.New(t, false) 52 | 53 | err := WithError(os.ErrExist).WithField("field") 54 | a.True(errors.Is(err, os.ErrExist)) 55 | 56 | a.Equal(errors.Unwrap(err), os.ErrExist) 57 | } 58 | 59 | func TestError_Relate(t *testing.T) { 60 | a := assert.New(t, false) 61 | 62 | err := NewError(locale.ErrInvalidUTF8Character) 63 | a.Empty(err.Related) 64 | err.Relate(Location{}, "msg") 65 | a.Equal(1, len(err.Related)).Equal(err.Related[0].Message, "msg") 66 | 67 | err.Relate(Location{}, "msg2") 68 | a.Equal(2, len(err.Related)).Equal(err.Related[1].Message, "msg2") 69 | } 70 | -------------------------------------------------------------------------------- /core/gbk.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caixw/apidoc/2f6aabc8047abc18722af23e931df0580951f6d3/core/gbk.php -------------------------------------------------------------------------------- /core/message.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package core 4 | 5 | import ( 6 | "golang.org/x/text/message" 7 | 8 | "github.com/caixw/apidoc/v7/internal/locale" 9 | ) 10 | 11 | // MessageType 表示消息的类型 12 | type MessageType int8 13 | 14 | // 消息的分类 15 | const ( 16 | Erro MessageType = iota 17 | Warn 18 | Info 19 | Succ 20 | ) 21 | 22 | func (t MessageType) String() string { 23 | switch t { 24 | case Erro: 25 | return "ERRO" 26 | case Warn: 27 | return "WARN" 28 | case Info: 29 | return "INFO" 30 | case Succ: 31 | return "SUCC" 32 | default: 33 | return "" 34 | } 35 | } 36 | 37 | // Message 输出消息的具体结构 38 | type Message struct { 39 | Type MessageType 40 | Message any 41 | } 42 | 43 | // HandlerFunc 错误处理函数 44 | type HandlerFunc func(*Message) 45 | 46 | // MessageHandler 异步的消息处理机制 47 | // 48 | // 包含了本地化的信息,输出时,会以指定的本地化内容输出 49 | type MessageHandler struct { 50 | messages chan *Message 51 | stop chan struct{} 52 | } 53 | 54 | // NewMessageHandler 声明新的 MessageHandler 实例 55 | func NewMessageHandler(f HandlerFunc) *MessageHandler { 56 | h := &MessageHandler{ 57 | messages: make(chan *Message, 100), 58 | stop: make(chan struct{}), 59 | } 60 | 61 | go func() { 62 | for msg := range h.messages { 63 | f(msg) 64 | } 65 | h.stop <- struct{}{} 66 | }() 67 | 68 | return h 69 | } 70 | 71 | // Stop 停止处理错误内容 72 | // 73 | // 只有在消息处理完成之后,才会返回。 74 | func (h *MessageHandler) Stop() { 75 | close(h.messages) 76 | 77 | // Stop() 调用可能是在主程序结束处。 78 | // 通过 h.stop 阻塞函数返回,直到所有消息都处理完成。 79 | <-h.stop 80 | } 81 | 82 | // Message 发送消息 83 | func (h *MessageHandler) Message(t MessageType, msg any) { 84 | h.messages <- &Message{ 85 | Type: t, 86 | Message: msg, 87 | } 88 | } 89 | 90 | // Locale 发送普通的文本信息 91 | func (h *MessageHandler) Locale(t MessageType, key message.Reference, val ...any) { 92 | h.Message(t, locale.New(key, val...)) 93 | } 94 | 95 | // Error 发送错误类型的值 96 | func (h *MessageHandler) Error(err any) { h.Message(Erro, err) } 97 | 98 | // Warning 发送错误类型的值 99 | func (h *MessageHandler) Warning(err any) { h.Message(Warn, err) } 100 | 101 | // Success 发送错误类型的值 102 | func (h *MessageHandler) Success(err any) { h.Message(Succ, err) } 103 | 104 | // Info 发送错误类型的值 105 | func (h *MessageHandler) Info(err any) { h.Message(Info, err) } 106 | -------------------------------------------------------------------------------- /core/message_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package core 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "testing" 9 | "time" 10 | 11 | "github.com/issue9/assert/v3" 12 | 13 | "github.com/caixw/apidoc/v7/internal/locale" 14 | ) 15 | 16 | var _ fmt.Stringer = Erro 17 | 18 | func TestType_String(t *testing.T) { 19 | a := assert.New(t, false) 20 | a.Equal("ERRO", Erro.String()) 21 | a.Equal("SUCC", Succ.String()) 22 | a.Equal("INFO", Info.String()) 23 | a.Equal("WARN", Warn.String()) 24 | a.Equal("", MessageType(-22).String()) 25 | } 26 | 27 | func TestHandler(t *testing.T) { 28 | a := assert.New(t, false) 29 | 30 | erro := new(bytes.Buffer) 31 | warn := new(bytes.Buffer) 32 | info := new(bytes.Buffer) 33 | succ := new(bytes.Buffer) 34 | h := NewMessageHandler(func(msg *Message) { 35 | switch msg.Type { 36 | case Erro: 37 | erro.WriteString("erro") 38 | case Warn: 39 | warn.WriteString("warn") 40 | case Info: 41 | info.WriteString("info") 42 | case Succ: 43 | succ.WriteString("succ") 44 | default: 45 | panic("panic") 46 | } 47 | }) 48 | a.NotNil(h) 49 | 50 | h.Error((Location{URI: "erro.go"}).NewError(locale.ErrInvalidUTF8Character)) 51 | h.Warning((Location{URI: "warn.go"}).NewError(locale.ErrInvalidUTF8Character)) 52 | h.Info((Location{URI: "info.go"}).NewError(locale.ErrInvalidUTF8Character)) 53 | h.Success((Location{URI: "succ.go"}).NewError(locale.ErrInvalidUTF8Character)) 54 | 55 | time.Sleep(1 * time.Second) // 等待 channel 完成 56 | a.Equal(erro.String(), "erro") 57 | a.Equal(warn.String(), "warn") 58 | a.Equal(info.String(), "info") 59 | a.Equal(succ.String(), "succ") 60 | 61 | h.Stop() 62 | a.Panic(func() { // 已经关闭 messages 63 | h.Error((Location{URI: "erro"}).NewError(locale.ErrInvalidUTF8Character)) 64 | }) 65 | } 66 | 67 | func TestHandler_Stop(t *testing.T) { 68 | a := assert.New(t, false) 69 | var exit bool 70 | 71 | h := NewMessageHandler(func(msg *Message) { 72 | time.Sleep(time.Second) 73 | exit = true 74 | }) 75 | a.NotNil(h) 76 | 77 | h.Locale(Erro, locale.ErrInvalidUTF8Character) 78 | h.Stop() // 此处会阻塞,等待完成 79 | a.True(exit) 80 | } 81 | -------------------------------------------------------------------------------- /core/messagetest/messagetest.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | // Package messagetest 提供测试生成 message 相关的测试工具 4 | package messagetest 5 | 6 | import "github.com/caixw/apidoc/v7/core" 7 | 8 | type Result struct { 9 | Errors, Warns, Infos, Successes []any 10 | Handler *core.MessageHandler 11 | } 12 | 13 | // NewMessageHandler 返回一个用于测试的 core.MessageHandler 实例 14 | func NewMessageHandler() *Result { 15 | rslt := &Result{ 16 | Errors: []any{}, 17 | Warns: []any{}, 18 | Infos: []any{}, 19 | Successes: []any{}, 20 | } 21 | 22 | rslt.Handler = core.NewMessageHandler(func(msg *core.Message) { 23 | switch msg.Type { 24 | case core.Erro: 25 | rslt.Errors = append(rslt.Errors, msg.Message) 26 | case core.Warn: 27 | rslt.Warns = append(rslt.Warns, msg.Message) 28 | case core.Info: 29 | rslt.Infos = append(rslt.Infos, msg.Message) 30 | case core.Succ: 31 | rslt.Successes = append(rslt.Successes, msg.Message) 32 | default: 33 | panic("unreached") 34 | } 35 | }) 36 | 37 | return rslt 38 | } 39 | -------------------------------------------------------------------------------- /core/messagetest/messagetest_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package messagetest 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/issue9/assert/v3" 9 | ) 10 | 11 | func TestNewMessageHandler(t *testing.T) { 12 | a := assert.New(t, false) 13 | 14 | rslt := NewMessageHandler() 15 | a.NotNil(rslt).NotNil(rslt.Handler) 16 | rslt.Handler.Error("error") 17 | rslt.Handler.Stop() 18 | a.Equal(rslt.Errors[0], "error") 19 | 20 | rslt = NewMessageHandler() 21 | a.NotNil(rslt).NotNil(rslt.Handler) 22 | rslt.Handler.Info("info") 23 | rslt.Handler.Stop() 24 | a.Equal(rslt.Infos[0], "info") 25 | 26 | rslt = NewMessageHandler() 27 | a.NotNil(rslt).NotNil(rslt.Handler) 28 | rslt.Handler.Success("success") 29 | rslt.Handler.Stop() 30 | a.Equal(rslt.Successes[0], "success") 31 | 32 | rslt = NewMessageHandler() 33 | a.NotNil(rslt).NotNil(rslt.Handler) 34 | rslt.Handler.Warning("warn") 35 | rslt.Handler.Stop() 36 | a.Equal(rslt.Warns[0], "warn") 37 | } 38 | -------------------------------------------------------------------------------- /core/version.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package core 4 | 5 | var ( 6 | mainVersion = "7.2.3" 7 | metadata string 8 | fullVersion = mainVersion 9 | ) 10 | 11 | func init() { 12 | if metadata != "" { 13 | fullVersion += "+" + metadata 14 | } 15 | } 16 | 17 | // FullVersion 完整的版本号 18 | // 19 | // 会包含版本号、构建日期和最后的提交 ID,大致格式如下: 20 | // 21 | // version+buildDate.commitHash 22 | func FullVersion() string { 23 | return fullVersion 24 | } 25 | 26 | // Version 程序的版本号 27 | // 28 | // 遵守 https://semver.org/lang/zh-CN/ 规则。 29 | // 程序不兼容或是文档格式不兼容时,需要提升主版本号。 30 | func Version() string { 31 | return mainVersion 32 | } 33 | -------------------------------------------------------------------------------- /core/version_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package core 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/issue9/assert/v3" 9 | "github.com/issue9/version" 10 | ) 11 | 12 | // 对一些堂量的基本检测。 13 | func TestVersion(t *testing.T) { 14 | a := assert.New(t, false) 15 | 16 | a.True(version.SemVerValid(Version())) 17 | a.True(version.SemVerValid(FullVersion())) 18 | } 19 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | apidoc.tools 2 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # docs 2 | 3 | 当前目录存放的是官网的内容。 4 | 5 | v5、v6 这些以版本名称命名的则是各个文档版本对应的转换工具。 6 | -------------------------------------------------------------------------------- /docs/docs.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package docs 4 | 5 | import "embed" 6 | 7 | //go:embed * 8 | var FS embed.FS 9 | -------------------------------------------------------------------------------- /docs/example/.apidoc.yaml: -------------------------------------------------------------------------------- 1 | version: 6.1.0 2 | inputs: 3 | - lang: c++ 4 | dir: . 5 | recursive: true 6 | 7 | - lang: rust 8 | dir: . 9 | recursive: true 10 | 11 | output: 12 | path: ./index.xml 13 | type: apidoc+xml 14 | style: ../v6/apidoc.xsl 15 | -------------------------------------------------------------------------------- /docs/example/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | 3 | !.apidoc.yaml 4 | -------------------------------------------------------------------------------- /docs/example/apis.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | // 4 | // client 5 | // admin 6 | // 7 | // 这是关于接口的详细说明文档

9 | //

可以是一个 HTML 内容

10 | // ]]> 11 | //
12 | // 13 | // 14 | // 15 | // 16 | // 17 | // 18 | // 19 | //
20 | //
21 | // 22 | // 23 | // 24 | // 25 | // 26 | // 27 | // 28 | // 29 | // 30 | // 39 | // 40 | // 41 | // 42 | // 43 | // 这是一个回调函数的详细说明

45 | //

为一个 html 文档

46 | // ]]> 47 | //
48 | // 49 | // 50 | // 51 | // 52 | // 58 | // 59 | // 60 | // 61 | // 62 | //
63 | // 64 | void logs() {} 65 | -------------------------------------------------------------------------------- /docs/example/doc.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | // 4 | // 示例文档 5 | // 6 | //


admin

8 | // ]]>
9 | //
10 | // 11 | // 12 | // application/xml 13 | // application/json 14 | // 15 | // 16 | // 17 | // 18 | // https://example.com 19 | // example@example.com 20 | // 21 | // 22 | // 23 | // 24 | // 25 | // 26 | // 27 | // 28 | // 29 | // 30 | // 31 | // 32 | //
33 | //
34 | // 35 | // 36 | // 这是一个用于测试的文档用例

38 | // 状态码: 39 | //
    40 | //
  • 40300:xxx
  • 41 | //
  • 40301:xxx
  • 42 | //
43 | // ]]> 44 | //
45 | // 46 | -------------------------------------------------------------------------------- /docs/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | apidoc 5 | https://apidoc.tools LOGO 6 | 这是一只诞生于羊年的小工具 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | window.onload = function() { 4 | initGotoTop(); 5 | }; 6 | 7 | function initGotoTop() { 8 | const top = document.querySelector('.goto-top'); 9 | 10 | // 在最顶部时,隐藏按钮 11 | window.addEventListener('scroll', (e) => { 12 | const body = document.querySelector('html'); 13 | if (body.scrollTop > 50) { 14 | top.style.display = 'block'; 15 | } else { 16 | top.style.display = 'none'; 17 | } 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /docs/site.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | apidoc 7 | 6.1.0 8 | https://github.com/caixw/apidoc 9 | https://apidoc.tools 10 | 11 | C# 12 | C/C++ 13 | D 14 | Dart 15 | Erlang 16 | Go 17 | Groovy 18 | Java 19 | JavaScript 20 | Julia 21 | Kotlin 22 | Lisp/Clojure 23 | Lua 24 | Nim 25 | Pascal/Delphi 26 | Perl 27 | PHP 28 | Python 29 | Ruby 30 | Rust 31 | Scala 32 | Swift 33 | TypeScript 34 | Zig 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /docs/v5/view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 33 | 34 | 35 | 36 | 40 | 41 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /docs/v6/view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 33 | 34 | 35 | 36 | 40 | 41 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/caixw/apidoc/v7 2 | 3 | require ( 4 | github.com/issue9/assert/v3 v3.0.1 5 | github.com/issue9/cmdopt v0.7.3 6 | github.com/issue9/errwrap v0.3.1 7 | github.com/issue9/jsonrpc v0.12.2 8 | github.com/issue9/localeutil v0.12.3 9 | github.com/issue9/mux/v7 v7.0.0-beta.2 10 | github.com/issue9/qheader v0.6.2 11 | github.com/issue9/rands v1.2.1 12 | github.com/issue9/sliceutil v0.11.0 13 | github.com/issue9/source v0.2.0 14 | github.com/issue9/term/v3 v3.0.1 15 | github.com/issue9/validation v0.8.0 16 | github.com/issue9/version v1.0.6 17 | golang.org/x/text v0.3.7 18 | gopkg.in/yaml.v3 v3.0.1 19 | ) 20 | 21 | require ( 22 | github.com/gorilla/websocket v1.5.0 // indirect 23 | github.com/issue9/autoinc v1.0.9 // indirect 24 | github.com/issue9/unique v1.3.2 // indirect 25 | golang.org/x/sys v0.1.0 // indirect 26 | ) 27 | 28 | go 1.18 29 | -------------------------------------------------------------------------------- /internal/ast/ast_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package ast 4 | 5 | import ( 6 | "strconv" 7 | "testing" 8 | 9 | "github.com/issue9/assert/v3" 10 | "github.com/issue9/version" 11 | ) 12 | 13 | func TestVersion(t *testing.T) { 14 | a := assert.New(t, false) 15 | a.True(version.SemVerValid(Version)) 16 | 17 | v := &version.SemVersion{} 18 | a.NotError(version.Parse(v, Version)) 19 | major, err := strconv.Atoi(MajorVersion[1:]) 20 | a.NotError(err) 21 | a.Equal(major, v.Major) 22 | } 23 | 24 | func TestParseType(t *testing.T) { 25 | a := assert.New(t, false) 26 | 27 | p, s := ParseType(TypeString) 28 | a.Equal(p, TypeString).Empty(s) 29 | 30 | p, s = ParseType(TypeURL) 31 | a.Equal(p, TypeString).Equal(s, "url") 32 | 33 | p, s = ParseType(TypeInt) 34 | a.Equal(p, TypeNumber).Equal(s, "int") 35 | } 36 | 37 | func TestTrimLeftSpace(t *testing.T) { 38 | a := assert.New(t, false) 39 | 40 | data := []*struct { 41 | input, output string 42 | }{ 43 | {}, 44 | { 45 | input: `abc`, 46 | output: `abc`, 47 | }, 48 | { 49 | input: ` abc`, 50 | output: `abc`, 51 | }, 52 | { 53 | input: " abc\n", 54 | output: "abc\n", 55 | }, 56 | { // 缩进一个空格 57 | input: " abc\n abc\n", 58 | output: " abc\nabc\n", 59 | }, 60 | { // 缩进一个空格 61 | input: "\n abc\n abc\n", 62 | output: "\n abc\nabc\n", 63 | }, 64 | { // 缩进格式不相同,不会有缩进 65 | input: "\t abc\n abc\n", 66 | output: "\t abc\n abc\n", 67 | }, 68 | 69 | { 70 | input: "\t abc\n\t abc\n\t xx\n", 71 | output: " abc\nabc\nxx\n", 72 | }, 73 | { 74 | input: "\t abc\n\t abc\nxx\n", 75 | output: "\t abc\n\t abc\nxx\n", 76 | }, 77 | 78 | { // 包含相同的 \t 内容 79 | input: "\t abc\n\t abc\n\t xx\n", 80 | output: "abc\nabc\nxx\n", 81 | }, 82 | 83 | { // 部分空格相同 84 | input: "\t\t abc\n\t abc\n\t xx\n", 85 | output: "\t abc\n abc\n xx\n", 86 | }, 87 | } 88 | 89 | for i, item := range data { 90 | output := trimLeftSpace(item.input) 91 | a.Equal(output, item.output, "not equal @ %d\nv1=%#v\nv2=%#v\n", i, output, item.output) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /internal/ast/asttest/asttest_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package asttest 4 | 5 | import ( 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/issue9/assert/v3" 10 | ) 11 | 12 | func TestXML(t *testing.T) { 13 | a := assert.New(t, false) 14 | data := XML(a) 15 | a.NotNil(data) 16 | } 17 | 18 | func TestURI(t *testing.T) { 19 | a := assert.New(t, false) 20 | 21 | p1, err := filepath.Abs(Filename) 22 | a.NotError(err).NotEmpty(p1) 23 | 24 | p2, err := URI(a).File() 25 | a.NotError(err).NotEmpty(p2) 26 | 27 | a.Equal(p1, p2) 28 | } 29 | 30 | func TestPath(t *testing.T) { 31 | a := assert.New(t, false) 32 | 33 | p1, err := filepath.Abs(Filename) 34 | a.NotError(err).NotEmpty(p1) 35 | 36 | p2, err := filepath.Abs(Path(a)) 37 | a.NotError(err).NotEmpty(p2) 38 | 39 | a.Equal(p1, p2) 40 | } 41 | 42 | func TestDir(t *testing.T) { 43 | a := assert.New(t, false) 44 | 45 | p1, err := filepath.Abs("./") 46 | a.NotError(err).NotEmpty(p1) 47 | 48 | p2, err := filepath.Abs(Dir(a)) 49 | a.NotError(err).NotEmpty(p2) 50 | 51 | a.Equal(p1, p2) 52 | } 53 | -------------------------------------------------------------------------------- /internal/ast/asttest/gen.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | //go:generate go run ./make.go 4 | 5 | package asttest 6 | -------------------------------------------------------------------------------- /internal/ast/asttest/index.xml: -------------------------------------------------------------------------------- 1 | 2 | test 3 | desc

]]>
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | desc

]]>
21 |
22 | t1 23 | t2 24 | admin 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | desc

]]>
36 |
37 | t1 38 | tag1 39 | admin 40 | client 41 |
42 | application/json 43 | application/xml 44 |
-------------------------------------------------------------------------------- /internal/ast/asttest/make.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | //go:build ignore 4 | // +build ignore 5 | 6 | package main 7 | 8 | import ( 9 | "io/ioutil" 10 | "os" 11 | 12 | "github.com/caixw/apidoc/v7/core" 13 | "github.com/caixw/apidoc/v7/internal/ast/asttest" 14 | "github.com/caixw/apidoc/v7/internal/xmlenc" 15 | ) 16 | 17 | func main() { 18 | data, err := xmlenc.Encode("\t", asttest.Get(), core.XMLNamespace, "aa") 19 | if err != nil { 20 | panic(err) 21 | } 22 | 23 | err = ioutil.WriteFile(asttest.Filename, data, os.ModePerm) 24 | if err != nil { 25 | panic(err) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /internal/ast/parse.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package ast 4 | 5 | import ( 6 | "bytes" 7 | "errors" 8 | "io" 9 | "sort" 10 | 11 | "github.com/caixw/apidoc/v7/core" 12 | "github.com/caixw/apidoc/v7/internal/locale" 13 | "github.com/caixw/apidoc/v7/internal/xmlenc" 14 | ) 15 | 16 | // ParseBlocks 从多个 core.Block 实例中解析文档内容 17 | // 18 | // g 必须是一个阻塞函数,直到所有代码块都写入参数之后,才能返回。 19 | func (doc *APIDoc) ParseBlocks(h *core.MessageHandler, g func(chan core.Block)) { 20 | done := make(chan struct{}) 21 | blocks := make(chan core.Block, 50) 22 | 23 | go func() { 24 | for block := range blocks { 25 | doc.Parse(h, block) 26 | } 27 | done <- struct{}{} 28 | }() 29 | 30 | g(blocks) 31 | close(blocks) 32 | <-done 33 | } 34 | 35 | // Parse 将注释块的内容添加到当前文档 36 | func (doc *APIDoc) Parse(h *core.MessageHandler, b core.Block) { 37 | if !isValid(b) { 38 | return 39 | } 40 | 41 | p, err := xmlenc.NewParser(h, b) 42 | if err != nil { 43 | h.Error(err) 44 | return 45 | } 46 | 47 | switch getTagName(p) { 48 | case "api": 49 | if doc.APIs == nil { 50 | doc.APIs = make([]*API, 0, 100) 51 | } 52 | 53 | api := &API{doc: doc} 54 | xmlenc.Decode(p, api, core.XMLNamespace) 55 | doc.APIs = append(doc.APIs, api) 56 | 57 | if doc.Title.V() != "" { // apidoc 已经初始化,检测依赖于 apidoc 的字段 58 | api.sanitizeTags(p) 59 | } 60 | case "apidoc": 61 | if doc.Title != nil { // 多个 apidoc 标签 62 | err := b.Location.NewError(locale.ErrDuplicateValue).WithField("apidoc"). 63 | Relate(doc.Location, locale.Sprintf(locale.ErrDuplicateValue)) 64 | h.Error(err) 65 | return 66 | } 67 | xmlenc.Decode(p, doc, core.XMLNamespace) 68 | default: 69 | return 70 | } 71 | 72 | // api 进入 doc 的顺序是未知的,进行排序可以保证文档的顺序一致。 73 | doc.sortAPIs() 74 | } 75 | 76 | // 简单预判是否是一个合规的 apidoc 内容 77 | func isValid(b core.Block) bool { 78 | bs := bytes.TrimSpace(b.Data) 79 | if len(bs) < minSize { 80 | return false 81 | } 82 | 83 | // 去除空格之后,必须保证以 < 开头,且不能以 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | tag1 11 | tag2 12 | admin 13 | client 14 | 15 | 16 | 这是描述信息

18 | ]]> 19 |
20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | 34 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /internal/ast/testdata/doc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | title 5 | 6 | 7 | test@example.com 8 | https://example.com 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | client api

20 | ]]> 21 |
22 |
23 | 24 |
25 |
26 | 27 | application/xml 28 | application/json 29 | 30 | 31 | h2 33 |

h3

34 | ]]> 35 |
36 | 37 | -------------------------------------------------------------------------------- /internal/cmd/build.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package cmd 4 | 5 | import ( 6 | "io" 7 | "time" 8 | 9 | "github.com/issue9/cmdopt" 10 | 11 | "github.com/caixw/apidoc/v7/build" 12 | "github.com/caixw/apidoc/v7/core" 13 | "github.com/caixw/apidoc/v7/internal/locale" 14 | ) 15 | 16 | var buildDir = uri("./") 17 | 18 | func initBuild(command *cmdopt.CmdOpt) { 19 | fs := command.New("build", locale.Sprintf(locale.CmdBuildUsage), doBuild) 20 | fs.Var(&buildDir, "d", locale.Sprintf(locale.FlagBuildDirUsage)) 21 | } 22 | 23 | func doBuild(io.Writer) error { 24 | start := time.Now() 25 | 26 | cfg, err := build.LoadConfig(core.URI(buildDir)) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | h := core.NewMessageHandler(messageHandle) 32 | defer h.Stop() 33 | 34 | cfg.Build(h) 35 | h.Locale(core.Info, locale.Complete, cfg.Output.Path, time.Since(start)) 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /internal/cmd/cmd.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | // Package cmd 提供子命令的相关功能 4 | package cmd 5 | 6 | import ( 7 | "flag" 8 | "fmt" 9 | "io" 10 | "os" 11 | 12 | "github.com/issue9/cmdopt" 13 | "github.com/issue9/term/v3/colors" 14 | "golang.org/x/text/message" 15 | 16 | "github.com/caixw/apidoc/v7/core" 17 | "github.com/caixw/apidoc/v7/internal/locale" 18 | ) 19 | 20 | // 命令行输出的表格中,每一列为了对齐填补的空格数量。 21 | const tail = 3 22 | 23 | var printers = map[core.MessageType]*printer{ 24 | core.Erro: { 25 | out: os.Stderr, 26 | color: colors.Red, 27 | prefix: locale.ErrorPrefix, 28 | }, 29 | core.Warn: { 30 | out: os.Stderr, 31 | color: colors.Cyan, 32 | prefix: locale.WarnPrefix, 33 | }, 34 | core.Info: { 35 | out: os.Stdout, 36 | color: colors.Default, 37 | prefix: locale.InfoPrefix, 38 | }, 39 | core.Succ: { 40 | out: os.Stdout, 41 | color: colors.Green, 42 | prefix: locale.SuccessPrefix, 43 | }, 44 | } 45 | 46 | type printer struct { 47 | out io.Writer 48 | color colors.Color 49 | prefix message.Reference 50 | } 51 | 52 | type uri core.URI 53 | 54 | func (u uri) Get() any { return string(u) } 55 | 56 | func (u *uri) Set(v string) error { 57 | *u = uri(core.FileURI(v)) 58 | return nil 59 | } 60 | 61 | func (u *uri) String() string { return core.URI(*u).String() } 62 | 63 | func (u uri) URI() core.URI { return core.URI(u) } 64 | 65 | // Init 初始化 cmdopt.CmdOpt 实例 66 | func Init(out io.Writer) *cmdopt.CmdOpt { 67 | command := &cmdopt.CmdOpt{ 68 | Output: out, 69 | ErrorHandling: flag.ExitOnError, 70 | Header: locale.Sprintf(locale.CmdUsage, core.Name), 71 | Footer: locale.Sprintf(locale.CmdUsageFooter, core.OfficialURL, core.RepoURL), 72 | OptionsTitle: locale.Sprintf(locale.CmdUsageOptions), 73 | CommandsTitle: locale.Sprintf(locale.CmdUsageCommands), 74 | NotFound: func(name string) string { 75 | return locale.Sprintf(locale.CmdNotFound, name) 76 | }, 77 | } 78 | 79 | command.Help("help", locale.Sprintf(locale.CmdHelpUsage)) 80 | initBuild(command) 81 | initDetect(command) 82 | initLang(command) 83 | initLocale(command) 84 | initSyntax(command) 85 | initVersion(command) 86 | initMock(command) 87 | initStatic(command) 88 | initLSP(command) 89 | 90 | return command 91 | } 92 | 93 | func messageHandle(msg *core.Message) { 94 | printers[msg.Type].print(msg.Message) 95 | } 96 | 97 | func (p *printer) print(msg any) { 98 | if _, err := colors.Fprint(p.out, colors.Normal, p.color, colors.Default, locale.New(p.prefix)); err != nil { 99 | panic(err) 100 | } 101 | 102 | if _, err := fmt.Fprintln(p.out, msg); err != nil { 103 | panic(err) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /internal/cmd/cmd_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package cmd 4 | 5 | import ( 6 | "bytes" 7 | "flag" 8 | "testing" 9 | 10 | "github.com/issue9/assert/v3" 11 | "github.com/issue9/term/v3/colors" 12 | 13 | "github.com/caixw/apidoc/v7/core" 14 | "github.com/caixw/apidoc/v7/internal/locale" 15 | ) 16 | 17 | var ( 18 | uuu = uri("") 19 | _ flag.Getter = &uuu 20 | ) 21 | 22 | func resetPrinters() (erro, warn, succ, info *bytes.Buffer) { 23 | erro = new(bytes.Buffer) 24 | warn = new(bytes.Buffer) 25 | succ = new(bytes.Buffer) 26 | info = new(bytes.Buffer) 27 | 28 | printers = map[core.MessageType]*printer{ 29 | core.Erro: { 30 | out: erro, 31 | color: colors.Red, 32 | prefix: locale.ErrorPrefix, 33 | }, 34 | core.Warn: { 35 | out: warn, 36 | color: colors.Cyan, 37 | prefix: locale.WarnPrefix, 38 | }, 39 | core.Info: { 40 | out: info, 41 | color: colors.Default, 42 | prefix: locale.InfoPrefix, 43 | }, 44 | core.Succ: { 45 | out: succ, 46 | color: colors.Green, 47 | prefix: locale.SuccessPrefix, 48 | }, 49 | } 50 | 51 | return 52 | } 53 | 54 | func TestMessageHandle(t *testing.T) { 55 | a := assert.New(t, false) 56 | 57 | erro := new(bytes.Buffer) 58 | warn := new(bytes.Buffer) 59 | info := new(bytes.Buffer) 60 | succ := new(bytes.Buffer) 61 | 62 | printers[core.Erro].out = erro 63 | printers[core.Warn].out = warn 64 | printers[core.Info].out = info 65 | printers[core.Succ].out = succ 66 | 67 | h := core.NewMessageHandler(messageHandle) 68 | a.NotNil(h) 69 | 70 | h.Locale(core.Erro, "erro") 71 | h.Locale(core.Warn, "warn") 72 | h.Locale(core.Info, "info") 73 | h.Locale(core.Succ, "succ") 74 | h.Stop() 75 | 76 | a.Contains(erro.String(), "erro") 77 | a.Contains(warn.String(), "warn") 78 | a.Contains(info.String(), "info") 79 | a.Contains(succ.String(), "succ") 80 | } 81 | -------------------------------------------------------------------------------- /internal/cmd/detect.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package cmd 4 | 5 | import ( 6 | "fmt" 7 | "io" 8 | 9 | "github.com/issue9/cmdopt" 10 | "gopkg.in/yaml.v3" 11 | 12 | "github.com/caixw/apidoc/v7/build" 13 | "github.com/caixw/apidoc/v7/core" 14 | "github.com/caixw/apidoc/v7/internal/locale" 15 | ) 16 | 17 | var ( 18 | detectRecursive bool 19 | detectWrite bool 20 | detectDir = uri("./") 21 | ) 22 | 23 | func initDetect(command *cmdopt.CmdOpt) { 24 | fs := command.New("detect", locale.Sprintf(locale.CmdDetectUsage), detect) 25 | fs.BoolVar(&detectRecursive, "r", true, locale.Sprintf(locale.FlagDetectRecursiveUsage)) 26 | fs.BoolVar(&detectWrite, "w", false, locale.Sprintf(locale.FlagDetectWrite)) 27 | fs.Var(&buildDir, "d", locale.Sprintf(locale.FlagDetectDirUsage)) 28 | } 29 | 30 | func detect(w io.Writer) error { 31 | h := core.NewMessageHandler(messageHandle) 32 | defer h.Stop() 33 | 34 | dir := detectDir.URI() 35 | cfg, err := build.DetectConfig(dir, detectRecursive) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | if !detectWrite { 41 | data, err := yaml.Marshal(cfg) 42 | if err != nil { 43 | return err 44 | } 45 | _, err = fmt.Fprint(w, string(data)) 46 | return err 47 | } 48 | 49 | if err = cfg.Save(dir); err != nil { 50 | return err 51 | } 52 | h.Locale(core.Succ, locale.ConfigWriteSuccess, dir) 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /internal/cmd/detect_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package cmd 4 | 5 | import ( 6 | "bytes" 7 | "testing" 8 | 9 | "github.com/issue9/assert/v3" 10 | "gopkg.in/yaml.v3" 11 | 12 | "github.com/caixw/apidoc/v7/build" 13 | "github.com/caixw/apidoc/v7/internal/ast" 14 | "github.com/caixw/apidoc/v7/internal/docs" 15 | ) 16 | 17 | func TestCmdDetect(t *testing.T) { 18 | a := assert.New(t, false) 19 | 20 | buf := new(bytes.Buffer) 21 | path := docs.Dir().Append("example") 22 | 23 | cmd := Init(buf) 24 | resetPrinters() 25 | err := cmd.Exec([]string{"detect", "-d", path.String()}) 26 | a.NotError(err) 27 | cfg := &build.Config{} 28 | a.NotError(yaml.Unmarshal(buf.Bytes(), cfg)) 29 | a.Equal(cfg.Version, ast.Version) 30 | 31 | cmd = Init(buf) 32 | resetPrinters() 33 | err = cmd.Exec([]string{"detect", "-d", path.String(), "-w"}) 34 | a.NotError(err) 35 | cfg2 := &build.Config{} 36 | data, err := build.LoadConfig(path) 37 | a.NotError(err).NotNil(data) 38 | a.NotError(yaml.Unmarshal(buf.Bytes(), cfg2)) 39 | a.Equal(cfg, cfg2) 40 | } 41 | -------------------------------------------------------------------------------- /internal/cmd/lang.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package cmd 4 | 5 | import ( 6 | "fmt" 7 | "io" 8 | "strings" 9 | 10 | "github.com/issue9/cmdopt" 11 | "golang.org/x/text/width" 12 | 13 | "github.com/caixw/apidoc/v7/internal/lang" 14 | "github.com/caixw/apidoc/v7/internal/locale" 15 | ) 16 | 17 | func initLang(command *cmdopt.CmdOpt) { 18 | command.New("lang", locale.Sprintf(locale.CmdLangUsage), doLang) 19 | } 20 | 21 | func doLang(w io.Writer) error { 22 | ls := lang.Langs() 23 | langs := make([]*lang.Language, 1, len(ls)+1) 24 | langs[0] = &lang.Language{ 25 | ID: locale.Sprintf(locale.LangID), 26 | DisplayName: locale.Sprintf(locale.LangName), 27 | Exts: []string{locale.Sprintf(locale.LangExts)}, 28 | } 29 | langs = append(langs, ls...) 30 | 31 | // 计算各列的最大长度值 32 | var maxName, maxID int 33 | for _, l := range langs { 34 | calcMaxWidth(l.DisplayName, &maxName) 35 | calcMaxWidth(l.ID, &maxID) 36 | } 37 | maxName += tail 38 | maxID += tail 39 | 40 | for _, l := range langs { 41 | id := l.ID + strings.Repeat(" ", maxID-textWidth(l.ID)) 42 | name := l.DisplayName + strings.Repeat(" ", maxName-textWidth(l.DisplayName)) 43 | if _, err := fmt.Fprintln(w, id, name, strings.Join(l.Exts, " ")); err != nil { 44 | return err 45 | } 46 | } 47 | 48 | return nil 49 | } 50 | 51 | func calcMaxWidth(content string, max *int) { 52 | if w := textWidth(content); w > *max { 53 | *max = w 54 | } 55 | } 56 | 57 | func textWidth(text string) (w int) { 58 | for _, r := range text { 59 | switch width.LookupRune(rune(r)).Kind() { 60 | case width.EastAsianFullwidth, width.EastAsianWide: 61 | w += 2 62 | default: 63 | w++ 64 | } 65 | } 66 | return 67 | } 68 | -------------------------------------------------------------------------------- /internal/cmd/lang_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package cmd 4 | 5 | import ( 6 | "bufio" 7 | "bytes" 8 | "io" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/issue9/assert/v3" 13 | 14 | "github.com/caixw/apidoc/v7/internal/lang" 15 | ) 16 | 17 | func TestDoLang(t *testing.T) { 18 | a := assert.New(t, false) 19 | w := new(bytes.Buffer) 20 | 21 | lines := func(w *bytes.Buffer) []string { 22 | b := bufio.NewReader(w) 23 | lines := make([]string, 0, 100) 24 | for line, err := b.ReadString('\n'); err != io.EOF; line, err = b.ReadString('\n') { 25 | lines = append(lines, line) 26 | } 27 | 28 | return lines 29 | } 30 | 31 | a.NotError(doLang(w)) 32 | ls := lines(w) 33 | a.Equal(len(ls), len(lang.Langs())+1) 34 | for _, l := range ls { 35 | cnt := strings.Count(l, strings.Repeat(" ", tail)) 36 | a.True(cnt >= 2) // 至少三列 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internal/cmd/locale.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package cmd 4 | 5 | import ( 6 | "fmt" 7 | "io" 8 | "strings" 9 | 10 | "github.com/issue9/cmdopt" 11 | "golang.org/x/text/language/display" 12 | 13 | "github.com/caixw/apidoc/v7" 14 | "github.com/caixw/apidoc/v7/internal/locale" 15 | ) 16 | 17 | func initLocale(command *cmdopt.CmdOpt) { 18 | command.New("locale", locale.Sprintf(locale.CmdLocaleUsage), doLocale) 19 | } 20 | 21 | func doLocale(w io.Writer) error { 22 | locales := make(map[string]string, len(apidoc.Locales())) 23 | 24 | // 计算各列的最大长度值 25 | var maxID int 26 | for _, tag := range apidoc.Locales() { 27 | id := tag.String() 28 | calcMaxWidth(id, &maxID) 29 | locales[id] = display.Self.Name(tag) 30 | } 31 | maxID += tail 32 | 33 | for k, v := range locales { 34 | id := k + strings.Repeat(" ", maxID-len(k)) 35 | if _, err := fmt.Fprintln(w, id, v); err != nil { 36 | return err 37 | } 38 | } 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/cmd/locale_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package cmd 4 | 5 | import ( 6 | "bufio" 7 | "bytes" 8 | "io" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/issue9/assert/v3" 13 | 14 | "github.com/caixw/apidoc/v7" 15 | ) 16 | 17 | func TestLocale(t *testing.T) { 18 | a := assert.New(t, false) 19 | w := new(bytes.Buffer) 20 | 21 | lines := func(w *bytes.Buffer) []string { 22 | b := bufio.NewReader(w) 23 | lines := make([]string, 0, 100) 24 | for line, err := b.ReadString('\n'); err != io.EOF; line, err = b.ReadString('\n') { 25 | lines = append(lines, line) 26 | } 27 | 28 | return lines 29 | } 30 | 31 | a.NotError(doLocale(w)) 32 | ls := lines(w) 33 | a.Equal(len(ls), len(apidoc.Locales())) 34 | for _, l := range ls { 35 | cnt := strings.Count(l, strings.Repeat(" ", tail)) 36 | a.True(cnt >= 1) // 至少两列 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internal/cmd/lsp.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package cmd 4 | 5 | import ( 6 | "io" 7 | "log" 8 | "time" 9 | 10 | "github.com/issue9/cmdopt" 11 | 12 | "github.com/caixw/apidoc/v7" 13 | "github.com/caixw/apidoc/v7/internal/locale" 14 | ) 15 | 16 | var ( 17 | lspPort string 18 | lspMode string 19 | lspHeader bool 20 | lspTimeout time.Duration 21 | ) 22 | 23 | func initLSP(command *cmdopt.CmdOpt) { 24 | ls := command.New("lsp", locale.Sprintf(locale.CmdLSPUsage), doLSP) 25 | ls.StringVar(&lspPort, "p", ":8080", locale.Sprintf(locale.FlagLSPPortUsage)) 26 | ls.StringVar(&lspMode, "m", "stdio", locale.Sprintf(locale.FlagLSPModeUsage)) 27 | ls.BoolVar(&lspHeader, "h", false, locale.Sprintf(locale.FlagLSPHeaderUsage)) 28 | ls.DurationVar(&lspTimeout, "t", time.Second, locale.Sprintf(locale.FlagLSPTimeoutUsage)) 29 | } 30 | 31 | func doLSP(o io.Writer) error { 32 | return apidoc.ServeLSP(lspHeader, lspMode, lspPort, lspTimeout, log.New(o, "", 0), log.New(o, "", 0)) 33 | } 34 | -------------------------------------------------------------------------------- /internal/cmd/mock_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package cmd 4 | 5 | import ( 6 | "flag" 7 | "testing" 8 | "time" 9 | 10 | "github.com/issue9/assert/v3" 11 | ) 12 | 13 | var ( 14 | _ flag.Getter = servers{} 15 | _ flag.Getter = &slice{} 16 | _ flag.Getter = &size{} 17 | _ flag.Getter = &dateRange{} 18 | ) 19 | 20 | func TestServers_Set(t *testing.T) { 21 | a := assert.New(t, false) 22 | 23 | srv := servers{} 24 | a.Equal(srv, srv.Get()) 25 | a.Equal(0, len(srv)) 26 | 27 | a.Error(srv.Set("")) 28 | a.Equal(0, len(srv)) 29 | a.Equal(srv.String(), "") 30 | 31 | a.NotError(srv.Set("k1=v1")) 32 | a.Equal(1, len(srv)) 33 | a.Equal(srv["k1"], "v1") 34 | a.NotEmpty(srv.String()) 35 | 36 | a.NotError(srv.Set("k1=v1,k2=v2")) 37 | a.Equal(2, len(srv)) 38 | a.Equal(srv["k1"], "v1") 39 | a.NotEmpty(srv.String()) 40 | 41 | a.NotError(srv.Set("k1= v1, k2= v2")) 42 | a.Equal(2, len(srv)) 43 | a.Equal(srv["k1"], " v1") 44 | a.Equal(srv["k2"], " v2") 45 | a.NotEmpty(srv.String()) 46 | } 47 | 48 | func TestSize_Set(t *testing.T) { 49 | a := assert.New(t, false) 50 | 51 | s := &size{} 52 | a.Error(s.Set("")) 53 | 54 | a.Error(s.Set(",")) 55 | a.Error(s.Set("1,")) 56 | a.Error(s.Set(",5")) 57 | 58 | a.NotError(s.Set("1,5")) 59 | a.Equal(s.Min, 1).Equal(s.Max, 5) 60 | } 61 | 62 | func TestDateRange_Set(t *testing.T) { 63 | a := assert.New(t, false) 64 | 65 | d := &dateRange{} 66 | a.Error(d.Set("")) 67 | 68 | a.Error(d.Set(",")) 69 | a.Error(d.Set("2020-01-02T17:04:15+01:00,")) 70 | a.Error(d.Set(",2020-01-07T17:04:15+01:00")) 71 | 72 | a.NotError(d.Set("2020-01-02T17:04:15+01:00,2020-01-05T17:04:15+01:00")) 73 | start, err := time.Parse(time.RFC3339, "2020-01-02T17:04:15+01:00") 74 | a.NotError(err) 75 | end, err := time.Parse(time.RFC3339, "2020-01-05T17:04:15+01:00") 76 | a.NotError(err) 77 | a.Equal(d.start, start).Equal(d.end, end) 78 | } 79 | 80 | func TestSlice_Set(t *testing.T) { 81 | a := assert.New(t, false) 82 | 83 | s := &slice{} 84 | a.NotError(s.Set("")).Equal(1, len(*s)) 85 | a.NotError(s.Set(",")).Equal(2, len(*s)) 86 | 87 | } 88 | -------------------------------------------------------------------------------- /internal/cmd/static.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package cmd 4 | 5 | import ( 6 | "io" 7 | "log" 8 | "net/http" 9 | 10 | "github.com/issue9/cmdopt" 11 | 12 | "github.com/caixw/apidoc/v7" 13 | "github.com/caixw/apidoc/v7/core" 14 | "github.com/caixw/apidoc/v7/internal/locale" 15 | ) 16 | 17 | var ( 18 | staticPort string 19 | staticDocs uri 20 | staticStylesheet bool 21 | staticContentType string 22 | staticURL string 23 | staticPath uri 24 | ) 25 | 26 | func initStatic(command *cmdopt.CmdOpt) { 27 | fs := command.New("static", locale.Sprintf(locale.CmdStaticUsage), static) 28 | fs.StringVar(&staticPort, "p", ":8080", locale.Sprintf(locale.FlagStaticPortUsage)) 29 | fs.Var(&staticDocs, "docs", locale.Sprintf(locale.FlagStaticDocsUsage)) 30 | fs.StringVar(&staticContentType, "ct", "", locale.Sprintf(locale.FlagStaticContentTypeUsage)) 31 | fs.StringVar(&staticURL, "url", "", locale.Sprintf(locale.FlagStaticURLUsage)) 32 | fs.BoolVar(&staticStylesheet, "stylesheet", false, locale.Sprintf(locale.FlagStaticStylesheetUsage)) 33 | fs.Var(&staticPath, "path", locale.Sprintf(locale.FlagStaticPathUsage)) 34 | } 35 | 36 | func static(io.Writer) (err error) { 37 | path := core.URI(staticPath) 38 | h := core.NewMessageHandler(messageHandle) 39 | defer h.Stop() 40 | 41 | var handler http.Handler 42 | 43 | if path == "" { 44 | handler = apidoc.Static(staticDocs.URI(), staticStylesheet, log.Default()) 45 | } else { 46 | 47 | s := &apidoc.Server{ 48 | Status: http.StatusOK, 49 | Path: staticURL, 50 | ContentType: staticContentType, 51 | Dir: staticDocs.URI(), 52 | Stylesheet: staticStylesheet, 53 | } 54 | handler, err = s.File(path) 55 | if err != nil { 56 | return err 57 | } 58 | } 59 | 60 | h.Locale(core.Succ, locale.ServerStart, staticPort) 61 | 62 | return http.ListenAndServe(staticPort, handler) 63 | } 64 | -------------------------------------------------------------------------------- /internal/cmd/syntax.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package cmd 4 | 5 | import ( 6 | "io" 7 | 8 | "github.com/issue9/cmdopt" 9 | 10 | "github.com/caixw/apidoc/v7/build" 11 | "github.com/caixw/apidoc/v7/core" 12 | "github.com/caixw/apidoc/v7/internal/locale" 13 | ) 14 | 15 | var syntaxDir uri = uri(core.FileURI("./")) 16 | 17 | func initSyntax(command *cmdopt.CmdOpt) { 18 | fs := command.New("syntax", locale.Sprintf(locale.CmdSyntaxUsage), syntax) 19 | fs.Var(&syntaxDir, "d", locale.Sprintf(locale.FlagSyntaxDirUsage)) 20 | } 21 | 22 | func syntax(w io.Writer) error { 23 | cfg, err := build.LoadConfig(syntaxDir.URI()) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | h := core.NewMessageHandler(messageHandle) 29 | defer h.Stop() 30 | 31 | cfg.CheckSyntax(h) 32 | h.Locale(core.Succ, locale.TestSuccess) 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /internal/cmd/syntax_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package cmd 4 | 5 | import ( 6 | "bytes" 7 | "testing" 8 | 9 | "github.com/issue9/assert/v3" 10 | 11 | "github.com/caixw/apidoc/v7/internal/docs" 12 | ) 13 | 14 | func TestCmdCheckSyntax(t *testing.T) { 15 | a := assert.New(t, false) 16 | 17 | buf := new(bytes.Buffer) 18 | cmd := Init(buf) 19 | erro, _, succ, _ := resetPrinters() 20 | err := cmd.Exec([]string{"syntax", "-d", docs.Dir().Append("example").String()}) 21 | a.NotError(err) 22 | a.Empty(buf.String()). 23 | Empty(erro.String()). 24 | NotEmpty(succ.String()) 25 | } 26 | -------------------------------------------------------------------------------- /internal/cmd/version.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package cmd 4 | 5 | import ( 6 | "fmt" 7 | "io" 8 | "runtime" 9 | "strings" 10 | 11 | "github.com/issue9/cmdopt" 12 | 13 | "github.com/caixw/apidoc/v7" 14 | "github.com/caixw/apidoc/v7/internal/locale" 15 | "github.com/caixw/apidoc/v7/internal/openapi" 16 | ) 17 | 18 | var versionKind string 19 | 20 | func initVersion(command *cmdopt.CmdOpt) { 21 | fs := command.New("version", locale.Sprintf(locale.CmdVersionUsage), version) 22 | fs.StringVar(&versionKind, "kind", "all", locale.FlagVersionKindUsage) 23 | } 24 | 25 | func version(w io.Writer) error { 26 | var msg string 27 | switch strings.ToLower(versionKind) { 28 | case "doc": 29 | msg = apidoc.DocVersion 30 | case "lsp": 31 | msg = apidoc.LSPVersion 32 | case "openapi": 33 | msg = openapi.LatestVersion 34 | case "apidoc": 35 | msg = apidoc.Version(true) 36 | default: // all 与 default 采取相同的输出 37 | goVersion := strings.TrimLeft(runtime.Version(), "go") 38 | msg = locale.Sprintf(locale.Version, apidoc.Version(true), apidoc.DocVersion, apidoc.LSPVersion, openapi.LatestVersion, goVersion) 39 | } 40 | _, err := fmt.Fprintln(w, msg) 41 | return err 42 | } 43 | -------------------------------------------------------------------------------- /internal/cmd/version_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package cmd 4 | 5 | import ( 6 | "bytes" 7 | "testing" 8 | 9 | "github.com/issue9/assert/v3" 10 | 11 | "github.com/caixw/apidoc/v7" 12 | "github.com/caixw/apidoc/v7/internal/openapi" 13 | ) 14 | 15 | func TestCmdVersion(t *testing.T) { 16 | a := assert.New(t, false) 17 | 18 | buf := new(bytes.Buffer) 19 | cmd := Init(buf) 20 | resetPrinters() 21 | a.NotError(cmd.Exec([]string{"version"})) 22 | a.Contains(buf.String(), apidoc.LSPVersion). 23 | Contains(buf.String(), apidoc.DocVersion). 24 | Contains(buf.String(), apidoc.Version(true)) 25 | 26 | buf.Reset() 27 | cmd = Init(buf) 28 | resetPrinters() 29 | a.NotError(cmd.Exec([]string{"version", "-kind", "apidoc"})) 30 | a.Equal(buf.String(), apidoc.Version(true)+"\n") 31 | 32 | buf.Reset() 33 | cmd = Init(buf) 34 | resetPrinters() 35 | a.NotError(cmd.Exec([]string{"version", "-kind", "lsp"})) 36 | a.Equal(buf.String(), apidoc.LSPVersion+"\n") 37 | 38 | buf.Reset() 39 | cmd = Init(buf) 40 | resetPrinters() 41 | a.NotError(cmd.Exec([]string{"version", "-kind", "doc"})) 42 | a.Equal(buf.String(), apidoc.DocVersion+"\n") 43 | 44 | buf.Reset() 45 | cmd = Init(buf) 46 | resetPrinters() 47 | a.NotError(cmd.Exec([]string{"version", "-kind", "openapi"})) 48 | a.Equal(buf.String(), openapi.LatestVersion+"\n") 49 | } 50 | -------------------------------------------------------------------------------- /internal/docs/gen.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | //go:generate go run ./make_site.go 4 | //go:generate go run ../../cmd/apidoc/ build -d=../../docs/example 5 | 6 | package docs 7 | -------------------------------------------------------------------------------- /internal/docs/make_site.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | //go:build ignore 4 | // +build ignore 5 | 6 | package main 7 | 8 | import ( 9 | "github.com/caixw/apidoc/v7/internal/docs" 10 | "github.com/caixw/apidoc/v7/internal/docs/site" 11 | ) 12 | 13 | func main() { 14 | if err := site.Write(docs.Dir()); err != nil { 15 | panic(err) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /internal/docs/site/command.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package site 4 | 5 | import ( 6 | "bytes" 7 | "io" 8 | 9 | "github.com/caixw/apidoc/v7/internal/cmd" 10 | ) 11 | 12 | func (d *doc) newCommands() error { 13 | out := new(bytes.Buffer) 14 | opt := cmd.Init(out) 15 | names := opt.Commands() 16 | 17 | for _, name := range names { 18 | out.Reset() 19 | if err := opt.Exec([]string{"help", name}); err != nil { 20 | return err 21 | } 22 | 23 | usage, err := out.ReadString('\n') 24 | if err != nil && err != io.EOF { 25 | return err 26 | } 27 | 28 | if usage[len(usage)-1] == '\n' { // 去掉换行符 29 | usage = usage[:len(usage)-1] 30 | } 31 | d.Commands = append(d.Commands, &command{ 32 | Name: name, 33 | Usage: usage, 34 | }) 35 | } 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /internal/docs/site/config.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package site 4 | 5 | import ( 6 | "fmt" 7 | "reflect" 8 | "strings" 9 | "unicode" 10 | 11 | "github.com/caixw/apidoc/v7/build" 12 | "github.com/caixw/apidoc/v7/internal/locale" 13 | ) 14 | 15 | func (d *doc) newConfig() error { 16 | return d.buildConfigObject("", reflect.TypeOf(build.Config{})) 17 | } 18 | 19 | func (d *doc) buildConfigItem(parent string, f reflect.StructField) error { 20 | name, omitempty := parseTag(f) 21 | if parent != "" { 22 | name = parent + "." + name 23 | } 24 | 25 | t := f.Type 26 | for t.Kind() == reflect.Ptr { 27 | t = t.Elem() 28 | } 29 | 30 | var array bool 31 | if t.Kind() == reflect.Array || t.Kind() == reflect.Slice { 32 | array = true 33 | t = t.Elem() 34 | for t.Kind() == reflect.Ptr { 35 | t = t.Elem() 36 | } 37 | } 38 | 39 | typeName := t.Kind().String() 40 | if t.Kind() == reflect.Struct { 41 | typeName = "object" 42 | } 43 | 44 | d.Config = append(d.Config, &item{ 45 | Name: name, 46 | Type: typeName, 47 | Array: array, 48 | Required: !omitempty, 49 | Usage: locale.Sprintf("usage-config-" + name), 50 | }) 51 | 52 | if isPrimitive(t) { 53 | return nil 54 | } else if t.Kind() != reflect.Struct { 55 | panic(fmt.Sprintf("字段 %s 的类型 %s 无法处理", f.Name, t.Kind())) 56 | } 57 | 58 | return d.buildConfigObject(name, t) 59 | } 60 | 61 | // 调用方需要保证 t.Kind() 为 reflect.Struct 62 | func (d *doc) buildConfigObject(parent string, t reflect.Type) error { 63 | for i := 0; i < t.NumField(); i++ { 64 | f := t.Field(i) 65 | if !unicode.IsUpper(rune(f.Name[0])) || f.Tag.Get("yaml") == "-" { 66 | continue 67 | } 68 | if err := d.buildConfigItem(parent, f); err != nil { 69 | return err 70 | } 71 | } 72 | return nil 73 | } 74 | 75 | func isPrimitive(t reflect.Type) bool { 76 | return t.Kind() == reflect.String || (t.Kind() >= reflect.Bool && t.Kind() <= reflect.Complex128) 77 | } 78 | 79 | func parseTag(f reflect.StructField) (string, bool) { 80 | tag := f.Tag.Get("yaml") 81 | if tag == "" { 82 | return f.Name, false 83 | } 84 | 85 | prop := strings.Split(tag, ",") 86 | if len(prop) == 1 { 87 | return getName(prop[0], f), false 88 | } 89 | 90 | return getName(prop[0], f), strings.TrimSpace(prop[1]) == "omitempty" 91 | } 92 | 93 | func getName(n string, f reflect.StructField) string { 94 | if n = strings.TrimSpace(n); n != "" { 95 | return n 96 | } 97 | return f.Name 98 | } 99 | -------------------------------------------------------------------------------- /internal/docs/site/config_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package site 4 | 5 | import ( 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/issue9/assert/v3" 10 | ) 11 | 12 | func TestParseTag(t *testing.T) { 13 | a := assert.New(t, false) 14 | 15 | name, omitempty := parseTag(reflect.StructField{Name: "F1"}) 16 | a.Equal(name, "F1").False(omitempty) 17 | 18 | name, omitempty = parseTag(reflect.StructField{Name: "F1", Tag: reflect.StructTag(`apidoc:"xx"`)}) 19 | a.Equal(name, "F1").False(omitempty) 20 | 21 | name, omitempty = parseTag(reflect.StructField{Name: "F1", Tag: reflect.StructTag(`yaml:"xx"`)}) 22 | a.Equal(name, "xx").False(omitempty) 23 | 24 | name, omitempty = parseTag(reflect.StructField{Name: "F1", Tag: reflect.StructTag(`yaml:"xx,omitempty"`)}) 25 | a.Equal(name, "xx").True(omitempty) 26 | 27 | name, omitempty = parseTag(reflect.StructField{Name: "F1", Tag: reflect.StructTag(`yaml:",omitempty"`)}) 28 | a.Equal(name, "F1").True(omitempty) 29 | 30 | name, omitempty = parseTag(reflect.StructField{Name: "F1", Tag: reflect.StructTag(`yaml:"xx,omit"`)}) 31 | a.Equal(name, "xx").False(omitempty) 32 | } 33 | -------------------------------------------------------------------------------- /internal/docs/site/site_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package site 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/issue9/assert/v3" 9 | 10 | "github.com/caixw/apidoc/v7/internal/lang" 11 | "github.com/caixw/apidoc/v7/internal/locale" 12 | ) 13 | 14 | func TestGen(t *testing.T) { 15 | a := assert.New(t, false) 16 | 17 | site, docs, err := gen() 18 | a.NotError(err). 19 | NotNil(site). 20 | NotNil(docs) 21 | 22 | a.Equal(len(site.Languages), len(lang.Langs())). 23 | Equal(len(site.Locales), len(locale.Tags())) 24 | 25 | a.Equal(len(docs), len(locale.Tags())) 26 | 27 | defLocale := docs[buildDocFilename(locale.DefaultLocaleID)] 28 | for _, cmd := range defLocale.Commands { 29 | if cmd.Name == "build" { 30 | a.Contains(locale.Translate(locale.DefaultLocaleID, locale.CmdBuildUsage), cmd.Usage) 31 | } 32 | } 33 | 34 | for _, item := range defLocale.Config { 35 | a.Equal(item.Usage, locale.Translate(locale.DefaultLocaleID, "usage-config-"+item.Name)) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/docs/site/spec.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package site 4 | 5 | import ( 6 | "reflect" 7 | "sort" 8 | 9 | "github.com/caixw/apidoc/v7/internal/locale" 10 | "github.com/caixw/apidoc/v7/internal/node" 11 | ) 12 | 13 | func (d *doc) newSpec(v any) error { 14 | n := node.New("", reflect.ValueOf(v)) 15 | if err := d.dumpToTypes(n); err != nil { 16 | return err 17 | } 18 | 19 | for _, t := range d.Spec { 20 | if len(t.Items) == 1 && t.Items[0].Name == "." { 21 | t.Items = nil 22 | } 23 | } 24 | 25 | sort.SliceStable(d.Spec, func(i, j int) bool { 26 | if len(d.Spec[i].Items) == 0 { 27 | return false 28 | } 29 | return len(d.Spec[j].Items) == 0 30 | }) 31 | 32 | return nil 33 | } 34 | 35 | func (d *doc) dumpToTypes(n *node.Node) error { 36 | t := &spec{ 37 | Name: n.TypeName, 38 | Usage: innerXML{Text: locale.Sprintf(n.Value.Usage)}, 39 | Items: make([]*item, 0, len(n.Attributes)+len(n.Elements)), 40 | } 41 | d.Spec = append(d.Spec, t) // 保证子元素在后显示 42 | 43 | for _, attr := range n.Attributes { 44 | appendItem(t, "@"+attr.Name, attr.Value, attr.Usage, !attr.Omitempty) 45 | 46 | if nn := node.New(attr.Name, attr.Value); nn.TypeName != "" && !d.typeExists(nn.TypeName) { 47 | if err := d.dumpToTypes(nn); err != nil { 48 | return err 49 | } 50 | } 51 | } 52 | 53 | for _, elem := range n.Elements { 54 | appendItem(t, elem.Name, elem.Value, elem.Usage, !elem.Omitempty) 55 | 56 | typ := node.RealType(elem.Type()) 57 | v := node.RealValue(elem.Value) 58 | 59 | if typ.Kind() == reflect.Slice || typ.Kind() == reflect.Array { 60 | typ = node.RealType(typ.Elem()) 61 | v = reflect.New(typ).Elem() 62 | } 63 | 64 | if nn := node.New(elem.Name, v); nn.TypeName != "" && !d.typeExists(nn.TypeName) { 65 | if err := d.dumpToTypes(nn); err != nil { 66 | return err 67 | } 68 | } 69 | } 70 | 71 | if n.CData != nil { 72 | appendItem(t, ".", n.CData.Value, n.CData.Usage, !n.CData.Omitempty) 73 | } 74 | 75 | if n.Content != nil { 76 | appendItem(t, ".", n.Content.Value, n.Content.Usage, !n.Content.Omitempty) 77 | } 78 | 79 | return nil 80 | } 81 | 82 | func appendItem(t *spec, name string, v reflect.Value, usageKey string, req bool) { 83 | var isSlice bool 84 | typ := node.RealValue(v).Type() 85 | for typ.Kind() == reflect.Slice || typ.Kind() == reflect.Array { 86 | isSlice = true 87 | typ = typ.Elem() 88 | } 89 | 90 | tt := typ.Name() 91 | if vv := node.ParseValue(reflect.New(typ).Elem()); vv != nil { 92 | tt = vv.Name 93 | } 94 | t.Items = append(t.Items, &item{ 95 | Name: name, 96 | Type: tt, 97 | Required: req, 98 | Array: isSlice, 99 | Usage: locale.Sprintf(usageKey), 100 | }) 101 | } 102 | 103 | func (d *doc) typeExists(typeName string) bool { 104 | for _, t := range d.Spec { 105 | if t.Name == typeName { 106 | return true 107 | } 108 | } 109 | return false 110 | } 111 | -------------------------------------------------------------------------------- /internal/docs/site/spec_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package site 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/issue9/assert/v3" 9 | 10 | "github.com/caixw/apidoc/v7/internal/locale" 11 | ) 12 | 13 | type ( 14 | objectTag struct { 15 | RootName struct{} `apidoc:"apidoc,meta,usage-root"` 16 | ID intAttr `apidoc:"id,attr,usage"` 17 | Name stringTag `apidoc:"name,elem,usage"` 18 | } 19 | 20 | stringTag struct { 21 | Value string `apidoc:"-"` 22 | RootName struct{} `apidoc:"string,meta,usage-string"` 23 | } 24 | 25 | intAttr struct { 26 | Value int `apidoc:"-"` 27 | RootName struct{} `apidoc:"number,meta,usage-number"` 28 | } 29 | ) 30 | 31 | func TestNewSpec(t *testing.T) { 32 | a := assert.New(t, false) 33 | 34 | ts := &doc{} 35 | err := ts.newSpec(&objectTag{}) 36 | a.NotError(err) 37 | ts2 := &doc{Spec: []*spec{ 38 | { 39 | Name: "apidoc", 40 | Usage: innerXML{Text: locale.Sprintf("usage-root")}, 41 | Items: []*item{ 42 | { 43 | Name: "@id", 44 | Usage: locale.Sprintf("usage"), 45 | Type: "number", 46 | Array: false, 47 | Required: true, 48 | }, 49 | { 50 | Name: "name", 51 | Usage: locale.Sprintf("usage"), 52 | Type: "string", 53 | Array: false, 54 | Required: true, 55 | }, 56 | }, 57 | }, 58 | { 59 | Name: "number", 60 | Usage: innerXML{Text: locale.Sprintf("usage-number")}, 61 | Items: []*item{}, 62 | }, 63 | { 64 | Name: "string", 65 | Usage: innerXML{Text: locale.Sprintf("usage-string")}, 66 | Items: []*item{}, 67 | }, 68 | }} 69 | a.Equal(ts.Spec, ts2.Spec, "not equal\nv1=%#v\nv2=%#v\n", ts.Spec, ts2.Spec) 70 | } 71 | -------------------------------------------------------------------------------- /internal/lang/lang_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package lang 4 | 5 | import ( 6 | "strings" 7 | "testing" 8 | "unicode" 9 | 10 | "github.com/issue9/assert/v3" 11 | ) 12 | 13 | func TestLangs(t *testing.T) { 14 | a := assert.New(t, false) 15 | 16 | isLower := func(str string) bool { 17 | for _, r := range str { 18 | if unicode.IsUpper(r) { 19 | return false 20 | } 21 | } 22 | return true 23 | } 24 | 25 | // Langs() 返回的应该和 langs 有相同的长度 26 | a.Equal(len(Langs()), len(langs)) 27 | 28 | for index, lang := range langs { 29 | a.NotEmpty(lang.ID, "语言名称不能为空,在 %d", index) 30 | a.True(isLower(lang.ID), "名称非小写 %s", lang.ID) 31 | 32 | // 检测 block 33 | a.NotEmpty(lang.blocks, "blocks 不能为空,在 %s", lang.ID) 34 | 35 | // 检测扩展名 36 | for _, ext := range lang.Exts { 37 | a.NotEmpty(ext, "空的扩展名在 %s", lang.ID). 38 | Equal(ext[0], '.', "扩展名 %s 必须以 . 开头在 %s", ext, lang.ID). 39 | Equal(strings.TrimSpace(ext), ext, "扩展名 %s 存在首尾空格", ext, lang.ID). 40 | True(isLower(ext), "非小写的扩展名 %s 在 %s", ext, lang.ID) 41 | } 42 | } 43 | } 44 | 45 | func TestGet(t *testing.T) { 46 | a := assert.New(t, false) 47 | 48 | l := Get("go") 49 | a.NotNil(l). 50 | Equal(l.ID, "go"). 51 | Equal(l.Exts, []string{".go"}) 52 | 53 | // 不比较大小写 54 | l = Get("Go") 55 | a.Nil(l) 56 | } 57 | 58 | func TestGetByExt(t *testing.T) { 59 | a := assert.New(t, false) 60 | 61 | l := GetByExt(".go") 62 | a.NotNil(l).Equal(l.ID, "go") 63 | 64 | l = GetByExt(".cxx") 65 | a.NotNil(l).Equal(l.ID, "c++") 66 | 67 | // 不存在 68 | l = GetByExt(".not-exists") 69 | a.Nil(l) 70 | 71 | a.Panic(func() { 72 | GetByExt("") 73 | }) 74 | 75 | a.Panic(func() { 76 | GetByExt("go") 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /internal/lang/nim.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package lang 4 | 5 | import "bytes" 6 | 7 | type nimRawString struct { 8 | escape, begin1, begin2, end string 9 | } 10 | 11 | type nimMultipleString struct{} 12 | 13 | func newNimRawString() blocker { 14 | return &nimRawString{ 15 | escape: `""`, 16 | begin1: `r"`, 17 | begin2: `R"`, 18 | end: `"`, 19 | } 20 | } 21 | 22 | func (s *nimRawString) beginFunc(l *parser) bool { 23 | return l.Match(s.begin1) || l.Match(s.begin2) 24 | } 25 | 26 | func (s *nimRawString) endFunc(l *parser) (data []byte, ok bool) { 27 | for { 28 | switch { 29 | case l.AtEOF(): 30 | return nil, false 31 | case l.Match(s.escape): // 转义 32 | break 33 | case l.Match(s.end): // 结束 34 | return nil, true 35 | default: 36 | l.Next(1) 37 | } 38 | } // end for 39 | } 40 | 41 | func newNimMultipleString() blocker { 42 | return &nimMultipleString{} 43 | } 44 | 45 | func (s *nimMultipleString) beginFunc(l *parser) bool { 46 | return l.Match(`"""`) 47 | } 48 | 49 | func (s *nimMultipleString) endFunc(l *parser) ([]byte, bool) { 50 | for { 51 | pos := l.Current() 52 | switch { 53 | case l.AtEOF(): 54 | return nil, false 55 | case l.Match(`"""`): 56 | if l.AtEOF() { // 后面没有内容了 57 | return nil, true 58 | } 59 | 60 | bs, ok := l.Delim('\n', true) 61 | if ok && len(bytes.TrimSpace(bs)) == 0 { // """ 后只有空格和回车符 62 | return nil, true 63 | } 64 | l.Move(pos) 65 | l.Next(1) 66 | default: 67 | l.Next(1) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /internal/lang/nim_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package lang 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/issue9/assert/v3" 9 | 10 | "github.com/caixw/apidoc/v7/core" 11 | "github.com/caixw/apidoc/v7/core/messagetest" 12 | ) 13 | 14 | func TestNimRawString(t *testing.T) { 15 | a := assert.New(t, false) 16 | 17 | b := newNimRawString() 18 | a.NotNil(b) 19 | 20 | rslt := messagetest.NewMessageHandler() 21 | l := newParser(rslt.Handler, core.Block{Data: []byte(`r"123""123"`)}, nil) 22 | a.NotNil(l) 23 | rslt.Handler.Stop() 24 | a.Empty(rslt.Errors).NotNil(l) 25 | a.True(b.beginFunc(l)) 26 | data, ok := b.endFunc(l) 27 | a.True(ok). 28 | Equal(len(data), 0) // 不返回内容 29 | bs := l.Next(1) // 继续向后推进,才会 30 | a.Empty(bs).True(l.AtEOF()) // 到达末尾 31 | 32 | rslt = messagetest.NewMessageHandler() 33 | l = newParser(rslt.Handler, core.Block{Data: []byte(`R"123123"`)}, nil) 34 | a.NotNil(l) 35 | rslt.Handler.Stop() 36 | a.Empty(rslt.Errors).NotNil(l) 37 | a.True(b.beginFunc(l)) 38 | data, ok = b.endFunc(l) 39 | a.True(ok). 40 | Empty(data) // 不返回内容 41 | bs = l.Next(1) // 继续向后推进,才会 42 | a.Empty(bs).True(l.AtEOF()) // 到达末尾 43 | } 44 | 45 | func TestNimMultipleString(t *testing.T) { 46 | a := assert.New(t, false) 47 | 48 | b := newNimMultipleString() 49 | a.NotNil(b) 50 | 51 | rslt := messagetest.NewMessageHandler() 52 | l := newParser(rslt.Handler, core.Block{Data: []byte(`"""line1 53 | a.NotNil(l) 54 | line2 55 | """`)}, nil) 56 | rslt.Handler.Stop() 57 | a.Empty(rslt.Errors).NotNil(l) 58 | a.True(b.beginFunc(l)) 59 | data, ok := b.endFunc(l) 60 | a.True(ok).Nil(data) 61 | 62 | // 结束符后带空格 63 | rslt = messagetest.NewMessageHandler() 64 | l = newParser(rslt.Handler, core.Block{Data: []byte(`"""line1 65 | line2 66 | """ 67 | `)}, nil) 68 | a.NotNil(l) 69 | rslt.Handler.Stop() 70 | a.Empty(rslt.Errors).NotNil(l) 71 | a.True(b.beginFunc(l)) 72 | data, ok = b.endFunc(l) 73 | a.True(ok).Nil(data) 74 | 75 | // 结束符后带字符 76 | rslt = messagetest.NewMessageHandler() 77 | l = newParser(rslt.Handler, core.Block{Data: []byte(`"""line1 78 | line2 79 | """ suffix 80 | `)}, nil) 81 | a.NotNil(l) 82 | rslt.Handler.Stop() 83 | a.Empty(rslt.Errors).NotNil(l) 84 | a.True(b.beginFunc(l)) 85 | data, ok = b.endFunc(l) 86 | a.False(ok).Nil(data) 87 | } 88 | -------------------------------------------------------------------------------- /internal/lang/parse.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package lang 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/caixw/apidoc/v7/core" 9 | "github.com/caixw/apidoc/v7/internal/lexer" 10 | "github.com/caixw/apidoc/v7/internal/locale" 11 | ) 12 | 13 | // Parse 分析 data 的内容并输出到到 blocks 14 | func Parse(h *core.MessageHandler, langID string, data core.Block, blocks chan core.Block) { 15 | l := Get(langID) 16 | if l == nil { 17 | panic(fmt.Sprintf("%s 指定的语言解析器并不存在", langID)) 18 | } 19 | 20 | if p := newParser(h, data, l.blocks); p != nil { 21 | p.parse(blocks) 22 | } 23 | } 24 | 25 | type parser struct { 26 | *lexer.Lexer 27 | blocks []blocker 28 | h *core.MessageHandler 29 | } 30 | 31 | func newParser(h *core.MessageHandler, block core.Block, blocks []blocker) *parser { 32 | l, err := lexer.New(block) 33 | if err != nil { 34 | h.Error(err) 35 | return nil 36 | } 37 | 38 | return &parser{ 39 | Lexer: l, 40 | blocks: blocks, 41 | h: h, 42 | } 43 | } 44 | 45 | // 从当前位置往后查找,直到找到第一个与 blocks 中某个相匹配的,并返回该 Blocker 。 46 | func (l *parser) block() (blocker, core.Position) { 47 | for { 48 | if l.AtEOF() { 49 | return nil, core.Position{} 50 | } 51 | 52 | pos := l.Current() 53 | for _, block := range l.blocks { 54 | if block.beginFunc(l) { 55 | return block, pos.Position 56 | } 57 | } 58 | 59 | l.Next(1) 60 | } 61 | } 62 | 63 | // 分析 l.data 的内容并输出到 blocks 64 | func (l *parser) parse(blocks chan core.Block) { 65 | var block blocker 66 | var pos core.Position 67 | for { 68 | if l.AtEOF() { 69 | return 70 | } 71 | 72 | if block == nil { 73 | if block, pos = l.block(); block == nil { // 没有匹配的 block 了 74 | return 75 | } 76 | } 77 | 78 | data, ok := block.endFunc(l) 79 | if !ok { // 没有找到结束标签,那肯定是到文件尾了,可以直接返回。 80 | loc := core.Location{ 81 | URI: l.Location.URI, 82 | Range: core.Range{ 83 | Start: pos, 84 | End: l.Current().Position, 85 | }, 86 | } 87 | l.h.Error(loc.NewError(locale.ErrNotFoundEndFlag)) 88 | return 89 | } 90 | 91 | block = nil // 重置 block 92 | 93 | if len(data) == 0 { 94 | continue 95 | } 96 | 97 | blocks <- core.Block{ 98 | Location: core.Location{ 99 | URI: l.Location.URI, 100 | Range: core.Range{ 101 | Start: pos, 102 | End: l.Current().Position, 103 | }, 104 | }, 105 | Data: data, 106 | } 107 | } // end for 108 | } 109 | -------------------------------------------------------------------------------- /internal/lang/pascal.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package lang 4 | 5 | // 描述了 pascal/delphi 语言的字符串,在 pascal 中 6 | // 转义字符即引号本身,不适合直接在 block 中定义。 7 | type pascalStringBlock struct { 8 | symbol string 9 | escape string 10 | } 11 | 12 | func newPascalStringBlock(symbol byte) blocker { 13 | s := string(symbol) 14 | return &pascalStringBlock{ 15 | symbol: s, 16 | escape: s + s, 17 | } 18 | } 19 | 20 | func (b *pascalStringBlock) beginFunc(l *parser) bool { 21 | return l.Match(b.symbol) 22 | } 23 | 24 | func (b *pascalStringBlock) endFunc(l *parser) (data []byte, ok bool) { 25 | for { 26 | switch { 27 | case l.AtEOF(): 28 | return nil, false 29 | case l.Match(b.escape): // 转义 30 | break 31 | case l.Match(b.symbol): // 结束 32 | return nil, true 33 | default: 34 | l.Next(1) 35 | } 36 | } // end for 37 | } 38 | -------------------------------------------------------------------------------- /internal/lang/pascal_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package lang 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/issue9/assert/v3" 9 | 10 | "github.com/caixw/apidoc/v7/core" 11 | "github.com/caixw/apidoc/v7/core/messagetest" 12 | ) 13 | 14 | func TestPascalStringBlock(t *testing.T) { 15 | a := assert.New(t, false) 16 | 17 | b := newPascalStringBlock('"') 18 | a.NotNil(b) 19 | 20 | rslt := messagetest.NewMessageHandler() 21 | l := newParser(rslt.Handler, core.Block{Data: []byte(`"123""123"`)}, nil) 22 | rslt.Handler.Stop() 23 | a.Empty(rslt.Errors).NotNil(l) 24 | a.True(b.beginFunc(l)) 25 | data, ok := b.endFunc(l) 26 | a.True(ok). 27 | Equal(len(data), 0) // 不返回内容 28 | bs := l.Next(1) // 继续向后推进,才会 29 | a.Empty(bs).True(l.AtEOF()) // 到达末尾 30 | 31 | rslt = messagetest.NewMessageHandler() 32 | l = newParser(rslt.Handler, core.Block{Data: []byte(`"123"""123"`)}, nil) 33 | rslt.Handler.Stop() 34 | a.Empty(rslt.Errors).NotNil(l) 35 | a.True(b.beginFunc(l)) 36 | data, ok = b.endFunc(l) 37 | a.True(ok). 38 | Equal(len(data), 0). // 不返回内容 39 | Equal(string(l.All()), "123\"") // 未到达末尾 40 | } 41 | -------------------------------------------------------------------------------- /internal/lang/php.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package lang 4 | 5 | const ( 6 | phpHerodoc int8 = iota + 1 7 | phpNowdoc 8 | ) 9 | 10 | type phpDocBlock struct { 11 | token1 string 12 | token2 string 13 | doctype int8 14 | } 15 | 16 | // herodoc 和 nowdoc 的实现。 17 | // 18 | // http://php.net/manual/zh/language.types.string.php#language.types.string.syntax.heredoc 19 | func newPHPDocBlock() blocker { 20 | return &phpDocBlock{ 21 | doctype: phpHerodoc, 22 | } 23 | } 24 | 25 | func (b *phpDocBlock) beginFunc(l *parser) bool { 26 | prev := l.Current() 27 | 28 | if !l.Match("<<<") { 29 | return false 30 | } 31 | 32 | token, found := l.Delim('\n', true) 33 | if !found || len(token) <= 1 { // <<< 之后直接是换行符,则应该退回 <<< 字符 34 | l.Move(prev) 35 | return false 36 | } 37 | token = token[:len(token)-1] // l.delim 会带上换行符,需要去掉 38 | 39 | if token[0] == '\'' && token[len(token)-1] == '\'' || 40 | token[0] == '"' && token[len(token)-1] == '"' { 41 | b.doctype = phpNowdoc 42 | token = token[1 : len(token)-1] 43 | } 44 | 45 | b.token1 = "\n" + string(token) + "\n" 46 | b.token2 = "\n" + string(token) + ";\n" 47 | 48 | return true 49 | } 50 | 51 | func (b *phpDocBlock) endFunc(l *parser) (data []byte, ok bool) { 52 | for { 53 | switch { 54 | case l.AtEOF(): 55 | return nil, false 56 | case l.Match(b.token1): 57 | return nil, true 58 | case l.Match(b.token2): 59 | return nil, true 60 | default: 61 | l.Next(1) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /internal/lang/php_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package lang 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/issue9/assert/v3" 9 | 10 | "github.com/caixw/apidoc/v7/core" 11 | "github.com/caixw/apidoc/v7/core/messagetest" 12 | ) 13 | 14 | func TestPHPDocBlock(t *testing.T) { 15 | a := assert.New(t, false) 16 | b := newPHPDocBlock() 17 | a.NotNil(b) 18 | 19 | // herodoc 20 | data := []byte(`<< 27 | 标题 28 | xml 29 | json 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | ` 39 | blk := core.Block{Data: []byte(b), Location: core.Location{URI: "file:///test/doc.go"}} 40 | rslt := messagetest.NewMessageHandler() 41 | doc := &ast.APIDoc{} 42 | doc.Parse(rslt.Handler, blk) 43 | rslt.Handler.Stop() 44 | a.Empty(rslt.Errors) 45 | 46 | s.folders = []*folder{ 47 | { 48 | WorkspaceFolder: protocol.WorkspaceFolder{Name: "test", URI: "file:///test"}, 49 | doc: doc, 50 | }, 51 | } 52 | 53 | h = &protocol.Hover{} 54 | err = s.textDocumentHover(false, &protocol.HoverParams{TextDocumentPositionParams: protocol.TextDocumentPositionParams{ 55 | TextDocument: protocol.TextDocumentIdentifier{URI: "file:///test/doc.go"}, 56 | Position: core.Position{Line: 1, Character: 1}, 57 | }}, h) 58 | a.NotError(err) 59 | a.Equal(h.Range, core.Range{ 60 | Start: core.Position{Line: 1, Character: 1}, 61 | End: core.Position{Line: 1, Character: 18}, 62 | }) 63 | a.Equal(h.Contents.Value, locale.Sprintf("usage-apidoc-title")) 64 | } 65 | -------------------------------------------------------------------------------- /internal/lsp/initialize_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package lsp 4 | 5 | import ( 6 | "io/ioutil" 7 | "log" 8 | "testing" 9 | 10 | "github.com/issue9/assert/v3" 11 | "golang.org/x/text/language" 12 | 13 | "github.com/caixw/apidoc/v7/core" 14 | "github.com/caixw/apidoc/v7/internal/locale" 15 | "github.com/caixw/apidoc/v7/internal/lsp/protocol" 16 | ) 17 | 18 | func TestServer_initialize(t *testing.T) { 19 | a := assert.New(t, false) 20 | s := newTestServer(true, log.New(ioutil.Discard, "", 0), log.New(ioutil.Discard, "", 0)) 21 | in := &protocol.InitializeParams{} 22 | out := &protocol.InitializeResult{} 23 | a.NotError(s.initialize(false, in, out)) 24 | a.Equal(out.ServerInfo.Name, core.Name) 25 | a.Equal(s.clientParams, in).Equal(s.serverResult, out) 26 | a.Equal(s.state, serverInitializing) 27 | 28 | s = newTestServer(true, log.New(ioutil.Discard, "", 0), log.New(ioutil.Discard, "", 0)) 29 | in = &protocol.InitializeParams{ 30 | Capabilities: protocol.ClientCapabilities{Workspace: &protocol.WorkspaceClientCapabilities{ 31 | WorkspaceFolders: true, 32 | }}, 33 | InitializationOptions: &protocol.InitializationOptions{Locale: "cmn-hant"}, 34 | } 35 | out = &protocol.InitializeResult{} 36 | a.NotError(s.initialize(false, in, out)) 37 | a.Equal(locale.Tag(), language.MustParse("cmn-hant")) 38 | a.True(out.Capabilities.Workspace.WorkspaceFolders.Supported). 39 | True(out.Capabilities.Workspace.WorkspaceFolders.ChangeNotifications) 40 | 41 | s = newTestServer(true, log.New(ioutil.Discard, "", 0), log.New(ioutil.Discard, "", 0)) 42 | in = &protocol.InitializeParams{ 43 | Capabilities: protocol.ClientCapabilities{TextDocument: protocol.TextDocumentClientCapabilities{ 44 | Hover: &protocol.HoverCapabilities{}, 45 | FoldingRange: &protocol.FoldingRangeClientCapabilities{}, 46 | Definition: &protocol.DefinitionClientCapabilities{}, 47 | }}, 48 | } 49 | out = &protocol.InitializeResult{} 50 | a.NotError(s.initialize(false, in, out)) 51 | a.False(out.Capabilities.HoverProvider). 52 | True(out.Capabilities.DefinitionProvider). 53 | True(out.Capabilities.FoldingRangeProvider) 54 | 55 | s = newTestServer(true, log.New(ioutil.Discard, "", 0), log.New(ioutil.Discard, "", 0)) 56 | in = &protocol.InitializeParams{ 57 | Capabilities: protocol.ClientCapabilities{TextDocument: protocol.TextDocumentClientCapabilities{ 58 | Hover: &protocol.HoverCapabilities{ContentFormat: []protocol.MarkupKind{protocol.MarkupKindMarkdown}}, 59 | References: &protocol.ReferenceClientCapabilities{}, 60 | Completion: &protocol.CompletionClientCapabilities{}, 61 | }}, 62 | } 63 | out = &protocol.InitializeResult{} 64 | a.NotError(s.initialize(false, in, out)) 65 | a.True(out.Capabilities.HoverProvider). 66 | False(out.Capabilities.DefinitionProvider). 67 | False(out.Capabilities.FoldingRangeProvider). 68 | True(out.Capabilities.ReferencesProvider). 69 | NotNil(out.Capabilities.CompletionProvider) 70 | } 71 | -------------------------------------------------------------------------------- /internal/lsp/lsp.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | // Package lsp 提供 language server protocol 服务 4 | package lsp 5 | 6 | import ( 7 | "log" 8 | "net" 9 | "os" 10 | "strings" 11 | "time" 12 | 13 | "github.com/issue9/jsonrpc" 14 | 15 | "github.com/caixw/apidoc/v7/core" 16 | "github.com/caixw/apidoc/v7/internal/locale" 17 | ) 18 | 19 | // Version lsp 的版本 20 | const Version = "3.16.0" 21 | 22 | // Serve 执行 LSP 服务 23 | // 24 | // t 表示服务的类型,可以是 stdio、udp、tcp 和 unix。 25 | func Serve(header bool, t string, addr string, timeout time.Duration, infolog, errlog *log.Logger) error { 26 | switch strings.ToLower(t) { 27 | case "stdio": 28 | return serveStdio(header, infolog, errlog) 29 | case "udp": 30 | return serveUDP(header, addr, timeout, infolog, errlog) 31 | case "tcp", "unix": 32 | return serveTCP(header, t, addr, timeout, infolog, errlog) 33 | } 34 | 35 | return core.NewError(locale.ErrInvalidValue) 36 | } 37 | 38 | func serveStdio(header bool, infolog, errlog *log.Logger) error { 39 | return serve(jsonrpc.NewStreamTransport(header, os.Stdin, os.Stdout, nil), infolog, errlog) 40 | } 41 | 42 | func serveUDP(header bool, addr string, timeout time.Duration, infolog, errlog *log.Logger) error { 43 | t, err := jsonrpc.NewUDPServerTransport(header, addr, timeout) 44 | if err != nil { 45 | return err 46 | } 47 | return serve(t, infolog, errlog) 48 | } 49 | 50 | // t 可以是 tcp 和 unix 51 | func serveTCP(header bool, t string, addr string, timeout time.Duration, infolog, errlog *log.Logger) error { 52 | l, err := net.Listen(t, addr) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | for { 58 | conn, err := l.Accept() 59 | if err != nil { 60 | errlog.Println(err) 61 | continue 62 | } 63 | return serve(jsonrpc.NewSocketTransport(header, conn, timeout), infolog, errlog) 64 | } 65 | } 66 | 67 | func serve(t jsonrpc.Transport, infolog, errlog *log.Logger) error { 68 | return newServe(t, infolog, errlog).serve() 69 | } 70 | -------------------------------------------------------------------------------- /internal/lsp/protocol/apidoc_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package protocol 4 | 5 | import ( 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/issue9/assert/v3" 10 | 11 | "github.com/caixw/apidoc/v7/internal/ast" 12 | "github.com/caixw/apidoc/v7/internal/ast/asttest" 13 | "github.com/caixw/apidoc/v7/internal/xmlenc" 14 | ) 15 | 16 | func TestBuildAPIDocOutline(t *testing.T) { 17 | a := assert.New(t, false) 18 | f := WorkspaceFolder{Name: "test"} 19 | 20 | doc := &ast.APIDoc{} 21 | outline := BuildAPIDocOutline(f, doc) 22 | a.Nil(outline) 23 | 24 | doc = asttest.Get() 25 | outline = BuildAPIDocOutline(f, doc) 26 | a.NotNil(outline) 27 | 28 | a.Equal(outline.Title, doc.Title.V()) 29 | a.Equal(2, len(outline.APIs)) 30 | } 31 | 32 | func TestAPIDocOutline_appendAPI(t *testing.T) { 33 | a := assert.New(t, false) 34 | 35 | outline := &APIDocOutline{ 36 | APIs: []*API{}, 37 | } 38 | 39 | // api.Path 为空,无法构建 API.Path 变量,忽略该值 40 | outline.appendAPI(&ast.API{}) 41 | a.Equal(len(outline.APIs), 1) 42 | api := outline.APIs[0] 43 | a.Equal(api.Path, "?").Empty(api.Method) 44 | 45 | outline = &APIDocOutline{ 46 | APIs: []*API{}, 47 | } 48 | outline.appendAPI(&ast.API{Method: &ast.MethodAttribute{Value: xmlenc.String{Value: http.MethodDelete}}}) 49 | a.Equal(len(outline.APIs), 1) 50 | api = outline.APIs[0] 51 | a.Equal(api.Path, "?").Equal(api.Method, http.MethodDelete) 52 | } 53 | -------------------------------------------------------------------------------- /internal/lsp/protocol/completion_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package protocol 4 | 5 | import ( 6 | "encoding/json" 7 | "testing" 8 | 9 | "github.com/issue9/assert/v3" 10 | ) 11 | 12 | func TestCompletionList_MarshalJSON(t *testing.T) { 13 | a := assert.New(t, false) 14 | 15 | list := &CompletionList{} 16 | data, err := json.Marshal(list) 17 | a.NotError(err).Equal(string(data), "null") 18 | 19 | list.IsIncomplete = true 20 | list.Items = make([]CompletionItem, 0) 21 | data, err = json.Marshal(list) 22 | a.NotError(err).Equal(string(data), "null") 23 | 24 | list.Items = append(list.Items, CompletionItem{}) 25 | data, err = json.Marshal(list) 26 | a.NotError(err).Equal(string(data), `{"isIncomplete":true,"items":[{"label":""}]}`) 27 | } 28 | -------------------------------------------------------------------------------- /internal/lsp/protocol/diagnostic_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package protocol 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/issue9/assert/v3" 9 | 10 | "github.com/caixw/apidoc/v7/core" 11 | "github.com/caixw/apidoc/v7/internal/locale" 12 | ) 13 | 14 | func TestPublishDiagnosticParams_AppendDiagnostic(t *testing.T) { 15 | a := assert.New(t, false) 16 | p := NewPublishDiagnosticsParams(core.URI("test.go")) 17 | a.NotNil(p). 18 | Equal(p.URI, core.URI("test.go")). 19 | Empty(p.Diagnostics) 20 | 21 | p.AppendDiagnostic(core.NewError(locale.ErrInvalidUTF8Character), core.Erro) 22 | a.Equal(1, len(p.Diagnostics)) 23 | p.AppendDiagnostic(core.NewError(locale.ErrInvalidUTF8Character), core.Warn) 24 | a.Equal(2, len(p.Diagnostics)) 25 | p.AppendDiagnostic(core.NewError(locale.ErrInvalidUTF8Character), core.Info) 26 | a.Equal(3, len(p.Diagnostics)) 27 | // 忽略 core.Succ 28 | p.AppendDiagnostic(core.NewError(locale.ErrInvalidUTF8Character), core.Succ) 29 | a.Equal(3, len(p.Diagnostics)) 30 | 31 | a.Panic(func() { 32 | p.AppendDiagnostic(core.NewError(locale.ErrInvalidUTF8Character), 100) 33 | }) 34 | } 35 | 36 | func TestBuildDiagnostic(t *testing.T) { 37 | a := assert.New(t, false) 38 | 39 | err := core.NewError(locale.ErrInvalidUTF8Character).WithLocation(core.Location{ 40 | Range: core.Range{Start: core.Position{Line: 1}}, 41 | }) 42 | d := buildDiagnostic(err, DiagnosticSeverityWarning) 43 | a.Equal(d.Severity, DiagnosticSeverityWarning) 44 | a.Empty(d.Tags) 45 | a.Equal(d.Range.Start.Line, 1). 46 | Empty(d.RelatedInformation). 47 | Empty(d.Tags) 48 | 49 | err = err.AddTypes(core.ErrorTypeDeprecated, core.ErrorTypeUnused) 50 | d = buildDiagnostic(err, DiagnosticSeverityError) 51 | a.Equal(d.Severity, DiagnosticSeverityError) 52 | a.Equal(d.Tags, []DiagnosticTag{DiagnosticTagDeprecated, DiagnosticTagUnnecessary}) 53 | a.Equal(d.Range.Start.Line, 1) 54 | 55 | err = err.Relate(core.Location{URI: "relate.go"}, "relate message") 56 | d = buildDiagnostic(err, DiagnosticSeverityError) 57 | a.Equal(d.Range.Start.Line, 1). 58 | Equal(1, len(d.RelatedInformation)). 59 | Equal(d.RelatedInformation[0].Location, core.Location{URI: "relate.go"}) 60 | } 61 | -------------------------------------------------------------------------------- /internal/lsp/protocol/folding.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package protocol 4 | 5 | import "github.com/caixw/apidoc/v7/internal/xmlenc" 6 | 7 | // 代码关折叠块的种类 8 | const ( 9 | FoldingRangeKindComment = "comment" // Folding range for a comment 10 | FoldingRangeKindImports = "imports" // Folding range for a imports or includes 11 | FoldingRangeKindRegion = "region" // Folding range for a region (e.g. `#region`) 12 | ) 13 | 14 | // FoldingRangeClientCapabilities 定义客户对代码拆叠功能的支持情况 15 | type FoldingRangeClientCapabilities struct { 16 | // Whether implementation supports dynamic registration for folding range providers. If this is set to `true` 17 | // the client supports the new `(FoldingRangeProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions)` 18 | // return value for the corresponding server capability as well. 19 | DynamicRegistration bool `json:"dynamicRegistration,omitempty"` 20 | 21 | // The maximum number of folding ranges that the client prefers to receive per document. The value serves as a 22 | // hint, servers are free to follow the limit. 23 | RangeLimit int `json:"rangeLimit,omitempty"` 24 | 25 | // If set, the client signals that it only supports folding complete lines. If set, client will 26 | // ignore specified `startCharacter` and `endCharacter` properties in a FoldingRange. 27 | LineFoldingOnly bool `json:"lineFoldingOnly,omitempty"` 28 | } 29 | 30 | // FoldingRangeParams 由用户传递的 textDocument/foldingRange 参数 31 | type FoldingRangeParams struct { 32 | WorkDoneProgressParams 33 | PartialResultParams 34 | 35 | // The text document. 36 | TextDocument TextDocumentIdentifier `json:"textDocument"` 37 | } 38 | 39 | // FoldingRange represents a folding range 40 | type FoldingRange struct { 41 | // The zero-based line number from where the folded range starts. 42 | StartLine int `json:"startLine"` 43 | 44 | // The zero-based character offset from where the folded range starts. If not defined, defaults to the length of the start line. 45 | // 46 | // 0 是一个合法的值,所以只能采用指针类型表示空值。 47 | StartCharacter *int `json:"startCharacter,omitempty"` 48 | 49 | // The zero-based line number where the folded range ends. 50 | EndLine int `json:"endLine"` 51 | 52 | // The zero-based character offset before the folded range ends. If not defined, defaults to the length of the end line. 53 | // 54 | // 0 是一个合法的值,所以只能采用指针类型表示空值。 55 | EndCharacter *int `json:"endCharacter,omitempty"` 56 | 57 | // Describes the kind of the folding range such as `comment` or `region`. The kind 58 | // is used to categorize folding ranges and used by commands like 'Fold all comments'. See 59 | // [FoldingRangeKind](#FoldingRangeKind) for an enumeration of standardized kinds. 60 | Kind string `json:"kind,omitempty"` 61 | } 62 | 63 | // BuildFoldingRange 根据参数构建 FoldingRange 实例 64 | func BuildFoldingRange(base xmlenc.Base, lineFoldingOnly bool) FoldingRange { 65 | item := FoldingRange{ 66 | StartLine: base.Location.Range.Start.Line, 67 | EndLine: base.Location.Range.End.Line, 68 | Kind: FoldingRangeKindComment, 69 | } 70 | 71 | if lineFoldingOnly { 72 | item.StartCharacter = &base.Location.Range.Start.Character 73 | item.EndCharacter = &base.Location.Range.End.Character 74 | } 75 | 76 | return item 77 | } 78 | -------------------------------------------------------------------------------- /internal/lsp/protocol/folding_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package protocol 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/issue9/assert/v3" 9 | 10 | "github.com/caixw/apidoc/v7/core" 11 | "github.com/caixw/apidoc/v7/internal/xmlenc" 12 | ) 13 | 14 | func TestBuildFoldingRange(t *testing.T) { 15 | a := assert.New(t, false) 16 | 17 | base := xmlenc.Base{} 18 | a.Equal(BuildFoldingRange(base, false), FoldingRange{Kind: FoldingRangeKindComment}) 19 | 20 | base = xmlenc.Base{ 21 | Location: core.Location{ 22 | Range: core.Range{ 23 | Start: core.Position{Line: 1, Character: 11}, 24 | End: core.Position{Line: 2, Character: 11}, 25 | }, 26 | }, 27 | } 28 | a.Equal(BuildFoldingRange(base, false), FoldingRange{ 29 | StartLine: 1, 30 | Kind: FoldingRangeKindComment, 31 | EndLine: 2, 32 | }) 33 | a.Equal(BuildFoldingRange(base, true), FoldingRange{ 34 | StartLine: 1, 35 | StartCharacter: &base.Location.Range.Start.Character, 36 | EndLine: 2, 37 | EndCharacter: &base.Location.Range.End.Character, 38 | Kind: FoldingRangeKindComment, 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /internal/lsp/protocol/hover.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package protocol 4 | 5 | import ( 6 | "encoding/json" 7 | 8 | "github.com/caixw/apidoc/v7/core" 9 | ) 10 | 11 | // HoverCapabilities 客户端有关 hover 功能的描述 12 | type HoverCapabilities struct { 13 | // Whether hover supports dynamic registration. 14 | DynamicRegistration bool `json:"dynamicRegistration,omitempty"` 15 | 16 | // The client supports the follow content formats for the content 17 | // property. The order describes the preferred format of the client. 18 | ContentFormat []MarkupKind `json:"contentFormat,omitempty"` 19 | } 20 | 21 | // HoverParams textDocument/hover 发送的参数 22 | type HoverParams struct { 23 | WorkDoneProgressParams 24 | TextDocumentPositionParams 25 | } 26 | 27 | // Hover textDocument/hover 的返回结果 28 | type Hover struct { 29 | // The hover's content 30 | Contents MarkupContent `json:"contents"` 31 | 32 | // An optional range is a range inside a text document 33 | // that is used to visualize a hover, e.g. by changing the background color. 34 | Range core.Range `json:"range"` 35 | } 36 | 37 | // MarshalJSON 允许在 hover 为空值是返回 null 38 | func (h *Hover) MarshalJSON() ([]byte, error) { 39 | if h.Contents.Kind == "" { 40 | return json.Marshal(nil) 41 | } 42 | 43 | type hoverShadow Hover 44 | shadow := (*hoverShadow)(h) 45 | return json.Marshal(shadow) 46 | } 47 | -------------------------------------------------------------------------------- /internal/lsp/protocol/hover_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package protocol 4 | 5 | import ( 6 | "encoding/json" 7 | "testing" 8 | 9 | "github.com/issue9/assert/v3" 10 | 11 | "github.com/caixw/apidoc/v7/core" 12 | ) 13 | 14 | func TestHover_MarshalJSON(t *testing.T) { 15 | a := assert.New(t, false) 16 | 17 | h := &Hover{} 18 | data, err := json.Marshal(h) 19 | a.NotError(err).Equal(string(data), `null`) 20 | 21 | h.Range = core.Range{ 22 | Start: core.Position{Line: 0, Character: 10}, 23 | End: core.Position{Line: 1, Character: 10}, 24 | } 25 | data, err = json.Marshal(h) 26 | a.NotError(err).Equal(string(data), `null`) 27 | 28 | h.Contents = MarkupContent{ 29 | Kind: MarkupKindPlainText, 30 | } 31 | data, err = json.MarshalIndent(h, "", "\t") 32 | a.NotError(err).Equal(string(data), `{ 33 | "contents": { 34 | "kind": "plaintext", 35 | "value": "" 36 | }, 37 | "range": { 38 | "start": { 39 | "line": 0, 40 | "character": 10 41 | }, 42 | "end": { 43 | "line": 1, 44 | "character": 10 45 | } 46 | } 47 | }`) 48 | } 49 | -------------------------------------------------------------------------------- /internal/lsp/protocol/initialize_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package protocol 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/issue9/assert/v3" 9 | ) 10 | 11 | func TestInitializeParams_Folders(t *testing.T) { 12 | a := assert.New(t, false) 13 | 14 | p := &InitializeParams{} 15 | a.Nil(p.Folders()) 16 | 17 | p.RootPath = "../../lsp/protocol" 18 | a.Equal(p.Folders(), []WorkspaceFolder{ 19 | { 20 | Name: "protocol", 21 | URI: "../../lsp/protocol", 22 | }, 23 | }) 24 | 25 | p.RootPath = "/../lsp/protocol2" 26 | a.Equal(p.Folders(), []WorkspaceFolder{ 27 | { 28 | Name: "protocol2", 29 | URI: "/../lsp/protocol2", 30 | }, 31 | }) 32 | 33 | p.RootURI = "file:///lsp/protocol" 34 | a.Equal(p.Folders(), []WorkspaceFolder{ 35 | { 36 | Name: "protocol", 37 | URI: "file:///lsp/protocol", 38 | }, 39 | }) 40 | 41 | p.WorkspaceFolders = []WorkspaceFolder{ 42 | { 43 | Name: "f1", 44 | URI: "file:///f1", 45 | }, 46 | { 47 | Name: "f2", 48 | URI: "https://example.com/f1", 49 | }, 50 | } 51 | a.Equal(p.Folders(), []WorkspaceFolder{ 52 | { 53 | Name: "f1", 54 | URI: "file:///f1", 55 | }, 56 | { 57 | Name: "f2", 58 | URI: "https://example.com/f1", 59 | }, 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /internal/lsp/protocol/message.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package protocol 4 | 5 | // SetTraceParams.Value 可用的值 6 | const ( 7 | TraceValueOff = "off" 8 | TraceValueMessage = "message" 9 | TraceValueVerbose = "verbose" 10 | ) 11 | 12 | // LogTraceParams $/logTrace 服务端下发参数 13 | type LogTraceParams struct { 14 | // The message to be logged. 15 | Message string `json:"message"` 16 | 17 | // Additional information that can be computed if the `trace` configuration is set to `'verbose'` 18 | Verbose string `json:"verbose,omitempty"` 19 | } 20 | 21 | // SetTraceParams $/setTrace 入口参数 22 | type SetTraceParams struct { 23 | // The new value that should be assigned to the trace setting. 24 | Value string `json:"value"` 25 | } 26 | 27 | // MessageType 传递的消息类型 28 | type MessageType int 29 | 30 | // MessageType 可能的值 31 | const ( 32 | MessageTypeError MessageType = iota + 1 // An error message. 33 | MessageTypeWarning // A warning message. 34 | MessageTypeInfo // An information message. 35 | MessageTypeLog // A log message. 36 | ) 37 | 38 | // LogMessageParams window/logMessage 传递的参数 39 | type LogMessageParams struct { 40 | // The message type. See {@link MessageType} 41 | Type MessageType `json:"type"` 42 | 43 | // The actual message 44 | Message string `json:"message"` 45 | } 46 | 47 | // IsValidTraceValue 是否是一个有效的 TraceValue 48 | func IsValidTraceValue(v string) bool { 49 | return v == TraceValueOff || v == TraceValueMessage || v == TraceValueVerbose 50 | } 51 | 52 | // BuildLogTrace 生成 logTrace 对象 53 | func BuildLogTrace(trace, message, verbose string) *LogTraceParams { 54 | switch trace { 55 | case TraceValueOff: 56 | return nil 57 | case TraceValueMessage: 58 | verbose = "" 59 | case TraceValueVerbose: 60 | default: 61 | panic("无效的 trace 值") 62 | } 63 | 64 | return &LogTraceParams{ 65 | Message: message, 66 | Verbose: verbose, 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /internal/lsp/protocol/message_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package protocol 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/issue9/assert/v3" 9 | ) 10 | 11 | func TestIsValidTraceValue(t *testing.T) { 12 | a := assert.New(t, false) 13 | a.True(IsValidTraceValue(TraceValueMessage)) 14 | a.False(IsValidTraceValue("invalid-value")) 15 | } 16 | 17 | func TestBuildLogTrace(t *testing.T) { 18 | a := assert.New(t, false) 19 | 20 | p := BuildLogTrace(TraceValueOff, "m1", "v2") 21 | a.Nil(p) 22 | 23 | p = BuildLogTrace(TraceValueMessage, "m1", "v2") 24 | a.NotNil(p).Equal(p.Message, "m1").Empty(p.Verbose) 25 | 26 | p = BuildLogTrace(TraceValueVerbose, "m1", "v2") 27 | a.NotNil(p).Equal(p.Message, "m1").Equal(p.Verbose, "v2") 28 | 29 | a.Panic(func() { 30 | BuildLogTrace("invalid-trace", "m2", "v2") 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /internal/lsp/protocol/reference.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package protocol 4 | 5 | // ReferenceClientCapabilities 客户端对 textDocument/references 的支持情况 6 | type ReferenceClientCapabilities struct { 7 | // Whether references supports dynamic registration. 8 | DynamicRegistration bool `json:"dynamicRegistration,omitempty"` 9 | } 10 | 11 | // ReferenceParams textDocument/references 的请求参数 12 | type ReferenceParams struct { 13 | TextDocumentPositionParams 14 | WorkDoneProgressParams 15 | PartialResultParams 16 | Context struct { 17 | // Include the declaration of the current symbol. 18 | IncludeDeclaration bool `json:"includeDeclaration"` 19 | } `json:"context"` 20 | } 21 | 22 | // DefinitionClientCapabilities 客户端对 textDocument/definition 的支持情况 23 | type DefinitionClientCapabilities struct { 24 | // Whether definition supports dynamic registration. 25 | DynamicRegistration bool `json:"dynamicRegistration,omitempty"` 26 | 27 | // The client supports additional metadata in the form of definition links. 28 | // 29 | // @since 3.14.0 30 | LinkSupport bool `json:"linkSupport,omitempty"` 31 | } 32 | 33 | // DefinitionParams textDocument/definition 服务的客户端请求参数 34 | type DefinitionParams struct { 35 | TextDocumentPositionParams 36 | WorkDoneProgressParams 37 | PartialResultParams 38 | } 39 | -------------------------------------------------------------------------------- /internal/lsp/protocol/server.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package protocol 4 | 5 | // ServerCapabilities 服务端的兼容列表 6 | type ServerCapabilities struct { 7 | // Defines how text documents are synced. 8 | // 9 | // Is either a detailed structure defining each notification or 10 | // for backwards compatibility the TextDocumentSyncKind number. 11 | // If omitted it defaults to `TextDocumentSyncKind.None`. 12 | // 13 | // ServerCapabilitiesTextDocumentSyncOptions | TextDocumentSyncKind; 14 | TextDocumentSync *ServerCapabilitiesTextDocumentSyncOptions `json:"textDocumentSync"` 15 | 16 | // The server provides completion support. 17 | CompletionProvider *CompletionOptions `json:"completionProvider,omitempty"` 18 | 19 | // The server provides hover support. 20 | HoverProvider bool `json:"hoverProvider,omitempty"` 21 | 22 | // The server provides goto definition support. 23 | DefinitionProvider bool `json:"definitionProvider,omitempty"` 24 | 25 | // The server provides find references support. 26 | ReferencesProvider bool `json:"referencesProvider,omitempty"` 27 | 28 | // The server provides folding provider support. 29 | // 30 | // Since 3.10.0 31 | FoldingRangeProvider bool `json:"foldingRangeProvider,omitempty"` 32 | 33 | // The server provides folding provider support. 34 | // 35 | // Since 3.16.0 36 | // 37 | // SemanticTokensOptions | SemanticTokensRegistrationOptions 38 | SemanticTokensProvider any `json:"semanticTokensProvider,omitempty"` 39 | 40 | // The server provides workspace symbol support. 41 | WorkspaceSymbolProvider bool `json:"workspaceSymbolProvider,omitempty"` 42 | 43 | // Workspace specific server capabilities 44 | Workspace *WorkspaceProvider `json:"workspace,omitempty"` 45 | 46 | // Experimental server capabilities. 47 | Experimental any `json:"experimental,omitempty"` 48 | } 49 | 50 | // SaveOptions Save options. 51 | type SaveOptions struct { 52 | // The client is supposed to include the content on save. 53 | IncludeText bool `json:"includeText,omitempty"` 54 | } 55 | 56 | // StaticRegistrationOptions static registration options to be returned in the initialize request. 57 | type StaticRegistrationOptions struct { 58 | // The id used to register the request. The id can be used to deregister 59 | // the request again. See also Registration#id. 60 | ID string `json:"id,omitempty"` 61 | } 62 | -------------------------------------------------------------------------------- /internal/lsp/protocol/textdocument_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package protocol 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/issue9/assert/v3" 9 | 10 | "github.com/caixw/apidoc/v7/core" 11 | ) 12 | 13 | func TestDidChangeTextDocumentParams_Blocks(t *testing.T) { 14 | a := assert.New(t, false) 15 | 16 | p := &DidChangeTextDocumentParams{} 17 | a.Empty(p.Blocks()) 18 | 19 | p = &DidChangeTextDocumentParams{ 20 | TextDocument: VersionedTextDocumentIdentifier{ 21 | TextDocumentIdentifier: TextDocumentIdentifier{URI: core.FileURI("test.go")}, 22 | }, 23 | ContentChanges: []TextDocumentContentChangeEvent{ 24 | { 25 | Range: &core.Range{End: core.Position{Line: 1, Character: 5}}, 26 | Text: "text", 27 | }, 28 | }, 29 | } 30 | a.Equal(p.Blocks(), []core.Block{ 31 | { 32 | Data: []byte("text"), 33 | Location: core.Location{ 34 | URI: core.FileURI("test.go"), 35 | Range: core.Range{End: core.Position{Line: 1, Character: 5}}, 36 | }, 37 | }, 38 | }) 39 | 40 | // 未指定 Range 41 | p = &DidChangeTextDocumentParams{ 42 | TextDocument: VersionedTextDocumentIdentifier{ 43 | TextDocumentIdentifier: TextDocumentIdentifier{URI: core.FileURI("test.go")}, 44 | }, 45 | ContentChanges: []TextDocumentContentChangeEvent{ 46 | { 47 | Text: "text", 48 | }, 49 | }, 50 | } 51 | a.Equal(p.Blocks(), []core.Block{ 52 | { 53 | Data: []byte("text"), 54 | Location: core.Location{ 55 | URI: core.FileURI("test.go"), 56 | }, 57 | }, 58 | }) 59 | 60 | // 多个元素 61 | p = &DidChangeTextDocumentParams{ 62 | TextDocument: VersionedTextDocumentIdentifier{ 63 | TextDocumentIdentifier: TextDocumentIdentifier{URI: core.FileURI("test.go")}, 64 | }, 65 | ContentChanges: []TextDocumentContentChangeEvent{ 66 | { 67 | Range: &core.Range{End: core.Position{Line: 1, Character: 5}}, 68 | Text: "text", 69 | }, 70 | 71 | { 72 | Range: &core.Range{End: core.Position{Line: 2, Character: 5}}, 73 | Text: "text2", 74 | }, 75 | }, 76 | } 77 | a.Equal(p.Blocks(), []core.Block{ 78 | { 79 | Data: []byte("text"), 80 | Location: core.Location{ 81 | URI: core.FileURI("test.go"), 82 | Range: core.Range{End: core.Position{Line: 1, Character: 5}}, 83 | }, 84 | }, 85 | { 86 | Data: []byte("text2"), 87 | Location: core.Location{ 88 | URI: core.FileURI("test.go"), 89 | Range: core.Range{End: core.Position{Line: 2, Character: 5}}, 90 | }, 91 | }, 92 | }) 93 | } 94 | -------------------------------------------------------------------------------- /internal/lsp/protocol/workspace_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package protocol 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/issue9/assert/v3" 9 | 10 | "github.com/caixw/apidoc/v7/core" 11 | ) 12 | 13 | func TestWorkspaceFolder_Contains(t *testing.T) { 14 | a := assert.New(t, false) 15 | 16 | data := []*struct { 17 | folder, path string 18 | contained bool 19 | }{ 20 | { 21 | contained: true, 22 | }, 23 | { 24 | folder: "file:///root/p1", 25 | path: "file:///root/p1/p2", 26 | contained: true, 27 | }, 28 | { 29 | folder: "file:///root/p1", 30 | path: "/root/p1/p2", 31 | contained: true, 32 | }, 33 | { 34 | folder: "http://root/p1", 35 | path: "http://root/p1/p2", 36 | contained: true, 37 | }, 38 | { 39 | folder: "https://root/p1", 40 | path: "https://root/p1/p2", 41 | contained: true, 42 | }, 43 | { 44 | folder: "http://root/p1", 45 | path: "https://root/p1/p2", 46 | }, 47 | { 48 | folder: "http:///root/p1", 49 | path: "/root/p1/p2", 50 | }, 51 | { 52 | folder: "file:///root/p1", 53 | path: "file:///root", 54 | }, 55 | } 56 | 57 | for _, item := range data { 58 | folder := WorkspaceFolder{URI: core.URI(item.folder)} 59 | a.Equal(folder.Contains(core.URI(item.path)), item.contained) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /internal/lsp/reference.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package lsp 4 | 5 | import ( 6 | "reflect" 7 | 8 | "github.com/caixw/apidoc/v7/core" 9 | "github.com/caixw/apidoc/v7/internal/ast" 10 | "github.com/caixw/apidoc/v7/internal/lsp/protocol" 11 | ) 12 | 13 | var ( 14 | referencerType = reflect.TypeOf((*ast.Referencer)(nil)).Elem() 15 | definitionerType = reflect.TypeOf((*ast.Definitioner)(nil)).Elem() 16 | ) 17 | 18 | // textDocument/references 19 | // 20 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references 21 | func (s *server) textDocumentReferences(notify bool, in *protocol.ReferenceParams, out *[]core.Location) error { 22 | f := s.findFolder(in.TextDocument.URI) 23 | if f == nil { 24 | return nil 25 | } 26 | 27 | f.parsedMux.RLock() 28 | defer f.parsedMux.RUnlock() 29 | 30 | *out = references(f.doc, in.TextDocument.URI, in.Position, in.Context.IncludeDeclaration) 31 | return nil 32 | } 33 | 34 | // textDocument/definition 35 | // 36 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition 37 | func (s *server) textDocumentDefinition(notify bool, in *protocol.DefinitionParams, out *[]core.Location) error { 38 | // NOTE: LSP 允许 out 的值是 null,而 jsonrpc 模块默认情况下是空值,而不是 nil, 39 | // 所以在可能的情况下,都尽量将其返回类型改为数组, 40 | // 或是像 protocol.Hover 一样为返回类型实现 json.Marshaler 接口。 41 | f := s.findFolder(in.TextDocument.URI) 42 | if f == nil { 43 | return nil 44 | } 45 | 46 | f.parsedMux.RLock() 47 | defer f.parsedMux.RUnlock() 48 | 49 | if r := f.doc.Search(in.TextDocument.URI, in.TextDocumentPositionParams.Position, definitionerType); r != nil { 50 | *out = []core.Location{r.(ast.Definitioner).Definition().Location} 51 | } 52 | return nil 53 | } 54 | 55 | func references(doc *ast.APIDoc, uri core.URI, pos core.Position, include bool) (locations []core.Location) { 56 | r := doc.Search(uri, pos, referencerType) 57 | if r == nil { 58 | return 59 | } 60 | 61 | referencer := r.(ast.Referencer) 62 | if include { 63 | locations = append(locations, referencer.Loc()) 64 | } 65 | 66 | for _, ref := range referencer.References() { 67 | locations = append(locations, ref.Location) 68 | } 69 | 70 | return 71 | } 72 | -------------------------------------------------------------------------------- /internal/lsp/server_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package lsp 4 | 5 | import ( 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "testing" 10 | 11 | "github.com/issue9/assert/v3" 12 | "github.com/issue9/jsonrpc" 13 | 14 | "github.com/caixw/apidoc/v7/internal/lsp/protocol" 15 | ) 16 | 17 | func newTestServer(header bool, info, erro *log.Logger) *server { 18 | return newServe(jsonrpc.NewStreamTransport(header, os.Stdin, os.Stdout, nil), info, erro) 19 | } 20 | 21 | func TestServer_setTrace(t *testing.T) { 22 | a := assert.New(t, false) 23 | s := newTestServer(true, log.New(ioutil.Discard, "", 0), log.New(ioutil.Discard, "", 0)) 24 | 25 | err := s.setTrace(false, &protocol.SetTraceParams{}, nil) 26 | a.Error(err) 27 | jerr, ok := err.(*jsonrpc.Error) 28 | a.True(ok).Equal(jerr.Code, jsonrpc.CodeInvalidParams) 29 | a.Equal(s.trace, protocol.TraceValueOff) 30 | 31 | err = s.setTrace(false, &protocol.SetTraceParams{Value: protocol.TraceValueOff}, nil) 32 | a.NotError(err).Equal(s.trace, protocol.TraceValueOff) 33 | 34 | err = s.setTrace(false, &protocol.SetTraceParams{Value: protocol.TraceValueVerbose}, nil) 35 | a.NotError(err).Equal(s.trace, protocol.TraceValueVerbose) 36 | } 37 | -------------------------------------------------------------------------------- /internal/lsp/workspace.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package lsp 4 | 5 | import ( 6 | "github.com/issue9/sliceutil" 7 | 8 | "github.com/caixw/apidoc/v7/internal/locale" 9 | "github.com/caixw/apidoc/v7/internal/lsp/protocol" 10 | ) 11 | 12 | // workspace/workspaceFolders 13 | // 14 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_workspaceFolders 15 | func (s *server) workspaceWorkspaceFolders() error { 16 | return s.Send("workspace/workspaceFolders", nil, func(folders *[]protocol.WorkspaceFolder) error { 17 | s.workspaceMux.Lock() 18 | defer s.workspaceMux.Unlock() 19 | 20 | for _, f := range s.folders { 21 | f.close() 22 | } 23 | s.folders = s.folders[:0] 24 | 25 | s.appendFolders(*folders...) 26 | return nil 27 | }) 28 | } 29 | 30 | // workspace/didChangeWorkspaceFolders 31 | // 32 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_didChangeWorkspaceFolders 33 | func (s *server) workspaceDidChangeWorkspaceFolders(notify bool, in *protocol.DidChangeWorkspaceFoldersParams, out *any) error { 34 | if s.getState() != serverInitialized { 35 | return newError(ErrInvalidRequest, locale.ErrInvalidLSPState) 36 | } 37 | 38 | s.workspaceMux.Lock() 39 | defer s.workspaceMux.Unlock() 40 | 41 | old := s.folders 42 | s.folders = sliceutil.Delete(s.folders, func(i *folder) bool { 43 | return sliceutil.Count(in.Event.Removed, func(j protocol.WorkspaceFolder) bool { 44 | return j.URI == i.URI 45 | }) > 0 46 | }) 47 | 48 | deleted := old[len(s.folders):] 49 | for _, f := range deleted { 50 | f.close() 51 | } 52 | 53 | s.appendFolders(in.Event.Added...) 54 | 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /internal/lsp/workspace_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package lsp 4 | 5 | import ( 6 | "io/ioutil" 7 | "log" 8 | "testing" 9 | 10 | "github.com/issue9/assert/v3" 11 | "github.com/issue9/jsonrpc" 12 | 13 | "github.com/caixw/apidoc/v7/core/messagetest" 14 | "github.com/caixw/apidoc/v7/internal/lsp/protocol" 15 | ) 16 | 17 | func TestServer_workspaceDidChangeWorkspaceFolders(t *testing.T) { 18 | a := assert.New(t, false) 19 | 20 | s := newTestServer(true, log.New(ioutil.Discard, "", 0), log.New(ioutil.Discard, "", 0)) 21 | in := &protocol.DidChangeWorkspaceFoldersParams{} 22 | err := s.workspaceDidChangeWorkspaceFolders(false, in, nil) 23 | a.Error(err) 24 | jerr, ok := err.(*jsonrpc.Error) 25 | a.True(ok).Equal(jerr.Code, ErrInvalidRequest) 26 | 27 | s = newTestServer(true, log.New(ioutil.Discard, "", 0), log.New(ioutil.Discard, "", 0)) 28 | s.setState(serverInitialized) 29 | in = &protocol.DidChangeWorkspaceFoldersParams{} 30 | a.NotError(s.workspaceDidChangeWorkspaceFolders(false, in, nil)) 31 | a.Equal(0, len(s.folders)) 32 | 33 | s = newTestServer(true, log.New(ioutil.Discard, "", 0), log.New(ioutil.Discard, "", 0)) 34 | s.setState(serverInitialized) 35 | s.folders = []*folder{ 36 | { 37 | srv: s, 38 | WorkspaceFolder: protocol.WorkspaceFolder{Name: "n0", URI: "file:///n0"}, 39 | h: messagetest.NewMessageHandler().Handler, 40 | }, 41 | { 42 | srv: s, 43 | WorkspaceFolder: protocol.WorkspaceFolder{Name: "n1", URI: "file:///n1"}, 44 | h: messagetest.NewMessageHandler().Handler, 45 | }, 46 | } 47 | 48 | in = &protocol.DidChangeWorkspaceFoldersParams{ 49 | Event: protocol.WorkspaceFoldersChangeEvent{ 50 | Added: []protocol.WorkspaceFolder{ 51 | {Name: "n3", URI: "file:///n3"}, 52 | }, 53 | Removed: []protocol.WorkspaceFolder{ 54 | {Name: "n1", URI: "file:///n1"}, 55 | }, 56 | }, 57 | } 58 | a.NotError(s.workspaceDidChangeWorkspaceFolders(false, in, nil)) 59 | a.Equal(2, len(s.folders)) 60 | } 61 | -------------------------------------------------------------------------------- /internal/mock/options.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package mock 4 | 5 | import ( 6 | "strconv" 7 | 8 | "github.com/caixw/apidoc/v7/internal/ast" 9 | ) 10 | 11 | // GenOptions 生成随机数据的函数 12 | type GenOptions struct { 13 | // 返回一个随机的数值 14 | // 15 | // 可以是浮点和整数类型。 16 | Number func(p *ast.Param) any 17 | 18 | // 返回一个随机长度的字符串 19 | String func(p *ast.Param) string 20 | 21 | // 返回一个随机的布尔值 22 | Bool func() bool 23 | 24 | // 返回一个随机的数值 25 | // 26 | // 该数值被用于声明 slice 长度,所以必须为正整数。 27 | SliceSize func() int 28 | 29 | // 返回一个介于 [0, max] 之间的数值 30 | // 31 | // 该数值被用于从数组中获取其中的某个元素。 32 | Index func(max int) int 33 | } 34 | 35 | func isEnum(p *ast.Param) bool { 36 | return len(p.Enums) > 0 37 | } 38 | 39 | func (g *GenOptions) generateBool() bool { 40 | return g.Bool() 41 | } 42 | 43 | func (g *GenOptions) generateNumber(p *ast.Param) any { 44 | if isEnum(p) { 45 | index := g.Index(len(p.Enums)) 46 | v, err := strconv.ParseInt(p.Enums[index].Value.V(), 10, 32) 47 | if err != nil { // 这属于文档定义错误,直接 panic 48 | panic(err) 49 | } 50 | return v 51 | } 52 | return g.Number(p) 53 | } 54 | 55 | func (g *GenOptions) generateString(p *ast.Param) string { 56 | if isEnum(p) { 57 | return p.Enums[g.Index(len(p.Enums))].Value.V() 58 | } 59 | return g.String(p) 60 | } 61 | 62 | func (g *GenOptions) generateSliceSize() int { 63 | return g.SliceSize() 64 | } 65 | -------------------------------------------------------------------------------- /internal/mock/options_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package mock 4 | 5 | import "github.com/caixw/apidoc/v7/internal/ast" 6 | 7 | const indent = " " 8 | 9 | var testOptions = &GenOptions{ 10 | Number: func(p *ast.Param) any { return 1024 }, 11 | String: func(p *ast.Param) string { 12 | switch p.Type.V() { 13 | case ast.TypeEmail: 14 | return "user@example.com" 15 | case ast.TypeURL: 16 | return "https://example.com" 17 | case ast.TypeDate: 18 | return "2020-01-02" 19 | case ast.TypeTime: 20 | return "15:16:17Z" 21 | case ast.TypeDateTime: 22 | return "2020-01-02T15:16:17Z" 23 | } 24 | return "1024" 25 | }, 26 | Bool: func() bool { return true }, 27 | SliceSize: func() int { return 5 }, 28 | Index: func(max int) int { return 0 }, 29 | } 30 | -------------------------------------------------------------------------------- /internal/node/value.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package node 4 | 5 | import ( 6 | "fmt" 7 | "reflect" 8 | "unicode" 9 | ) 10 | 11 | // Value 表示 XML 节点的值的反射表示方式 12 | type Value struct { 13 | reflect.Value 14 | Omitempty bool 15 | Name string // 节点的名称 16 | 17 | // 当前值可能未初始化,所以保存 usage 的值, 18 | // 等 Value 初始化之后再赋值给 Base.UsageKey 19 | Usage string 20 | } 21 | 22 | // NewValue 声明 *Value 实例 23 | func NewValue(name string, v reflect.Value, omitempty bool, usage string) *Value { 24 | return &Value{ 25 | Name: name, 26 | Value: v, 27 | Omitempty: omitempty, 28 | Usage: usage, 29 | } 30 | } 31 | 32 | // ParseValue 分析 v 并返回 *Value 实例 33 | // 34 | // 与 NewValue 的不同在于,ParseValue 会分析对象字段中是否带有 meta 的结构体标签, 35 | // 如果有才初始化 *Value 对象,否则返回 nil。 36 | func ParseValue(v reflect.Value) *Value { 37 | v = RealValue(v) 38 | t := v.Type() 39 | 40 | if t.Kind() != reflect.Struct { 41 | panic(fmt.Sprintf("%s 的 Kind() 必须为 reflect.Struct", v.Type())) 42 | } 43 | 44 | num := t.NumField() 45 | for i := 0; i < num; i++ { 46 | field := t.Field(i) 47 | if field.Anonymous || unicode.IsLower(rune(field.Name[0])) || field.Tag.Get(TagName) == "-" { 48 | continue 49 | } 50 | 51 | if name, node, usage, omitempty := parseTag(field); node == meta { 52 | return NewValue(name, v, omitempty, usage) 53 | } 54 | } 55 | return nil 56 | } 57 | 58 | // IsPrimitive 是否为有效的 Go 原始类型 59 | func IsPrimitive(v reflect.Value) bool { 60 | return v.IsValid() && 61 | (v.Kind() == reflect.String || (v.Kind() >= reflect.Bool && v.Kind() <= reflect.Complex128)) 62 | } 63 | 64 | // RealType 获取指针指向的类型 65 | func RealType(t reflect.Type) reflect.Type { 66 | for t.Kind() == reflect.Ptr { 67 | t = t.Elem() 68 | } 69 | return t 70 | } 71 | 72 | // RealValue 获取指针指向的值 73 | // 74 | // 如果未初始化,则会对其进行初始化。 75 | func RealValue(v reflect.Value) reflect.Value { 76 | for v.Kind() == reflect.Ptr { 77 | if v.IsNil() { 78 | v.Set(reflect.New(v.Type().Elem())) 79 | } 80 | v = v.Elem() 81 | } 82 | return v 83 | } 84 | -------------------------------------------------------------------------------- /internal/node/value_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package node 4 | 5 | import ( 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/issue9/assert/v3" 10 | ) 11 | 12 | func TestParseValue(t *testing.T) { 13 | a := assert.New(t, false) 14 | 15 | v := ParseValue(reflect.ValueOf(intTag{})) 16 | a.Equal(v.Name, "number"). 17 | Equal(v.Usage, "usage-number"). 18 | False(v.Omitempty) 19 | 20 | v = ParseValue(reflect.ValueOf(struct{}{})) 21 | a.Nil(v) 22 | 23 | v = ParseValue(reflect.ValueOf(&struct { 24 | Value int `apidoc:"-"` 25 | }{})) 26 | a.Nil(v) 27 | 28 | a.Panic(func() { 29 | v = ParseValue(reflect.ValueOf(1)) 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /internal/openapi/errors.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package openapi 4 | 5 | import "github.com/caixw/apidoc/v7/core" 6 | 7 | // 数据验证接口 8 | type sanitizer interface { 9 | Sanitize() *core.Error 10 | } 11 | -------------------------------------------------------------------------------- /internal/openapi/info.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package openapi 4 | 5 | import ( 6 | "github.com/issue9/validation/is" 7 | "github.com/issue9/version" 8 | 9 | "github.com/caixw/apidoc/v7/core" 10 | "github.com/caixw/apidoc/v7/internal/ast" 11 | "github.com/caixw/apidoc/v7/internal/locale" 12 | ) 13 | 14 | // Info 接口文档的基本信息 15 | type Info struct { 16 | Title string `json:"title" yaml:"title"` 17 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 18 | TermsOfService string `json:"termsOfService,omitempty" json:"termsOfService,omitempty"` 19 | Contact *Contact `json:"contact,omitempty" yaml:"contact,omitempty"` 20 | License *License `json:"license,omitempty" yaml:"license,omitempty"` 21 | Version string `json:"version" yaml:"version"` 22 | } 23 | 24 | // Contact 描述联系方式 25 | type Contact struct { 26 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 27 | URL string `json:"url,omitempty" yaml:"url,omitempty"` 28 | Email string `json:"email,omitempty" yaml:"email,omitempty"` 29 | } 30 | 31 | // License 授权信息 32 | type License struct { 33 | Name string `json:"name" yaml:"name"` 34 | URL string `json:"url,omitempty" yaml:"url,omitempty"` 35 | } 36 | 37 | func (info *Info) sanitize() *core.Error { 38 | if info.Title == "" { 39 | return core.NewError(locale.ErrIsEmpty, "title").WithField("title") 40 | } 41 | 42 | if !version.SemVerValid(info.Version) { 43 | return core.NewError(locale.ErrInvalidFormat).WithField("version") 44 | } 45 | 46 | if info.TermsOfService != "" && !is.URL(info.TermsOfService) { 47 | return core.NewError(locale.ErrInvalidFormat).WithField("termsOfService") 48 | } 49 | 50 | if info.Contact != nil { 51 | if err := info.Contact.sanitize(); err != nil { 52 | err.Field = "contact." + err.Field 53 | return err 54 | } 55 | } 56 | 57 | if info.License != nil { 58 | if err := info.License.sanitize(); err != nil { 59 | err.Field = "license." + err.Field 60 | return err 61 | } 62 | } 63 | 64 | return nil 65 | } 66 | 67 | func (l *License) sanitize() *core.Error { 68 | if l.URL != "" && !is.URL(l.URL) { 69 | return core.NewError(locale.ErrInvalidFormat).WithField("url") 70 | } 71 | 72 | return nil 73 | } 74 | 75 | func newLicense(l *ast.Link) *License { 76 | if l == nil { 77 | return nil 78 | } 79 | 80 | return &License{ 81 | Name: l.Text.V(), 82 | URL: l.URL.V(), 83 | } 84 | } 85 | 86 | func newContact(c *ast.Contact) *Contact { 87 | if c == nil { 88 | return nil 89 | } 90 | 91 | return &Contact{ 92 | Name: c.Name.V(), 93 | URL: c.URL.V(), 94 | Email: c.Email.V(), 95 | } 96 | } 97 | 98 | func (c *Contact) sanitize() *core.Error { 99 | if c.URL != "" && !is.URL(c.URL) { 100 | return core.NewError(locale.ErrInvalidFormat).WithField("url") 101 | } 102 | 103 | if c.Email != "" && !is.Email(c.Email) { 104 | return core.NewError(locale.ErrInvalidFormat).WithField("email") 105 | } 106 | 107 | return nil 108 | } 109 | -------------------------------------------------------------------------------- /internal/openapi/info_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package openapi 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/issue9/assert/v3" 9 | 10 | "github.com/caixw/apidoc/v7/internal/ast" 11 | "github.com/caixw/apidoc/v7/internal/xmlenc" 12 | ) 13 | 14 | func TestNewContact(t *testing.T) { 15 | a := assert.New(t, false) 16 | 17 | input := &ast.Contact{ 18 | Email: &ast.Element{Content: ast.Content{Value: "user@example.com"}}, 19 | URL: &ast.Element{Content: ast.Content{Value: "https://example.com"}}, 20 | Name: &ast.Attribute{Value: xmlenc.String{Value: "name"}}, 21 | } 22 | 23 | output := newContact(input) 24 | a.Equal(output.Email, input.Email.Content.Value) 25 | 26 | output = newContact(nil) 27 | a.Nil(output) 28 | } 29 | 30 | func TestInfo_sanitize(t *testing.T) { 31 | a := assert.New(t, false) 32 | 33 | info := &Info{} 34 | a.Error(info.sanitize()) 35 | 36 | info.Title = "title" 37 | a.Error(info.sanitize()) 38 | 39 | info.Title = "title" 40 | info.Version = "3.3.1" 41 | a.NotError(info.sanitize()) 42 | 43 | info.TermsOfService = "invalid url" 44 | a.Error(info.sanitize()) 45 | 46 | info.TermsOfService = "https://example.com" 47 | a.NotError(info.sanitize()) 48 | 49 | // contact 50 | 51 | info.Contact = &Contact{ 52 | Name: "name", 53 | Email: "invalid-email", 54 | URL: "invalid-url", 55 | } 56 | a.Error(info.sanitize()) 57 | 58 | info.Contact.URL = "https://example.com" 59 | a.Error(info.sanitize()) 60 | 61 | info.Contact.Email = "user@example.com" 62 | a.NotError(info.sanitize()) 63 | 64 | // License 65 | info.License = &License{ 66 | Name: "license", 67 | URL: "invalid-url", 68 | } 69 | a.Error(info.sanitize()) 70 | 71 | info.License.URL = "https://example.com" 72 | a.NotError(info.sanitize()) 73 | } 74 | -------------------------------------------------------------------------------- /internal/openapi/openapi_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package openapi 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/issue9/assert/v3" 9 | "github.com/issue9/version" 10 | ) 11 | 12 | func TestLatestVersion(t *testing.T) { 13 | a := assert.New(t, false) 14 | 15 | a.True(version.SemVerValid(LatestVersion)) 16 | } 17 | 18 | func TestOpenAPI_sanitize(t *testing.T) { 19 | a := assert.New(t, false) 20 | 21 | oa := &OpenAPI{Info: &Info{ 22 | Title: "title", 23 | Version: "3.3.3", 24 | }, 25 | Paths: map[string]*PathItem{ 26 | "/api": { 27 | Get: &Operation{ 28 | Responses: map[string]*Response{ 29 | "json": { 30 | Description: "desc", 31 | }, 32 | }, 33 | }, 34 | }, 35 | }, 36 | } 37 | a.NotError(oa.sanitize()) 38 | a.Equal(oa.OpenAPI, LatestVersion) 39 | } 40 | 41 | func TestExternalDocumentation_sanitize(t *testing.T) { 42 | a := assert.New(t, false) 43 | 44 | ed := &ExternalDocumentation{} 45 | a.Error(ed.sanitize()) 46 | 47 | ed.URL = "url" 48 | a.Error(ed.sanitize()) 49 | 50 | ed.URL = "https://example.com" 51 | a.NotError(ed.sanitize()) 52 | } 53 | 54 | func TestTag_sanitize(t *testing.T) { 55 | a := assert.New(t, false) 56 | 57 | tag := &Tag{ 58 | ExternalDocs: &ExternalDocumentation{}, 59 | } 60 | a.Error(tag.sanitize()) 61 | 62 | tag.Name = "name" 63 | a.Error(tag.sanitize()) 64 | 65 | tag.ExternalDocs.URL = "https://example.com" 66 | a.NotError(tag.sanitize()) 67 | } 68 | -------------------------------------------------------------------------------- /internal/openapi/parameter.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package openapi 4 | 5 | import ( 6 | "github.com/caixw/apidoc/v7/core" 7 | "github.com/caixw/apidoc/v7/internal/locale" 8 | ) 9 | 10 | // Parameter.IN 的可选值 11 | const ( 12 | ParameterINPath = "path" 13 | ParameterINQuery = "query" 14 | ParameterINHeader = "header" 15 | ParameterINCookie = "cookie" 16 | ) 17 | 18 | // Header 即 Parameter 的别名,但 Name 字段必须不能存在。 19 | type Header Parameter 20 | 21 | // Parameter 参数信息 22 | // 可同时作用于路径参数、请求参数、报头内容和 Cookie 值。 23 | type Parameter struct { 24 | Style 25 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 26 | IN string `json:"in,omitempty" yaml:"in,omitempty"` 27 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 28 | Required bool `json:"required,omitempty" yaml:"required,omitempty"` 29 | Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` 30 | AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` 31 | Schema *Schema `json:"schema,omitempty" yaml:"schema,omitempty"` 32 | Example ExampleValue `json:"example,omitempty" yaml:"example,omitempty"` 33 | Examples map[string]*Example `json:"examples,omitempty" yaml:"examples,omitempty"` 34 | Content map[string]*MediaType `json:"content,omitempty" yaml:"content,omitempty"` 35 | 36 | Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` 37 | } 38 | 39 | func (p *Parameter) sanitize() *core.Error { 40 | if err := p.Style.sanitize(); err != nil { 41 | return err 42 | } 43 | 44 | switch p.IN { 45 | case ParameterINCookie, ParameterINHeader, ParameterINPath, ParameterINQuery: 46 | default: 47 | return core.NewError(locale.ErrInvalidValue).WithField("in") 48 | } 49 | 50 | return nil 51 | } 52 | 53 | func (h *Header) sanitize() *core.Error { 54 | if err := h.Style.sanitize(); err != nil { 55 | return err 56 | } 57 | 58 | if h.IN != "" { 59 | return core.NewError(locale.ErrInvalidValue).WithField("in") 60 | } 61 | 62 | if h.Name != "" { 63 | return core.NewError(locale.ErrInvalidValue).WithField("name") 64 | } 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /internal/openapi/parameter_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package openapi 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/issue9/assert/v3" 9 | ) 10 | 11 | func TestParameter_sanitize(t *testing.T) { 12 | a := assert.New(t, false) 13 | 14 | p := &Parameter{} 15 | a.Error(p.sanitize()) 16 | 17 | p.Style = Style{Style: StyleDeepObject} 18 | a.Error(p.sanitize()) 19 | 20 | p.IN = ParameterINPath 21 | a.NotError(p.sanitize()) 22 | } 23 | 24 | func TestHeader_sanitize(t *testing.T) { 25 | a := assert.New(t, false) 26 | 27 | p := &Header{} 28 | a.Error(p.sanitize()) 29 | 30 | p.Style = Style{Style: StyleDeepObject} 31 | a.NotError(p.sanitize()) 32 | 33 | // IN 只能为空 34 | p.IN = ParameterINPath 35 | a.Error(p.sanitize()) 36 | 37 | p.IN = "" 38 | p.Name = "test" 39 | a.Error(p.sanitize()) 40 | 41 | p.Name = "" 42 | a.NotError(p.sanitize()) 43 | } 44 | -------------------------------------------------------------------------------- /internal/openapi/parse_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package openapi 4 | 5 | import ( 6 | "encoding/json" 7 | "net/http" 8 | "strconv" 9 | "testing" 10 | 11 | "github.com/issue9/assert/v3" 12 | 13 | "github.com/caixw/apidoc/v7/core" 14 | "github.com/caixw/apidoc/v7/internal/ast/asttest" 15 | ) 16 | 17 | func TestJSON(t *testing.T) { 18 | a := assert.New(t, false) 19 | data, err := JSON(asttest.Get()) 20 | a.NotError(err).NotNil(data) 21 | 22 | openapi := &OpenAPI{} 23 | a.NotError(json.Unmarshal(data, openapi)). 24 | Equal(3, len(openapi.Tags)). 25 | Equal(1, len(openapi.Paths)). 26 | Equal(openapi.ExternalDocs.URL, core.OfficialURL). 27 | NotEmpty(openapi.ExternalDocs.Description) 28 | 29 | path := openapi.Paths["/users"] 30 | a.NotNil(path) 31 | a.NotNil(path.Post).NotNil(path.Get).Nil(path.Patch) 32 | a.True(path.Post.Deprecated) 33 | a.Equal(path.Post.Summary, "summary") 34 | a.NotNil(path.Get).NotNil(path.Post) 35 | 36 | get := path.Get 37 | a.Equal(1, len(get.Responses)) 38 | a.Equal(len(get.Responses[strconv.Itoa(http.StatusOK)].Headers), 1) 39 | } 40 | 41 | func TestYAML(t *testing.T) { 42 | a := assert.New(t, false) 43 | data, err := YAML(asttest.Get()) 44 | a.NotError(err).NotNil(data) 45 | } 46 | -------------------------------------------------------------------------------- /internal/openapi/schema_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package openapi 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/issue9/assert/v3" 9 | 10 | "github.com/caixw/apidoc/v7/internal/ast" 11 | "github.com/caixw/apidoc/v7/internal/xmlenc" 12 | ) 13 | 14 | func TestNewSchema(t *testing.T) { 15 | a := assert.New(t, false) 16 | 17 | d := &ast.APIDoc{} 18 | input := &ast.Param{ 19 | Name: &ast.Attribute{Value: xmlenc.String{Value: "name"}}, 20 | Type: &ast.TypeAttribute{Value: xmlenc.String{Value: ast.TypeBool}}, 21 | Deprecated: &ast.VersionAttribute{Value: xmlenc.String{Value: "v1.1.0"}}, 22 | Default: &ast.Attribute{Value: xmlenc.String{Value: "true"}}, 23 | Optional: &ast.BoolAttribute{Value: ast.Bool{Value: true}}, 24 | Array: &ast.BoolAttribute{Value: ast.Bool{Value: false}}, 25 | Summary: &ast.Attribute{Value: xmlenc.String{Value: "summary"}}, 26 | } 27 | output := newSchema(d, input, true) 28 | a.Equal(output.Type, TypeBool). 29 | True(output.Deprecated). 30 | Equal(output.Title, input.Summary.V()) 31 | 32 | input.Array = &ast.BoolAttribute{Value: ast.Bool{Value: true}} 33 | output = newSchema(d, input, true) 34 | a.Equal(output.Type, TypeArray). 35 | Equal(output.Items.Type, TypeBool). 36 | True(output.Items.Deprecated). 37 | Equal(output.Items.Title, input.Summary.V()) 38 | 39 | input.Enums = []*ast.Enum{ 40 | { 41 | Value: &ast.Attribute{Value: xmlenc.String{Value: "v1"}}, 42 | Summary: &ast.Attribute{Value: xmlenc.String{Value: "s1"}}, 43 | }, 44 | { 45 | Value: &ast.Attribute{Value: xmlenc.String{Value: "v2"}}, 46 | Description: &ast.Richtext{ 47 | Text: &ast.CData{Value: xmlenc.String{Value: "s2"}}, 48 | }, 49 | Deprecated: &ast.VersionAttribute{Value: xmlenc.String{Value: "1.0.1"}}, 50 | }, 51 | } 52 | output = newSchema(d, input, false) 53 | a.Equal(output.Type, TypeBool). 54 | Equal(2, len(output.Enum)). 55 | Equal(output.Enum, []string{"v1", "v2"}) 56 | 57 | input = &ast.Param{ 58 | Type: &ast.TypeAttribute{Value: xmlenc.String{Value: ast.TypeNumber}}, 59 | Items: []*ast.Param{ 60 | { 61 | Name: &ast.Attribute{Value: xmlenc.String{Value: "p1"}}, 62 | Type: &ast.TypeAttribute{Value: xmlenc.String{Value: ast.TypeString}}, 63 | }, 64 | { 65 | Name: &ast.Attribute{Value: xmlenc.String{Value: "p2"}}, 66 | Type: &ast.TypeAttribute{Value: xmlenc.String{Value: ast.TypeNumber}}, 67 | }, 68 | }, 69 | } 70 | output = newSchema(d, input, true) 71 | a.Equal(output.Type, ""). 72 | Equal(len(input.Items), len(output.Properties)). 73 | Equal(output.Properties["p1"].Type, TypeString). 74 | Equal(output.Properties["p2"].Type, TypeDouble) 75 | 76 | a.NotError(output.sanitize()) 77 | } 78 | -------------------------------------------------------------------------------- /internal/openapi/security.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package openapi 4 | 5 | // SecurityScheme.IN 的可选值 6 | const ( 7 | SecurityInQuery = "query" 8 | SecurityInHeader = "header" 9 | SecurityInCookie = "cookie" 10 | ) 11 | 12 | // Security.Type 的可选值 13 | const ( 14 | SecurityTypeAPIKey = "apikey" 15 | SecurityTypeHTTP = "http" 16 | SecurityTypeOAuth2 = "oauth2" 17 | SecurityTypeOpenIDConnect = "openIdConnect" 18 | ) 19 | 20 | // SecurityRequirement Object 21 | // 22 | // 键名指向的是 Components.SecuritySchemes 中的名称。 23 | // 若 SecurityScheme.Type 是 oauth2 或是 openIDConnect, 24 | // 则 SecurityRequirement 的键值必须是个空值,否则键值为一个 scope 列表。 25 | type SecurityRequirement map[string][]string 26 | 27 | // SecurityScheme Object 28 | type SecurityScheme struct { 29 | Type string `json:"type" yaml:"type"` 30 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 31 | Name string `json:"name" yaml:"name"` // 报头或是 cookie 的名称 32 | IN string `json:"in" yaml:"in"` // 位置, header, query 和 cookie 33 | Scheme string `json:"scheme" yaml:"scheme"` 34 | BearerFormat string `json:"bearerFormat,omitempty" yaml:"bearerFormat,omitempty"` 35 | Flows *OAuthFlows `json:"flows" yaml:"flows"` 36 | OpenIDConnectURL string `json:"openIdConnectUrl" yaml:"openIdConnectUrl"` 37 | 38 | Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` 39 | } 40 | 41 | // OAuthFlows Object 42 | type OAuthFlows struct { 43 | Implicit *OAuthFlow `json:"implicit,omitempty" yaml:"implicit,omitempty"` 44 | Password *OAuthFlow `json:"password,omitempty" yaml:"password,omitempty"` 45 | ClientCredentials *OAuthFlow `json:"clientCredentials,omitempty" yaml:"clientCredentials,omitempty"` 46 | AuthorizationCode *OAuthFlow `json:"authorizationCode,omitempty" yaml:"authorizationCode,omitempty"` 47 | } 48 | 49 | // OAuthFlow Object 50 | type OAuthFlow struct { 51 | AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` 52 | TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` 53 | RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` 54 | Scopes map[string]string `json:"scopes,omitempty" yaml:"scopes,omitempty"` 55 | } 56 | -------------------------------------------------------------------------------- /internal/openapi/server.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package openapi 4 | 5 | import ( 6 | "strings" 7 | 8 | "github.com/caixw/apidoc/v7/core" 9 | "github.com/caixw/apidoc/v7/internal/ast" 10 | "github.com/caixw/apidoc/v7/internal/locale" 11 | ) 12 | 13 | // 去掉 URL 中的 {} 模板参数。使其符合 is.URL 的判断规则 14 | var urlreplace = strings.NewReplacer("{", "", "}", "") 15 | 16 | // Server 服务器描述信息 17 | type Server struct { 18 | URL string `json:"url" yaml:"url"` 19 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 20 | Variables map[string]*ServerVariable `json:"variables,omitempty" yaml:"variables,omitempty"` 21 | } 22 | 23 | // ServerVariable Server 中 URL 模板中对应的参数变量值 24 | type ServerVariable struct { 25 | Enum []string `json:"enum,omitempty" yaml:"enum,omitempty"` 26 | Default string `json:"default" yaml:"default"` 27 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 28 | } 29 | 30 | func newServer(srv *ast.Server) *Server { 31 | desc := srv.Summary.V() 32 | if srv.Description != nil && srv.Description.Text != nil { 33 | desc = srv.Description.V() 34 | } 35 | 36 | return &Server{ 37 | URL: srv.URL.V(), 38 | Description: desc, 39 | } 40 | } 41 | 42 | func (srv *Server) sanitize() *core.Error { 43 | url := urlreplace.Replace(srv.URL) 44 | if url == "" { // 可以是 / 未必是一个 URL 45 | return core.NewError(locale.ErrIsEmpty, "url").WithField("url") 46 | } 47 | 48 | for key, val := range srv.Variables { 49 | if err := val.sanitize(); err != nil { 50 | err.Field = "variables[" + key + "]." + err.Field 51 | return err 52 | } 53 | 54 | k := "{" + key + "}" 55 | if !strings.Contains(srv.URL, k) { 56 | return core.NewError(locale.ErrInvalidValue).WithField("variables[" + key + "]") 57 | } 58 | } 59 | 60 | return nil 61 | } 62 | 63 | func (v *ServerVariable) sanitize() *core.Error { 64 | if v.Default == "" { 65 | return core.NewError(locale.ErrIsEmpty, "default").WithField("default") 66 | } 67 | 68 | if len(v.Enum) == 0 { 69 | return nil 70 | } 71 | 72 | found := false 73 | for _, item := range v.Enum { 74 | if item == v.Default { 75 | found = true 76 | break 77 | } 78 | } 79 | 80 | if !found { 81 | return core.NewError(locale.ErrInvalidValue).WithField("default") 82 | } 83 | 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /internal/openapi/server_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package openapi 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/issue9/assert/v3" 9 | 10 | "github.com/caixw/apidoc/v7/internal/ast" 11 | "github.com/caixw/apidoc/v7/internal/xmlenc" 12 | ) 13 | 14 | func TestNewServer(t *testing.T) { 15 | a := assert.New(t, false) 16 | 17 | input := &ast.Server{ 18 | URL: &ast.Attribute{Value: xmlenc.String{Value: "https://example.com"}}, 19 | Name: &ast.Attribute{Value: xmlenc.String{Value: "name"}}, 20 | Summary: &ast.Attribute{Value: xmlenc.String{Value: "summary"}}, 21 | } 22 | 23 | output := newServer(input) 24 | a.NotNil(output). 25 | Equal(output.URL, "https://example.com"). 26 | Equal(output.Description, "summary") 27 | 28 | input.Description = &ast.Richtext{Text: &ast.CData{Value: xmlenc.String{Value: "desc"}}} 29 | output = newServer(input) 30 | a.NotNil(output). 31 | Equal(output.URL, "https://example.com"). 32 | Equal(output.Description, "desc") 33 | } 34 | 35 | func TestServer_sanitize(t *testing.T) { 36 | a := assert.New(t, false) 37 | 38 | srv := &Server{} 39 | a.Error(srv.sanitize()) 40 | 41 | srv.URL = "https://example.com/{tpl1}/{tpl2}/path3" 42 | a.NotError(srv.sanitize()) 43 | 44 | srv.Variables = map[string]*ServerVariable{ 45 | "tpl1": {Default: "1"}, 46 | "tpl2": {Default: "2", Enum: []string{"1", "2"}}, 47 | } 48 | a.NotError(srv.sanitize()) 49 | 50 | // variable 不在 URL 中 51 | srv.Variables = map[string]*ServerVariable{ 52 | "tpl3": {Default: "1"}, 53 | } 54 | a.Error(srv.sanitize()) 55 | 56 | // variables 存在错误 57 | srv.Variables = map[string]*ServerVariable{ 58 | "tpl2": {Default: "not-exists", Enum: []string{"1", "2"}}, 59 | } 60 | a.Error(srv.sanitize()) 61 | } 62 | 63 | func TestServerVariable_sanitize(t *testing.T) { 64 | a := assert.New(t, false) 65 | 66 | sv := &ServerVariable{} 67 | a.Error(sv.sanitize()) 68 | 69 | sv.Enum = []string{"e1", "e2"} 70 | a.Error(sv.sanitize()) 71 | 72 | sv.Default = "not-in-enum" 73 | a.Error(sv.sanitize()) 74 | 75 | sv.Default = "e1" 76 | a.NotError(sv.sanitize()) 77 | } 78 | -------------------------------------------------------------------------------- /internal/openapi/style.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package openapi 4 | 5 | import ( 6 | "github.com/caixw/apidoc/v7/core" 7 | "github.com/caixw/apidoc/v7/internal/locale" 8 | ) 9 | 10 | // Style.Style 的可选值 11 | const ( 12 | StyleMatrix = "matrix" 13 | StyleLabel = "label" 14 | StyleForm = "form" 15 | StyleSimple = "simple" 16 | StyleSpaceDelimited = "spaceDelimited" 17 | StylePipeDelimited = "pipeDelimited" 18 | StyleDeepObject = "deepObject" 19 | ) 20 | 21 | // Style 民法风格的相关定义 22 | // 23 | // 不直接作用于对象,被部分对象包含,比如 Encoding 和 Parameter 等 24 | type Style struct { 25 | Style string `json:"style,omitempty" yaml:"style,omitempty"` 26 | Explode bool `json:"explode,omitempty" yaml:"explode,omitempty"` 27 | AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` 28 | } 29 | 30 | func (style *Style) sanitize() *core.Error { 31 | switch style.Style { 32 | case StyleMatrix, StyleLabel, StyleForm, StyleSimple, StyleSpaceDelimited, StylePipeDelimited, StyleDeepObject: 33 | default: 34 | return core.NewError(locale.ErrInvalidValue).WithField("style") 35 | } 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /internal/openapi/style_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package openapi 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/issue9/assert/v3" 9 | ) 10 | 11 | func TestStyle_sanitize(t *testing.T) { 12 | a := assert.New(t, false) 13 | 14 | s := &Style{} 15 | a.Error(s.sanitize()) 16 | 17 | s.Style = StyleDeepObject 18 | a.NotError(s.sanitize()) 19 | 20 | s.Style = "invalid-value..." 21 | a.Error(s.sanitize()) 22 | } 23 | -------------------------------------------------------------------------------- /internal/xmlenc/token.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package xmlenc 4 | 5 | import "github.com/caixw/apidoc/v7/core" 6 | 7 | type ( 8 | // Name 表示 XML 中的名称 9 | Name struct { 10 | core.Location 11 | Prefix String 12 | Local String 13 | } 14 | 15 | // StartElement 表示 XML 的元素 16 | StartElement struct { 17 | core.Location 18 | Name Name 19 | Attributes []*Attribute 20 | SelfClose bool // 是否自闭合 21 | } 22 | 23 | // EndElement XML 的结束元素 24 | EndElement struct { 25 | core.Location 26 | Name Name 27 | } 28 | 29 | // Instruction 表示 XML 的指令 30 | Instruction struct { 31 | core.Location 32 | Name String 33 | Attributes []*Attribute 34 | } 35 | 36 | // Attribute 表示 XML 属性 37 | Attribute struct { 38 | core.Location 39 | Name Name 40 | Value String 41 | } 42 | 43 | // String 表示 XML 的字符串数据 44 | String struct { 45 | core.Location 46 | Value string 47 | } 48 | 49 | // CData 表示 XML 的 CDATA 数据 50 | CData struct { 51 | BaseTag 52 | Value String 53 | } 54 | 55 | // Comment 表示 XML 的注释 56 | Comment struct { 57 | core.Location 58 | Value String 59 | } 60 | ) 61 | 62 | // Match 是否与 end 相匹配 63 | func (s *StartElement) Match(end *EndElement) bool { 64 | return s.Name.Equal(end.Name) 65 | } 66 | 67 | // Equal 两个 name 是否相等 68 | func (n Name) Equal(v Name) bool { 69 | return n.Prefix.Value == v.Prefix.Value && 70 | n.Local.Value == v.Local.Value 71 | } 72 | 73 | // String fmt.Stringer 74 | func (n Name) String() string { 75 | if n.Prefix.Value == "" { 76 | return n.Local.Value 77 | } 78 | return n.Prefix.Value + ":" + n.Local.Value 79 | } 80 | -------------------------------------------------------------------------------- /internal/xmlenc/xmlenc.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | // Package xmlenc 有关文档格式编解码处理 4 | package xmlenc 5 | 6 | import ( 7 | "golang.org/x/text/message" 8 | 9 | "github.com/caixw/apidoc/v7/core" 10 | "github.com/caixw/apidoc/v7/internal/locale" 11 | ) 12 | 13 | // Base 所有文档节点的基本元素 14 | type Base struct { 15 | core.Location 16 | UsageKey message.Reference `apidoc:"-"` // 表示对当前元素的一个说明内容的翻译 ID 17 | } 18 | 19 | // BaseAttribute 所有 XML 属性节点的基本元素 20 | type BaseAttribute struct { 21 | Base 22 | AttributeName Name `apidoc:"-"` 23 | } 24 | 25 | // BaseTag 所有 XML 标签的基本元素 26 | type BaseTag struct { 27 | Base 28 | StartTag Name `apidoc:"-"` // 表示起始标签名 29 | EndTag Name `apidoc:"-"` // 表示标签的结束名称,如果是自闭合的标签,此值的 Local.Value 为空。 30 | } 31 | 32 | // Usage 本地化的当前字段介绍内容 33 | func (b Base) Usage() string { 34 | if b.UsageKey == nil { 35 | return "" 36 | } 37 | 38 | return locale.Sprintf(b.UsageKey) 39 | } 40 | 41 | // SelfClose 当前是否为自闭合标签 42 | func (b *BaseTag) SelfClose() bool { 43 | return b.EndTag.Local.Value == "" 44 | } 45 | -------------------------------------------------------------------------------- /internal/xmlenc/xmlenc_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package xmlenc 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/issue9/assert/v3" 9 | 10 | "github.com/caixw/apidoc/v7/internal/locale" 11 | ) 12 | 13 | func TestBaseTag_SelfClose(t *testing.T) { 14 | a := assert.New(t, false) 15 | 16 | b := &BaseTag{} 17 | a.True(b.SelfClose()) 18 | 19 | b = &BaseTag{EndTag: Name{ 20 | Prefix: String{ 21 | Value: "name", 22 | }, 23 | }} 24 | a.True(b.SelfClose()) 25 | 26 | b.EndTag.Local = String{ 27 | Value: "name", 28 | } 29 | a.False(b.SelfClose()) 30 | } 31 | 32 | func TestBase_Usage(t *testing.T) { 33 | a := assert.New(t, false) 34 | 35 | b := Base{} 36 | a.Empty(b.Usage()) 37 | 38 | b.UsageKey = locale.ErrInvalidUTF8Character 39 | a.Equal(b.Usage(), locale.Sprintf(locale.ErrInvalidUTF8Character)) 40 | } 41 | -------------------------------------------------------------------------------- /scripts/build-env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # SPDX-License-Identifier: MIT 3 | 4 | # 指定工作目录 5 | wd=$(dirname $0)/../cmd/apidoc 6 | 7 | # 指定编译日期 8 | date=`date -u '+%Y%m%d'` 9 | 10 | # 获取最后一条前交的 hash 11 | hash=`git rev-parse HEAD` 12 | 13 | # 需要修改变量的地址,若为 main,则指接使用 main,而不是全地址 14 | path=github.com/caixw/apidoc/v7/core 15 | 16 | ldflags="-X ${path}.metadata=${date}.${hash}" 17 | -------------------------------------------------------------------------------- /scripts/build.cmd: -------------------------------------------------------------------------------- 1 | :: SPDX-License-Identifier: MIT 2 | 3 | set wd=%~dp0\..\cmd\apidoc 4 | 5 | set mainPath=github.com\caixw\apidoc 6 | 7 | set varsPath=%mainPath%\v7\core 8 | 9 | set builddate=%date:~0,4%%date:~5,2%%date:~8,2% 10 | 11 | for /f "delims=" %%t in ('git rev-parse HEAD') do set hash=%%t 12 | 13 | echo compile 14 | go build -o %wd%\apidoc.exe -ldflags "-X %varsPath%.metadata=%builddate%.%hash%" -v %wd% 15 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # SPDX-License-Identifier: MIT 3 | 4 | source $(dirname $0)/build-env.sh 5 | 6 | cd ${wd} 7 | 8 | echo '开始编译' 9 | go build -o ./apidoc -ldflags "${ldflags}" -v 10 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # SPDX-License-Identifier: MIT 3 | 4 | source $(dirname $0)/build-env.sh 5 | 6 | cd ${wd} 7 | 8 | echo '开始编译' 9 | go install -ldflags "${ldflags}" -v 10 | --------------------------------------------------------------------------------