├── .gitattributes ├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── .gomarkdoc.yml ├── .goreleaser.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cmd └── gomarkdoc │ ├── README.md │ ├── command.go │ ├── command_test.go │ ├── doc.go │ ├── main.go │ └── output.go ├── codecov.yml ├── doc.go ├── format ├── README.md ├── devops.go ├── devops_test.go ├── doc.go ├── format.go ├── formatcore │ ├── README.md │ ├── base.go │ ├── base_test.go │ └── doc.go ├── github.go ├── github_test.go ├── plain.go └── plain_test.go ├── gentmpl.sh ├── go.mod ├── go.sum ├── lang ├── README.md ├── block.go ├── config.go ├── config_test.go ├── doc.go ├── example.go ├── file.go ├── func.go ├── func_test.go ├── godoc.go ├── list.go ├── package.go ├── package_test.go ├── span.go ├── symbol.go ├── type.go ├── type_test.go ├── util.go ├── value.go └── value_test.go ├── logger ├── README.md └── logger.go ├── magefile.go ├── renderer.go ├── renderer_test.go ├── templates.go ├── templates ├── doc.gotxt ├── example.gotxt ├── file.gotxt ├── func.gotxt ├── import.gotxt ├── index.gotxt ├── list.gotxt ├── package.gotxt ├── text.gotxt ├── type.gotxt └── value.gotxt └── testData ├── .gomarkdoc-empty.yml ├── docs ├── .gomarkdoc.yml ├── README-azure-devops.md ├── README-github.md ├── README-plain.md ├── anotherFile.go └── docs.go ├── embed ├── .gomarkdoc.yml ├── README-azure-devops.md ├── README-github-invalid.md ├── README-github.md ├── README-plain.md ├── README-template.md └── embed.go ├── generics ├── README-azure-devops.md ├── README-github.md ├── README-plain.md └── generics.go ├── lang └── function │ ├── README-azure-devops.md │ ├── README-github.md │ ├── README-plain.md │ ├── func.go │ ├── func_test.go │ └── value.go ├── nested ├── .gomarkdoc.yml ├── README-azure-devops.md ├── README-github.md ├── README-plain.md ├── inner │ ├── README-azure-devops.md │ ├── README-github.md │ ├── README-plain.md │ └── child.go └── parent.go ├── simple ├── .gomarkdoc.yml ├── README-azure-devops.md ├── README-github.md ├── README-plain.md └── main.go ├── tags ├── .gomarkdoc.yml ├── README-azure-devops.md ├── README-github.md ├── README-plain.md ├── tagged.go └── untagged.go ├── unexported ├── .gomarkdoc.yml ├── README-azure-devops.md ├── README-github.md ├── README-plain.md └── main.go └── untagged ├── .gomarkdoc.yml ├── README-azure-devops.md ├── README-github.md ├── README-plain.md ├── tagged.go └── untagged.go /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches-ignore: 5 | - '**' 6 | tags: 7 | - 'v*.*.*' 8 | - 'v*.*.*-*.*' 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v1 15 | - name: Install Go 16 | uses: actions/setup-go@v1 17 | with: 18 | go-version: 1.20.x 19 | - name: Run GoReleaser 20 | uses: goreleaser/goreleaser-action@v1 21 | with: 22 | version: latest 23 | args: release --rm-dist 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | jobs: 8 | test: 9 | strategy: 10 | matrix: 11 | platform: 12 | - ubuntu-latest 13 | - macos-latest 14 | - windows-latest 15 | runs-on: ${{matrix.platform}} 16 | steps: 17 | - name: Install Go 18 | uses: actions/setup-go@v1 19 | with: 20 | go-version: 1.20.x 21 | - name: Checkout code 22 | uses: actions/checkout@v2 23 | - name: Lint 24 | uses: magefile/mage-action@v1 25 | with: 26 | version: latest 27 | args: lint 28 | - name: Doc Verify 29 | uses: magefile/mage-action@v1 30 | with: 31 | version: latest 32 | args: docVerify 33 | - name: Test 34 | uses: magefile/mage-action@v1 35 | with: 36 | version: latest 37 | args: test 38 | - name: Upload Coverage 39 | if: ${{ matrix.platform == 'ubuntu-latest' }} 40 | uses: codecov/codecov-action@v2 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage.txt 2 | /gomarkdoc 3 | /bin/ 4 | README-*-test.md 5 | 6 | # editor and IDE paraphernalia 7 | .vscode/* 8 | .idea 9 | *.iml 10 | *.swp 11 | *.swo 12 | *~ 13 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - bodyclose 4 | - gochecknoinits 5 | - gocritic 6 | - gocyclo 7 | - goimports 8 | - govet 9 | - lll 10 | - misspell 11 | - revive 12 | - unconvert 13 | 14 | # Not currently compatible with go 1.18 15 | disable: 16 | - gosimple 17 | - staticcheck 18 | - structcheck 19 | - unused 20 | 21 | linters-settings: 22 | gocyclo: 23 | min-complexity: 15 24 | lll: 25 | tab-width: 4 26 | nakedret: 27 | max-func-lines: 10 28 | revive: 29 | min-confidence: 0.5 30 | 31 | issues: 32 | exclude-rules: 33 | # Exclude assertion lines in tests from line length 34 | - path: "_test\\.go" 35 | linters: 36 | - lll 37 | source: "^\\s*is." 38 | 39 | # Exclude documentation files from line length 40 | - path: "doc\\.go" 41 | linters: 42 | - lll 43 | 44 | exclude: 45 | # errcheck: Almost all programs ignore errors on these functions and in most cases it's ok 46 | - Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked 47 | 48 | exclude-use-default: false 49 | 50 | run: 51 | skip-dirs: 52 | - testData -------------------------------------------------------------------------------- /.gomarkdoc.yml: -------------------------------------------------------------------------------- 1 | output: "{{.Dir}}/README.md" 2 | header: |- 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/princjef/gomarkdoc)](https://goreportcard.com/report/github.com/princjef/gomarkdoc) 4 | [![GitHub Actions](https://github.com/princjef/gomarkdoc/workflows/Test/badge.svg)](https://github.com/princjef/gomarkdoc/actions?query=workflow%3ATest+branch%3Amaster) 5 | [![Go Reference](https://pkg.go.dev/badge/github.com/princjef/gomarkdoc.svg)](https://pkg.go.dev/github.com/princjef/gomarkdoc) 6 | [![codecov](https://codecov.io/gh/princjef/gomarkdoc/branch/master/graph/badge.svg?token=171XNH5XLT)](https://codecov.io/gh/princjef/gomarkdoc) 7 | repository: 8 | url: https://github.com/princjef/gomarkdoc 9 | defaultBranch: master 10 | path: / 11 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - main: ./cmd/gomarkdoc/ 3 | goos: 4 | - darwin 5 | - windows 6 | - linux 7 | - freebsd 8 | goarch: 9 | - amd64 10 | - arm64 11 | - arm 12 | - 386 13 | - ppc64le 14 | - s390x 15 | goarm: 16 | - 6 17 | - 7 18 | env: 19 | - CGO_ENABLED=0 20 | ldflags: -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} 21 | archives: 22 | - wrap_in_directory: true 23 | format_overrides: 24 | - goos: windows 25 | format: zip 26 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | ## Issues 4 | 5 | If you have a bug to report or a feature that you'd like to see, head over to 6 | the [issues][] section of the repository to open an issue. Some guidelines: 7 | 8 | - Do a check of the existing issues (both open and closed) to see if anyone had 9 | the same issue/request. If you find something, feel free to add your voice to 10 | the conversation if there's something new to add or or [give the issue a 11 | thumbs up][github reactions] to show you have the same issue/request. 12 | 13 | - Please include the version of Aster that you are using. If you're not on the 14 | latest version, try using that first and see if it fixes your issue. 15 | 16 | - Include specific details about what you were doing and what went wrong and 17 | how to reproduce the issue. Some sample code or a sample gist/repository go a 18 | long way to helping with debugging and finding a fix. 19 | 20 | - Be respectful of others in the conversation. Issues should be a place where 21 | people can discuss what they're seeing, learn and work toward a solution 22 | without worrying about being judged or lambasted. 23 | 24 | - If you have a question rather than an issue or feature request, don't be 25 | afraid to post it in the issues, but please be patient if it takes longer to 26 | get a response. All are encouraged to help answer questions. 27 | 28 | ## Pull Requests 29 | 30 | Do you have a bugfix for an issue, a new feature, or even a fix for a typo in 31 | the documentation? You should open a Pull Request! Some steps to follow: 32 | 33 | 1. If your change is a substantial addition or it will result in a breaking 34 | change to the library, consider first [opening an issue](#issues) to dicuss 35 | the problem and the proposed solution. 36 | 37 | 2. [Fork the repository][github fork] if you haven't before 38 | 39 | 3. [Set up your development environment](#developer-setup) and make your 40 | changes. Also be sure to add tests for your change. 41 | 42 | 4. When you're ready to push your changes, run `npm t` to lint, build and test 43 | your code. Any failures here will cause your pull request's continuous 44 | integration to fail, so it's best to catch it early. 45 | 46 | 5. Once you've pushed your code into your fork, [open a pull request][new pull 47 | request] and follow the template to fill in the pull request information. 48 | 49 | ## Developer Setup 50 | 51 | ### Prerequisites 52 | 53 | - [Golang][] 1.13.x or later 54 | - [Mage][] 1.9.x or later 55 | 56 | ### Setting Up 57 | 58 | You can verify that things are up and running properly by executing: 59 | 60 | ``` 61 | mage test 62 | ``` 63 | 64 | ### Testing 65 | 66 | Once you have made a change, you'll want to make sure that all of the tests are 67 | passing. You can do so by running: 68 | 69 | ``` 70 | mage test 71 | ``` 72 | 73 | This will run tests and compute code coverage on the source code. You can view a 74 | detailed html coverage report by running: 75 | 76 | ``` 77 | mage coverage 78 | ``` 79 | 80 | You can also lint the code by running: 81 | 82 | ``` 83 | mage lint 84 | ``` 85 | 86 | Finally, when you're ready to submit a change, you'll want to make sure that the 87 | documentation for this repository has been regenerated: 88 | 89 | ``` 90 | mage doc 91 | ``` 92 | 93 | [issues]: https://github.com/princjef/gomarkdoc/issues 94 | [new pull request]: https://github.com/princjef/gomarkdoc/compare 95 | [github reactions]: https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/ 96 | [github fork]: https://help.github.com/articles/fork-a-repo 97 | [golang]: https://golang.org/ 98 | [mage]: https://magefile.org/ 99 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jeff Principe 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 | -------------------------------------------------------------------------------- /cmd/gomarkdoc/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # gomarkdoc 4 | 5 | ```go 6 | import "github.com/princjef/gomarkdoc/cmd/gomarkdoc" 7 | ``` 8 | 9 | Package gomarkdoc provides a command line interface for writing golang documentation in markdown format. 10 | 11 | See https://github.com/princjef/gomarkdoc for full documentation of this tool. 12 | 13 | ## Index 14 | 15 | - [type PackageSpec](<#PackageSpec>) 16 | 17 | 18 | 19 | ## type [PackageSpec]() 20 | 21 | PackageSpec defines the data available to the \-\-output option's template. Information is recomputed for each package generated. 22 | 23 | ```go 24 | type PackageSpec struct { 25 | // Dir holds the local path where the package is located. If the package is 26 | // a remote package, this will always be ".". 27 | Dir string 28 | 29 | // ImportPath holds a representation of the package that should be unique 30 | // for most purposes. If a package is on the filesystem, this is equivalent 31 | // to the value of Dir. For remote packages, this holds the string used to 32 | // import that package in code (e.g. "encoding/json"). 33 | ImportPath string 34 | // contains filtered or unexported fields 35 | } 36 | ``` 37 | 38 | Generated by [gomarkdoc]() 39 | -------------------------------------------------------------------------------- /cmd/gomarkdoc/doc.go: -------------------------------------------------------------------------------- 1 | // Package gomarkdoc provides a command line interface for writing golang 2 | // documentation in markdown format. 3 | // 4 | // See https://github.com/princjef/gomarkdoc for full documentation of this 5 | // tool. 6 | package main 7 | -------------------------------------------------------------------------------- /cmd/gomarkdoc/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | func main() { 8 | log.SetFlags(0) 9 | 10 | cmd := buildCommand() 11 | 12 | if err := cmd.Execute(); err != nil { 13 | log.Fatal(err) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /cmd/gomarkdoc/output.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "regexp" 11 | "time" 12 | 13 | "github.com/princjef/gomarkdoc" 14 | "github.com/princjef/gomarkdoc/lang" 15 | "github.com/princjef/gomarkdoc/logger" 16 | "github.com/princjef/termdiff" 17 | "github.com/sergi/go-diff/diffmatchpatch" 18 | ) 19 | 20 | func writeOutput(specs []*PackageSpec, opts commandOptions) error { 21 | log := logger.New(getLogLevel(opts.verbosity)) 22 | 23 | overrides, err := resolveOverrides(opts) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | out, err := gomarkdoc.NewRenderer(overrides...) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | header, err := resolveHeader(opts) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | footer, err := resolveFooter(opts) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | filePkgs := make(map[string][]*lang.Package) 44 | 45 | for _, spec := range specs { 46 | if spec.pkg == nil { 47 | continue 48 | } 49 | 50 | filePkgs[spec.outputFile] = append(filePkgs[spec.outputFile], spec.pkg) 51 | } 52 | 53 | var checkErr error 54 | for fileName, pkgs := range filePkgs { 55 | file := lang.NewFile(header, footer, pkgs) 56 | 57 | text, err := out.File(file) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | checkErr, err = handleFile(log, fileName, text, opts) 63 | if err != nil { 64 | return err 65 | } 66 | } 67 | 68 | if checkErr != nil { 69 | return checkErr 70 | } 71 | 72 | return nil 73 | } 74 | 75 | func handleFile(log logger.Logger, fileName string, text string, opts commandOptions) (error, error) { 76 | if opts.embed && fileName != "" { 77 | text = embedContents(log, fileName, text) 78 | } 79 | 80 | switch { 81 | case fileName == "": 82 | fmt.Fprint(os.Stdout, text) 83 | case opts.check: 84 | var b bytes.Buffer 85 | fmt.Fprint(&b, text) 86 | if err := checkFile(&b, fileName); err != nil { 87 | return err, nil 88 | } 89 | default: 90 | if err := writeFile(fileName, text); err != nil { 91 | return nil, fmt.Errorf("failed to write output file %s: %w", fileName, err) 92 | } 93 | } 94 | return nil, nil 95 | } 96 | 97 | func writeFile(fileName string, text string) error { 98 | folder := filepath.Dir(fileName) 99 | 100 | if folder != "" { 101 | if err := os.MkdirAll(folder, 0755); err != nil { 102 | return fmt.Errorf("failed to create folder %s: %w", folder, err) 103 | } 104 | } 105 | 106 | if err := ioutil.WriteFile(fileName, []byte(text), 0664); err != nil { 107 | return fmt.Errorf("failed to write file %s: %w", fileName, err) 108 | } 109 | 110 | return nil 111 | } 112 | 113 | func checkFile(b *bytes.Buffer, path string) error { 114 | checkErr := errors.New("output does not match current files. Did you forget to run gomarkdoc?") 115 | 116 | fileContents, err := os.ReadFile(path) 117 | if err == os.ErrNotExist { 118 | fileContents = []byte{} 119 | } else if err != nil { 120 | return fmt.Errorf("failed to open file %s for checking: %w", path, err) 121 | } 122 | 123 | differ := diffmatchpatch.New() 124 | diff := differ.DiffBisect(b.String(), string(fileContents), time.Now().Add(time.Second)) 125 | 126 | // Remove equal diffs 127 | var filtered = make([]diffmatchpatch.Diff, 0, len(diff)) 128 | for _, d := range diff { 129 | if d.Type == diffmatchpatch.DiffEqual { 130 | continue 131 | } 132 | 133 | filtered = append(filtered, d) 134 | } 135 | 136 | if len(filtered) != 0 { 137 | diffs := termdiff.DiffsFromDiffMatchPatch(diff) 138 | fmt.Fprintln(os.Stderr) 139 | termdiff.Fprint( 140 | os.Stderr, 141 | path, 142 | diffs, 143 | termdiff.WithBeforeText("(expected)"), 144 | termdiff.WithAfterText("(actual)"), 145 | ) 146 | return checkErr 147 | } 148 | 149 | return nil 150 | } 151 | 152 | var ( 153 | embedStandaloneRegex = regexp.MustCompile(`(?m:^ *)(?m:\s*?$)`) 154 | embedStartRegex = regexp.MustCompile( 155 | `(?m:^ *)(?s:.*?)(?m:\s*?$)`, 156 | ) 157 | ) 158 | 159 | func embedContents(log logger.Logger, fileName string, text string) string { 160 | embedText := fmt.Sprintf("\n\n%s\n\n", text) 161 | 162 | data, err := os.ReadFile(fileName) 163 | if err != nil { 164 | log.Debugf("unable to find output file %s for embedding. Creating a new file instead", fileName) 165 | return embedText 166 | } 167 | 168 | var replacements int 169 | data = embedStandaloneRegex.ReplaceAllFunc(data, func(_ []byte) []byte { 170 | replacements++ 171 | return []byte(embedText) 172 | }) 173 | 174 | data = embedStartRegex.ReplaceAllFunc(data, func(_ []byte) []byte { 175 | replacements++ 176 | return []byte(embedText) 177 | }) 178 | 179 | if replacements == 0 { 180 | log.Debugf("no embed markers found. Appending documentation to the end of the file instead") 181 | return fmt.Sprintf("%s\n\n%s", string(data), text) 182 | } 183 | 184 | return string(data) 185 | } 186 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - testData -------------------------------------------------------------------------------- /format/devops.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "path/filepath" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/princjef/gomarkdoc/format/formatcore" 11 | "github.com/princjef/gomarkdoc/lang" 12 | ) 13 | 14 | // AzureDevOpsMarkdown provides a Format which is compatible with Azure 15 | // DevOps's syntax and semantics. See the Azure DevOps documentation for more 16 | // details about their markdown format: 17 | // https://docs.microsoft.com/en-us/azure/devops/project/wiki/markdown-guidance?view=azure-devops 18 | type AzureDevOpsMarkdown struct{} 19 | 20 | // Bold converts the provided text to bold 21 | func (f *AzureDevOpsMarkdown) Bold(text string) (string, error) { 22 | return formatcore.Bold(text), nil 23 | } 24 | 25 | // CodeBlock wraps the provided code as a code block and tags it with the 26 | // provided language (or no language if the empty string is provided). 27 | func (f *AzureDevOpsMarkdown) CodeBlock(language, code string) (string, error) { 28 | return formatcore.GFMCodeBlock(language, code), nil 29 | } 30 | 31 | // Anchor produces an anchor for the provided link. 32 | func (f *AzureDevOpsMarkdown) Anchor(anchor string) string { 33 | return formatcore.Anchor(anchor) 34 | } 35 | 36 | // AnchorHeader converts the provided text and custom anchor link into a header 37 | // of the provided level. The level is expected to be at least 1. 38 | func (f *AzureDevOpsMarkdown) AnchorHeader(level int, text, anchor string) (string, error) { 39 | return formatcore.AnchorHeader(level, formatcore.Escape(text), anchor) 40 | } 41 | 42 | // Header converts the provided text into a header of the provided level. The 43 | // level is expected to be at least 1. 44 | func (f *AzureDevOpsMarkdown) Header(level int, text string) (string, error) { 45 | return formatcore.Header(level, formatcore.Escape(text)) 46 | } 47 | 48 | // RawAnchorHeader converts the provided text and custom anchor link into a 49 | // header of the provided level without escaping the header text. The level is 50 | // expected to be at least 1. 51 | func (f *AzureDevOpsMarkdown) RawAnchorHeader(level int, text, anchor string) (string, error) { 52 | return formatcore.AnchorHeader(level, text, anchor) 53 | } 54 | 55 | // RawHeader converts the provided text into a header of the provided level 56 | // without escaping the header text. The level is expected to be at least 1. 57 | func (f *AzureDevOpsMarkdown) RawHeader(level int, text string) (string, error) { 58 | return formatcore.Header(level, text) 59 | } 60 | 61 | var devOpsWhitespaceRegex = regexp.MustCompile(`\s`) 62 | 63 | // LocalHref generates an href for navigating to a header with the given 64 | // headerText located within the same document as the href itself. Link 65 | // generation follows the guidelines here: 66 | // https://docs.microsoft.com/en-us/azure/devops/project/wiki/markdown-guidance?view=azure-devops#anchor-links 67 | func (f *AzureDevOpsMarkdown) LocalHref(headerText string) (string, error) { 68 | result := strings.ToLower(headerText) 69 | result = strings.TrimSpace(result) 70 | result = devOpsWhitespaceRegex.ReplaceAllString(result, "-") 71 | result = url.PathEscape(result) 72 | // We also have to escape the `:` character if present 73 | result = strings.ReplaceAll(result, ":", "%3A") 74 | 75 | return fmt.Sprintf("#%s", result), nil 76 | } 77 | 78 | // RawLocalHref generates an href within the same document but with a direct 79 | // link provided instead of text to slugify. 80 | func (f *AzureDevOpsMarkdown) RawLocalHref(anchor string) string { 81 | return fmt.Sprintf("#%s", anchor) 82 | } 83 | 84 | // CodeHref generates an href to the provided code entry. 85 | func (f *AzureDevOpsMarkdown) CodeHref(loc lang.Location) (string, error) { 86 | // If there's no repo, we can't compute an href 87 | if loc.Repo == nil { 88 | return "", nil 89 | } 90 | 91 | var ( 92 | relative string 93 | err error 94 | ) 95 | if filepath.IsAbs(loc.Filepath) { 96 | relative, err = filepath.Rel(loc.WorkDir, loc.Filepath) 97 | if err != nil { 98 | return "", err 99 | } 100 | } else { 101 | relative = loc.Filepath 102 | } 103 | 104 | full := filepath.Join(loc.Repo.PathFromRoot, relative) 105 | p, err := filepath.Rel(string(filepath.Separator), full) 106 | if err != nil { 107 | return "", err 108 | } 109 | 110 | return fmt.Sprintf( 111 | "%s?path=%s&version=GB%s&lineStyle=plain&line=%d&lineEnd=%d&lineStartColumn=%d&lineEndColumn=%d", 112 | loc.Repo.Remote, 113 | url.PathEscape(filepath.ToSlash(p)), 114 | loc.Repo.DefaultBranch, 115 | loc.Start.Line, 116 | loc.End.Line, 117 | loc.Start.Col, 118 | loc.End.Col, 119 | ), nil 120 | } 121 | 122 | // Link generates a link with the given text and href values. 123 | func (f *AzureDevOpsMarkdown) Link(text, href string) (string, error) { 124 | return formatcore.Link(text, href), nil 125 | } 126 | 127 | // ListEntry generates an unordered list entry with the provided text at the 128 | // provided zero-indexed depth. A depth of 0 is considered the topmost level of 129 | // list. 130 | func (f *AzureDevOpsMarkdown) ListEntry(depth int, text string) (string, error) { 131 | return formatcore.ListEntry(depth, text), nil 132 | } 133 | 134 | // Accordion generates a collapsible content. The accordion's visible title 135 | // while collapsed is the provided title and the expanded content is the body. 136 | func (f *AzureDevOpsMarkdown) Accordion(title, body string) (string, error) { 137 | return formatcore.GFMAccordion(title, body), nil 138 | } 139 | 140 | // AccordionHeader generates the header visible when an accordion is collapsed. 141 | // 142 | // The AccordionHeader is expected to be used in conjunction with 143 | // AccordionTerminator() when the demands of the body's rendering requires it to 144 | // be generated independently. The result looks conceptually like the following: 145 | // 146 | // accordion := format.AccordionHeader("Accordion Title") + "Accordion Body" + format.AccordionTerminator() 147 | func (f *AzureDevOpsMarkdown) AccordionHeader(title string) (string, error) { 148 | return formatcore.GFMAccordionHeader(title), nil 149 | } 150 | 151 | // AccordionTerminator generates the code necessary to terminate an accordion 152 | // after the body. It is expected to be used in conjunction with 153 | // AccordionHeader(). See AccordionHeader for a full description. 154 | func (f *AzureDevOpsMarkdown) AccordionTerminator() (string, error) { 155 | return formatcore.GFMAccordionTerminator(), nil 156 | } 157 | 158 | // Escape escapes special markdown characters from the provided text. 159 | func (f *AzureDevOpsMarkdown) Escape(text string) string { 160 | return formatcore.Escape(text) 161 | } 162 | -------------------------------------------------------------------------------- /format/devops_test.go: -------------------------------------------------------------------------------- 1 | package format_test 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/matryer/is" 9 | "github.com/princjef/gomarkdoc/format" 10 | "github.com/princjef/gomarkdoc/lang" 11 | ) 12 | 13 | func TestBold(t *testing.T) { 14 | is := is.New(t) 15 | 16 | var f format.AzureDevOpsMarkdown 17 | res, err := f.Bold("sample text") 18 | is.NoErr(err) 19 | is.Equal(res, "**sample text**") 20 | } 21 | 22 | func TestCodeBlock(t *testing.T) { 23 | is := is.New(t) 24 | 25 | var f format.AzureDevOpsMarkdown 26 | res, err := f.CodeBlock("go", "Line 1\nLine 2") 27 | is.NoErr(err) 28 | is.Equal(res, "```go\nLine 1\nLine 2\n```") 29 | } 30 | 31 | func TestCodeBlock_noLanguage(t *testing.T) { 32 | is := is.New(t) 33 | 34 | var f format.AzureDevOpsMarkdown 35 | res, err := f.CodeBlock("", "Line 1\nLine 2") 36 | is.NoErr(err) 37 | is.Equal(res, "```\nLine 1\nLine 2\n```") 38 | } 39 | 40 | func TestHeader(t *testing.T) { 41 | tests := []struct { 42 | text string 43 | level int 44 | result string 45 | }{ 46 | {"header text", 1, "# header text"}, 47 | {"level 2", 2, "## level 2"}, 48 | {"level 3", 3, "### level 3"}, 49 | {"level 4", 4, "#### level 4"}, 50 | {"level 5", 5, "##### level 5"}, 51 | {"level 6", 6, "###### level 6"}, 52 | {"other level", 12, "###### other level"}, 53 | {"with * escape", 2, "## with \\* escape"}, 54 | } 55 | 56 | for _, test := range tests { 57 | t.Run(fmt.Sprintf("%s (level %d)", test.text, test.level), func(t *testing.T) { 58 | is := is.New(t) 59 | 60 | var f format.AzureDevOpsMarkdown 61 | res, err := f.Header(test.level, test.text) 62 | is.NoErr(err) 63 | is.Equal(res, test.result) 64 | }) 65 | } 66 | } 67 | 68 | func TestHeader_invalidLevel(t *testing.T) { 69 | is := is.New(t) 70 | 71 | var f format.AzureDevOpsMarkdown 72 | _, err := f.Header(-1, "invalid") 73 | is.Equal(err.Error(), "format: header level cannot be less than 1") 74 | } 75 | 76 | func TestRawHeader(t *testing.T) { 77 | tests := []struct { 78 | text string 79 | level int 80 | result string 81 | }{ 82 | {"header text", 1, "# header text"}, 83 | {"with * escape", 2, "## with * escape"}, 84 | } 85 | 86 | for _, test := range tests { 87 | t.Run(fmt.Sprintf("%s (level %d)", test.text, test.level), func(t *testing.T) { 88 | is := is.New(t) 89 | 90 | var f format.AzureDevOpsMarkdown 91 | res, err := f.RawHeader(test.level, test.text) 92 | is.NoErr(err) 93 | is.Equal(res, test.result) 94 | }) 95 | } 96 | } 97 | 98 | func TestLocalHref(t *testing.T) { 99 | tests := map[string]string{ 100 | "Normal Header": "#normal-header", 101 | " Leading whitespace": "#leading-whitespace", 102 | "Multiple whitespace": "#multiple--whitespace", 103 | "Special(#)%^Characters": "#special%28%23%29%25%5Echaracters", 104 | "With:colon": "#with%3Acolon", 105 | } 106 | 107 | for input, output := range tests { 108 | t.Run(input, func(t *testing.T) { 109 | is := is.New(t) 110 | 111 | var f format.AzureDevOpsMarkdown 112 | res, err := f.LocalHref(input) 113 | is.NoErr(err) 114 | is.Equal(res, output) 115 | }) 116 | } 117 | } 118 | 119 | func TestCodeHref(t *testing.T) { 120 | is := is.New(t) 121 | 122 | wd, err := filepath.Abs(".") 123 | is.NoErr(err) 124 | locPath := filepath.Join(wd, "subdir", "file.go") 125 | 126 | var f format.AzureDevOpsMarkdown 127 | res, err := f.CodeHref(lang.Location{ 128 | Start: lang.Position{Line: 12, Col: 1}, 129 | End: lang.Position{Line: 14, Col: 43}, 130 | Filepath: locPath, 131 | WorkDir: wd, 132 | Repo: &lang.Repo{ 133 | Remote: "https://dev.azure.com/org/project/_git/repo", 134 | DefaultBranch: "master", 135 | PathFromRoot: "/", 136 | }, 137 | }) 138 | is.NoErr(err) 139 | is.Equal(res, "https://dev.azure.com/org/project/_git/repo?path=subdir%2Ffile.go&version=GBmaster&lineStyle=plain&line=12&lineEnd=14&lineStartColumn=1&lineEndColumn=43") 140 | } 141 | 142 | func TestCodeHref_noRepo(t *testing.T) { 143 | is := is.New(t) 144 | 145 | wd, err := filepath.Abs(".") 146 | is.NoErr(err) 147 | locPath := filepath.Join(wd, "subdir", "file.go") 148 | 149 | var f format.AzureDevOpsMarkdown 150 | res, err := f.CodeHref(lang.Location{ 151 | Start: lang.Position{Line: 12, Col: 1}, 152 | End: lang.Position{Line: 14, Col: 43}, 153 | Filepath: locPath, 154 | WorkDir: wd, 155 | Repo: nil, 156 | }) 157 | is.NoErr(err) 158 | is.Equal(res, "") 159 | } 160 | 161 | func TestLink(t *testing.T) { 162 | is := is.New(t) 163 | 164 | var f format.AzureDevOpsMarkdown 165 | res, err := f.Link("link text", "https://test.com/a/b/c") 166 | is.NoErr(err) 167 | is.Equal(res, "[link text]()") 168 | } 169 | 170 | func TestListEntry(t *testing.T) { 171 | is := is.New(t) 172 | 173 | var f format.AzureDevOpsMarkdown 174 | res, err := f.ListEntry(0, "list entry text") 175 | is.NoErr(err) 176 | is.Equal(res, "- list entry text") 177 | } 178 | 179 | func TestListEntry_nested(t *testing.T) { 180 | is := is.New(t) 181 | 182 | var f format.AzureDevOpsMarkdown 183 | res, err := f.ListEntry(2, "nested text") 184 | is.NoErr(err) 185 | is.Equal(res, " - nested text") 186 | } 187 | 188 | func TestListEntry_empty(t *testing.T) { 189 | is := is.New(t) 190 | 191 | var f format.AzureDevOpsMarkdown 192 | res, err := f.ListEntry(0, "") 193 | is.NoErr(err) 194 | is.Equal(res, "") 195 | } 196 | -------------------------------------------------------------------------------- /format/doc.go: -------------------------------------------------------------------------------- 1 | // Package format defines output formats for emitting documentation 2 | // information. 3 | // 4 | // Each of the formats in this package contains the same set of formatting 5 | // functions, but not all formats support all of the functions natively. Where 6 | // possible, a fallback format is provided. See the documentation for the 7 | // individual formats for more information. 8 | package format 9 | -------------------------------------------------------------------------------- /format/format.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import "github.com/princjef/gomarkdoc/lang" 4 | 5 | // Format is a generic interface for formatting documentation contents in a 6 | // particular way. 7 | type Format interface { 8 | // Bold converts the provided text to bold 9 | Bold(text string) (string, error) 10 | 11 | // CodeBlock wraps the provided code as a code block and tags it with the 12 | // provided language (or no language if the empty string is provided). 13 | CodeBlock(language, code string) (string, error) 14 | 15 | // Anchor produces an anchor for the provided link. 16 | Anchor(anchor string) string 17 | 18 | // AnchorHeader converts the provided text and custom anchor link into a 19 | // header of the provided level. The level is expected to be at least 1. 20 | AnchorHeader(level int, text, anchor string) (string, error) 21 | 22 | // Header converts the provided text into a header of the provided level. 23 | // The level is expected to be at least 1. 24 | Header(level int, text string) (string, error) 25 | 26 | // RawAnchorHeader converts the provided text and custom anchor link into a 27 | // header of the provided level without escaping the header text. The level 28 | // is expected to be at least 1. 29 | RawAnchorHeader(level int, text, anchor string) (string, error) 30 | 31 | // RawHeader converts the provided text into a header of the provided level 32 | // without escaping the header text. The level is expected to be at least 1. 33 | RawHeader(level int, text string) (string, error) 34 | 35 | // LocalHref generates an href for navigating to a header with the given 36 | // headerText located within the same document as the href itself. 37 | LocalHref(headerText string) (string, error) 38 | 39 | // RawLocalHref generates an href within the same document but with a direct 40 | // link provided instead of text to slugify. 41 | RawLocalHref(anchor string) string 42 | 43 | // Link generates a link with the given text and href values. 44 | Link(text, href string) (string, error) 45 | 46 | // CodeHref generates an href to the provided code entry. 47 | CodeHref(loc lang.Location) (string, error) 48 | 49 | // ListEntry generates an unordered list entry with the provided text at the 50 | // provided zero-indexed depth. A depth of 0 is considered the topmost level 51 | // of list. 52 | ListEntry(depth int, text string) (string, error) 53 | 54 | // Accordion generates a collapsible content. The accordion's visible title 55 | // while collapsed is the provided title and the expanded content is the 56 | // body. 57 | Accordion(title, body string) (string, error) 58 | 59 | // AccordionHeader generates the header visible when an accordion is 60 | // collapsed. 61 | // 62 | // The AccordionHeader is expected to be used in conjunction with 63 | // AccordionTerminator() when the demands of the body's rendering requires 64 | // it to be generated independently. The result looks conceptually like the 65 | // following: 66 | // 67 | // accordion := formatter.AccordionHeader("Accordion Title") + "Accordion Body" + formatter.AccordionTerminator() 68 | AccordionHeader(title string) (string, error) 69 | 70 | // AccordionTerminator generates the code necessary to terminate an 71 | // accordion after the body. It is expected to be used in conjunction with 72 | // AccordionHeader(). See AccordionHeader for a full description. 73 | AccordionTerminator() (string, error) 74 | 75 | // Escape escapes special markdown characters from the provided text. 76 | Escape(text string) string 77 | } 78 | -------------------------------------------------------------------------------- /format/formatcore/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # formatcore 4 | 5 | ```go 6 | import "github.com/princjef/gomarkdoc/format/formatcore" 7 | ``` 8 | 9 | Package formatcore provides utilities for creating formatters like those found in the format package. 10 | 11 | ## Index 12 | 13 | - [func Anchor\(anchor string\) string](<#Anchor>) 14 | - [func AnchorHeader\(level int, text, anchor string\) \(string, error\)](<#AnchorHeader>) 15 | - [func Bold\(text string\) string](<#Bold>) 16 | - [func CodeBlock\(code string\) string](<#CodeBlock>) 17 | - [func Escape\(text string\) string](<#Escape>) 18 | - [func GFMAccordion\(title, body string\) string](<#GFMAccordion>) 19 | - [func GFMAccordionHeader\(title string\) string](<#GFMAccordionHeader>) 20 | - [func GFMAccordionTerminator\(\) string](<#GFMAccordionTerminator>) 21 | - [func GFMCodeBlock\(language, code string\) string](<#GFMCodeBlock>) 22 | - [func Header\(level int, text string\) \(string, error\)](<#Header>) 23 | - [func Link\(text, href string\) string](<#Link>) 24 | - [func ListEntry\(depth int, text string\) string](<#ListEntry>) 25 | - [func PlainText\(text string\) string](<#PlainText>) 26 | 27 | 28 | 29 | ## func [Anchor]() 30 | 31 | ```go 32 | func Anchor(anchor string) string 33 | ``` 34 | 35 | Anchor produces an anchor for the provided link. 36 | 37 | 38 | ## func [AnchorHeader]() 39 | 40 | ```go 41 | func AnchorHeader(level int, text, anchor string) (string, error) 42 | ``` 43 | 44 | AnchorHeader converts the provided text and custom anchor link into a header of the provided level. The level is expected to be at least 1. 45 | 46 | 47 | ## func [Bold]() 48 | 49 | ```go 50 | func Bold(text string) string 51 | ``` 52 | 53 | Bold converts the provided text to bold 54 | 55 | 56 | ## func [CodeBlock]() 57 | 58 | ```go 59 | func CodeBlock(code string) string 60 | ``` 61 | 62 | CodeBlock wraps the provided code as a code block. Language syntax highlighting is not supported. 63 | 64 | 65 | ## func [Escape]() 66 | 67 | ```go 68 | func Escape(text string) string 69 | ``` 70 | 71 | Escape escapes the special characters in the provided text, but leaves URLs found intact. Note that the URLs included must begin with a scheme to skip the escaping. 72 | 73 | 74 | ## func [GFMAccordion]() 75 | 76 | ```go 77 | func GFMAccordion(title, body string) string 78 | ``` 79 | 80 | GFMAccordion generates a collapsible content. The accordion's visible title while collapsed is the provided title and the expanded content is the body. 81 | 82 | 83 | ## func [GFMAccordionHeader]() 84 | 85 | ```go 86 | func GFMAccordionHeader(title string) string 87 | ``` 88 | 89 | GFMAccordionHeader generates the header visible when an accordion is collapsed. 90 | 91 | The GFMAccordionHeader is expected to be used in conjunction with GFMAccordionTerminator\(\) when the demands of the body's rendering requires it to be generated independently. The result looks conceptually like the following: 92 | 93 | ``` 94 | accordion := GFMAccordionHeader("Accordion Title") + "Accordion Body" + GFMAccordionTerminator() 95 | ``` 96 | 97 | 98 | ## func [GFMAccordionTerminator]() 99 | 100 | ```go 101 | func GFMAccordionTerminator() string 102 | ``` 103 | 104 | GFMAccordionTerminator generates the code necessary to terminate an accordion after the body. It is expected to be used in conjunction with GFMAccordionHeader\(\). See GFMAccordionHeader for a full description. 105 | 106 | 107 | ## func [GFMCodeBlock]() 108 | 109 | ```go 110 | func GFMCodeBlock(language, code string) string 111 | ``` 112 | 113 | GFMCodeBlock wraps the provided code as a code block and tags it with the provided language \(or no language if the empty string is provided\), using the triple backtick format from GitHub Flavored Markdown. 114 | 115 | 116 | ## func [Header]() 117 | 118 | ```go 119 | func Header(level int, text string) (string, error) 120 | ``` 121 | 122 | Header converts the provided text into a header of the provided level. The level is expected to be at least 1. 123 | 124 | 125 | ## func [Link]() 126 | 127 | ```go 128 | func Link(text, href string) string 129 | ``` 130 | 131 | Link generates a link with the given text and href values. 132 | 133 | 134 | ## func [ListEntry]() 135 | 136 | ```go 137 | func ListEntry(depth int, text string) string 138 | ``` 139 | 140 | ListEntry generates an unordered list entry with the provided text at the provided zero\-indexed depth. A depth of 0 is considered the topmost level of list. 141 | 142 | 143 | ## func [PlainText]() 144 | 145 | ```go 146 | func PlainText(text string) string 147 | ``` 148 | 149 | PlainText converts a markdown string to the plain text that appears in the rendered output. 150 | 151 | Generated by [gomarkdoc]() 152 | -------------------------------------------------------------------------------- /format/formatcore/base.go: -------------------------------------------------------------------------------- 1 | package formatcore 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "html" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/russross/blackfriday/v2" 11 | "mvdan.cc/xurls/v2" 12 | ) 13 | 14 | // Bold converts the provided text to bold 15 | func Bold(text string) string { 16 | if text == "" { 17 | return "" 18 | } 19 | 20 | return fmt.Sprintf("**%s**", Escape(text)) 21 | } 22 | 23 | // CodeBlock wraps the provided code as a code block. Language syntax 24 | // highlighting is not supported. 25 | func CodeBlock(code string) string { 26 | var builder strings.Builder 27 | 28 | lines := strings.Split(code, "\n") 29 | for i, line := range lines { 30 | if i != 0 { 31 | builder.WriteRune('\n') 32 | } 33 | 34 | builder.WriteRune('\t') 35 | builder.WriteString(line) 36 | } 37 | 38 | return builder.String() 39 | } 40 | 41 | // GFMCodeBlock wraps the provided code as a code block and tags it with the 42 | // provided language (or no language if the empty string is provided), using 43 | // the triple backtick format from GitHub Flavored Markdown. 44 | func GFMCodeBlock(language, code string) string { 45 | return fmt.Sprintf("```%s\n%s\n```", language, strings.TrimSpace(code)) 46 | } 47 | 48 | // Anchor produces an anchor for the provided link. 49 | func Anchor(anchor string) string { 50 | return fmt.Sprintf( 51 | "", 52 | html.EscapeString(anchor), 53 | ) 54 | } 55 | 56 | // AnchorHeader converts the provided text and custom anchor link into a header 57 | // of the provided level. The level is expected to be at least 1. 58 | func AnchorHeader(level int, text, anchor string) (string, error) { 59 | header, err := Header(level, text) 60 | if err != nil { 61 | return "", err 62 | } 63 | 64 | return fmt.Sprintf("%s\n%s", Anchor(anchor), header), nil 65 | } 66 | 67 | // Header converts the provided text into a header of the provided level. The 68 | // level is expected to be at least 1. 69 | func Header(level int, text string) (string, error) { 70 | if level < 1 { 71 | return "", errors.New("format: header level cannot be less than 1") 72 | } 73 | 74 | switch level { 75 | case 1: 76 | return fmt.Sprintf("# %s", text), nil 77 | case 2: 78 | return fmt.Sprintf("## %s", text), nil 79 | case 3: 80 | return fmt.Sprintf("### %s", text), nil 81 | case 4: 82 | return fmt.Sprintf("#### %s", text), nil 83 | case 5: 84 | return fmt.Sprintf("##### %s", text), nil 85 | default: 86 | // Only go up to 6 levels. Anything higher is also level 6 87 | return fmt.Sprintf("###### %s", text), nil 88 | } 89 | } 90 | 91 | // Link generates a link with the given text and href values. 92 | func Link(text, href string) string { 93 | if text == "" { 94 | return "" 95 | } 96 | 97 | if href == "" { 98 | return text 99 | } 100 | 101 | return fmt.Sprintf("[%s](<%s>)", Escape(text), href) 102 | } 103 | 104 | // ListEntry generates an unordered list entry with the provided text at the 105 | // provided zero-indexed depth. A depth of 0 is considered the topmost level of 106 | // list. 107 | func ListEntry(depth int, text string) string { 108 | // TODO: this is a weird special case 109 | if text == "" { 110 | return "" 111 | } 112 | 113 | prefix := strings.Repeat(" ", depth) 114 | return fmt.Sprintf("%s- %s", prefix, text) 115 | } 116 | 117 | // GFMAccordion generates a collapsible content. The accordion's visible title 118 | // while collapsed is the provided title and the expanded content is the body. 119 | func GFMAccordion(title, body string) string { 120 | return fmt.Sprintf("
%s\n

%s

\n
", title, Escape(body)) 121 | } 122 | 123 | // GFMAccordionHeader generates the header visible when an accordion is 124 | // collapsed. 125 | // 126 | // The GFMAccordionHeader is expected to be used in conjunction with 127 | // GFMAccordionTerminator() when the demands of the body's rendering requires 128 | // it to be generated independently. The result looks conceptually like the 129 | // following: 130 | // 131 | // accordion := GFMAccordionHeader("Accordion Title") + "Accordion Body" + GFMAccordionTerminator() 132 | func GFMAccordionHeader(title string) string { 133 | return fmt.Sprintf("
%s\n

", title) 134 | } 135 | 136 | // GFMAccordionTerminator generates the code necessary to terminate an 137 | // accordion after the body. It is expected to be used in conjunction with 138 | // GFMAccordionHeader(). See GFMAccordionHeader for a full description. 139 | func GFMAccordionTerminator() string { 140 | return "

\n
" 141 | } 142 | 143 | var ( 144 | specialCharacterRegex = regexp.MustCompile("([\\\\`*_{}\\[\\]()<>#+\\-!~])") 145 | urlRegex = xurls.Strict() // Require a scheme in URLs 146 | ) 147 | 148 | // Escape escapes the special characters in the provided text, but leaves URLs 149 | // found intact. Note that the URLs included must begin with a scheme to skip 150 | // the escaping. 151 | func Escape(text string) string { 152 | b := []byte(text) 153 | 154 | var ( 155 | cursor int 156 | builder strings.Builder 157 | ) 158 | 159 | for _, urlLoc := range urlRegex.FindAllIndex(b, -1) { 160 | // Walk through each found URL, escaping the text before the URL and 161 | // leaving the text in the URL unchanged. 162 | if urlLoc[0] > cursor { 163 | // Escape the previous section if its length is nonzero 164 | builder.Write(escapeRaw(b[cursor:urlLoc[0]])) 165 | } 166 | 167 | // Add the unescaped URL to the end of it 168 | builder.Write(b[urlLoc[0]:urlLoc[1]]) 169 | 170 | // Move the cursor forward for the next iteration 171 | cursor = urlLoc[1] 172 | } 173 | 174 | // Escape the end of the string after the last URL if there's anything left 175 | if len(b) > cursor { 176 | builder.Write(escapeRaw(b[cursor:])) 177 | } 178 | 179 | return builder.String() 180 | } 181 | 182 | func escapeRaw(segment []byte) []byte { 183 | return specialCharacterRegex.ReplaceAll(segment, []byte("\\$1")) 184 | } 185 | 186 | // PlainText converts a markdown string to the plain text that appears in the 187 | // rendered output. 188 | func PlainText(text string) string { 189 | md := blackfriday.New(blackfriday.WithExtensions(blackfriday.CommonExtensions)) 190 | node := md.Parse([]byte(text)) 191 | 192 | var builder strings.Builder 193 | plainTextInner(node, &builder) 194 | 195 | return builder.String() 196 | } 197 | 198 | func plainTextInner(node *blackfriday.Node, builder *strings.Builder) { 199 | // Only text nodes produce output 200 | if node.Type == blackfriday.Text { 201 | builder.Write(node.Literal) 202 | } 203 | 204 | // Run the children first 205 | if node.FirstChild != nil { 206 | plainTextInner(node.FirstChild, builder) 207 | } 208 | 209 | // Then run any other siblings 210 | if node.Next != nil { 211 | // Add extra space if necessary between nodes 212 | if node.Type == blackfriday.Paragraph || 213 | node.Type == blackfriday.CodeBlock || 214 | node.Type == blackfriday.Heading { 215 | builder.WriteRune(' ') 216 | } 217 | 218 | plainTextInner(node.Next, builder) 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /format/formatcore/base_test.go: -------------------------------------------------------------------------------- 1 | package formatcore 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/matryer/is" 7 | ) 8 | 9 | func TestPlainText(t *testing.T) { 10 | tests := []struct { 11 | in, out string 12 | }{ 13 | { 14 | in: "plain text", 15 | out: "plain text", 16 | }, 17 | { 18 | in: "[linked](https://foo.bar)", 19 | out: "linked", 20 | }, 21 | { 22 | in: "[linked 2]()", 23 | out: "linked 2", 24 | }, 25 | { 26 | in: "type [foo]()", 27 | out: "type foo", 28 | }, 29 | { 30 | in: "**bold** text", 31 | out: "bold text", 32 | }, 33 | { 34 | in: "*italicized* text", 35 | out: "italicized text", 36 | }, 37 | { 38 | in: "~~strikethrough~~ text", 39 | out: "strikethrough text", 40 | }, 41 | { 42 | in: "paragraph 1\n\nparagraph 2", 43 | out: "paragraph 1 paragraph 2", 44 | }, 45 | { 46 | in: "# header\n\nparagraph", 47 | out: "header paragraph", 48 | }, 49 | } 50 | 51 | for _, test := range tests { 52 | t.Run(test.in, func(t *testing.T) { 53 | is := is.New(t) 54 | is.Equal(PlainText(test.in), test.out) // Wrong output for plainText() 55 | }) 56 | } 57 | } 58 | 59 | func TestEscape(t *testing.T) { 60 | tests := []struct { 61 | in, out string 62 | }{ 63 | { 64 | in: "plain, text.", 65 | out: `plain, text.`, 66 | }, 67 | { 68 | in: "**bold** text", 69 | out: `\*\*bold\*\* text`, 70 | }, 71 | { 72 | in: "*italicized* text", 73 | out: `\*italicized\* text`, 74 | }, 75 | { 76 | in: "~~strikethrough~~ text", 77 | out: `\~\~strikethrough\~\~ text`, 78 | }, 79 | { 80 | in: "# header", 81 | out: `\# header`, 82 | }, 83 | { 84 | in: "markdown [link](https://foo.bar)", 85 | out: `markdown \[link\]\(https://foo.bar\)`, 86 | }, 87 | { 88 | in: "# header then complex URL: http://abc.def/sdfklj/sdf?key=value&special=%323%20sd " + 89 | "with http://simple.url and **bold** after", 90 | out: `\# header then complex URL: http://abc.def/sdfklj/sdf?key=value&special=%323%20sd ` + 91 | `with http://simple.url and \*\*bold\*\* after`, 92 | }, 93 | } 94 | 95 | for _, test := range tests { 96 | t.Run(test.in, func(t *testing.T) { 97 | is := is.New(t) 98 | is.Equal(Escape(test.in), test.out) // Wrong output for escape() 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /format/formatcore/doc.go: -------------------------------------------------------------------------------- 1 | // Package formatcore provides utilities for creating formatters like those 2 | // found in the format package. 3 | package formatcore 4 | -------------------------------------------------------------------------------- /format/github.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/princjef/gomarkdoc/format/formatcore" 10 | "github.com/princjef/gomarkdoc/lang" 11 | ) 12 | 13 | // GitHubFlavoredMarkdown provides a Format which is compatible with GitHub 14 | // Flavored Markdown's syntax and semantics. See GitHub's documentation for 15 | // more details about their markdown format: 16 | // https://guides.github.com/features/mastering-markdown/ 17 | type GitHubFlavoredMarkdown struct{} 18 | 19 | // Bold converts the provided text to bold 20 | func (f *GitHubFlavoredMarkdown) Bold(text string) (string, error) { 21 | return formatcore.Bold(text), nil 22 | } 23 | 24 | // CodeBlock wraps the provided code as a code block and tags it with the 25 | // provided language (or no language if the empty string is provided). 26 | func (f *GitHubFlavoredMarkdown) CodeBlock(language, code string) (string, error) { 27 | return formatcore.GFMCodeBlock(language, code), nil 28 | } 29 | 30 | // Anchor produces an anchor for the provided link. 31 | func (f *GitHubFlavoredMarkdown) Anchor(anchor string) string { 32 | return formatcore.Anchor(anchor) 33 | } 34 | 35 | // AnchorHeader converts the provided text and custom anchor link into a header 36 | // of the provided level. The level is expected to be at least 1. 37 | func (f *GitHubFlavoredMarkdown) AnchorHeader(level int, text, anchor string) (string, error) { 38 | return formatcore.AnchorHeader(level, formatcore.Escape(text), anchor) 39 | } 40 | 41 | // Header converts the provided text into a header of the provided level. The 42 | // level is expected to be at least 1. 43 | func (f *GitHubFlavoredMarkdown) Header(level int, text string) (string, error) { 44 | return formatcore.Header(level, formatcore.Escape(text)) 45 | } 46 | 47 | // RawAnchorHeader converts the provided text and custom anchor link into a 48 | // header of the provided level without escaping the header text. The level is 49 | // expected to be at least 1. 50 | func (f *GitHubFlavoredMarkdown) RawAnchorHeader(level int, text, anchor string) (string, error) { 51 | return formatcore.AnchorHeader(level, text, anchor) 52 | } 53 | 54 | // RawHeader converts the provided text into a header of the provided level 55 | // without escaping the header text. The level is expected to be at least 1. 56 | func (f *GitHubFlavoredMarkdown) RawHeader(level int, text string) (string, error) { 57 | return formatcore.Header(level, text) 58 | } 59 | 60 | var ( 61 | gfmWhitespaceRegex = regexp.MustCompile(`\s`) 62 | gfmRemoveRegex = regexp.MustCompile(`[^\pL-_\d]+`) 63 | ) 64 | 65 | // LocalHref generates an href for navigating to a header with the given 66 | // headerText located within the same document as the href itself. 67 | func (f *GitHubFlavoredMarkdown) LocalHref(headerText string) (string, error) { 68 | result := formatcore.PlainText(headerText) 69 | result = strings.ToLower(result) 70 | result = strings.TrimSpace(result) 71 | result = gfmWhitespaceRegex.ReplaceAllString(result, "-") 72 | result = gfmRemoveRegex.ReplaceAllString(result, "") 73 | 74 | return fmt.Sprintf("#%s", result), nil 75 | } 76 | 77 | // RawLocalHref generates an href within the same document but with a direct 78 | // link provided instead of text to slugify. 79 | func (f *GitHubFlavoredMarkdown) RawLocalHref(anchor string) string { 80 | return fmt.Sprintf("#%s", anchor) 81 | } 82 | 83 | // Link generates a link with the given text and href values. 84 | func (f *GitHubFlavoredMarkdown) Link(text, href string) (string, error) { 85 | return formatcore.Link(text, href), nil 86 | } 87 | 88 | // CodeHref generates an href to the provided code entry. 89 | func (f *GitHubFlavoredMarkdown) CodeHref(loc lang.Location) (string, error) { 90 | // If there's no repo, we can't compute an href 91 | if loc.Repo == nil { 92 | return "", nil 93 | } 94 | 95 | var ( 96 | relative string 97 | err error 98 | ) 99 | if filepath.IsAbs(loc.Filepath) { 100 | relative, err = filepath.Rel(loc.WorkDir, loc.Filepath) 101 | if err != nil { 102 | return "", err 103 | } 104 | } else { 105 | relative = loc.Filepath 106 | } 107 | 108 | full := filepath.Join(loc.Repo.PathFromRoot, relative) 109 | p, err := filepath.Rel(string(filepath.Separator), full) 110 | if err != nil { 111 | return "", err 112 | } 113 | 114 | var locStr string 115 | if loc.Start.Line == loc.End.Line { 116 | locStr = fmt.Sprintf("L%d", loc.Start.Line) 117 | } else { 118 | locStr = fmt.Sprintf("L%d-L%d", loc.Start.Line, loc.End.Line) 119 | } 120 | 121 | return fmt.Sprintf( 122 | "%s/blob/%s/%s#%s", 123 | loc.Repo.Remote, 124 | loc.Repo.DefaultBranch, 125 | filepath.ToSlash(p), 126 | locStr, 127 | ), nil 128 | } 129 | 130 | // ListEntry generates an unordered list entry with the provided text at the 131 | // provided zero-indexed depth. A depth of 0 is considered the topmost level of 132 | // list. 133 | func (f *GitHubFlavoredMarkdown) ListEntry(depth int, text string) (string, error) { 134 | return formatcore.ListEntry(depth, text), nil 135 | } 136 | 137 | // Accordion generates a collapsible content. The accordion's visible title 138 | // while collapsed is the provided title and the expanded content is the body. 139 | func (f *GitHubFlavoredMarkdown) Accordion(title, body string) (string, error) { 140 | return formatcore.GFMAccordion(title, body), nil 141 | } 142 | 143 | // AccordionHeader generates the header visible when an accordion is collapsed. 144 | // 145 | // The AccordionHeader is expected to be used in conjunction with 146 | // AccordionTerminator() when the demands of the body's rendering requires it to 147 | // be generated independently. The result looks conceptually like the following: 148 | // 149 | // accordion := format.AccordionHeader("Accordion Title") + "Accordion Body" + format.AccordionTerminator() 150 | func (f *GitHubFlavoredMarkdown) AccordionHeader(title string) (string, error) { 151 | return formatcore.GFMAccordionHeader(title), nil 152 | } 153 | 154 | // AccordionTerminator generates the code necessary to terminate an accordion 155 | // after the body. It is expected to be used in conjunction with 156 | // AccordionHeader(). See AccordionHeader for a full description. 157 | func (f *GitHubFlavoredMarkdown) AccordionTerminator() (string, error) { 158 | return formatcore.GFMAccordionTerminator(), nil 159 | } 160 | 161 | // Escape escapes special markdown characters from the provided text. 162 | func (f *GitHubFlavoredMarkdown) Escape(text string) string { 163 | return formatcore.Escape(text) 164 | } 165 | -------------------------------------------------------------------------------- /format/github_test.go: -------------------------------------------------------------------------------- 1 | package format_test 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/matryer/is" 9 | "github.com/princjef/gomarkdoc/format" 10 | "github.com/princjef/gomarkdoc/lang" 11 | ) 12 | 13 | func TestGitHubFlavoredMarkdown_Bold(t *testing.T) { 14 | is := is.New(t) 15 | 16 | var f format.GitHubFlavoredMarkdown 17 | res, err := f.Bold("sample text") 18 | is.NoErr(err) 19 | is.Equal(res, "**sample text**") 20 | } 21 | 22 | func TestGitHubFlavoredMarkdown_CodeBlock(t *testing.T) { 23 | is := is.New(t) 24 | 25 | var f format.GitHubFlavoredMarkdown 26 | res, err := f.CodeBlock("go", "Line 1\nLine 2") 27 | is.NoErr(err) 28 | is.Equal(res, "```go\nLine 1\nLine 2\n```") 29 | } 30 | 31 | func TestGitHubFlavoredMarkdown_CodeBlock_noLanguage(t *testing.T) { 32 | is := is.New(t) 33 | 34 | var f format.GitHubFlavoredMarkdown 35 | res, err := f.CodeBlock("", "Line 1\nLine 2") 36 | is.NoErr(err) 37 | is.Equal(res, "```\nLine 1\nLine 2\n```") 38 | } 39 | 40 | func TestGitHubFlavoredMarkdown_Header(t *testing.T) { 41 | tests := []struct { 42 | text string 43 | level int 44 | result string 45 | }{ 46 | {"header text", 1, "# header text"}, 47 | {"level 2", 2, "## level 2"}, 48 | {"level 3", 3, "### level 3"}, 49 | {"level 4", 4, "#### level 4"}, 50 | {"level 5", 5, "##### level 5"}, 51 | {"level 6", 6, "###### level 6"}, 52 | {"other level", 12, "###### other level"}, 53 | {"with * escape", 2, "## with \\* escape"}, 54 | } 55 | 56 | for _, test := range tests { 57 | t.Run(fmt.Sprintf("%s (level %d)", test.text, test.level), func(t *testing.T) { 58 | is := is.New(t) 59 | 60 | var f format.GitHubFlavoredMarkdown 61 | res, err := f.Header(test.level, test.text) 62 | is.NoErr(err) 63 | is.Equal(res, test.result) 64 | }) 65 | } 66 | } 67 | 68 | func TestGitHubFlavoredMarkdown_Header_invalidLevel(t *testing.T) { 69 | is := is.New(t) 70 | 71 | var f format.GitHubFlavoredMarkdown 72 | _, err := f.Header(-1, "invalid") 73 | is.Equal(err.Error(), "format: header level cannot be less than 1") 74 | } 75 | 76 | func TestGitHubFlavoredMarkdown_RawHeader(t *testing.T) { 77 | tests := []struct { 78 | text string 79 | level int 80 | result string 81 | }{ 82 | {"header text", 1, "# header text"}, 83 | {"with * escape", 2, "## with * escape"}, 84 | } 85 | 86 | for _, test := range tests { 87 | t.Run(fmt.Sprintf("%s (level %d)", test.text, test.level), func(t *testing.T) { 88 | is := is.New(t) 89 | 90 | var f format.GitHubFlavoredMarkdown 91 | res, err := f.RawHeader(test.level, test.text) 92 | is.NoErr(err) 93 | is.Equal(res, test.result) 94 | }) 95 | } 96 | } 97 | 98 | func TestGitHubFlavoredMarkdown_LocalHref(t *testing.T) { 99 | tests := map[string]string{ 100 | "Normal Header": "#normal-header", 101 | " Leading whitespace": "#leading-whitespace", 102 | "Multiple whitespace": "#multiple--whitespace", 103 | "Special(#)%^Characters": "#specialcharacters", 104 | "With:colon": "#withcolon", 105 | } 106 | 107 | for input, output := range tests { 108 | t.Run(input, func(t *testing.T) { 109 | is := is.New(t) 110 | 111 | var f format.GitHubFlavoredMarkdown 112 | res, err := f.LocalHref(input) 113 | is.NoErr(err) 114 | is.Equal(res, output) 115 | }) 116 | } 117 | } 118 | 119 | func TestGitHubFlavoredMarkdown_CodeHref(t *testing.T) { 120 | is := is.New(t) 121 | 122 | wd, err := filepath.Abs(".") 123 | is.NoErr(err) 124 | locPath := filepath.Join(wd, "subdir", "file.go") 125 | 126 | var f format.GitHubFlavoredMarkdown 127 | res, err := f.CodeHref(lang.Location{ 128 | Start: lang.Position{Line: 12, Col: 1}, 129 | End: lang.Position{Line: 14, Col: 43}, 130 | Filepath: locPath, 131 | WorkDir: wd, 132 | Repo: &lang.Repo{ 133 | Remote: "https://dev.azure.com/org/project/_git/repo", 134 | DefaultBranch: "master", 135 | PathFromRoot: "/", 136 | }, 137 | }) 138 | is.NoErr(err) 139 | is.Equal(res, "https://dev.azure.com/org/project/_git/repo/blob/master/subdir/file.go#L12-L14") 140 | } 141 | 142 | func TestGitHubFlavoredMarkdown_CodeHref_noRepo(t *testing.T) { 143 | is := is.New(t) 144 | 145 | wd, err := filepath.Abs(".") 146 | is.NoErr(err) 147 | locPath := filepath.Join(wd, "subdir", "file.go") 148 | 149 | var f format.GitHubFlavoredMarkdown 150 | res, err := f.CodeHref(lang.Location{ 151 | Start: lang.Position{Line: 12, Col: 1}, 152 | End: lang.Position{Line: 14, Col: 43}, 153 | Filepath: locPath, 154 | WorkDir: wd, 155 | Repo: nil, 156 | }) 157 | is.NoErr(err) 158 | is.Equal(res, "") 159 | } 160 | 161 | func TestGitHubFlavoredMarkdown_Link(t *testing.T) { 162 | is := is.New(t) 163 | 164 | var f format.GitHubFlavoredMarkdown 165 | res, err := f.Link("link text", "https://test.com/a/b/c") 166 | is.NoErr(err) 167 | is.Equal(res, "[link text]()") 168 | } 169 | 170 | func TestGitHubFlavoredMarkdown_ListEntry(t *testing.T) { 171 | is := is.New(t) 172 | 173 | var f format.GitHubFlavoredMarkdown 174 | res, err := f.ListEntry(0, "list entry text") 175 | is.NoErr(err) 176 | is.Equal(res, "- list entry text") 177 | } 178 | 179 | func TestGitHubFlavoredMarkdown_ListEntry_nested(t *testing.T) { 180 | is := is.New(t) 181 | 182 | var f format.GitHubFlavoredMarkdown 183 | res, err := f.ListEntry(2, "nested text") 184 | is.NoErr(err) 185 | is.Equal(res, " - nested text") 186 | } 187 | 188 | func TestGitHubFlavoredMarkdown_ListEntry_empty(t *testing.T) { 189 | is := is.New(t) 190 | 191 | var f format.GitHubFlavoredMarkdown 192 | res, err := f.ListEntry(0, "") 193 | is.NoErr(err) 194 | is.Equal(res, "") 195 | } 196 | -------------------------------------------------------------------------------- /format/plain.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/princjef/gomarkdoc/format/formatcore" 7 | "github.com/princjef/gomarkdoc/lang" 8 | ) 9 | 10 | // PlainMarkdown provides a Format which is compatible with the base Markdown 11 | // format specification. 12 | type PlainMarkdown struct{} 13 | 14 | // Bold converts the provided text to bold 15 | func (f *PlainMarkdown) Bold(text string) (string, error) { 16 | return formatcore.Bold(text), nil 17 | } 18 | 19 | // CodeBlock wraps the provided code as a code block. The provided language is 20 | // ignored as it is not supported in plain markdown. 21 | func (f *PlainMarkdown) CodeBlock(language, code string) (string, error) { 22 | return formatcore.CodeBlock(code), nil 23 | } 24 | 25 | // Anchor produces an anchor for the provided link. 26 | func (f *PlainMarkdown) Anchor(anchor string) string { 27 | return formatcore.Anchor(anchor) 28 | } 29 | 30 | // AnchorHeader converts the provided text and custom anchor link into a header 31 | // of the provided level. The level is expected to be at least 1. 32 | func (f *PlainMarkdown) AnchorHeader(level int, text, anchor string) (string, error) { 33 | return formatcore.AnchorHeader(level, formatcore.Escape(text), anchor) 34 | } 35 | 36 | // Header converts the provided text into a header of the provided level. The 37 | // level is expected to be at least 1. 38 | func (f *PlainMarkdown) Header(level int, text string) (string, error) { 39 | return formatcore.Header(level, formatcore.Escape(text)) 40 | } 41 | 42 | // RawAnchorHeader converts the provided text and custom anchor link into a 43 | // header of the provided level without escaping the header text. The level is 44 | // expected to be at least 1. 45 | func (f *PlainMarkdown) RawAnchorHeader(level int, text, anchor string) (string, error) { 46 | return formatcore.AnchorHeader(level, text, anchor) 47 | } 48 | 49 | // RawHeader converts the provided text into a header of the provided level 50 | // without escaping the header text. The level is expected to be at least 1. 51 | func (f *PlainMarkdown) RawHeader(level int, text string) (string, error) { 52 | return formatcore.Header(level, text) 53 | } 54 | 55 | // LocalHref always returns the empty string, as header links are not supported 56 | // in plain markdown. 57 | func (f *PlainMarkdown) LocalHref(headerText string) (string, error) { 58 | return "", nil 59 | } 60 | 61 | // RawLocalHref generates an href within the same document but with a direct 62 | // link provided instead of text to slugify. 63 | func (f *PlainMarkdown) RawLocalHref(anchor string) string { 64 | return fmt.Sprintf("#%s", anchor) 65 | } 66 | 67 | // CodeHref always returns the empty string, as there is no defined file linking 68 | // format in standard markdown. 69 | func (f *PlainMarkdown) CodeHref(loc lang.Location) (string, error) { 70 | return "", nil 71 | } 72 | 73 | // Link generates a link with the given text and href values. 74 | func (f *PlainMarkdown) Link(text, href string) (string, error) { 75 | return formatcore.Link(text, href), nil 76 | } 77 | 78 | // ListEntry generates an unordered list entry with the provided text at the 79 | // provided zero-indexed depth. A depth of 0 is considered the topmost level of 80 | // list. 81 | func (f *PlainMarkdown) ListEntry(depth int, text string) (string, error) { 82 | return formatcore.ListEntry(depth, text), nil 83 | } 84 | 85 | // Accordion generates a collapsible content. Since accordions are not supported 86 | // by plain markdown, this generates a level 6 header followed by a paragraph. 87 | func (f *PlainMarkdown) Accordion(title, body string) (string, error) { 88 | h, err := formatcore.Header(6, title) 89 | if err != nil { 90 | return "", err 91 | } 92 | 93 | return fmt.Sprintf("%s%s", h, body), nil 94 | } 95 | 96 | // AccordionHeader generates the header visible when an accordion is collapsed. 97 | // Since accordions are not supported in plain markdown, this generates a level 98 | // 6 header. 99 | // 100 | // The AccordionHeader is expected to be used in conjunction with 101 | // AccordionTerminator() when the demands of the body's rendering requires it to 102 | // be generated independently. The result looks conceptually like the following: 103 | // 104 | // accordion := format.AccordionHeader("Accordion Title") + "Accordion Body" + format.AccordionTerminator() 105 | func (f *PlainMarkdown) AccordionHeader(title string) (string, error) { 106 | return formatcore.Header(6, title) 107 | } 108 | 109 | // AccordionTerminator generates the code necessary to terminate an accordion 110 | // after the body. Since accordions are not supported in plain markdown, this 111 | // completes a paragraph section. It is expected to be used in conjunction with 112 | // AccordionHeader(). See AccordionHeader for a full description. 113 | func (f *PlainMarkdown) AccordionTerminator() (string, error) { 114 | return "\n\n", nil 115 | } 116 | 117 | // Escape escapes special markdown characters from the provided text. 118 | func (f *PlainMarkdown) Escape(text string) string { 119 | return formatcore.Escape(text) 120 | } 121 | -------------------------------------------------------------------------------- /format/plain_test.go: -------------------------------------------------------------------------------- 1 | package format_test 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/matryer/is" 9 | "github.com/princjef/gomarkdoc/format" 10 | "github.com/princjef/gomarkdoc/lang" 11 | ) 12 | 13 | func TestPlainMarkdown_Bold(t *testing.T) { 14 | is := is.New(t) 15 | 16 | var f format.PlainMarkdown 17 | res, err := f.Bold("sample text") 18 | is.NoErr(err) 19 | is.Equal(res, "**sample text**") 20 | } 21 | 22 | func TestPlainMarkdown_CodeBlock(t *testing.T) { 23 | is := is.New(t) 24 | 25 | var f format.PlainMarkdown 26 | res, err := f.CodeBlock("go", "Line 1\nLine 2") 27 | is.NoErr(err) 28 | is.Equal(res, "\tLine 1\n\tLine 2") 29 | } 30 | 31 | func TestPlainMarkdown_CodeBlock_noLanguage(t *testing.T) { 32 | is := is.New(t) 33 | 34 | var f format.PlainMarkdown 35 | res, err := f.CodeBlock("", "Line 1\nLine 2") 36 | is.NoErr(err) 37 | is.Equal(res, "\tLine 1\n\tLine 2") 38 | } 39 | 40 | func TestPlainMarkdown_Header(t *testing.T) { 41 | tests := []struct { 42 | text string 43 | level int 44 | result string 45 | }{ 46 | {"header text", 1, "# header text"}, 47 | {"level 2", 2, "## level 2"}, 48 | {"level 3", 3, "### level 3"}, 49 | {"level 4", 4, "#### level 4"}, 50 | {"level 5", 5, "##### level 5"}, 51 | {"level 6", 6, "###### level 6"}, 52 | {"other level", 12, "###### other level"}, 53 | {"with * escape", 2, "## with \\* escape"}, 54 | } 55 | 56 | for _, test := range tests { 57 | t.Run(fmt.Sprintf("%s (level %d)", test.text, test.level), func(t *testing.T) { 58 | is := is.New(t) 59 | 60 | var f format.PlainMarkdown 61 | res, err := f.Header(test.level, test.text) 62 | is.NoErr(err) 63 | is.Equal(res, test.result) 64 | }) 65 | } 66 | } 67 | 68 | func TestPlainMarkdown_Header_invalidLevel(t *testing.T) { 69 | is := is.New(t) 70 | 71 | var f format.PlainMarkdown 72 | _, err := f.Header(-1, "invalid") 73 | is.Equal(err.Error(), "format: header level cannot be less than 1") 74 | } 75 | 76 | func TestPlainMarkdown_RawHeader(t *testing.T) { 77 | tests := []struct { 78 | text string 79 | level int 80 | result string 81 | }{ 82 | {"header text", 1, "# header text"}, 83 | {"with * escape", 2, "## with * escape"}, 84 | } 85 | 86 | for _, test := range tests { 87 | t.Run(fmt.Sprintf("%s (level %d)", test.text, test.level), func(t *testing.T) { 88 | is := is.New(t) 89 | 90 | var f format.PlainMarkdown 91 | res, err := f.RawHeader(test.level, test.text) 92 | is.NoErr(err) 93 | is.Equal(res, test.result) 94 | }) 95 | } 96 | } 97 | 98 | func TestPlainMarkdown_LocalHref(t *testing.T) { 99 | is := is.New(t) 100 | 101 | var f format.PlainMarkdown 102 | res, err := f.LocalHref("Normal Header") 103 | is.NoErr(err) 104 | is.Equal(res, "") 105 | } 106 | 107 | func TestPlainMarkdown_CodeHref(t *testing.T) { 108 | is := is.New(t) 109 | 110 | wd, err := filepath.Abs(".") 111 | is.NoErr(err) 112 | locPath := filepath.Join(wd, "subdir", "file.go") 113 | 114 | var f format.PlainMarkdown 115 | res, err := f.CodeHref(lang.Location{ 116 | Start: lang.Position{Line: 12, Col: 1}, 117 | End: lang.Position{Line: 14, Col: 43}, 118 | Filepath: locPath, 119 | WorkDir: wd, 120 | Repo: &lang.Repo{ 121 | Remote: "https://dev.azure.com/org/project/_git/repo", 122 | DefaultBranch: "master", 123 | PathFromRoot: "/", 124 | }, 125 | }) 126 | is.NoErr(err) 127 | is.Equal(res, "") 128 | } 129 | 130 | func TestPlainMarkdown_CodeHref_noRepo(t *testing.T) { 131 | is := is.New(t) 132 | 133 | wd, err := filepath.Abs(".") 134 | is.NoErr(err) 135 | locPath := filepath.Join(wd, "subdir", "file.go") 136 | 137 | var f format.PlainMarkdown 138 | res, err := f.CodeHref(lang.Location{ 139 | Start: lang.Position{Line: 12, Col: 1}, 140 | End: lang.Position{Line: 14, Col: 43}, 141 | Filepath: locPath, 142 | WorkDir: wd, 143 | Repo: nil, 144 | }) 145 | is.NoErr(err) 146 | is.Equal(res, "") 147 | } 148 | 149 | func TestPlainMarkdown_Link(t *testing.T) { 150 | is := is.New(t) 151 | 152 | var f format.PlainMarkdown 153 | res, err := f.Link("link text", "https://test.com/a/b/c") 154 | is.NoErr(err) 155 | is.Equal(res, "[link text]()") 156 | } 157 | 158 | func TestPlainMarkdown_ListEntry(t *testing.T) { 159 | is := is.New(t) 160 | 161 | var f format.PlainMarkdown 162 | res, err := f.ListEntry(0, "list entry text") 163 | is.NoErr(err) 164 | is.Equal(res, "- list entry text") 165 | } 166 | 167 | func TestPlainMarkdown_ListEntry_nested(t *testing.T) { 168 | is := is.New(t) 169 | 170 | var f format.PlainMarkdown 171 | res, err := f.ListEntry(2, "nested text") 172 | is.NoErr(err) 173 | is.Equal(res, " - nested text") 174 | } 175 | 176 | func TestPlainMarkdown_ListEntry_empty(t *testing.T) { 177 | is := is.New(t) 178 | 179 | var f format.PlainMarkdown 180 | res, err := f.ListEntry(0, "") 181 | is.NoErr(err) 182 | is.Equal(res, "") 183 | } 184 | -------------------------------------------------------------------------------- /gentmpl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mapName=$1 4 | filename=$2 5 | 6 | printf "// Code generated by gentmpl.sh; DO NOT EDIT.\n\npackage ${GOPACKAGE}\n\nvar ${mapName} = map[string]string{\n" > "${filename}.go" 7 | 8 | for f in ./templates/*.gotxt 9 | do 10 | f=${f##*/} 11 | name=${f%.*} 12 | printf "\t\"$name\": \`" >> "${filename}.go" 13 | cat ./templates/$f >> "${filename}.go" 14 | printf "\`,\n" >> "${filename}.go" 15 | done 16 | 17 | printf "}\n" >> "${filename}.go" 18 | 19 | gofmt -s -w "${filename}.go" 20 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/princjef/gomarkdoc 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/go-git/go-git/v5 v5.7.0 7 | github.com/matryer/is v1.4.0 8 | github.com/princjef/mageutil v1.0.0 9 | github.com/princjef/termdiff v0.1.0 10 | github.com/russross/blackfriday/v2 v2.1.0 11 | github.com/sergi/go-diff v1.3.1 12 | github.com/sirupsen/logrus v1.9.3 13 | github.com/spf13/cobra v1.7.0 14 | github.com/spf13/viper v1.16.0 15 | github.com/x-cray/logrus-prefixed-formatter v0.5.2 16 | mvdan.cc/xurls/v2 v2.5.0 17 | ) 18 | 19 | require ( 20 | github.com/Microsoft/go-winio v0.6.1 // indirect 21 | github.com/ProtonMail/go-crypto v0.0.0-20230619160724-3fbb1f12458c // indirect 22 | github.com/VividCortex/ewma v1.2.0 // indirect 23 | github.com/acomagu/bufpipe v1.0.4 // indirect 24 | github.com/cheggaaa/pb/v3 v3.1.2 // indirect 25 | github.com/cloudflare/circl v1.3.3 // indirect 26 | github.com/emirpasic/gods v1.18.1 // indirect 27 | github.com/fatih/color v1.15.0 // indirect 28 | github.com/fsnotify/fsnotify v1.6.0 // indirect 29 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 30 | github.com/go-git/go-billy/v5 v5.4.1 // indirect 31 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 32 | github.com/hashicorp/hcl v1.0.0 // indirect 33 | github.com/imdario/mergo v0.3.16 // indirect 34 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 35 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 36 | github.com/kevinburke/ssh_config v1.2.0 // indirect 37 | github.com/logrusorgru/aurora/v4 v4.0.0 // indirect 38 | github.com/magiconair/properties v1.8.7 // indirect 39 | github.com/mattn/go-colorable v0.1.13 // indirect 40 | github.com/mattn/go-isatty v0.0.19 // indirect 41 | github.com/mattn/go-runewidth v0.0.14 // indirect 42 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 43 | github.com/mitchellh/mapstructure v1.5.0 // indirect 44 | github.com/onsi/ginkgo v1.14.2 // indirect 45 | github.com/onsi/gomega v1.10.3 // indirect 46 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 47 | github.com/pjbgf/sha1cd v0.3.0 // indirect 48 | github.com/rivo/uniseg v0.4.4 // indirect 49 | github.com/skeema/knownhosts v1.1.1 // indirect 50 | github.com/spf13/afero v1.9.5 // indirect 51 | github.com/spf13/cast v1.5.1 // indirect 52 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 53 | github.com/spf13/pflag v1.0.5 // indirect 54 | github.com/subosito/gotenv v1.4.2 // indirect 55 | github.com/xanzy/ssh-agent v0.3.3 // indirect 56 | golang.org/x/crypto v0.10.0 // indirect 57 | golang.org/x/mod v0.11.0 // indirect 58 | golang.org/x/net v0.11.0 // indirect 59 | golang.org/x/sys v0.9.0 // indirect 60 | golang.org/x/term v0.9.0 // indirect 61 | golang.org/x/text v0.10.0 // indirect 62 | golang.org/x/tools v0.10.0 // indirect 63 | gopkg.in/ini.v1 v1.67.0 // indirect 64 | gopkg.in/warnings.v0 v0.1.2 // indirect 65 | gopkg.in/yaml.v3 v3.0.1 // indirect 66 | ) 67 | -------------------------------------------------------------------------------- /lang/block.go: -------------------------------------------------------------------------------- 1 | package lang 2 | 3 | import ( 4 | "go/doc/comment" 5 | "strings" 6 | ) 7 | 8 | type ( 9 | // Block defines a single block element (e.g. paragraph, code block) in the 10 | // documentation for a symbol or package. 11 | Block struct { 12 | cfg *Config 13 | kind BlockKind 14 | spans []*Span 15 | list *List 16 | inline bool 17 | } 18 | 19 | // BlockKind identifies the type of block element represented by the 20 | // corresponding Block. 21 | BlockKind string 22 | ) 23 | 24 | const ( 25 | // ParagraphBlock defines a block that represents a paragraph of text. 26 | ParagraphBlock BlockKind = "paragraph" 27 | 28 | // CodeBlock defines a block that represents a section of code. 29 | CodeBlock BlockKind = "code" 30 | 31 | // HeaderBlock defines a block that represents a section header. 32 | HeaderBlock BlockKind = "header" 33 | 34 | // ListBlock defines a block that represents an ordered or unordered list. 35 | ListBlock BlockKind = "list" 36 | ) 37 | 38 | // NewBlock creates a new block element of the provided kind and with the given 39 | // text spans and a flag indicating whether this block is part of an inline 40 | // element. 41 | func NewBlock(cfg *Config, kind BlockKind, spans []*Span, inline bool) *Block { 42 | return &Block{cfg, kind, spans, nil, inline} 43 | } 44 | 45 | // NewListBlock creates a new list block element and with the given list 46 | // definition and a flag indicating whether this block is part of an inline 47 | // element. 48 | func NewListBlock(cfg *Config, list *List, inline bool) *Block { 49 | return &Block{cfg, ListBlock, nil, list, inline} 50 | } 51 | 52 | // Level provides the default level that a block of kind HeaderBlock will render 53 | // at in the output. The level is not used for other block types. 54 | func (b *Block) Level() int { 55 | return b.cfg.Level 56 | } 57 | 58 | // Kind provides the kind of data that this block's text should be interpreted 59 | // as. 60 | func (b *Block) Kind() BlockKind { 61 | return b.kind 62 | } 63 | 64 | // Spans provides the raw text of the block's contents as a set of text spans. 65 | // The text is pre-scrubbed and sanitized as determined by the block's Kind(), 66 | // but it is not wrapped in any special constructs for rendering purposes (such 67 | // as markdown code blocks). 68 | func (b *Block) Spans() []*Span { 69 | return b.spans 70 | } 71 | 72 | // List provides the list contents for a list block. Only relevant for blocks of 73 | // type ListBlock. 74 | func (b *Block) List() *List { 75 | return b.list 76 | } 77 | 78 | // Inline indicates whether the block is part of an inline element, such as a 79 | // list item. 80 | func (b *Block) Inline() bool { 81 | return b.inline 82 | } 83 | 84 | // ParseBlocks produces a set of blocks from the corresponding comment blocks. 85 | // It also takes a flag indicating whether the blocks are part of an inline 86 | // element such as a list item. 87 | func ParseBlocks(cfg *Config, blocks []comment.Block, inline bool) []*Block { 88 | res := make([]*Block, len(blocks)) 89 | for i, b := range blocks { 90 | switch v := b.(type) { 91 | case *comment.Code: 92 | res[i] = NewBlock( 93 | cfg.Inc(0), 94 | CodeBlock, 95 | []*Span{NewSpan(cfg.Inc(0), RawTextSpan, v.Text, "")}, 96 | inline, 97 | ) 98 | case *comment.Heading: 99 | var b strings.Builder 100 | printText(&b, v.Text...) 101 | res[i] = NewBlock(cfg.Inc(0), HeaderBlock, ParseSpans(cfg, v.Text), inline) 102 | case *comment.List: 103 | list := NewList(cfg.Inc(0), v) 104 | res[i] = NewListBlock(cfg.Inc(0), list, inline) 105 | case *comment.Paragraph: 106 | res[i] = NewBlock(cfg.Inc(0), ParagraphBlock, ParseSpans(cfg, v.Text), inline) 107 | } 108 | } 109 | 110 | return res 111 | } 112 | -------------------------------------------------------------------------------- /lang/config_test.go: -------------------------------------------------------------------------------- 1 | package lang 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/matryer/is" 7 | ) 8 | 9 | func TestNormalizeRemote(t *testing.T) { 10 | tests := map[string]struct { 11 | raw string 12 | normalized string 13 | }{ 14 | "GitHub https": { 15 | raw: "https://github.com/org/repo.git", 16 | normalized: "https://github.com/org/repo", 17 | }, 18 | "GitHub ssh": { 19 | raw: "git@github.com:org/repo.git", 20 | normalized: "https://github.com/org/repo", 21 | }, 22 | "Azure DevOps https": { 23 | raw: "https://org@dev.azure.com/org/project/_git/repo", 24 | normalized: "https://dev.azure.com/org/project/_git/repo", 25 | }, 26 | "Azure DevOps ssh": { 27 | raw: "git@ssh.dev.azure.com:v3/org/project/repo", 28 | normalized: "https://dev.azure.com/org/project/_git/repo", 29 | }, 30 | "Azure DevOps https (visualstudio.com)": { 31 | raw: "https://org.visualstudio.com/DefaultCollection/project/_git/repo", 32 | normalized: "https://dev.azure.com/org/project/_git/repo", 33 | }, 34 | "Azure DevOps ssh (visualstudio.com)": { 35 | raw: "org@vs-ssh.visualstudio.com:v3/org/project/repo", 36 | normalized: "https://dev.azure.com/org/project/_git/repo", 37 | }, 38 | "GitLab https": { 39 | raw: "https://gitlab.com/org/repo.git", 40 | normalized: "https://gitlab.com/org/repo", 41 | }, 42 | "GitLab ssh": { 43 | raw: "git@gitlab.com:org/repo.git", 44 | normalized: "https://gitlab.com/org/repo", 45 | }, 46 | } 47 | 48 | for name, test := range tests { 49 | t.Run(name, func(t *testing.T) { 50 | is := is.New(t) 51 | 52 | normalized, ok := normalizeRemote(test.raw) 53 | is.True(ok) // Didn't produce a normlized value 54 | is.Equal(normalized, test.normalized) // Wrong normalized value 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lang/doc.go: -------------------------------------------------------------------------------- 1 | package lang 2 | 3 | // Doc provides access to the documentation comment contents for a package or 4 | // symbol in a structured form. 5 | type Doc struct { 6 | cfg *Config 7 | blocks []*Block 8 | } 9 | 10 | // NewDoc initializes a Doc struct from the provided raw documentation text and 11 | // with headers rendered by default at the heading level provided. Documentation 12 | // is separated into block level elements using the standard rules from golang's 13 | // documentation conventions. 14 | func NewDoc(cfg *Config, text string) *Doc { 15 | // Replace CRLF with LF 16 | rawText := normalizeDoc(text) 17 | 18 | parsed := cfg.Pkg.Parser().Parse(rawText) 19 | 20 | blocks := ParseBlocks(cfg, parsed.Content, false) 21 | 22 | return &Doc{cfg, blocks} 23 | } 24 | 25 | // Level provides the default level that headers within the documentation should 26 | // be rendered 27 | func (d *Doc) Level() int { 28 | return d.cfg.Level 29 | } 30 | 31 | // Blocks holds the list of block elements that makes up the documentation 32 | // contents. 33 | func (d *Doc) Blocks() []*Block { 34 | return d.blocks 35 | } 36 | -------------------------------------------------------------------------------- /lang/example.go: -------------------------------------------------------------------------------- 1 | package lang 2 | 3 | import ( 4 | "fmt" 5 | "go/doc" 6 | "go/printer" 7 | "strings" 8 | ) 9 | 10 | // Example holds a single documentation example for a package or symbol. 11 | type Example struct { 12 | cfg *Config 13 | name string 14 | doc *doc.Example 15 | } 16 | 17 | // NewExample creates a new example from the example function's name, its 18 | // documentation example and the files holding code related to the example. 19 | func NewExample(cfg *Config, name string, doc *doc.Example) *Example { 20 | return &Example{cfg, name, doc} 21 | } 22 | 23 | // Level provides the default level that headers for the example should be 24 | // rendered. 25 | func (ex *Example) Level() int { 26 | return ex.cfg.Level 27 | } 28 | 29 | // Name provides a pretty-printed name for the specific example, if one was 30 | // provided. 31 | func (ex *Example) Name() string { 32 | return splitCamel(ex.name) 33 | } 34 | 35 | // Title provides a formatted string to print as the title of the example. It 36 | // incorporates the example's name, if present. 37 | func (ex *Example) Title() string { 38 | name := ex.Name() 39 | if name == "" { 40 | return "Example" 41 | } 42 | 43 | return fmt.Sprintf("Example (%s)", name) 44 | } 45 | 46 | // Location returns a representation of the node's location in a file within a 47 | // repository. 48 | func (ex *Example) Location() Location { 49 | return NewLocation(ex.cfg, ex.doc.Code) 50 | } 51 | 52 | // Summary provides the one-sentence summary of the example's documentation 53 | // comment. 54 | func (ex *Example) Summary() string { 55 | return extractSummary(ex.doc.Doc) 56 | } 57 | 58 | // Doc provides the structured contents of the documentation comment for the 59 | // example. 60 | func (ex *Example) Doc() *Doc { 61 | return NewDoc(ex.cfg.Inc(1), ex.doc.Doc) 62 | } 63 | 64 | // Code provides the raw text code representation of the example's contents. 65 | func (ex *Example) Code() (string, error) { 66 | var codeNode interface{} 67 | if ex.doc.Play != nil { 68 | codeNode = ex.doc.Play 69 | } else { 70 | codeNode = &printer.CommentedNode{Node: ex.doc.Code, Comments: ex.doc.Comments} 71 | } 72 | 73 | var code strings.Builder 74 | p := &printer.Config{Mode: printer.TabIndent | printer.UseSpaces, Tabwidth: 8} 75 | err := p.Fprint(&code, ex.cfg.FileSet, codeNode) 76 | if err != nil { 77 | return "", err 78 | } 79 | 80 | str := code.String() 81 | 82 | // additional formatting if this is a function body 83 | if i := len(str); i >= 2 && str[0] == '{' && str[i-1] == '}' { 84 | // remove surrounding braces 85 | str = str[1 : i-1] 86 | // unindent 87 | str = strings.ReplaceAll(str, "\n\t", "\n") 88 | } 89 | 90 | return str, nil 91 | } 92 | 93 | // Output provides the code's example output. 94 | func (ex *Example) Output() string { 95 | return ex.doc.Output 96 | } 97 | 98 | // HasOutput indicates whether the example contains any example output. 99 | func (ex *Example) HasOutput() bool { 100 | return ex.doc.Output != "" || ex.doc.EmptyOutput 101 | } 102 | -------------------------------------------------------------------------------- /lang/file.go: -------------------------------------------------------------------------------- 1 | package lang 2 | 3 | // File holds information for rendering a single file that contains one or more 4 | // packages. 5 | type File struct { 6 | Header string 7 | Footer string 8 | Packages []*Package 9 | } 10 | 11 | // NewFile creates a new instance of File with the provided information. 12 | func NewFile(header, footer string, packages []*Package) *File { 13 | return &File{ 14 | Header: header, 15 | Footer: footer, 16 | Packages: packages, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lang/func.go: -------------------------------------------------------------------------------- 1 | package lang 2 | 3 | import ( 4 | "fmt" 5 | "go/doc" 6 | "go/token" 7 | "strings" 8 | ) 9 | 10 | // Func holds documentation information for a single func declaration within a 11 | // package or type. 12 | type Func struct { 13 | cfg *Config 14 | doc *doc.Func 15 | examples []*doc.Example 16 | } 17 | 18 | // NewFunc creates a new Func from the corresponding documentation construct 19 | // from the standard library, the related token.FileSet for the package and 20 | // the list of examples for the package. 21 | func NewFunc(cfg *Config, doc *doc.Func, examples []*doc.Example) *Func { 22 | return &Func{cfg, doc, examples} 23 | } 24 | 25 | // Level provides the default level at which headers for the func should be 26 | // rendered in the final documentation. 27 | func (fn *Func) Level() int { 28 | return fn.cfg.Level 29 | } 30 | 31 | // Name provides the name of the function. 32 | func (fn *Func) Name() string { 33 | return fn.doc.Name 34 | } 35 | 36 | // Title provides the formatted name of the func. It is primarily designed for 37 | // generating headers. 38 | func (fn *Func) Title() string { 39 | if fn.doc.Recv != "" { 40 | return fmt.Sprintf("func (%s) %s", fn.doc.Recv, fn.doc.Name) 41 | } 42 | 43 | return fmt.Sprintf("func %s", fn.doc.Name) 44 | } 45 | 46 | // Receiver provides the type of the receiver for the function, or empty string 47 | // if there is no receiver type. 48 | func (fn *Func) Receiver() string { 49 | return fn.doc.Recv 50 | } 51 | 52 | // Location returns a representation of the node's location in a file within a 53 | // repository. 54 | func (fn *Func) Location() Location { 55 | return NewLocation(fn.cfg, fn.doc.Decl) 56 | } 57 | 58 | // Summary provides the one-sentence summary of the function's documentation 59 | // comment 60 | func (fn *Func) Summary() string { 61 | return extractSummary(fn.doc.Doc) 62 | } 63 | 64 | // Doc provides the structured contents of the documentation comment for the 65 | // function. 66 | func (fn *Func) Doc() *Doc { 67 | return NewDoc(fn.cfg.Inc(1), fn.doc.Doc) 68 | } 69 | 70 | // Signature provides the raw text representation of the code for the 71 | // function's signature. 72 | func (fn *Func) Signature() (string, error) { 73 | // We use a custom FileSet so that we don't inherit multiline formatting 74 | return printNode(fn.doc.Decl, token.NewFileSet()) 75 | } 76 | 77 | // Examples provides the list of examples from the list given on initialization 78 | // that pertain to the function. 79 | func (fn *Func) Examples() (examples []*Example) { 80 | var fullName string 81 | if fn.doc.Recv != "" { 82 | fullName = fmt.Sprintf("%s_%s", fn.rawRecv(), fn.doc.Name) 83 | } else { 84 | fullName = fn.doc.Name 85 | } 86 | underscorePrefix := fmt.Sprintf("%s_", fullName) 87 | 88 | for _, example := range fn.examples { 89 | var name string 90 | switch { 91 | case example.Name == fullName: 92 | name = "" 93 | case strings.HasPrefix(example.Name, underscorePrefix): 94 | name = example.Name[len(underscorePrefix):] 95 | default: 96 | // TODO: better filtering 97 | continue 98 | } 99 | 100 | examples = append(examples, NewExample(fn.cfg.Inc(1), name, example)) 101 | } 102 | 103 | return 104 | } 105 | 106 | // Anchor produces anchor text for the func. 107 | func (fn *Func) Anchor() string { 108 | if fn.doc.Recv != "" { 109 | return Symbol{ 110 | Kind: MethodSymbolKind, 111 | Receiver: fn.doc.Recv, 112 | Name: fn.doc.Name, 113 | }.Anchor() 114 | } 115 | 116 | return Symbol{ 117 | Kind: FuncSymbolKind, 118 | Name: fn.doc.Name, 119 | }.Anchor() 120 | } 121 | 122 | func (fn *Func) rawRecv() string { 123 | // remove type parameters 124 | recv := strings.Split(fn.doc.Recv, "[")[0] 125 | 126 | if strings.HasPrefix(recv, "*") { 127 | return recv[1:] 128 | } 129 | 130 | return recv 131 | } 132 | -------------------------------------------------------------------------------- /lang/func_test.go: -------------------------------------------------------------------------------- 1 | package lang_test 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/matryer/is" 9 | "github.com/princjef/gomarkdoc/lang" 10 | "github.com/princjef/gomarkdoc/logger" 11 | ) 12 | 13 | func TestFunc_Level_standalone(t *testing.T) { 14 | is := is.New(t) 15 | 16 | fn, err := loadFunc("../testData/lang/function", "Standalone") 17 | is.NoErr(err) 18 | 19 | is.Equal(fn.Level(), 2) 20 | } 21 | 22 | func TestFunc_Level_receiver(t *testing.T) { 23 | is := is.New(t) 24 | 25 | fn, err := loadFunc("../testData/lang/function", "WithReceiver") 26 | is.NoErr(err) 27 | 28 | is.Equal(fn.Level(), 3) 29 | } 30 | 31 | func TestFunc_Level_initializer(t *testing.T) { 32 | is := is.New(t) 33 | 34 | fn, err := loadFunc("../testData/lang/function", "New") 35 | is.NoErr(err) 36 | 37 | is.Equal(fn.Level(), 3) 38 | } 39 | 40 | func TestFunc_Name_standalone(t *testing.T) { 41 | is := is.New(t) 42 | 43 | fn, err := loadFunc("../testData/lang/function", "Standalone") 44 | is.NoErr(err) 45 | 46 | is.Equal(fn.Name(), "Standalone") 47 | } 48 | 49 | func TestFunc_Name_receiver(t *testing.T) { 50 | is := is.New(t) 51 | 52 | fn, err := loadFunc("../testData/lang/function", "WithReceiver") 53 | is.NoErr(err) 54 | 55 | is.Equal(fn.Name(), "WithReceiver") 56 | } 57 | 58 | func TestFunc_Receiver_standalone(t *testing.T) { 59 | is := is.New(t) 60 | 61 | fn, err := loadFunc("../testData/lang/function", "Standalone") 62 | is.NoErr(err) 63 | 64 | is.Equal(fn.Receiver(), "") 65 | } 66 | 67 | func TestFunc_Receiver_receiver(t *testing.T) { 68 | is := is.New(t) 69 | 70 | fn, err := loadFunc("../testData/lang/function", "WithReceiver") 71 | is.NoErr(err) 72 | 73 | is.Equal(fn.Receiver(), "Receiver") 74 | } 75 | 76 | func TestFunc_Location(t *testing.T) { 77 | is := is.New(t) 78 | 79 | fn, err := loadFunc("../testData/lang/function", "Standalone") 80 | is.NoErr(err) 81 | 82 | loc := fn.Location() 83 | is.Equal(loc.Start.Line, 14) 84 | is.Equal(loc.Start.Col, 1) 85 | is.Equal(loc.End.Line, 14) 86 | is.Equal(loc.End.Col, 48) 87 | is.True(strings.HasSuffix(loc.Filepath, "func.go")) 88 | } 89 | 90 | func TestFunc_Examples_generic(t *testing.T) { 91 | is := is.New(t) 92 | fn, err := loadFunc("../testData/lang/function", "WithGenericReceiver") 93 | is.NoErr(err) 94 | 95 | examples := fn.Examples() 96 | is.Equal(len(examples), 1) 97 | 98 | ex := examples[0] 99 | is.Equal(ex.Name(), "") 100 | } 101 | 102 | func TestFunc_stringsCompare(t *testing.T) { 103 | is := is.New(t) 104 | 105 | buildPkg, err := getBuildPackage("strings") 106 | is.NoErr(err) 107 | 108 | log := logger.New(logger.ErrorLevel) 109 | pkg, err := lang.NewPackageFromBuild(log, buildPkg) 110 | is.NoErr(err) 111 | 112 | var fn *lang.Func 113 | for _, f := range pkg.Funcs() { 114 | if f.Name() == "Compare" { 115 | fn = f 116 | break 117 | } 118 | } 119 | 120 | is.True(fn != nil) // didn't find the function we were looking for 121 | 122 | sig, err := fn.Signature() 123 | is.NoErr(err) 124 | 125 | is.Equal(fn.Name(), "Compare") 126 | is.Equal(fn.Level(), 2) 127 | is.Equal(fn.Title(), "func Compare") 128 | is.Equal(fn.Summary(), "Compare returns an integer comparing two strings lexicographically.") 129 | is.Equal(sig, "func Compare(a, b string) int") 130 | is.Equal(len(fn.Examples()), 1) 131 | } 132 | 133 | func TestFunc_textScannerInit(t *testing.T) { 134 | is := is.New(t) 135 | 136 | buildPkg, err := getBuildPackage("text/scanner") 137 | is.NoErr(err) 138 | 139 | log := logger.New(logger.ErrorLevel) 140 | pkg, err := lang.NewPackageFromBuild(log, buildPkg) 141 | is.NoErr(err) 142 | 143 | var typ *lang.Type 144 | for _, t := range pkg.Types() { 145 | if t.Name() == "Scanner" { 146 | typ = t 147 | break 148 | } 149 | } 150 | is.True(typ != nil) 151 | 152 | var fn *lang.Func 153 | for _, f := range typ.Methods() { 154 | if f.Name() == "Init" { 155 | fn = f 156 | break 157 | } 158 | } 159 | 160 | is.True(fn != nil) // didn't find the function we were looking for 161 | 162 | sig, err := fn.Signature() 163 | is.NoErr(err) 164 | 165 | is.Equal(fn.Name(), "Init") 166 | is.Equal(fn.Level(), 3) 167 | is.Equal(fn.Title(), "func (*Scanner) Init") 168 | is.Equal(fn.Summary(), "Init initializes a Scanner with a new source and returns s.") 169 | is.Equal(sig, "func (s *Scanner) Init(src io.Reader) *Scanner") 170 | is.Equal(len(fn.Examples()), 0) 171 | } 172 | 173 | func TestFunc_ioIoutilTempFile(t *testing.T) { 174 | is := is.New(t) 175 | 176 | buildPkg, err := getBuildPackage("io/ioutil") 177 | is.NoErr(err) 178 | 179 | log := logger.New(logger.ErrorLevel) 180 | pkg, err := lang.NewPackageFromBuild(log, buildPkg) 181 | is.NoErr(err) 182 | 183 | var fn *lang.Func 184 | for _, f := range pkg.Funcs() { 185 | if f.Name() == "TempFile" { 186 | fn = f 187 | break 188 | } 189 | } 190 | 191 | is.True(fn != nil) // didn't find the function we were looking for 192 | 193 | sig, err := fn.Signature() 194 | is.NoErr(err) 195 | 196 | is.Equal(fn.Name(), "TempFile") 197 | is.Equal(fn.Level(), 2) 198 | is.Equal(fn.Title(), "func TempFile") 199 | is.Equal(fn.Summary(), "TempFile creates a new temporary file in the directory dir, opens the file for reading and writing, and returns the resulting *os.File.") 200 | is.Equal(sig, "func TempFile(dir, pattern string) (f *os.File, err error)") 201 | is.Equal(len(fn.Examples()), 2) 202 | } 203 | 204 | func loadFunc(dir, name string) (*lang.Func, error) { 205 | buildPkg, err := getBuildPackage(dir) 206 | if err != nil { 207 | return nil, err 208 | } 209 | 210 | log := logger.New(logger.ErrorLevel) 211 | pkg, err := lang.NewPackageFromBuild(log, buildPkg) 212 | if err != nil { 213 | return nil, err 214 | } 215 | 216 | for _, f := range pkg.Funcs() { 217 | if f.Name() == name { 218 | return f, nil 219 | } 220 | } 221 | 222 | for _, t := range pkg.Types() { 223 | for _, f := range t.Funcs() { 224 | if f.Name() == name { 225 | return f, nil 226 | } 227 | } 228 | 229 | for _, f := range t.Methods() { 230 | if f.Name() == name { 231 | return f, nil 232 | } 233 | } 234 | } 235 | 236 | return nil, errors.New("func not found") 237 | } 238 | -------------------------------------------------------------------------------- /lang/godoc.go: -------------------------------------------------------------------------------- 1 | // Package lang provides constructs for defining golang language constructs and 2 | // extracting information from them for documentation purposes. 3 | package lang 4 | -------------------------------------------------------------------------------- /lang/list.go: -------------------------------------------------------------------------------- 1 | package lang 2 | 3 | import ( 4 | "go/doc/comment" 5 | "strconv" 6 | ) 7 | 8 | // List defines a list block element in the documentation for a symbol or 9 | // package. 10 | type List struct { 11 | blankBetween bool 12 | items []*Item 13 | } 14 | 15 | // NewList initializes a list from the equivalent type from the comment package. 16 | func NewList(cfg *Config, docList *comment.List) *List { 17 | var l List 18 | l.items = make([]*Item, len(docList.Items)) 19 | for i, item := range docList.Items { 20 | l.items[i] = NewItem(cfg.Inc(0), item) 21 | } 22 | 23 | l.blankBetween = docList.BlankBetween() 24 | 25 | return &l 26 | } 27 | 28 | // BlankBetween returns true if there should be a blank line between list items. 29 | func (l *List) BlankBetween() bool { 30 | return l.blankBetween 31 | } 32 | 33 | // Items returns the slice of items in the list. 34 | func (l *List) Items() []*Item { 35 | return l.items 36 | } 37 | 38 | // ItemKind identifies the kind of item 39 | type ItemKind string 40 | 41 | const ( 42 | // OrderedItem identifies an ordered (i.e. numbered) item. 43 | OrderedItem ItemKind = "ordered" 44 | 45 | // UnorderedItem identifies an unordered (i.e. bulletted) item. 46 | UnorderedItem ItemKind = "unordered" 47 | ) 48 | 49 | // Item defines a single item in a list in the documentation for a symbol or 50 | // package. 51 | type Item struct { 52 | blocks []*Block 53 | kind ItemKind 54 | number int 55 | } 56 | 57 | // NewItem initializes a list item from the equivalent type from the comment 58 | // package. 59 | func NewItem(cfg *Config, docItem *comment.ListItem) *Item { 60 | var ( 61 | num int 62 | kind ItemKind 63 | ) 64 | if n, err := strconv.Atoi(docItem.Number); err == nil { 65 | num = n 66 | kind = OrderedItem 67 | } else { 68 | kind = UnorderedItem 69 | } 70 | 71 | return &Item{ 72 | blocks: ParseBlocks(cfg, docItem.Content, true), 73 | kind: kind, 74 | number: num, 75 | } 76 | } 77 | 78 | // Blocks returns the blocks of documentation in a list item. 79 | func (i *Item) Blocks() []*Block { 80 | return i.blocks 81 | } 82 | 83 | // Kind returns the kind of the list item. 84 | func (i *Item) Kind() ItemKind { 85 | return i.kind 86 | } 87 | 88 | // Number returns the number of the list item. Only populated if the item is of 89 | // the OrderedItem kind. 90 | func (i *Item) Number() int { 91 | return i.number 92 | } 93 | -------------------------------------------------------------------------------- /lang/package_test.go: -------------------------------------------------------------------------------- 1 | package lang_test 2 | 3 | import ( 4 | "go/build" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/matryer/is" 11 | "github.com/princjef/gomarkdoc/lang" 12 | "github.com/princjef/gomarkdoc/logger" 13 | ) 14 | 15 | func TestPackage_Consts(t *testing.T) { 16 | is := is.New(t) 17 | 18 | pkg, err := loadPackage("../testData/lang/function") 19 | is.NoErr(err) 20 | 21 | consts := pkg.Consts() 22 | is.Equal(len(consts), 1) 23 | 24 | decl, err := consts[0].Decl() 25 | is.NoErr(err) 26 | is.Equal(decl, `const ( 27 | ConstA = "string" 28 | ConstB = true 29 | )`) 30 | } 31 | 32 | func TestPackage_Vars(t *testing.T) { 33 | is := is.New(t) 34 | 35 | pkg, err := loadPackage("../testData/lang/function") 36 | is.NoErr(err) 37 | 38 | vars := pkg.Vars() 39 | is.Equal(len(vars), 1) 40 | 41 | decl, err := vars[0].Decl() 42 | is.NoErr(err) 43 | is.Equal(decl, `var Variable = 5`) 44 | } 45 | 46 | func TestPackage_dotImport(t *testing.T) { 47 | is := is.New(t) 48 | 49 | err := os.Chdir("../testData/lang/function") 50 | is.NoErr(err) 51 | 52 | defer func() { 53 | _ = os.Chdir("../../../lang") 54 | }() 55 | 56 | pkg, err := loadPackage(".") 57 | is.NoErr(err) 58 | 59 | is.Equal(pkg.Import(), `import "github.com/princjef/gomarkdoc/testData/lang/function"`) 60 | is.Equal(pkg.ImportPath(), `github.com/princjef/gomarkdoc/testData/lang/function`) 61 | } 62 | 63 | func TestPackage_strings(t *testing.T) { 64 | is := is.New(t) 65 | 66 | buildPkg, err := getBuildPackage("strings") 67 | is.NoErr(err) 68 | 69 | log := logger.New(logger.ErrorLevel) 70 | pkg, err := lang.NewPackageFromBuild(log, buildPkg) 71 | is.NoErr(err) 72 | 73 | is.Equal(pkg.Level(), 1) // level should be root 74 | is.True(strings.HasSuffix(pkg.Dir(), filepath.FromSlash("/strings"))) 75 | is.Equal(pkg.Dirname(), "strings") 76 | is.Equal(pkg.Name(), "strings") 77 | is.Equal(pkg.Import(), `import "strings"`) 78 | is.Equal(pkg.ImportPath(), `strings`) 79 | is.Equal(pkg.Summary(), "Package strings implements simple functions to manipulate UTF-8 encoded strings.") 80 | is.Equal(len(pkg.Consts()), 0) // strings should have no constants 81 | is.Equal(len(pkg.Vars()), 0) // strings should have no vars 82 | is.True(len(pkg.Funcs()) > 0) // strings should have top-level functions 83 | is.True(len(pkg.Types()) > 0) // strings should have top-level types 84 | is.Equal(len(pkg.Examples()), 0) // strings should have no top-level examples 85 | } 86 | 87 | func TestPackage_textScanner(t *testing.T) { 88 | is := is.New(t) 89 | 90 | buildPkg, err := getBuildPackage("text/scanner") 91 | is.NoErr(err) 92 | 93 | log := logger.New(logger.ErrorLevel) 94 | pkg, err := lang.NewPackageFromBuild(log, buildPkg) 95 | is.NoErr(err) 96 | 97 | is.Equal(pkg.Level(), 1) // level should be root 98 | is.True(strings.HasSuffix(pkg.Dir(), filepath.FromSlash("/text/scanner"))) 99 | is.Equal(pkg.Dirname(), "scanner") 100 | is.Equal(pkg.Name(), "scanner") 101 | is.Equal(pkg.Import(), `import "text/scanner"`) 102 | is.Equal(pkg.ImportPath(), `text/scanner`) 103 | is.Equal(pkg.Summary(), "Package scanner provides a scanner and tokenizer for UTF-8-encoded text.") 104 | is.True(len(pkg.Consts()) > 0) // text/scanner should have constants 105 | is.Equal(len(pkg.Vars()), 0) // text/scanner should have no vars 106 | is.True(len(pkg.Funcs()) > 0) // text/scanner should have top-level functions 107 | is.True(len(pkg.Types()) > 0) // text/scanner should have top-level types 108 | is.True(len(pkg.Examples()) > 0) // text/scanner should have top-level examples 109 | } 110 | 111 | func TestPackage_ioIoutil(t *testing.T) { 112 | is := is.New(t) 113 | 114 | buildPkg, err := getBuildPackage("io/ioutil") 115 | is.NoErr(err) 116 | 117 | log := logger.New(logger.ErrorLevel) 118 | pkg, err := lang.NewPackageFromBuild(log, buildPkg) 119 | is.NoErr(err) 120 | 121 | is.Equal(pkg.Level(), 1) // level should be root 122 | is.True(strings.HasSuffix(pkg.Dir(), filepath.FromSlash("/io/ioutil"))) 123 | is.Equal(pkg.Dirname(), "ioutil") 124 | is.Equal(pkg.Name(), "ioutil") 125 | is.Equal(pkg.Import(), `import "io/ioutil"`) 126 | is.Equal(pkg.ImportPath(), `io/ioutil`) 127 | is.Equal(pkg.Summary(), "Package ioutil implements some I/O utility functions.") 128 | is.Equal(len(pkg.Consts()), 0) // io/ioutil should have no constants 129 | is.True(len(pkg.Vars()) > 0) // io/ioutil should have vars 130 | is.True(len(pkg.Funcs()) > 0) // io/ioutil should have top-level functions 131 | is.Equal(len(pkg.Types()), 0) // io/ioutil should have no top-level types 132 | is.Equal(len(pkg.Examples()), 0) // io/ioutil should have no top-level examples 133 | } 134 | 135 | func TestPackage_encoding(t *testing.T) { 136 | is := is.New(t) 137 | 138 | buildPkg, err := getBuildPackage("encoding") 139 | is.NoErr(err) 140 | 141 | log := logger.New(logger.ErrorLevel) 142 | pkg, err := lang.NewPackageFromBuild(log, buildPkg) 143 | is.NoErr(err) 144 | 145 | is.Equal(pkg.Level(), 1) // level should be root 146 | is.True(strings.HasSuffix(pkg.Dir(), filepath.FromSlash("/encoding"))) 147 | is.Equal(pkg.Dirname(), "encoding") 148 | is.Equal(pkg.Name(), "encoding") 149 | is.Equal(pkg.Import(), `import "encoding"`) 150 | is.Equal(pkg.ImportPath(), `encoding`) 151 | is.Equal(pkg.Summary(), "Package encoding defines interfaces shared by other packages that convert data to and from byte-level and textual representations.") 152 | is.Equal(len(pkg.Consts()), 0) // encoding should have no constants 153 | is.Equal(len(pkg.Vars()), 0) // encoding should have no vars 154 | is.Equal(len(pkg.Funcs()), 0) // encoding should have no top-level functions 155 | is.True(len(pkg.Types()) > 0) // encoding should have top-level types 156 | is.Equal(len(pkg.Examples()), 0) // encoding should have no top-level examples 157 | } 158 | 159 | func getBuildPackage(path string) (*build.Package, error) { 160 | wd, err := os.Getwd() 161 | if err != nil { 162 | return nil, err 163 | } 164 | 165 | return build.Import(path, wd, build.ImportComment) 166 | } 167 | 168 | func loadPackage(dir string) (*lang.Package, error) { 169 | buildPkg, err := getBuildPackage(dir) 170 | if err != nil { 171 | return nil, err 172 | } 173 | 174 | log := logger.New(logger.ErrorLevel) 175 | pkg, err := lang.NewPackageFromBuild(log, buildPkg) 176 | if err != nil { 177 | return nil, err 178 | } 179 | 180 | return pkg, nil 181 | } 182 | -------------------------------------------------------------------------------- /lang/span.go: -------------------------------------------------------------------------------- 1 | package lang 2 | 3 | import ( 4 | "fmt" 5 | "go/doc/comment" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | type ( 11 | // Span defines a single text span in a block for documentation of a symbol 12 | // or package. 13 | Span struct { 14 | cfg *Config 15 | kind SpanKind 16 | text string 17 | url string 18 | } 19 | 20 | // SpanKind identifies the type of span element represented by the 21 | // corresponding Span. 22 | SpanKind string 23 | ) 24 | 25 | const ( 26 | // TextSpan defines a span that represents plain text. 27 | TextSpan SpanKind = "text" 28 | 29 | // RawTextSpan defines a span that represents plain text that should be 30 | // displayed as-is. 31 | RawTextSpan SpanKind = "rawText" 32 | 33 | // LinkSpan defines a span that represents a link. 34 | LinkSpan SpanKind = "link" 35 | 36 | // AutolinkSpan defines a span that represents text which is itself a link. 37 | AutolinkSpan SpanKind = "autolink" 38 | ) 39 | 40 | // NewSpan creates a new span. 41 | func NewSpan(cfg *Config, kind SpanKind, text string, url string) *Span { 42 | return &Span{cfg, kind, text, url} 43 | } 44 | 45 | // Kind provides the kind of data that this span represents. 46 | func (s *Span) Kind() SpanKind { 47 | return s.kind 48 | } 49 | 50 | // Text provides the raw text for the span. 51 | func (s *Span) Text() string { 52 | return s.text 53 | } 54 | 55 | // URL provides the url associated with the span, if any. 56 | func (s *Span) URL() string { 57 | return s.url 58 | } 59 | 60 | // ParseSpans turns a set of *comment.Text entries into a slice of spans. 61 | func ParseSpans(cfg *Config, texts []comment.Text) []*Span { 62 | var s []*Span 63 | for _, t := range texts { 64 | switch v := t.(type) { 65 | case comment.Plain: 66 | s = append(s, NewSpan(cfg.Inc(0), TextSpan, collapseWhitespace(string(v)), "")) 67 | case comment.Italic: 68 | s = append(s, NewSpan(cfg.Inc(0), TextSpan, collapseWhitespace(string(v)), "")) 69 | case *comment.DocLink: 70 | var b strings.Builder 71 | printText(&b, v.Text...) 72 | str := collapseWhitespace(b.String()) 73 | 74 | // Replace local links as needed 75 | if v.ImportPath == "" { 76 | name := symbolName(v.Recv, v.Name) 77 | if sym, ok := cfg.Symbols[name]; ok { 78 | s = append(s, NewSpan(cfg.Inc(0), LinkSpan, str, fmt.Sprintf("#%s", sym.Anchor()))) 79 | } else { 80 | cfg.Log.Warnf("Unable to find symbol %s", name) 81 | s = append(s, NewSpan(cfg.Inc(0), TextSpan, collapseWhitespace(str), "")) 82 | } 83 | break 84 | } 85 | 86 | s = append(s, NewSpan(cfg.Inc(0), LinkSpan, str, v.DefaultURL("https://pkg.go.dev/"))) 87 | case *comment.Link: 88 | var b strings.Builder 89 | printText(&b, v.Text...) 90 | str := collapseWhitespace(b.String()) 91 | 92 | if v.Auto { 93 | s = append(s, NewSpan(cfg.Inc(0), AutolinkSpan, str, str)) 94 | break 95 | } 96 | 97 | s = append(s, NewSpan(cfg.Inc(0), LinkSpan, str, v.URL)) 98 | } 99 | } 100 | 101 | return s 102 | } 103 | 104 | func printText(b *strings.Builder, text ...comment.Text) { 105 | for i, t := range text { 106 | if i > 0 { 107 | b.WriteRune(' ') 108 | } 109 | 110 | switch v := t.(type) { 111 | case comment.Plain: 112 | b.WriteString(string(v)) 113 | case comment.Italic: 114 | b.WriteString(string(v)) 115 | case *comment.DocLink: 116 | printText(b, v.Text...) 117 | case *comment.Link: 118 | // No need to linkify implicit links 119 | if v.Auto { 120 | printText(b, v.Text...) 121 | continue 122 | } 123 | 124 | b.WriteRune('[') 125 | printText(b, v.Text...) 126 | b.WriteRune(']') 127 | b.WriteRune('(') 128 | b.WriteString(v.URL) 129 | b.WriteRune(')') 130 | } 131 | } 132 | } 133 | 134 | var whitespaceRegex = regexp.MustCompile(`\s+`) 135 | 136 | func collapseWhitespace(s string) string { 137 | return string(whitespaceRegex.ReplaceAll([]byte(s), []byte(" "))) 138 | } 139 | -------------------------------------------------------------------------------- /lang/symbol.go: -------------------------------------------------------------------------------- 1 | package lang 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/doc" 7 | "strings" 8 | ) 9 | 10 | type ( 11 | // Symbol provides identity information for a symbol in a package. 12 | Symbol struct { 13 | // Receiver holds the receiver for a method or field. 14 | Receiver string 15 | // Name holds the name of the symbol itself. 16 | Name string 17 | // Kind identifies the category of the symbol. 18 | Kind SymbolKind 19 | // Parent holds the linkable parent symbol which contains this one. 20 | Parent *Symbol 21 | } 22 | 23 | // SymbolKind identifies the type of symbol. 24 | SymbolKind int 25 | ) 26 | 27 | // The list of valid symbol kinds. 28 | const ( 29 | TypeSymbolKind SymbolKind = iota + 1 30 | FuncSymbolKind 31 | ConstSymbolKind 32 | VarSymbolKind 33 | MethodSymbolKind 34 | FieldSymbolKind 35 | ) 36 | 37 | // PackageSymbols gets the list of symbols for a doc package. 38 | func PackageSymbols(pkg *doc.Package) map[string]Symbol { 39 | sym := make(map[string]Symbol) 40 | for _, c := range pkg.Consts { 41 | parent := Symbol{ 42 | Name: c.Names[0], 43 | Kind: ConstSymbolKind, 44 | } 45 | 46 | for _, n := range c.Names { 47 | sym[symbolName("", n)] = Symbol{ 48 | Name: n, 49 | Kind: ConstSymbolKind, 50 | Parent: &parent, 51 | } 52 | } 53 | } 54 | 55 | for _, v := range pkg.Vars { 56 | parent := Symbol{ 57 | Name: v.Names[0], 58 | Kind: VarSymbolKind, 59 | } 60 | 61 | for _, n := range v.Names { 62 | sym[symbolName("", n)] = Symbol{ 63 | Name: n, 64 | Kind: VarSymbolKind, 65 | Parent: &parent, 66 | } 67 | } 68 | } 69 | 70 | for _, v := range pkg.Funcs { 71 | sym[symbolName("", v.Name)] = Symbol{ 72 | Name: v.Name, 73 | Kind: FuncSymbolKind, 74 | } 75 | } 76 | 77 | for _, t := range pkg.Types { 78 | typeSymbols(sym, t) 79 | } 80 | 81 | return sym 82 | } 83 | 84 | func typeSymbols(sym map[string]Symbol, t *doc.Type) { 85 | typeSym := Symbol{ 86 | Name: t.Name, 87 | Kind: TypeSymbolKind, 88 | } 89 | 90 | sym[t.Name] = typeSym 91 | 92 | for _, f := range t.Methods { 93 | sym[symbolName(t.Name, f.Name)] = Symbol{ 94 | Receiver: t.Name, 95 | Name: f.Name, 96 | Kind: MethodSymbolKind, 97 | } 98 | } 99 | 100 | for _, s := range t.Decl.Specs { 101 | typ, ok := s.(*ast.TypeSpec).Type.(*ast.StructType) 102 | if !ok { 103 | continue 104 | } 105 | 106 | for _, f := range typ.Fields.List { 107 | for _, n := range f.Names { 108 | sym[symbolName(t.Name, n.String())] = Symbol{ 109 | Receiver: t.Name, 110 | Name: n.String(), 111 | Kind: FieldSymbolKind, 112 | Parent: &typeSym, 113 | } 114 | } 115 | } 116 | } 117 | 118 | for _, f := range t.Funcs { 119 | sym[symbolName("", f.Name)] = Symbol{ 120 | Name: f.Name, 121 | Kind: FuncSymbolKind, 122 | } 123 | } 124 | 125 | for _, c := range t.Consts { 126 | parent := Symbol{ 127 | Name: c.Names[0], 128 | Kind: ConstSymbolKind, 129 | } 130 | 131 | for _, n := range c.Names { 132 | sym[symbolName("", n)] = Symbol{ 133 | Name: n, 134 | Kind: ConstSymbolKind, 135 | Parent: &parent, 136 | } 137 | } 138 | } 139 | 140 | for _, v := range t.Vars { 141 | parent := Symbol{ 142 | Name: v.Names[0], 143 | Kind: VarSymbolKind, 144 | } 145 | 146 | for _, n := range v.Names { 147 | sym[symbolName("", n)] = Symbol{ 148 | Name: n, 149 | Kind: VarSymbolKind, 150 | Parent: &parent, 151 | } 152 | } 153 | } 154 | 155 | } 156 | 157 | // Anchor produces anchor text for the symbol. 158 | func (s Symbol) Anchor() string { 159 | if s.Parent != nil { 160 | return s.Parent.Anchor() 161 | } 162 | 163 | switch s.Kind { 164 | case MethodSymbolKind, FieldSymbolKind: 165 | return fmt.Sprintf("%s.%s", strings.TrimLeft(s.Receiver, "*"), s.Name) 166 | default: 167 | return s.Name 168 | } 169 | } 170 | 171 | // symbolName returns the string representation of the symbol. 172 | func symbolName(receiver string, name string) string { 173 | receiver = strings.TrimLeft(receiver, "*") 174 | name = strings.TrimLeft(name, "*") 175 | 176 | if receiver == "" { 177 | return name 178 | } 179 | 180 | return fmt.Sprintf("%s.%s", receiver, name) 181 | } 182 | -------------------------------------------------------------------------------- /lang/type.go: -------------------------------------------------------------------------------- 1 | package lang 2 | 3 | import ( 4 | "fmt" 5 | "go/doc" 6 | "strings" 7 | ) 8 | 9 | // Type holds documentation information for a type declaration. 10 | type Type struct { 11 | cfg *Config 12 | doc *doc.Type 13 | examples []*doc.Example 14 | } 15 | 16 | // NewType creates a Type from the raw documentation representation of the type, 17 | // the token.FileSet for the package's files and the full list of examples from 18 | // the containing package. 19 | func NewType(cfg *Config, doc *doc.Type, examples []*doc.Example) *Type { 20 | return &Type{cfg, doc, examples} 21 | } 22 | 23 | // Level provides the default level that headers for the type should be 24 | // rendered. 25 | func (typ *Type) Level() int { 26 | return typ.cfg.Level 27 | } 28 | 29 | // Name provides the name of the type 30 | func (typ *Type) Name() string { 31 | return typ.doc.Name 32 | } 33 | 34 | // Title provides a formatted name suitable for use in a header identifying the 35 | // type. 36 | func (typ *Type) Title() string { 37 | return fmt.Sprintf("type %s", typ.doc.Name) 38 | } 39 | 40 | // Location returns a representation of the node's location in a file within a 41 | // repository. 42 | func (typ *Type) Location() Location { 43 | return NewLocation(typ.cfg, typ.doc.Decl) 44 | } 45 | 46 | // Summary provides the one-sentence summary of the type's documentation 47 | // comment. 48 | func (typ *Type) Summary() string { 49 | return extractSummary(typ.doc.Doc) 50 | } 51 | 52 | // Doc provides the structured contents of the documentation comment for the 53 | // type. 54 | func (typ *Type) Doc() *Doc { 55 | return NewDoc(typ.cfg.Inc(1), typ.doc.Doc) 56 | } 57 | 58 | // Decl provides the raw text representation of the code for the type's 59 | // declaration. 60 | func (typ *Type) Decl() (string, error) { 61 | return printNode(typ.doc.Decl, typ.cfg.FileSet) 62 | } 63 | 64 | // Examples lists the examples pertaining to the type from the set provided on 65 | // initialization. 66 | func (typ *Type) Examples() (examples []*Example) { 67 | underscorePrefix := fmt.Sprintf("%s_", typ.doc.Name) 68 | for _, example := range typ.examples { 69 | var name string 70 | switch { 71 | case example.Name == typ.doc.Name: 72 | name = "" 73 | case strings.HasPrefix(example.Name, underscorePrefix) && !typ.isSubexample(example.Name): 74 | name = example.Name[len(underscorePrefix):] 75 | default: 76 | // TODO: better filtering 77 | continue 78 | } 79 | 80 | examples = append(examples, NewExample(typ.cfg.Inc(1), name, example)) 81 | } 82 | 83 | return 84 | } 85 | 86 | func (typ *Type) isSubexample(exampleName string) bool { 87 | for _, m := range typ.doc.Methods { 88 | fullName := fmt.Sprintf("%s_%s", typ.doc.Name, m.Name) 89 | underscorePrefix := fmt.Sprintf("%s_", fullName) 90 | if exampleName == fullName || strings.HasPrefix(exampleName, underscorePrefix) { 91 | return true 92 | } 93 | } 94 | 95 | return false 96 | } 97 | 98 | // Funcs lists the funcs related to the type. This only includes functions which 99 | // return an instance of the type or its pointer. 100 | func (typ *Type) Funcs() []*Func { 101 | funcs := make([]*Func, len(typ.doc.Funcs)) 102 | for i, fn := range typ.doc.Funcs { 103 | funcs[i] = NewFunc(typ.cfg.Inc(1), fn, typ.examples) 104 | } 105 | 106 | return funcs 107 | } 108 | 109 | // Methods lists the funcs that use the type as a value or pointer receiver. 110 | func (typ *Type) Methods() []*Func { 111 | methods := make([]*Func, len(typ.doc.Methods)) 112 | for i, fn := range typ.doc.Methods { 113 | methods[i] = NewFunc(typ.cfg.Inc(1), fn, typ.examples) 114 | } 115 | 116 | return methods 117 | } 118 | 119 | // Consts lists the const declaration blocks containing values of this type. 120 | func (typ *Type) Consts() []*Value { 121 | consts := make([]*Value, len(typ.doc.Consts)) 122 | for i, c := range typ.doc.Consts { 123 | consts[i] = NewValue(typ.cfg.Inc(1), c) 124 | } 125 | 126 | return consts 127 | } 128 | 129 | // Vars lists the var declaration blocks containing values of this type. 130 | func (typ *Type) Vars() []*Value { 131 | vars := make([]*Value, len(typ.doc.Vars)) 132 | for i, v := range typ.doc.Vars { 133 | vars[i] = NewValue(typ.cfg.Inc(1), v) 134 | } 135 | 136 | return vars 137 | } 138 | 139 | // Anchor produces anchor text for the type. 140 | func (typ *Type) Anchor() string { 141 | return Symbol{ 142 | Kind: TypeSymbolKind, 143 | Name: typ.doc.Name, 144 | }.Anchor() 145 | } 146 | -------------------------------------------------------------------------------- /lang/type_test.go: -------------------------------------------------------------------------------- 1 | package lang_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/matryer/is" 8 | "github.com/princjef/gomarkdoc/lang" 9 | "github.com/princjef/gomarkdoc/logger" 10 | ) 11 | 12 | func TestType_Examples(t *testing.T) { 13 | is := is.New(t) 14 | 15 | typ, err := loadType("../testData/lang/function", "Receiver") 16 | is.NoErr(err) 17 | 18 | ex := typ.Examples() 19 | is.Equal(len(ex), 2) 20 | 21 | is.Equal(ex[0].Name(), "") 22 | is.Equal(ex[1].Name(), "Sub Test") 23 | } 24 | 25 | func loadType(dir, name string) (*lang.Type, error) { 26 | buildPkg, err := getBuildPackage(dir) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | log := logger.New(logger.ErrorLevel) 32 | pkg, err := lang.NewPackageFromBuild(log, buildPkg) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | for _, t := range pkg.Types() { 38 | if t.Name() == name { 39 | return t, nil 40 | } 41 | } 42 | 43 | return nil, errors.New("type not found") 44 | } 45 | -------------------------------------------------------------------------------- /lang/util.go: -------------------------------------------------------------------------------- 1 | package lang 2 | 3 | import ( 4 | "go/ast" 5 | "go/printer" 6 | "go/token" 7 | "regexp" 8 | "strings" 9 | "unicode" 10 | ) 11 | 12 | func printNode(node ast.Node, fs *token.FileSet) (string, error) { 13 | cfg := printer.Config{ 14 | Mode: printer.UseSpaces, 15 | Tabwidth: 4, 16 | } 17 | 18 | var out strings.Builder 19 | if err := cfg.Fprint(&out, fs, node); err != nil { 20 | return "", err 21 | } 22 | 23 | return out.String(), nil 24 | } 25 | 26 | func runeIsUpper(r rune) bool { 27 | return r >= 'A' && r <= 'Z' 28 | } 29 | 30 | const lowerToUpper = 'a' - 'A' 31 | 32 | func runeToUpper(r rune) rune { 33 | return r - lowerToUpper 34 | } 35 | 36 | func splitCamel(text string) string { 37 | var builder strings.Builder 38 | var previousRune rune 39 | var wordLength int 40 | for i, r := range text { 41 | if i == 0 { 42 | previousRune = runeToUpper(r) 43 | continue 44 | } 45 | 46 | switch { 47 | case runeIsUpper(previousRune) && !runeIsUpper(r) && wordLength > 0: 48 | // If we have a capital followed by a lower, that capital should 49 | // begin a word. Throw a space before the runes if there is a word 50 | // there. 51 | builder.WriteRune(' ') 52 | builder.WriteRune(previousRune) 53 | wordLength = 1 54 | case !runeIsUpper(previousRune) && runeIsUpper(r): 55 | // If we have a lower followed by a capital, the capital should 56 | // begin a word. Throw a space in between the runes. We don't have 57 | // to check word length because we're writing the previous rune to 58 | // the previous word, automaticall giving it a length of 1. 59 | builder.WriteRune(previousRune) 60 | builder.WriteRune(' ') 61 | wordLength = 0 62 | default: 63 | // Otherwise, just throw the rune onto the previous word 64 | builder.WriteRune(previousRune) 65 | wordLength++ 66 | } 67 | 68 | previousRune = r 69 | } 70 | 71 | // Write the last rune 72 | if previousRune != 0 { 73 | builder.WriteRune(previousRune) 74 | } 75 | 76 | return builder.String() 77 | } 78 | 79 | func extractSummary(doc string) string { 80 | firstParagraph := normalizeDoc(doc) 81 | 82 | // Trim to first paragraph if there are multiple 83 | if idx := strings.Index(firstParagraph, "\n\n"); idx != -1 { 84 | firstParagraph = firstParagraph[:idx] 85 | } 86 | 87 | var builder strings.Builder 88 | var lookback1 rune 89 | var lookback2 rune 90 | var lookback3 rune 91 | for _, r := range formatDocParagraph(firstParagraph) { 92 | // We terminate the sequence if we see a space preceded by a '.' which 93 | // does not have exactly one word character before it (to avoid 94 | // treating initials as the end of a sentence). 95 | isPeriod := r == ' ' && lookback1 == '.' 96 | isInitial := unicode.IsUpper(lookback2) && !unicode.IsLetter(lookback3) && !unicode.IsDigit(lookback3) 97 | if isPeriod && !isInitial { 98 | break 99 | } 100 | 101 | // Write the rune 102 | builder.WriteRune(r) 103 | 104 | // Update tracking variables 105 | lookback3 = lookback2 106 | lookback2 = lookback1 107 | lookback1 = r 108 | } 109 | 110 | // Make the summary end with a period if it is nonempty and doesn't already. 111 | if lookback1 != '.' && lookback1 != 0 { 112 | builder.WriteRune('.') 113 | } 114 | 115 | return builder.String() 116 | } 117 | 118 | var crlfRegex = regexp.MustCompile("\r\n") 119 | 120 | func normalizeDoc(doc string) string { 121 | return strings.TrimSpace(crlfRegex.ReplaceAllString(doc, "\n")) 122 | } 123 | 124 | func formatDocParagraph(paragraph string) string { 125 | var mergedParagraph strings.Builder 126 | for i, line := range strings.Split(paragraph, "\n") { 127 | if i > 0 { 128 | mergedParagraph.WriteRune(' ') 129 | } 130 | 131 | mergedParagraph.WriteString(strings.TrimSpace(line)) 132 | } 133 | 134 | return mergedParagraph.String() 135 | } 136 | -------------------------------------------------------------------------------- /lang/value.go: -------------------------------------------------------------------------------- 1 | package lang 2 | 3 | import ( 4 | "go/doc" 5 | ) 6 | 7 | // Value holds documentation for a var or const declaration within a package. 8 | type Value struct { 9 | cfg *Config 10 | doc *doc.Value 11 | } 12 | 13 | // NewValue creates a new Value from the raw const or var documentation and the 14 | // token.FileSet of files for the containing package. 15 | func NewValue(cfg *Config, doc *doc.Value) *Value { 16 | return &Value{cfg, doc} 17 | } 18 | 19 | // Level provides the default level that headers for the value should be 20 | // rendered. 21 | func (v *Value) Level() int { 22 | return v.cfg.Level 23 | } 24 | 25 | // Location returns a representation of the node's location in a file within a 26 | // repository. 27 | func (v *Value) Location() Location { 28 | return NewLocation(v.cfg, v.doc.Decl) 29 | } 30 | 31 | // Summary provides the one-sentence summary of the value's documentation 32 | // comment. 33 | func (v *Value) Summary() string { 34 | return extractSummary(v.doc.Doc) 35 | } 36 | 37 | // Doc provides the structured contents of the documentation comment for the 38 | // example. 39 | func (v *Value) Doc() *Doc { 40 | return NewDoc(v.cfg.Inc(1), v.doc.Doc) 41 | } 42 | 43 | // Decl provides the raw text representation of the code for declaring the const 44 | // or var. 45 | func (v *Value) Decl() (string, error) { 46 | return printNode(v.doc.Decl, v.cfg.FileSet) 47 | } 48 | 49 | // Anchor produces anchor text for the value. 50 | func (v *Value) Anchor() string { 51 | var kind SymbolKind 52 | if v.doc.Decl.Tok.String() == "const" { 53 | kind = ConstSymbolKind 54 | } else { 55 | kind = VarSymbolKind 56 | } 57 | 58 | return Symbol{ 59 | Kind: kind, 60 | Name: v.doc.Names[0], 61 | }.Anchor() 62 | } 63 | -------------------------------------------------------------------------------- /lang/value_test.go: -------------------------------------------------------------------------------- 1 | package lang_test 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/matryer/is" 9 | "github.com/princjef/gomarkdoc/lang" 10 | "github.com/princjef/gomarkdoc/logger" 11 | ) 12 | 13 | func TestValue_Level(t *testing.T) { 14 | is := is.New(t) 15 | 16 | val, err := loadValue("../testData/lang/function", "Variable") 17 | is.NoErr(err) 18 | 19 | is.Equal(val.Level(), 2) 20 | } 21 | 22 | func TestValue_Summary(t *testing.T) { 23 | is := is.New(t) 24 | 25 | val, err := loadValue("../testData/lang/function", "Variable") 26 | is.NoErr(err) 27 | 28 | is.Equal(val.Summary(), "Variable is a package-level variable.") 29 | } 30 | 31 | func TestValue_Decl(t *testing.T) { 32 | is := is.New(t) 33 | 34 | val, err := loadValue("../testData/lang/function", "Variable") 35 | is.NoErr(err) 36 | 37 | decl, err := val.Decl() 38 | is.NoErr(err) 39 | 40 | is.Equal(decl, "var Variable = 5") 41 | } 42 | 43 | func TestValue_Location(t *testing.T) { 44 | is := is.New(t) 45 | 46 | val, err := loadValue("../testData/lang/function", "Variable") 47 | is.NoErr(err) 48 | 49 | loc := val.Location() 50 | is.Equal(loc.Start.Line, 4) 51 | is.Equal(loc.Start.Col, 1) 52 | is.Equal(loc.End.Line, 4) 53 | is.Equal(loc.End.Col, 17) 54 | is.True(strings.HasSuffix(loc.Filepath, "value.go")) 55 | } 56 | 57 | func loadValue(dir, name string) (*lang.Value, error) { 58 | buildPkg, err := getBuildPackage(dir) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | log := logger.New(logger.ErrorLevel) 64 | pkg, err := lang.NewPackageFromBuild(log, buildPkg) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | for _, v := range pkg.Vars() { 70 | d, err := v.Decl() 71 | if err == nil && strings.Contains(d, name) { 72 | return v, nil 73 | } 74 | } 75 | 76 | for _, v := range pkg.Consts() { 77 | d, err := v.Decl() 78 | if err == nil && strings.Contains(d, name) { 79 | return v, nil 80 | } 81 | } 82 | 83 | return nil, errors.New("value not found") 84 | } 85 | -------------------------------------------------------------------------------- /logger/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # logger 4 | 5 | ```go 6 | import "github.com/princjef/gomarkdoc/logger" 7 | ``` 8 | 9 | Package logger provides a simple console logger for reporting information about execution to stderr. 10 | 11 | ## Index 12 | 13 | - [type Level](<#Level>) 14 | - [type Logger](<#Logger>) 15 | - [func New\(level Level, opts ...Option\) Logger](<#New>) 16 | - [type Option](<#Option>) 17 | - [func WithField\(key string, value interface\{\}\) Option](<#WithField>) 18 | 19 | 20 | 21 | ## type [Level]() 22 | 23 | Level defines valid logging levels for a Logger. 24 | 25 | ```go 26 | type Level int 27 | ``` 28 | 29 | Valid logging levels 30 | 31 | ```go 32 | const ( 33 | DebugLevel Level = iota + 1 34 | InfoLevel 35 | WarnLevel 36 | ErrorLevel 37 | ) 38 | ``` 39 | 40 | 41 | ## type [Logger]() 42 | 43 | Logger provides basic logging capabilities at different logging levels. 44 | 45 | ```go 46 | type Logger interface { 47 | Debug(a ...interface{}) 48 | Debugf(format string, a ...interface{}) 49 | Info(a ...interface{}) 50 | Infof(format string, a ...interface{}) 51 | Warn(a ...interface{}) 52 | Warnf(format string, a ...interface{}) 53 | Error(a ...interface{}) 54 | Errorf(format string, a ...interface{}) 55 | } 56 | ``` 57 | 58 | 59 | ### func [New]() 60 | 61 | ```go 62 | func New(level Level, opts ...Option) Logger 63 | ``` 64 | 65 | New initializes a new Logger. 66 | 67 | 68 | ## type [Option]() 69 | 70 | Option defines an option for configuring the logger. 71 | 72 | ```go 73 | type Option func(opts *options) 74 | ``` 75 | 76 | 77 | ### func [WithField]() 78 | 79 | ```go 80 | func WithField(key string, value interface{}) Option 81 | ``` 82 | 83 | WithField sets the provided key/value pair for use on all logs. 84 | 85 | Generated by [gomarkdoc]() 86 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | // Package logger provides a simple console logger for reporting information 2 | // about execution to stderr. 3 | package logger 4 | 5 | import ( 6 | "github.com/sirupsen/logrus" 7 | prefixed "github.com/x-cray/logrus-prefixed-formatter" 8 | ) 9 | 10 | type ( 11 | // Logger provides basic logging capabilities at different logging levels. 12 | Logger interface { 13 | Debug(a ...interface{}) 14 | Debugf(format string, a ...interface{}) 15 | Info(a ...interface{}) 16 | Infof(format string, a ...interface{}) 17 | Warn(a ...interface{}) 18 | Warnf(format string, a ...interface{}) 19 | Error(a ...interface{}) 20 | Errorf(format string, a ...interface{}) 21 | } 22 | 23 | // Level defines valid logging levels for a Logger. 24 | Level int 25 | 26 | // Option defines an option for configuring the logger. 27 | Option func(opts *options) 28 | 29 | // options defines options for configuring the logger 30 | options struct { 31 | fields map[string]interface{} 32 | } 33 | ) 34 | 35 | // Valid logging levels 36 | const ( 37 | DebugLevel Level = iota + 1 38 | InfoLevel 39 | WarnLevel 40 | ErrorLevel 41 | ) 42 | 43 | // New initializes a new Logger. 44 | func New(level Level, opts ...Option) Logger { 45 | var options options 46 | for _, opt := range opts { 47 | opt(&options) 48 | } 49 | 50 | log := logrus.New() 51 | 52 | formatter := &prefixed.TextFormatter{ 53 | DisableTimestamp: true, 54 | } 55 | formatter.SetColorScheme(&prefixed.ColorScheme{ 56 | DebugLevelStyle: "cyan", 57 | PrefixStyle: "black+h", 58 | }) 59 | 60 | log.Formatter = formatter 61 | 62 | switch level { 63 | case DebugLevel: 64 | log.SetLevel(logrus.DebugLevel) 65 | case InfoLevel: 66 | log.SetLevel(logrus.InfoLevel) 67 | case WarnLevel: 68 | log.SetLevel(logrus.WarnLevel) 69 | case ErrorLevel: 70 | log.SetLevel(logrus.ErrorLevel) 71 | default: 72 | log.SetLevel(logrus.ErrorLevel) 73 | } 74 | 75 | if options.fields != nil { 76 | return log.WithFields(options.fields) 77 | } 78 | 79 | return log 80 | } 81 | 82 | // WithField sets the provided key/value pair for use on all logs. 83 | func WithField(key string, value interface{}) Option { 84 | return func(opts *options) { 85 | if opts.fields == nil { 86 | opts.fields = make(map[string]interface{}) 87 | } 88 | 89 | opts.fields[key] = value 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /magefile.go: -------------------------------------------------------------------------------- 1 | //go:build mage 2 | // +build mage 3 | 4 | package main 5 | 6 | import ( 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/princjef/mageutil/bintool" 11 | "github.com/princjef/mageutil/shellcmd" 12 | ) 13 | 14 | var linter = bintool.Must(bintool.New( 15 | "golangci-lint{{.BinExt}}", 16 | "1.51.1", 17 | "https://github.com/golangci/golangci-lint/releases/download/v{{.Version}}/golangci-lint-{{.Version}}-{{.GOOS}}-{{.GOARCH}}{{.ArchiveExt}}", 18 | )) 19 | 20 | func Lint() error { 21 | if err := linter.Ensure(); err != nil { 22 | return err 23 | } 24 | 25 | return linter.Command(`run --timeout 5m`).Run() 26 | } 27 | 28 | func Generate() error { 29 | return shellcmd.Command(`go generate .`).Run() 30 | } 31 | 32 | func Build() error { 33 | return shellcmd.Command(`go build -o ./bin/gomarkdoc ./cmd/gomarkdoc`).Run() 34 | } 35 | 36 | func Doc() error { 37 | return shellcmd.RunAll( 38 | `go run ./cmd/gomarkdoc .`, 39 | `go run ./cmd/gomarkdoc --header "" --exclude-dirs . --exclude-dirs ./testData/... ./...`, 40 | ) 41 | } 42 | 43 | func DocVerify() error { 44 | return shellcmd.RunAll( 45 | `go run ./cmd/gomarkdoc -c .`, 46 | `go run ./cmd/gomarkdoc -c --header "" --exclude-dirs . --exclude-dirs ./testData/... ./...`, 47 | ) 48 | } 49 | 50 | func RegenerateTestDocs() error { 51 | dirs, err := os.ReadDir("./testData") 52 | if err != nil { 53 | return err 54 | } 55 | 56 | base, err := os.Getwd() 57 | if err != nil { 58 | return err 59 | } 60 | 61 | for _, dir := range dirs { 62 | if !dir.IsDir() { 63 | continue 64 | } 65 | 66 | os.Chdir(filepath.Join(base, "./testData", dir.Name())) 67 | if err := shellcmd.Command(`go run ../../cmd/gomarkdoc -o "{{.Dir}}/README-github.md" --format github ./...`).Run(); err != nil { 68 | return err 69 | } 70 | 71 | if err := shellcmd.Command(`go run ../../cmd/gomarkdoc -o "{{.Dir}}/README-plain.md" --format plain ./...`).Run(); err != nil { 72 | return err 73 | } 74 | 75 | if err := shellcmd.Command(`go run ../../cmd/gomarkdoc -o "{{.Dir}}/README-azure-devops.md" --format azure-devops ./...`).Run(); err != nil { 76 | return err 77 | } 78 | } 79 | 80 | return nil 81 | } 82 | 83 | func Test() error { 84 | return shellcmd.Command(`go test -count 1 -coverpkg=.,./cmd/...,./format/...,./lang/...,./logger/... -cover -coverprofile=coverage.txt ./...`).Run() 85 | } 86 | 87 | func Coverage() error { 88 | return shellcmd.Command(`go tool cover -html=coverage.txt`).Run() 89 | } 90 | -------------------------------------------------------------------------------- /renderer_test.go: -------------------------------------------------------------------------------- 1 | package gomarkdoc_test 2 | 3 | import ( 4 | "errors" 5 | "go/build" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/matryer/is" 11 | "github.com/princjef/gomarkdoc" 12 | "github.com/princjef/gomarkdoc/format/formatcore" 13 | "github.com/princjef/gomarkdoc/lang" 14 | "github.com/princjef/gomarkdoc/logger" 15 | ) 16 | 17 | func TestWithTemplateFunc(t *testing.T) { 18 | is := is.New(t) 19 | 20 | fn, err := loadFunc("./testData/docs", "Func") 21 | is.NoErr(err) 22 | 23 | r, err := gomarkdoc.NewRenderer() 24 | is.NoErr(err) 25 | 26 | r2, err := gomarkdoc.NewRenderer( 27 | gomarkdoc.WithTemplateFunc("escape", func(text string) string { 28 | return formatcore.Escape(strings.ToUpper(text)) 29 | }), 30 | ) 31 | is.NoErr(err) 32 | 33 | f, err := r.Func(fn) 34 | is.NoErr(err) 35 | 36 | f2, err := r2.Func(fn) 37 | is.NoErr(err) 38 | 39 | is.True(strings.Contains(f, "Func is present in this file.")) 40 | is.True(strings.Contains(f2, "FUNC IS PRESENT IN THIS FILE.")) 41 | } 42 | 43 | func getBuildPackage(path string) (*build.Package, error) { 44 | wd, err := os.Getwd() 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | return build.Import(path, wd, build.ImportComment) 50 | } 51 | 52 | func loadFunc(dir, name string) (*lang.Func, error) { 53 | buildPkg, err := getBuildPackage(dir) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | log := logger.New(logger.ErrorLevel) 59 | pkg, err := lang.NewPackageFromBuild(log, buildPkg) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | for _, f := range pkg.Funcs() { 65 | if f.Name() == name { 66 | return f, nil 67 | } 68 | } 69 | 70 | for _, t := range pkg.Types() { 71 | for _, f := range t.Funcs() { 72 | if f.Name() == name { 73 | return f, nil 74 | } 75 | } 76 | 77 | for _, f := range t.Methods() { 78 | if f.Name() == name { 79 | return f, nil 80 | } 81 | } 82 | } 83 | 84 | return nil, errors.New("func not found") 85 | } 86 | -------------------------------------------------------------------------------- /templates.go: -------------------------------------------------------------------------------- 1 | // Code generated by gentmpl.sh; DO NOT EDIT. 2 | 3 | package gomarkdoc 4 | 5 | var templates = map[string]string{ 6 | "doc": `{{- range (iter .Blocks) -}} 7 | {{- if eq .Entry.Kind "paragraph" -}} 8 | {{- template "text" .Entry.Spans -}} 9 | {{- else if eq .Entry.Kind "code" -}} 10 | {{- codeBlock "" (include "text" .Entry.Spans) -}} 11 | {{- else if eq .Entry.Kind "header" -}} 12 | {{- header .Entry.Level (include "text" .Entry.Spans) -}} 13 | {{- else if eq .Entry.Kind "list" -}} 14 | {{- template "list" .Entry.List -}} 15 | {{- end -}} 16 | {{- if (not .Last) -}}{{- spacer -}}{{- end -}} 17 | {{- end -}} 18 | `, 19 | "example": `{{- accordionHeader .Title -}} 20 | {{- spacer -}} 21 | 22 | {{- template "doc" .Doc -}} 23 | {{- spacer -}} 24 | 25 | {{- codeBlock "go" .Code -}} 26 | {{- spacer -}} 27 | 28 | {{- if .HasOutput -}} 29 | 30 | {{- header 4 "Output" -}} 31 | {{- spacer -}} 32 | 33 | {{- codeBlock "" .Output -}} 34 | {{- spacer -}} 35 | 36 | {{- end -}} 37 | 38 | {{- accordionTerminator -}} 39 | 40 | `, 41 | "file": ` 42 | 43 | {{if .Header -}} 44 | {{- .Header -}} 45 | {{- spacer -}} 46 | {{- end -}} 47 | 48 | {{- range .Packages -}} 49 | {{- template "package" . -}} 50 | {{- spacer -}} 51 | {{- end -}} 52 | 53 | {{- if .Footer -}} 54 | {{- .Footer -}} 55 | {{- spacer -}} 56 | {{- end -}} 57 | 58 | Generated by {{link "gomarkdoc" "https://github.com/princjef/gomarkdoc"}} 59 | `, 60 | "func": `{{- if .Receiver -}} 61 | {{- rawAnchorHeader .Level (codeHref .Location | link (escape .Name) | printf "func \\(%s\\) %s" (escape .Receiver)) .Anchor -}} 62 | {{- else -}} 63 | {{- rawAnchorHeader .Level (codeHref .Location | link (escape .Name) | printf "func %s") .Anchor -}} 64 | {{- end -}} 65 | {{- spacer -}} 66 | 67 | {{- codeBlock "go" .Signature -}} 68 | {{- spacer -}} 69 | 70 | {{- template "doc" .Doc -}} 71 | 72 | {{- if len .Examples -}} 73 | {{- spacer -}} 74 | 75 | {{- range (iter .Examples) -}} 76 | {{- template "example" .Entry -}} 77 | {{- if (not .Last) -}}{{- spacer -}}{{- end -}} 78 | {{- end -}} 79 | {{- end -}} 80 | 81 | `, 82 | "import": `{{- codeBlock "go" .Import -}}`, 83 | "index": `{{- if len .Consts -}} 84 | 85 | {{- localHref "Constants" | link "Constants" | listEntry 0 -}} 86 | {{- inlineSpacer -}} 87 | 88 | {{- end -}} 89 | 90 | {{- if len .Vars -}} 91 | 92 | {{- localHref "Variables" | link "Variables" | listEntry 0 -}} 93 | {{- inlineSpacer -}} 94 | 95 | {{- end -}} 96 | 97 | {{- range .Funcs -}} 98 | 99 | {{- (link .Signature (rawLocalHref .Anchor)) | listEntry 0 -}} 100 | {{- inlineSpacer -}} 101 | 102 | {{- end -}} 103 | 104 | {{- range .Types -}} 105 | 106 | {{- (link .Title (rawLocalHref .Anchor)) | listEntry 0 -}} 107 | {{- inlineSpacer -}} 108 | 109 | {{- range .Funcs -}} 110 | {{- (link .Signature (rawLocalHref .Anchor)) | listEntry 1 -}} 111 | {{- inlineSpacer -}} 112 | {{- end -}} 113 | 114 | {{- range .Methods -}} 115 | {{- (link .Signature (rawLocalHref .Anchor)) | listEntry 1 -}} 116 | {{- inlineSpacer -}} 117 | {{- end -}} 118 | 119 | {{- end -}} 120 | `, 121 | "list": `{{- range (iter .Items) -}} 122 | {{- if eq .Entry.Kind "ordered" -}} 123 | {{- .Entry.Number -}}. {{ hangingIndent (include "doc" .Entry) 2 -}} 124 | {{- else -}} 125 | - {{ hangingIndent (include "doc" .Entry) 2 -}} 126 | {{- end -}} 127 | 128 | {{- if (not .Last) -}} 129 | {{- if $.BlankBetween -}} 130 | {{- spacer -}} 131 | {{- else -}} 132 | {{- inlineSpacer -}} 133 | {{- end -}} 134 | {{- end -}} 135 | 136 | {{- end -}}`, 137 | "package": `{{- if eq .Name "main" -}} 138 | {{- header .Level .Dirname -}} 139 | {{- else -}} 140 | {{- header .Level .Name -}} 141 | {{- end -}} 142 | {{- spacer -}} 143 | 144 | {{- template "import" . -}} 145 | {{- spacer -}} 146 | 147 | {{- if len .Doc.Blocks -}} 148 | {{- template "doc" .Doc -}} 149 | {{- spacer -}} 150 | {{- end -}} 151 | 152 | {{- range (iter .Examples) -}} 153 | {{- template "example" .Entry -}} 154 | {{- spacer -}} 155 | {{- end -}} 156 | 157 | {{- header (add .Level 1) "Index" -}} 158 | {{- spacer -}} 159 | 160 | {{- template "index" . -}} 161 | 162 | {{- if len .Consts -}} 163 | {{- spacer -}} 164 | 165 | {{- header (add .Level 1) "Constants" -}} 166 | {{- spacer -}} 167 | 168 | {{- range (iter .Consts) -}} 169 | {{- template "value" .Entry -}} 170 | {{- if (not .Last) -}}{{- spacer -}}{{- end -}} 171 | {{- end -}} 172 | 173 | {{- end -}} 174 | 175 | {{- if len .Vars -}} 176 | {{- spacer -}} 177 | 178 | {{- header (add .Level 1) "Variables" -}} 179 | {{- spacer -}} 180 | 181 | {{- range (iter .Vars) -}} 182 | {{- template "value" .Entry -}} 183 | {{- if (not .Last) -}}{{- spacer -}}{{- end -}} 184 | {{- end -}} 185 | 186 | {{- end -}} 187 | 188 | {{- if len .Funcs -}} 189 | {{- spacer -}} 190 | 191 | {{- range (iter .Funcs) -}} 192 | {{- template "func" .Entry -}} 193 | {{- if (not .Last) -}}{{- spacer -}}{{- end -}} 194 | {{- end -}} 195 | {{- end -}} 196 | 197 | {{- if len .Types -}} 198 | {{- spacer -}} 199 | 200 | {{- range (iter .Types) -}} 201 | {{- template "type" .Entry -}} 202 | {{- if (not .Last) -}}{{- spacer -}}{{- end -}} 203 | {{- end -}} 204 | {{- end -}} 205 | `, 206 | "text": `{{- range . -}} 207 | {{- if eq .Kind "text" -}} 208 | {{- escape .Text -}} 209 | {{- else if eq .Kind "rawText" -}} 210 | {{- .Text -}} 211 | {{- else if eq .Kind "autolink" -}} 212 | {{- .Text -}} 213 | {{- else if eq .Kind "link" -}} 214 | {{- link (escape .Text) .URL -}} 215 | {{- end -}} 216 | {{- end -}}`, 217 | "type": `{{- rawAnchorHeader .Level (codeHref .Location | link (escape .Name) | printf "type %s") .Anchor -}} 218 | {{- spacer -}} 219 | 220 | {{- template "doc" .Doc -}} 221 | {{- spacer -}} 222 | 223 | {{- codeBlock "go" .Decl -}} 224 | 225 | {{- if len .Consts -}} 226 | {{- spacer -}} 227 | 228 | {{- range (iter .Consts) -}} 229 | {{- template "value" .Entry -}} 230 | {{- if (not .Last) -}}{{- spacer -}}{{- end -}} 231 | {{- end -}} 232 | {{- end -}} 233 | 234 | {{- if len .Vars -}} 235 | {{- spacer -}} 236 | 237 | {{- range (iter .Vars) -}} 238 | {{- template "value" .Entry -}} 239 | {{- if (not .Last) -}}{{- spacer -}}{{- end -}} 240 | {{- end -}} 241 | {{- end -}} 242 | 243 | {{- if len .Examples -}} 244 | {{- spacer -}} 245 | 246 | {{- range (iter .Examples) -}} 247 | {{- template "example" .Entry -}} 248 | {{- if (not .Last) -}}{{- spacer -}}{{- end -}} 249 | {{- end -}} 250 | {{- end -}} 251 | 252 | {{- if len .Funcs -}} 253 | {{- spacer -}} 254 | 255 | {{- range (iter .Funcs) -}} 256 | {{- template "func" .Entry -}} 257 | {{- if (not .Last) -}}{{- spacer -}}{{- end -}} 258 | {{- end -}} 259 | {{- end -}} 260 | 261 | {{- if len .Methods -}} 262 | {{- spacer -}} 263 | 264 | {{- range (iter .Methods) -}} 265 | {{- template "func" .Entry -}} 266 | {{- if (not .Last) -}}{{- spacer -}}{{- end -}} 267 | {{- end -}} 268 | {{- end -}} 269 | 270 | `, 271 | "value": `{{- anchor .Anchor -}} 272 | {{- template "doc" .Doc -}} 273 | {{- spacer -}} 274 | 275 | {{- codeBlock "go" .Decl -}} 276 | 277 | `, 278 | } 279 | -------------------------------------------------------------------------------- /templates/doc.gotxt: -------------------------------------------------------------------------------- 1 | {{- range (iter .Blocks) -}} 2 | {{- if eq .Entry.Kind "paragraph" -}} 3 | {{- template "text" .Entry.Spans -}} 4 | {{- else if eq .Entry.Kind "code" -}} 5 | {{- codeBlock "" (include "text" .Entry.Spans) -}} 6 | {{- else if eq .Entry.Kind "header" -}} 7 | {{- header .Entry.Level (include "text" .Entry.Spans) -}} 8 | {{- else if eq .Entry.Kind "list" -}} 9 | {{- template "list" .Entry.List -}} 10 | {{- end -}} 11 | {{- if (not .Last) -}}{{- spacer -}}{{- end -}} 12 | {{- end -}} 13 | -------------------------------------------------------------------------------- /templates/example.gotxt: -------------------------------------------------------------------------------- 1 | {{- accordionHeader .Title -}} 2 | {{- spacer -}} 3 | 4 | {{- template "doc" .Doc -}} 5 | {{- spacer -}} 6 | 7 | {{- codeBlock "go" .Code -}} 8 | {{- spacer -}} 9 | 10 | {{- if .HasOutput -}} 11 | 12 | {{- header 4 "Output" -}} 13 | {{- spacer -}} 14 | 15 | {{- codeBlock "" .Output -}} 16 | {{- spacer -}} 17 | 18 | {{- end -}} 19 | 20 | {{- accordionTerminator -}} 21 | 22 | -------------------------------------------------------------------------------- /templates/file.gotxt: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{if .Header -}} 4 | {{- .Header -}} 5 | {{- spacer -}} 6 | {{- end -}} 7 | 8 | {{- range .Packages -}} 9 | {{- template "package" . -}} 10 | {{- spacer -}} 11 | {{- end -}} 12 | 13 | {{- if .Footer -}} 14 | {{- .Footer -}} 15 | {{- spacer -}} 16 | {{- end -}} 17 | 18 | Generated by {{link "gomarkdoc" "https://github.com/princjef/gomarkdoc"}} 19 | -------------------------------------------------------------------------------- /templates/func.gotxt: -------------------------------------------------------------------------------- 1 | {{- if .Receiver -}} 2 | {{- rawAnchorHeader .Level (codeHref .Location | link (escape .Name) | printf "func \\(%s\\) %s" (escape .Receiver)) .Anchor -}} 3 | {{- else -}} 4 | {{- rawAnchorHeader .Level (codeHref .Location | link (escape .Name) | printf "func %s") .Anchor -}} 5 | {{- end -}} 6 | {{- spacer -}} 7 | 8 | {{- codeBlock "go" .Signature -}} 9 | {{- spacer -}} 10 | 11 | {{- template "doc" .Doc -}} 12 | 13 | {{- if len .Examples -}} 14 | {{- spacer -}} 15 | 16 | {{- range (iter .Examples) -}} 17 | {{- template "example" .Entry -}} 18 | {{- if (not .Last) -}}{{- spacer -}}{{- end -}} 19 | {{- end -}} 20 | {{- end -}} 21 | 22 | -------------------------------------------------------------------------------- /templates/import.gotxt: -------------------------------------------------------------------------------- 1 | {{- codeBlock "go" .Import -}} -------------------------------------------------------------------------------- /templates/index.gotxt: -------------------------------------------------------------------------------- 1 | {{- if len .Consts -}} 2 | 3 | {{- localHref "Constants" | link "Constants" | listEntry 0 -}} 4 | {{- inlineSpacer -}} 5 | 6 | {{- end -}} 7 | 8 | {{- if len .Vars -}} 9 | 10 | {{- localHref "Variables" | link "Variables" | listEntry 0 -}} 11 | {{- inlineSpacer -}} 12 | 13 | {{- end -}} 14 | 15 | {{- range .Funcs -}} 16 | 17 | {{- (link .Signature (rawLocalHref .Anchor)) | listEntry 0 -}} 18 | {{- inlineSpacer -}} 19 | 20 | {{- end -}} 21 | 22 | {{- range .Types -}} 23 | 24 | {{- (link .Title (rawLocalHref .Anchor)) | listEntry 0 -}} 25 | {{- inlineSpacer -}} 26 | 27 | {{- range .Funcs -}} 28 | {{- (link .Signature (rawLocalHref .Anchor)) | listEntry 1 -}} 29 | {{- inlineSpacer -}} 30 | {{- end -}} 31 | 32 | {{- range .Methods -}} 33 | {{- (link .Signature (rawLocalHref .Anchor)) | listEntry 1 -}} 34 | {{- inlineSpacer -}} 35 | {{- end -}} 36 | 37 | {{- end -}} 38 | -------------------------------------------------------------------------------- /templates/list.gotxt: -------------------------------------------------------------------------------- 1 | {{- range (iter .Items) -}} 2 | {{- if eq .Entry.Kind "ordered" -}} 3 | {{- .Entry.Number -}}. {{ hangingIndent (include "doc" .Entry) 2 -}} 4 | {{- else -}} 5 | - {{ hangingIndent (include "doc" .Entry) 2 -}} 6 | {{- end -}} 7 | 8 | {{- if (not .Last) -}} 9 | {{- if $.BlankBetween -}} 10 | {{- spacer -}} 11 | {{- else -}} 12 | {{- inlineSpacer -}} 13 | {{- end -}} 14 | {{- end -}} 15 | 16 | {{- end -}} -------------------------------------------------------------------------------- /templates/package.gotxt: -------------------------------------------------------------------------------- 1 | {{- if eq .Name "main" -}} 2 | {{- header .Level .Dirname -}} 3 | {{- else -}} 4 | {{- header .Level .Name -}} 5 | {{- end -}} 6 | {{- spacer -}} 7 | 8 | {{- template "import" . -}} 9 | {{- spacer -}} 10 | 11 | {{- if len .Doc.Blocks -}} 12 | {{- template "doc" .Doc -}} 13 | {{- spacer -}} 14 | {{- end -}} 15 | 16 | {{- range (iter .Examples) -}} 17 | {{- template "example" .Entry -}} 18 | {{- spacer -}} 19 | {{- end -}} 20 | 21 | {{- header (add .Level 1) "Index" -}} 22 | {{- spacer -}} 23 | 24 | {{- template "index" . -}} 25 | 26 | {{- if len .Consts -}} 27 | {{- spacer -}} 28 | 29 | {{- header (add .Level 1) "Constants" -}} 30 | {{- spacer -}} 31 | 32 | {{- range (iter .Consts) -}} 33 | {{- template "value" .Entry -}} 34 | {{- if (not .Last) -}}{{- spacer -}}{{- end -}} 35 | {{- end -}} 36 | 37 | {{- end -}} 38 | 39 | {{- if len .Vars -}} 40 | {{- spacer -}} 41 | 42 | {{- header (add .Level 1) "Variables" -}} 43 | {{- spacer -}} 44 | 45 | {{- range (iter .Vars) -}} 46 | {{- template "value" .Entry -}} 47 | {{- if (not .Last) -}}{{- spacer -}}{{- end -}} 48 | {{- end -}} 49 | 50 | {{- end -}} 51 | 52 | {{- if len .Funcs -}} 53 | {{- spacer -}} 54 | 55 | {{- range (iter .Funcs) -}} 56 | {{- template "func" .Entry -}} 57 | {{- if (not .Last) -}}{{- spacer -}}{{- end -}} 58 | {{- end -}} 59 | {{- end -}} 60 | 61 | {{- if len .Types -}} 62 | {{- spacer -}} 63 | 64 | {{- range (iter .Types) -}} 65 | {{- template "type" .Entry -}} 66 | {{- if (not .Last) -}}{{- spacer -}}{{- end -}} 67 | {{- end -}} 68 | {{- end -}} 69 | -------------------------------------------------------------------------------- /templates/text.gotxt: -------------------------------------------------------------------------------- 1 | {{- range . -}} 2 | {{- if eq .Kind "text" -}} 3 | {{- escape .Text -}} 4 | {{- else if eq .Kind "rawText" -}} 5 | {{- .Text -}} 6 | {{- else if eq .Kind "autolink" -}} 7 | {{- .Text -}} 8 | {{- else if eq .Kind "link" -}} 9 | {{- link (escape .Text) .URL -}} 10 | {{- end -}} 11 | {{- end -}} -------------------------------------------------------------------------------- /templates/type.gotxt: -------------------------------------------------------------------------------- 1 | {{- rawAnchorHeader .Level (codeHref .Location | link (escape .Name) | printf "type %s") .Anchor -}} 2 | {{- spacer -}} 3 | 4 | {{- template "doc" .Doc -}} 5 | {{- spacer -}} 6 | 7 | {{- codeBlock "go" .Decl -}} 8 | 9 | {{- if len .Consts -}} 10 | {{- spacer -}} 11 | 12 | {{- range (iter .Consts) -}} 13 | {{- template "value" .Entry -}} 14 | {{- if (not .Last) -}}{{- spacer -}}{{- end -}} 15 | {{- end -}} 16 | {{- end -}} 17 | 18 | {{- if len .Vars -}} 19 | {{- spacer -}} 20 | 21 | {{- range (iter .Vars) -}} 22 | {{- template "value" .Entry -}} 23 | {{- if (not .Last) -}}{{- spacer -}}{{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{- if len .Examples -}} 28 | {{- spacer -}} 29 | 30 | {{- range (iter .Examples) -}} 31 | {{- template "example" .Entry -}} 32 | {{- if (not .Last) -}}{{- spacer -}}{{- end -}} 33 | {{- end -}} 34 | {{- end -}} 35 | 36 | {{- if len .Funcs -}} 37 | {{- spacer -}} 38 | 39 | {{- range (iter .Funcs) -}} 40 | {{- template "func" .Entry -}} 41 | {{- if (not .Last) -}}{{- spacer -}}{{- end -}} 42 | {{- end -}} 43 | {{- end -}} 44 | 45 | {{- if len .Methods -}} 46 | {{- spacer -}} 47 | 48 | {{- range (iter .Methods) -}} 49 | {{- template "func" .Entry -}} 50 | {{- if (not .Last) -}}{{- spacer -}}{{- end -}} 51 | {{- end -}} 52 | {{- end -}} 53 | 54 | -------------------------------------------------------------------------------- /templates/value.gotxt: -------------------------------------------------------------------------------- 1 | {{- anchor .Anchor -}} 2 | {{- template "doc" .Doc -}} 3 | {{- spacer -}} 4 | 5 | {{- codeBlock "go" .Decl -}} 6 | 7 | -------------------------------------------------------------------------------- /testData/.gomarkdoc-empty.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/princjef/gomarkdoc/8b1606ae4e72fc0a2a8bfa2d3d280245afc2d006/testData/.gomarkdoc-empty.yml -------------------------------------------------------------------------------- /testData/docs/.gomarkdoc.yml: -------------------------------------------------------------------------------- 1 | output: "{{.Dir}}/README.md" -------------------------------------------------------------------------------- /testData/docs/README-azure-devops.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # docs 4 | 5 | ```go 6 | import "github.com/princjef/gomarkdoc/testData/docs" 7 | ``` 8 | 9 | Package docs exercises the documentation features of golang 1.19 and above at the package documentation level. 10 | 11 | ### This is a heading 12 | 13 | This heading has a paragraph with a reference to the standard library [math/rand]() as well as a function in the file [Func](<#Func>), a type [Type](<#Type>), a type's function [Type.Func](<#Type.Func>), a non\-standard library package [golang.org/x/crypto/bcrypt.Cost](), an external link [Outside Link]() and a \[broken link\]. We can also place links directly like https://github.com which get turned into links. 14 | 15 | It also has a numbered list: 16 | 17 | 1. First 18 | 2. Second 19 | 3. Third 20 | 21 | Plus one with blank lines: 22 | 23 | 1. First 24 | 25 | 2. Second 26 | 27 | 3. Third 28 | 29 | Non\-numbered lists 30 | 31 | - First another line 32 | - Second 33 | - Third 34 | 35 | Plus blank lines: 36 | 37 | - First 38 | 39 | another paragraph 40 | 41 | - Second 42 | 43 | - Third 44 | 45 | And a golang code block: 46 | 47 | ``` 48 | func GolangCode(t int) int { 49 | return t + 1 50 | } 51 | ``` 52 | 53 | And a random code block: 54 | 55 | ``` 56 | something 57 | preformatted 58 | in a random 59 | way 60 | ``` 61 | 62 | There's also another file with a struct called [AnotherStruct](<#AnotherStruct>) that has additional methods and fields. 63 | 64 | We also have constants like [Constant](<#Constant>) and [Const1](<#Const1>) plus variables like [Var](<#Var>) and and [VarB](<#VarA>). 65 | 66 | ## Index 67 | 68 | - [Constants](<#constants>) 69 | - [Variables](<#variables>) 70 | - [func Func\(param int\) int](<#Func>) 71 | - [type AnotherStruct](<#AnotherStruct>) 72 | - [func NewAnotherStruct\(\) \*AnotherStruct](<#NewAnotherStruct>) 73 | - [func \(s \*AnotherStruct\) GetField\(\) string](<#AnotherStruct.GetField>) 74 | - [type Type](<#Type>) 75 | - [func \(t \*Type\) Func\(\)](<#Type.Func>) 76 | 77 | 78 | ## Constants 79 | 80 | This is a constant block 81 | 82 | ```go 83 | const ( 84 | Const1 = 1 85 | Const2 = 2 86 | Const3 = 3 87 | ) 88 | ``` 89 | 90 | Constant is a constant. 91 | 92 | ```go 93 | const Constant = 3 94 | ``` 95 | 96 | ## Variables 97 | 98 | This is a var block 99 | 100 | ```go 101 | var ( 102 | VarA = 'a' 103 | VarB = 'b' 104 | VarC = 'c' 105 | ) 106 | ``` 107 | 108 | Var is a var. 109 | 110 | ```go 111 | var Var = 2 112 | ``` 113 | 114 | 115 | ## func [Func]() 116 | 117 | ```go 118 | func Func(param int) int 119 | ``` 120 | 121 | Func is present in this file. 122 | 123 | 124 | ## type [AnotherStruct]() 125 | 126 | AnotherStruct has methods like [\\\*AnotherStruct.GetField](<#AnotherStruct.GetField>) and also has an initializer called [NewAnotherStruct](<#NewAnotherStruct>). 127 | 128 | ```go 129 | type AnotherStruct struct { 130 | Field string 131 | } 132 | ``` 133 | 134 | 135 | ### func [NewAnotherStruct]() 136 | 137 | ```go 138 | func NewAnotherStruct() *AnotherStruct 139 | ``` 140 | 141 | NewAnotherStruct\(\) makes [\\\*AnotherStruct](<#AnotherStruct>). 142 | 143 | 144 | ### func \(\*AnotherStruct\) [GetField]() 145 | 146 | ```go 147 | func (s *AnotherStruct) GetField() string 148 | ``` 149 | 150 | GetField gets \[\*AnotherStruct.Field\]. 151 | 152 | 153 | ## type [Type]() 154 | 155 | Type is a type in this file. 156 | 157 | ```go 158 | type Type struct{} 159 | ``` 160 | 161 | 162 | ### func \(\*Type\) [Func]() 163 | 164 | ```go 165 | func (t *Type) Func() 166 | ``` 167 | 168 | TypeFunc is a func within a type in this file. 169 | 170 | Generated by [gomarkdoc]() 171 | -------------------------------------------------------------------------------- /testData/docs/README-github.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # docs 4 | 5 | ```go 6 | import "github.com/princjef/gomarkdoc/testData/docs" 7 | ``` 8 | 9 | Package docs exercises the documentation features of golang 1.19 and above at the package documentation level. 10 | 11 | ### This is a heading 12 | 13 | This heading has a paragraph with a reference to the standard library [math/rand]() as well as a function in the file [Func](<#Func>), a type [Type](<#Type>), a type's function [Type.Func](<#Type.Func>), a non\-standard library package [golang.org/x/crypto/bcrypt.Cost](), an external link [Outside Link]() and a \[broken link\]. We can also place links directly like https://github.com which get turned into links. 14 | 15 | It also has a numbered list: 16 | 17 | 1. First 18 | 2. Second 19 | 3. Third 20 | 21 | Plus one with blank lines: 22 | 23 | 1. First 24 | 25 | 2. Second 26 | 27 | 3. Third 28 | 29 | Non\-numbered lists 30 | 31 | - First another line 32 | - Second 33 | - Third 34 | 35 | Plus blank lines: 36 | 37 | - First 38 | 39 | another paragraph 40 | 41 | - Second 42 | 43 | - Third 44 | 45 | And a golang code block: 46 | 47 | ``` 48 | func GolangCode(t int) int { 49 | return t + 1 50 | } 51 | ``` 52 | 53 | And a random code block: 54 | 55 | ``` 56 | something 57 | preformatted 58 | in a random 59 | way 60 | ``` 61 | 62 | There's also another file with a struct called [AnotherStruct](<#AnotherStruct>) that has additional methods and fields. 63 | 64 | We also have constants like [Constant](<#Constant>) and [Const1](<#Const1>) plus variables like [Var](<#Var>) and and [VarB](<#VarA>). 65 | 66 | ## Index 67 | 68 | - [Constants](<#constants>) 69 | - [Variables](<#variables>) 70 | - [func Func\(param int\) int](<#Func>) 71 | - [type AnotherStruct](<#AnotherStruct>) 72 | - [func NewAnotherStruct\(\) \*AnotherStruct](<#NewAnotherStruct>) 73 | - [func \(s \*AnotherStruct\) GetField\(\) string](<#AnotherStruct.GetField>) 74 | - [type Type](<#Type>) 75 | - [func \(t \*Type\) Func\(\)](<#Type.Func>) 76 | 77 | 78 | ## Constants 79 | 80 | This is a constant block 81 | 82 | ```go 83 | const ( 84 | Const1 = 1 85 | Const2 = 2 86 | Const3 = 3 87 | ) 88 | ``` 89 | 90 | Constant is a constant. 91 | 92 | ```go 93 | const Constant = 3 94 | ``` 95 | 96 | ## Variables 97 | 98 | This is a var block 99 | 100 | ```go 101 | var ( 102 | VarA = 'a' 103 | VarB = 'b' 104 | VarC = 'c' 105 | ) 106 | ``` 107 | 108 | Var is a var. 109 | 110 | ```go 111 | var Var = 2 112 | ``` 113 | 114 | 115 | ## func [Func]() 116 | 117 | ```go 118 | func Func(param int) int 119 | ``` 120 | 121 | Func is present in this file. 122 | 123 | 124 | ## type [AnotherStruct]() 125 | 126 | AnotherStruct has methods like [\\\*AnotherStruct.GetField](<#AnotherStruct.GetField>) and also has an initializer called [NewAnotherStruct](<#NewAnotherStruct>). 127 | 128 | ```go 129 | type AnotherStruct struct { 130 | Field string 131 | } 132 | ``` 133 | 134 | 135 | ### func [NewAnotherStruct]() 136 | 137 | ```go 138 | func NewAnotherStruct() *AnotherStruct 139 | ``` 140 | 141 | NewAnotherStruct\(\) makes [\\\*AnotherStruct](<#AnotherStruct>). 142 | 143 | 144 | ### func \(\*AnotherStruct\) [GetField]() 145 | 146 | ```go 147 | func (s *AnotherStruct) GetField() string 148 | ``` 149 | 150 | GetField gets \[\*AnotherStruct.Field\]. 151 | 152 | 153 | ## type [Type]() 154 | 155 | Type is a type in this file. 156 | 157 | ```go 158 | type Type struct{} 159 | ``` 160 | 161 | 162 | ### func \(\*Type\) [Func]() 163 | 164 | ```go 165 | func (t *Type) Func() 166 | ``` 167 | 168 | TypeFunc is a func within a type in this file. 169 | 170 | Generated by [gomarkdoc]() 171 | -------------------------------------------------------------------------------- /testData/docs/README-plain.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # docs 4 | 5 | import "github.com/princjef/gomarkdoc/testData/docs" 6 | 7 | Package docs exercises the documentation features of golang 1.19 and above at the package documentation level. 8 | 9 | ### This is a heading 10 | 11 | This heading has a paragraph with a reference to the standard library [math/rand]() as well as a function in the file [Func](<#Func>), a type [Type](<#Type>), a type's function [Type.Func](<#Type.Func>), a non\-standard library package [golang.org/x/crypto/bcrypt.Cost](), an external link [Outside Link]() and a \[broken link\]. We can also place links directly like https://github.com which get turned into links. 12 | 13 | It also has a numbered list: 14 | 15 | 1. First 16 | 2. Second 17 | 3. Third 18 | 19 | Plus one with blank lines: 20 | 21 | 1. First 22 | 23 | 2. Second 24 | 25 | 3. Third 26 | 27 | Non\-numbered lists 28 | 29 | - First another line 30 | - Second 31 | - Third 32 | 33 | Plus blank lines: 34 | 35 | - First 36 | 37 | another paragraph 38 | 39 | - Second 40 | 41 | - Third 42 | 43 | And a golang code block: 44 | 45 | func GolangCode(t int) int { 46 | return t + 1 47 | } 48 | 49 | 50 | And a random code block: 51 | 52 | something 53 | preformatted 54 | in a random 55 | way 56 | 57 | 58 | There's also another file with a struct called [AnotherStruct](<#AnotherStruct>) that has additional methods and fields. 59 | 60 | We also have constants like [Constant](<#Constant>) and [Const1](<#Const1>) plus variables like [Var](<#Var>) and and [VarB](<#VarA>). 61 | 62 | ## Index 63 | 64 | - Constants 65 | - Variables 66 | - [func Func\(param int\) int](<#Func>) 67 | - [type AnotherStruct](<#AnotherStruct>) 68 | - [func NewAnotherStruct\(\) \*AnotherStruct](<#NewAnotherStruct>) 69 | - [func \(s \*AnotherStruct\) GetField\(\) string](<#AnotherStruct.GetField>) 70 | - [type Type](<#Type>) 71 | - [func \(t \*Type\) Func\(\)](<#Type.Func>) 72 | 73 | 74 | ## Constants 75 | 76 | This is a constant block 77 | 78 | const ( 79 | Const1 = 1 80 | Const2 = 2 81 | Const3 = 3 82 | ) 83 | 84 | Constant is a constant. 85 | 86 | const Constant = 3 87 | 88 | ## Variables 89 | 90 | This is a var block 91 | 92 | var ( 93 | VarA = 'a' 94 | VarB = 'b' 95 | VarC = 'c' 96 | ) 97 | 98 | Var is a var. 99 | 100 | var Var = 2 101 | 102 | 103 | ## func Func 104 | 105 | func Func(param int) int 106 | 107 | Func is present in this file. 108 | 109 | 110 | ## type AnotherStruct 111 | 112 | AnotherStruct has methods like [\\\*AnotherStruct.GetField](<#AnotherStruct.GetField>) and also has an initializer called [NewAnotherStruct](<#NewAnotherStruct>). 113 | 114 | type AnotherStruct struct { 115 | Field string 116 | } 117 | 118 | 119 | ### func NewAnotherStruct 120 | 121 | func NewAnotherStruct() *AnotherStruct 122 | 123 | NewAnotherStruct\(\) makes [\\\*AnotherStruct](<#AnotherStruct>). 124 | 125 | 126 | ### func \(\*AnotherStruct\) GetField 127 | 128 | func (s *AnotherStruct) GetField() string 129 | 130 | GetField gets \[\*AnotherStruct.Field\]. 131 | 132 | 133 | ## type Type 134 | 135 | Type is a type in this file. 136 | 137 | type Type struct{} 138 | 139 | 140 | ### func \(\*Type\) Func 141 | 142 | func (t *Type) Func() 143 | 144 | TypeFunc is a func within a type in this file. 145 | 146 | Generated by [gomarkdoc]() 147 | -------------------------------------------------------------------------------- /testData/docs/anotherFile.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | // AnotherStruct has methods like [*AnotherStruct.GetField] and also has an 4 | // initializer called [NewAnotherStruct]. 5 | type AnotherStruct struct { 6 | Field string 7 | } 8 | 9 | // NewAnotherStruct() makes [*AnotherStruct]. 10 | func NewAnotherStruct() *AnotherStruct { 11 | return &AnotherStruct{ 12 | Field: "test", 13 | } 14 | } 15 | 16 | // GetField gets [*AnotherStruct.Field]. 17 | func (s *AnotherStruct) GetField() string { 18 | return s.Field 19 | } 20 | -------------------------------------------------------------------------------- /testData/docs/docs.go: -------------------------------------------------------------------------------- 1 | // Package docs exercises the documentation features of golang 1.19 and above at 2 | // the package documentation level. 3 | // 4 | // # This is a heading 5 | // 6 | // This heading has a paragraph with a reference to the standard library 7 | // [math/rand] as well as a function in the file [Func], a type [Type], a type's 8 | // function [Type.Func], a non-standard library package 9 | // [golang.org/x/crypto/bcrypt.Cost], an external link [Outside Link] and 10 | // a [broken link]. We can also place links directly like https://github.com 11 | // which get turned into links. 12 | // 13 | // It also has a numbered list: 14 | // 1. First 15 | // 2. Second 16 | // 3. Third 17 | // 18 | // Plus one with blank lines: 19 | // 20 | // 1. First 21 | // 22 | // 2. Second 23 | // 24 | // 3. Third 25 | // 26 | // Non-numbered lists 27 | // - First 28 | // another line 29 | // - Second 30 | // - Third 31 | // 32 | // Plus blank lines: 33 | // 34 | // - First 35 | // 36 | // another paragraph 37 | // 38 | // - Second 39 | // 40 | // - Third 41 | // 42 | // And a golang code block: 43 | // 44 | // func GolangCode(t int) int { 45 | // return t + 1 46 | // } 47 | // 48 | // And a random code block: 49 | // 50 | // something 51 | // preformatted 52 | // in a random 53 | // way 54 | // 55 | // There's also another file with a struct called [AnotherStruct] that has 56 | // additional methods and fields. 57 | // 58 | // We also have constants like [Constant] and [Const1] plus variables like 59 | // [Var] and and [VarB]. 60 | // 61 | // [Outside Link]: https://golang.org/doc/articles/json_and_go.html 62 | package docs 63 | 64 | // Func is present in this file. 65 | func Func(param int) int { 66 | return param 67 | } 68 | 69 | // Type is a type in this file. 70 | type Type struct{} 71 | 72 | // TypeFunc is a func within a type in this file. 73 | func (t *Type) Func() {} 74 | 75 | // Constant is a constant. 76 | const Constant = 3 77 | 78 | // Var is a var. 79 | var Var = 2 80 | 81 | // This is a constant block 82 | const ( 83 | Const1 = 1 84 | Const2 = 2 85 | Const3 = 3 86 | ) 87 | 88 | // This is a var block 89 | var ( 90 | VarA = 'a' 91 | VarB = 'b' 92 | VarC = 'c' 93 | ) 94 | -------------------------------------------------------------------------------- /testData/embed/.gomarkdoc.yml: -------------------------------------------------------------------------------- 1 | output: "{{.Dir}}/README.md" 2 | embed: true -------------------------------------------------------------------------------- /testData/embed/README-azure-devops.md: -------------------------------------------------------------------------------- 1 | This is content before the embed 2 | 3 | 4 | 5 | 6 | 7 | # embed 8 | 9 | ```go 10 | import "github.com/princjef/gomarkdoc/testData/embed" 11 | ``` 12 | 13 | Package embed tests out embedding of documentation in an existing readme. 14 | 15 | ## Index 16 | 17 | - [func EmbeddedFunc\(param int\) int](<#EmbeddedFunc>) 18 | 19 | 20 | 21 | ## func [EmbeddedFunc]() 22 | 23 | ```go 24 | func EmbeddedFunc(param int) int 25 | ``` 26 | 27 | EmbeddedFunc is present in embedded content. 28 | 29 | Generated by [gomarkdoc]() 30 | 31 | 32 | 33 | 34 | This is content after the embed 35 | 36 | 37 | 38 | 39 | 40 | # embed 41 | 42 | ```go 43 | import "github.com/princjef/gomarkdoc/testData/embed" 44 | ``` 45 | 46 | Package embed tests out embedding of documentation in an existing readme. 47 | 48 | ## Index 49 | 50 | - [func EmbeddedFunc\(param int\) int](<#EmbeddedFunc>) 51 | 52 | 53 | 54 | ## func [EmbeddedFunc]() 55 | 56 | ```go 57 | func EmbeddedFunc(param int) int 58 | ``` 59 | 60 | EmbeddedFunc is present in embedded content. 61 | 62 | Generated by [gomarkdoc]() 63 | 64 | 65 | 66 | 67 | This is content after the second embed 68 | 69 | 70 | 71 | 72 | 73 | # embed 74 | 75 | ```go 76 | import "github.com/princjef/gomarkdoc/testData/embed" 77 | ``` 78 | 79 | Package embed tests out embedding of documentation in an existing readme. 80 | 81 | ## Index 82 | 83 | - [func EmbeddedFunc\(param int\) int](<#EmbeddedFunc>) 84 | 85 | 86 | 87 | ## func [EmbeddedFunc]() 88 | 89 | ```go 90 | func EmbeddedFunc(param int) int 91 | ``` 92 | 93 | EmbeddedFunc is present in embedded content. 94 | 95 | Generated by [gomarkdoc]() 96 | 97 | 98 | 99 | 100 | This is content after the third embed -------------------------------------------------------------------------------- /testData/embed/README-github-invalid.md: -------------------------------------------------------------------------------- 1 | This is content before the embed 2 | 3 | 4 | 5 | 6 | 7 | # embed 8 | 9 | ```go 10 | import "github.com/princjef/gomarkdoc/testData/embed" 11 | ``` 12 | 13 | Package embed tests out embedding of documentation in an existing readme. This is an extra comment that is invalid. 14 | 15 | ## Index 16 | 17 | - [func EmbeddedFunc\(param int\) int](<#EmbeddedFunc>) 18 | 19 | 20 | 21 | ## func [EmbeddedFunc]() 22 | 23 | ```go 24 | func EmbeddedFunc(param int) int 25 | ``` 26 | 27 | EmbeddedFunc is present in embedded content. 28 | 29 | Generated by [gomarkdoc]() 30 | 31 | 32 | 33 | 34 | This is content after the embed 35 | 36 | 37 | 38 | 39 | 40 | # embed 41 | 42 | ```go 43 | import "github.com/princjef/gomarkdoc/testData/embed" 44 | ``` 45 | 46 | Package embed tests out embedding of documentation in an existing readme. 47 | 48 | ## Index 49 | 50 | - [func EmbeddedFunc\(param int\) int](<#EmbeddedFunc>) 51 | 52 | 53 | 54 | ## func [EmbeddedFunc]() 55 | 56 | ```go 57 | func EmbeddedFunc(param int) int 58 | ``` 59 | 60 | EmbeddedFunc is present in embedded content. 61 | 62 | Generated by [gomarkdoc]() 63 | 64 | 65 | 66 | 67 | This is content after the second embed 68 | 69 | 70 | 71 | 72 | 73 | # embed 74 | 75 | ```go 76 | import "github.com/princjef/gomarkdoc/testData/embed" 77 | ``` 78 | 79 | Package embed tests out embedding of documentation in an existing readme. 80 | 81 | ## Index 82 | 83 | - [func EmbeddedFunc\(param int\) int](<#EmbeddedFunc>) 84 | 85 | 86 | 87 | ## func [EmbeddedFunc]() 88 | 89 | ```go 90 | func EmbeddedFunc(param int) int 91 | ``` 92 | 93 | EmbeddedFunc is present in embedded content. 94 | 95 | Generated by [gomarkdoc]() 96 | 97 | 98 | 99 | 100 | This is content after the third embed -------------------------------------------------------------------------------- /testData/embed/README-github.md: -------------------------------------------------------------------------------- 1 | This is content before the embed 2 | 3 | 4 | 5 | 6 | 7 | # embed 8 | 9 | ```go 10 | import "github.com/princjef/gomarkdoc/testData/embed" 11 | ``` 12 | 13 | Package embed tests out embedding of documentation in an existing readme. 14 | 15 | ## Index 16 | 17 | - [func EmbeddedFunc\(param int\) int](<#EmbeddedFunc>) 18 | 19 | 20 | 21 | ## func [EmbeddedFunc]() 22 | 23 | ```go 24 | func EmbeddedFunc(param int) int 25 | ``` 26 | 27 | EmbeddedFunc is present in embedded content. 28 | 29 | Generated by [gomarkdoc]() 30 | 31 | 32 | 33 | 34 | This is content after the embed 35 | 36 | 37 | 38 | 39 | 40 | # embed 41 | 42 | ```go 43 | import "github.com/princjef/gomarkdoc/testData/embed" 44 | ``` 45 | 46 | Package embed tests out embedding of documentation in an existing readme. 47 | 48 | ## Index 49 | 50 | - [func EmbeddedFunc\(param int\) int](<#EmbeddedFunc>) 51 | 52 | 53 | 54 | ## func [EmbeddedFunc]() 55 | 56 | ```go 57 | func EmbeddedFunc(param int) int 58 | ``` 59 | 60 | EmbeddedFunc is present in embedded content. 61 | 62 | Generated by [gomarkdoc]() 63 | 64 | 65 | 66 | 67 | This is content after the second embed 68 | 69 | 70 | 71 | 72 | 73 | # embed 74 | 75 | ```go 76 | import "github.com/princjef/gomarkdoc/testData/embed" 77 | ``` 78 | 79 | Package embed tests out embedding of documentation in an existing readme. 80 | 81 | ## Index 82 | 83 | - [func EmbeddedFunc\(param int\) int](<#EmbeddedFunc>) 84 | 85 | 86 | 87 | ## func [EmbeddedFunc]() 88 | 89 | ```go 90 | func EmbeddedFunc(param int) int 91 | ``` 92 | 93 | EmbeddedFunc is present in embedded content. 94 | 95 | Generated by [gomarkdoc]() 96 | 97 | 98 | 99 | 100 | This is content after the third embed -------------------------------------------------------------------------------- /testData/embed/README-plain.md: -------------------------------------------------------------------------------- 1 | This is content before the embed 2 | 3 | 4 | 5 | 6 | 7 | # embed 8 | 9 | import "github.com/princjef/gomarkdoc/testData/embed" 10 | 11 | Package embed tests out embedding of documentation in an existing readme. 12 | 13 | ## Index 14 | 15 | - [func EmbeddedFunc\(param int\) int](<#EmbeddedFunc>) 16 | 17 | 18 | 19 | ## func EmbeddedFunc 20 | 21 | func EmbeddedFunc(param int) int 22 | 23 | EmbeddedFunc is present in embedded content. 24 | 25 | Generated by [gomarkdoc]() 26 | 27 | 28 | 29 | 30 | This is content after the embed 31 | 32 | 33 | 34 | 35 | 36 | # embed 37 | 38 | import "github.com/princjef/gomarkdoc/testData/embed" 39 | 40 | Package embed tests out embedding of documentation in an existing readme. 41 | 42 | ## Index 43 | 44 | - [func EmbeddedFunc\(param int\) int](<#EmbeddedFunc>) 45 | 46 | 47 | 48 | ## func EmbeddedFunc 49 | 50 | func EmbeddedFunc(param int) int 51 | 52 | EmbeddedFunc is present in embedded content. 53 | 54 | Generated by [gomarkdoc]() 55 | 56 | 57 | 58 | 59 | This is content after the second embed 60 | 61 | 62 | 63 | 64 | 65 | # embed 66 | 67 | import "github.com/princjef/gomarkdoc/testData/embed" 68 | 69 | Package embed tests out embedding of documentation in an existing readme. 70 | 71 | ## Index 72 | 73 | - [func EmbeddedFunc\(param int\) int](<#EmbeddedFunc>) 74 | 75 | 76 | 77 | ## func EmbeddedFunc 78 | 79 | func EmbeddedFunc(param int) int 80 | 81 | EmbeddedFunc is present in embedded content. 82 | 83 | Generated by [gomarkdoc]() 84 | 85 | 86 | 87 | 88 | This is content after the third embed -------------------------------------------------------------------------------- /testData/embed/README-template.md: -------------------------------------------------------------------------------- 1 | This is content before the embed 2 | 3 | 4 | 5 | This is content after the embed 6 | 7 | 13 | 14 | This is content after the second embed 15 | 16 | 17 | 18 | This content will be replaced with the embed 19 | 20 | 21 | 22 | This is content after the third embed -------------------------------------------------------------------------------- /testData/embed/embed.go: -------------------------------------------------------------------------------- 1 | // Package embed tests out embedding of documentation in an 2 | // existing readme. 3 | package embed 4 | 5 | // EmbeddedFunc is present in embedded content. 6 | func EmbeddedFunc(param int) int { 7 | return param 8 | } 9 | -------------------------------------------------------------------------------- /testData/generics/README-azure-devops.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # generics 4 | 5 | ```go 6 | import "github.com/princjef/gomarkdoc/testData/generics" 7 | ``` 8 | 9 | ## Index 10 | 11 | - [func Func\[S int | float64\]\(s S\) S](<#Func>) 12 | - [type Generic](<#Generic>) 13 | - [func NewGeneric\[T any\]\(param T\) Generic\[T\]](<#NewGeneric>) 14 | - [func \(g Generic\[T\]\) Method\(\)](<#Generic[T].Method>) 15 | 16 | 17 | 18 | ## func [Func]() 19 | 20 | ```go 21 | func Func[S int | float64](s S) S 22 | ``` 23 | 24 | Func is a generic function. 25 | 26 | 27 | ## type [Generic]() 28 | 29 | Generic is a generic struct. 30 | 31 | ```go 32 | type Generic[T any] struct { 33 | Field T 34 | } 35 | ``` 36 | 37 | 38 | ### func [NewGeneric]() 39 | 40 | ```go 41 | func NewGeneric[T any](param T) Generic[T] 42 | ``` 43 | 44 | NewGeneric produces a new [Generic](<#Generic>) struct. 45 | 46 | 47 | ### func \(Generic\[T\]\) [Method]() 48 | 49 | ```go 50 | func (g Generic[T]) Method() 51 | ``` 52 | 53 | Method is a method of a generic type. 54 | 55 | Generated by [gomarkdoc]() 56 | -------------------------------------------------------------------------------- /testData/generics/README-github.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # generics 4 | 5 | ```go 6 | import "github.com/princjef/gomarkdoc/testData/generics" 7 | ``` 8 | 9 | ## Index 10 | 11 | - [func Func\[S int | float64\]\(s S\) S](<#Func>) 12 | - [type Generic](<#Generic>) 13 | - [func NewGeneric\[T any\]\(param T\) Generic\[T\]](<#NewGeneric>) 14 | - [func \(g Generic\[T\]\) Method\(\)](<#Generic[T].Method>) 15 | 16 | 17 | 18 | ## func [Func]() 19 | 20 | ```go 21 | func Func[S int | float64](s S) S 22 | ``` 23 | 24 | Func is a generic function. 25 | 26 | 27 | ## type [Generic]() 28 | 29 | Generic is a generic struct. 30 | 31 | ```go 32 | type Generic[T any] struct { 33 | Field T 34 | } 35 | ``` 36 | 37 | 38 | ### func [NewGeneric]() 39 | 40 | ```go 41 | func NewGeneric[T any](param T) Generic[T] 42 | ``` 43 | 44 | NewGeneric produces a new [Generic](<#Generic>) struct. 45 | 46 | 47 | ### func \(Generic\[T\]\) [Method]() 48 | 49 | ```go 50 | func (g Generic[T]) Method() 51 | ``` 52 | 53 | Method is a method of a generic type. 54 | 55 | Generated by [gomarkdoc]() 56 | -------------------------------------------------------------------------------- /testData/generics/README-plain.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # generics 4 | 5 | import "github.com/princjef/gomarkdoc/testData/generics" 6 | 7 | ## Index 8 | 9 | - [func Func\[S int | float64\]\(s S\) S](<#Func>) 10 | - [type Generic](<#Generic>) 11 | - [func NewGeneric\[T any\]\(param T\) Generic\[T\]](<#NewGeneric>) 12 | - [func \(g Generic\[T\]\) Method\(\)](<#Generic[T].Method>) 13 | 14 | 15 | 16 | ## func Func 17 | 18 | func Func[S int | float64](s S) S 19 | 20 | Func is a generic function. 21 | 22 | 23 | ## type Generic 24 | 25 | Generic is a generic struct. 26 | 27 | type Generic[T any] struct { 28 | Field T 29 | } 30 | 31 | 32 | ### func NewGeneric 33 | 34 | func NewGeneric[T any](param T) Generic[T] 35 | 36 | NewGeneric produces a new [Generic](<#Generic>) struct. 37 | 38 | 39 | ### func \(Generic\[T\]\) Method 40 | 41 | func (g Generic[T]) Method() 42 | 43 | Method is a method of a generic type. 44 | 45 | Generated by [gomarkdoc]() 46 | -------------------------------------------------------------------------------- /testData/generics/generics.go: -------------------------------------------------------------------------------- 1 | package generics 2 | 3 | // Generic is a generic struct. 4 | type Generic[T any] struct { 5 | Field T 6 | } 7 | 8 | // NewGeneric produces a new [Generic] struct. 9 | func NewGeneric[T any](param T) Generic[T] { 10 | return Generic[T]{Field: param} 11 | } 12 | 13 | // Method is a method of a generic type. 14 | func (g Generic[T]) Method() {} 15 | 16 | // Func is a generic function. 17 | func Func[S int | float64](s S) S { 18 | return s 19 | } 20 | -------------------------------------------------------------------------------- /testData/lang/function/README-azure-devops.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # function 4 | 5 | ```go 6 | import "github.com/princjef/gomarkdoc/testData/lang/function" 7 | ``` 8 | 9 | ## Index 10 | 11 | - [Constants](<#constants>) 12 | - [Variables](<#variables>) 13 | - [func Standalone\(p1 int, p2 string\) \(int, error\)](<#Standalone>) 14 | - [type Generic](<#Generic>) 15 | - [func \(r Generic\[T\]\) WithGenericReceiver\(\)](<#Generic[T].WithGenericReceiver>) 16 | - [type Receiver](<#Receiver>) 17 | - [func New\(\) Receiver](<#New>) 18 | - [func \(r \*Receiver\) WithPtrReceiver\(\)](<#Receiver.WithPtrReceiver>) 19 | - [func \(r Receiver\) WithReceiver\(\)](<#Receiver.WithReceiver>) 20 | 21 | 22 | ## Constants 23 | 24 | Set of constants for this package. 25 | 26 | ```go 27 | const ( 28 | ConstA = "string" 29 | ConstB = true 30 | ) 31 | ``` 32 | 33 | ## Variables 34 | 35 | Variable is a package\-level variable. 36 | 37 | ```go 38 | var Variable = 5 39 | ``` 40 | 41 | 42 | ## func [Standalone]() 43 | 44 | ```go 45 | func Standalone(p1 int, p2 string) (int, error) 46 | ``` 47 | 48 | Standalone provides a function that is not part of a type. 49 | 50 | Additional description can be provided in subsequent paragraphs, including code blocks and headers 51 | 52 | ### Header A 53 | 54 | This section contains a code block. 55 | 56 | ``` 57 | Code Block 58 | More of Code Block 59 | ``` 60 | 61 |
Example 62 |

63 | 64 | 65 | 66 | ```go 67 | package main 68 | 69 | import ( 70 | "fmt" 71 | 72 | "github.com/princjef/gomarkdoc/testData/lang/function" 73 | ) 74 | 75 | func main() { 76 | // Comment 77 | res, _ := function.Standalone(2, "abc") 78 | fmt.Println(res) 79 | } 80 | ``` 81 | 82 | #### Output 83 | 84 | ``` 85 | 2 86 | ``` 87 | 88 |

89 |
90 | 91 |
Example (Zero) 92 |

93 | 94 | 95 | 96 | ```go 97 | package main 98 | 99 | import ( 100 | "fmt" 101 | 102 | "github.com/princjef/gomarkdoc/testData/lang/function" 103 | ) 104 | 105 | func main() { 106 | res, _ := function.Standalone(0, "def") 107 | fmt.Println(res) 108 | } 109 | ``` 110 | 111 | #### Output 112 | 113 | ``` 114 | 0 115 | ``` 116 | 117 |

118 |
119 | 120 | 121 | ## type [Generic]() 122 | 123 | Generic is a struct with a generic type. 124 | 125 | ```go 126 | type Generic[T any] struct{} 127 | ``` 128 | 129 | 130 | ### func \(Generic\[T\]\) [WithGenericReceiver]() 131 | 132 | ```go 133 | func (r Generic[T]) WithGenericReceiver() 134 | ``` 135 | 136 | WithGenericReceiver has a receiver with a generic type. 137 | 138 |
Example 139 |

140 | 141 | 142 | 143 | ```go 144 | package main 145 | 146 | import ( 147 | "github.com/princjef/gomarkdoc/testData/lang/function" 148 | ) 149 | 150 | func main() { 151 | r := function.Generic[int]{} 152 | r.WithGenericReceiver() 153 | } 154 | ``` 155 | 156 |

157 |
158 | 159 | 160 | ## type [Receiver]() 161 | 162 | Receiver is a type used to demonstrate functions with receivers. 163 | 164 | ```go 165 | type Receiver struct{} 166 | ``` 167 | 168 |
Example 169 |

170 | 171 | 172 | 173 | ```go 174 | package main 175 | 176 | import ( 177 | "fmt" 178 | 179 | "github.com/princjef/gomarkdoc/testData/lang/function" 180 | ) 181 | 182 | func main() { 183 | // Add some comments 184 | r := &function.Receiver{} 185 | // And some more 186 | fmt.Println(r) 187 | } 188 | ``` 189 | 190 |

191 |
192 | 193 |
Example (Sub Test) 194 |

195 | 196 | 197 | 198 | ```go 199 | package main 200 | 201 | import ( 202 | "github.com/princjef/gomarkdoc/testData/lang/function" 203 | ) 204 | 205 | func main() { 206 | var r function.Receiver 207 | r.WithReceiver() 208 | } 209 | ``` 210 | 211 |

212 |
213 | 214 | 215 | ### func [New]() 216 | 217 | ```go 218 | func New() Receiver 219 | ``` 220 | 221 | New is an initializer for Receiver. 222 | 223 | 224 | ### func \(\*Receiver\) [WithPtrReceiver]() 225 | 226 | ```go 227 | func (r *Receiver) WithPtrReceiver() 228 | ``` 229 | 230 | WithPtrReceiver has a pointer receiver. 231 | 232 | 233 | ### func \(Receiver\) [WithReceiver]() 234 | 235 | ```go 236 | func (r Receiver) WithReceiver() 237 | ``` 238 | 239 | WithReceiver has a receiver. 240 | 241 | Generated by [gomarkdoc]() 242 | -------------------------------------------------------------------------------- /testData/lang/function/README-github.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # function 4 | 5 | ```go 6 | import "github.com/princjef/gomarkdoc/testData/lang/function" 7 | ``` 8 | 9 | ## Index 10 | 11 | - [Constants](<#constants>) 12 | - [Variables](<#variables>) 13 | - [func Standalone\(p1 int, p2 string\) \(int, error\)](<#Standalone>) 14 | - [type Generic](<#Generic>) 15 | - [func \(r Generic\[T\]\) WithGenericReceiver\(\)](<#Generic[T].WithGenericReceiver>) 16 | - [type Receiver](<#Receiver>) 17 | - [func New\(\) Receiver](<#New>) 18 | - [func \(r \*Receiver\) WithPtrReceiver\(\)](<#Receiver.WithPtrReceiver>) 19 | - [func \(r Receiver\) WithReceiver\(\)](<#Receiver.WithReceiver>) 20 | 21 | 22 | ## Constants 23 | 24 | Set of constants for this package. 25 | 26 | ```go 27 | const ( 28 | ConstA = "string" 29 | ConstB = true 30 | ) 31 | ``` 32 | 33 | ## Variables 34 | 35 | Variable is a package\-level variable. 36 | 37 | ```go 38 | var Variable = 5 39 | ``` 40 | 41 | 42 | ## func [Standalone]() 43 | 44 | ```go 45 | func Standalone(p1 int, p2 string) (int, error) 46 | ``` 47 | 48 | Standalone provides a function that is not part of a type. 49 | 50 | Additional description can be provided in subsequent paragraphs, including code blocks and headers 51 | 52 | ### Header A 53 | 54 | This section contains a code block. 55 | 56 | ``` 57 | Code Block 58 | More of Code Block 59 | ``` 60 | 61 |
Example 62 |

63 | 64 | 65 | 66 | ```go 67 | package main 68 | 69 | import ( 70 | "fmt" 71 | 72 | "github.com/princjef/gomarkdoc/testData/lang/function" 73 | ) 74 | 75 | func main() { 76 | // Comment 77 | res, _ := function.Standalone(2, "abc") 78 | fmt.Println(res) 79 | } 80 | ``` 81 | 82 | #### Output 83 | 84 | ``` 85 | 2 86 | ``` 87 | 88 |

89 |
90 | 91 |
Example (Zero) 92 |

93 | 94 | 95 | 96 | ```go 97 | package main 98 | 99 | import ( 100 | "fmt" 101 | 102 | "github.com/princjef/gomarkdoc/testData/lang/function" 103 | ) 104 | 105 | func main() { 106 | res, _ := function.Standalone(0, "def") 107 | fmt.Println(res) 108 | } 109 | ``` 110 | 111 | #### Output 112 | 113 | ``` 114 | 0 115 | ``` 116 | 117 |

118 |
119 | 120 | 121 | ## type [Generic]() 122 | 123 | Generic is a struct with a generic type. 124 | 125 | ```go 126 | type Generic[T any] struct{} 127 | ``` 128 | 129 | 130 | ### func \(Generic\[T\]\) [WithGenericReceiver]() 131 | 132 | ```go 133 | func (r Generic[T]) WithGenericReceiver() 134 | ``` 135 | 136 | WithGenericReceiver has a receiver with a generic type. 137 | 138 |
Example 139 |

140 | 141 | 142 | 143 | ```go 144 | package main 145 | 146 | import ( 147 | "github.com/princjef/gomarkdoc/testData/lang/function" 148 | ) 149 | 150 | func main() { 151 | r := function.Generic[int]{} 152 | r.WithGenericReceiver() 153 | } 154 | ``` 155 | 156 |

157 |
158 | 159 | 160 | ## type [Receiver]() 161 | 162 | Receiver is a type used to demonstrate functions with receivers. 163 | 164 | ```go 165 | type Receiver struct{} 166 | ``` 167 | 168 |
Example 169 |

170 | 171 | 172 | 173 | ```go 174 | package main 175 | 176 | import ( 177 | "fmt" 178 | 179 | "github.com/princjef/gomarkdoc/testData/lang/function" 180 | ) 181 | 182 | func main() { 183 | // Add some comments 184 | r := &function.Receiver{} 185 | // And some more 186 | fmt.Println(r) 187 | } 188 | ``` 189 | 190 |

191 |
192 | 193 |
Example (Sub Test) 194 |

195 | 196 | 197 | 198 | ```go 199 | package main 200 | 201 | import ( 202 | "github.com/princjef/gomarkdoc/testData/lang/function" 203 | ) 204 | 205 | func main() { 206 | var r function.Receiver 207 | r.WithReceiver() 208 | } 209 | ``` 210 | 211 |

212 |
213 | 214 | 215 | ### func [New]() 216 | 217 | ```go 218 | func New() Receiver 219 | ``` 220 | 221 | New is an initializer for Receiver. 222 | 223 | 224 | ### func \(\*Receiver\) [WithPtrReceiver]() 225 | 226 | ```go 227 | func (r *Receiver) WithPtrReceiver() 228 | ``` 229 | 230 | WithPtrReceiver has a pointer receiver. 231 | 232 | 233 | ### func \(Receiver\) [WithReceiver]() 234 | 235 | ```go 236 | func (r Receiver) WithReceiver() 237 | ``` 238 | 239 | WithReceiver has a receiver. 240 | 241 | Generated by [gomarkdoc]() 242 | -------------------------------------------------------------------------------- /testData/lang/function/README-plain.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # function 4 | 5 | import "github.com/princjef/gomarkdoc/testData/lang/function" 6 | 7 | ## Index 8 | 9 | - Constants 10 | - Variables 11 | - [func Standalone\(p1 int, p2 string\) \(int, error\)](<#Standalone>) 12 | - [type Generic](<#Generic>) 13 | - [func \(r Generic\[T\]\) WithGenericReceiver\(\)](<#Generic[T].WithGenericReceiver>) 14 | - [type Receiver](<#Receiver>) 15 | - [func New\(\) Receiver](<#New>) 16 | - [func \(r \*Receiver\) WithPtrReceiver\(\)](<#Receiver.WithPtrReceiver>) 17 | - [func \(r Receiver\) WithReceiver\(\)](<#Receiver.WithReceiver>) 18 | 19 | 20 | ## Constants 21 | 22 | Set of constants for this package. 23 | 24 | const ( 25 | ConstA = "string" 26 | ConstB = true 27 | ) 28 | 29 | ## Variables 30 | 31 | Variable is a package\-level variable. 32 | 33 | var Variable = 5 34 | 35 | 36 | ## func Standalone 37 | 38 | func Standalone(p1 int, p2 string) (int, error) 39 | 40 | Standalone provides a function that is not part of a type. 41 | 42 | Additional description can be provided in subsequent paragraphs, including code blocks and headers 43 | 44 | ### Header A 45 | 46 | This section contains a code block. 47 | 48 | Code Block 49 | More of Code Block 50 | 51 | 52 | ###### Example 53 | 54 | 55 | 56 | package main 57 | 58 | import ( 59 | "fmt" 60 | 61 | "github.com/princjef/gomarkdoc/testData/lang/function" 62 | ) 63 | 64 | func main() { 65 | // Comment 66 | res, _ := function.Standalone(2, "abc") 67 | fmt.Println(res) 68 | } 69 | 70 | 71 | #### Output 72 | 73 | 2 74 | 75 | 76 | 77 | 78 | 79 | 80 | ###### Example (Zero) 81 | 82 | 83 | 84 | package main 85 | 86 | import ( 87 | "fmt" 88 | 89 | "github.com/princjef/gomarkdoc/testData/lang/function" 90 | ) 91 | 92 | func main() { 93 | res, _ := function.Standalone(0, "def") 94 | fmt.Println(res) 95 | } 96 | 97 | 98 | #### Output 99 | 100 | 0 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | ## type Generic 109 | 110 | Generic is a struct with a generic type. 111 | 112 | type Generic[T any] struct{} 113 | 114 | 115 | ### func \(Generic\[T\]\) WithGenericReceiver 116 | 117 | func (r Generic[T]) WithGenericReceiver() 118 | 119 | WithGenericReceiver has a receiver with a generic type. 120 | 121 | ###### Example 122 | 123 | 124 | 125 | package main 126 | 127 | import ( 128 | "github.com/princjef/gomarkdoc/testData/lang/function" 129 | ) 130 | 131 | func main() { 132 | r := function.Generic[int]{} 133 | r.WithGenericReceiver() 134 | } 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | ## type Receiver 143 | 144 | Receiver is a type used to demonstrate functions with receivers. 145 | 146 | type Receiver struct{} 147 | 148 | ###### Example 149 | 150 | 151 | 152 | package main 153 | 154 | import ( 155 | "fmt" 156 | 157 | "github.com/princjef/gomarkdoc/testData/lang/function" 158 | ) 159 | 160 | func main() { 161 | // Add some comments 162 | r := &function.Receiver{} 163 | // And some more 164 | fmt.Println(r) 165 | } 166 | 167 | 168 | 169 | 170 | 171 | 172 | ###### Example (Sub Test) 173 | 174 | 175 | 176 | package main 177 | 178 | import ( 179 | "github.com/princjef/gomarkdoc/testData/lang/function" 180 | ) 181 | 182 | func main() { 183 | var r function.Receiver 184 | r.WithReceiver() 185 | } 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | ### func New 194 | 195 | func New() Receiver 196 | 197 | New is an initializer for Receiver. 198 | 199 | 200 | ### func \(\*Receiver\) WithPtrReceiver 201 | 202 | func (r *Receiver) WithPtrReceiver() 203 | 204 | WithPtrReceiver has a pointer receiver. 205 | 206 | 207 | ### func \(Receiver\) WithReceiver 208 | 209 | func (r Receiver) WithReceiver() 210 | 211 | WithReceiver has a receiver. 212 | 213 | Generated by [gomarkdoc]() 214 | -------------------------------------------------------------------------------- /testData/lang/function/func.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | // Standalone provides a function that is not part of a type. 4 | // 5 | // Additional description can be provided in subsequent paragraphs, including 6 | // code blocks and headers 7 | // 8 | // Header A 9 | // 10 | // This section contains a code block. 11 | // 12 | // Code Block 13 | // More of Code Block 14 | func Standalone(p1 int, p2 string) (int, error) { 15 | return p1, nil 16 | } 17 | 18 | // Receiver is a type used to demonstrate functions with receivers. 19 | type Receiver struct{} 20 | 21 | // New is an initializer for Receiver. 22 | func New() Receiver { 23 | return Receiver{} 24 | } 25 | 26 | // WithReceiver has a receiver. 27 | func (r Receiver) WithReceiver() {} 28 | 29 | // WithPtrReceiver has a pointer receiver. 30 | func (r *Receiver) WithPtrReceiver() {} 31 | 32 | // Generic is a struct with a generic type. 33 | type Generic[T any] struct{} 34 | 35 | // WithGenericReceiver has a receiver with a generic type. 36 | func (r Generic[T]) WithGenericReceiver() {} 37 | -------------------------------------------------------------------------------- /testData/lang/function/func_test.go: -------------------------------------------------------------------------------- 1 | package function_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/princjef/gomarkdoc/testData/lang/function" 7 | ) 8 | 9 | func ExampleStandalone() { 10 | // Comment 11 | res, _ := function.Standalone(2, "abc") 12 | fmt.Println(res) 13 | // Output: 2 14 | } 15 | 16 | func ExampleStandalone_zero() { 17 | res, _ := function.Standalone(0, "def") 18 | fmt.Println(res) 19 | // Output: 0 20 | } 21 | 22 | func ExampleReceiver() { 23 | // Add some comments 24 | r := &function.Receiver{} 25 | // And some more 26 | fmt.Println(r) 27 | } 28 | 29 | func ExampleReceiver_subTest() { 30 | var r function.Receiver 31 | r.WithReceiver() 32 | } 33 | 34 | func ExampleGeneric_WithGenericReceiver() { 35 | r := function.Generic[int]{} 36 | r.WithGenericReceiver() 37 | } 38 | -------------------------------------------------------------------------------- /testData/lang/function/value.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | // Variable is a package-level variable. 4 | var Variable = 5 5 | 6 | // Set of constants for this package. 7 | const ( 8 | ConstA = "string" 9 | ConstB = true 10 | ) 11 | -------------------------------------------------------------------------------- /testData/nested/.gomarkdoc.yml: -------------------------------------------------------------------------------- 1 | output: "{{.Dir}}/README.md" 2 | -------------------------------------------------------------------------------- /testData/nested/README-azure-devops.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # nested 4 | 5 | ```go 6 | import "github.com/princjef/gomarkdoc/testData/nested" 7 | ``` 8 | 9 | ## Index 10 | 11 | - [func Parent\(\) int](<#Parent>) 12 | 13 | 14 | 15 | ## func [Parent]() 16 | 17 | ```go 18 | func Parent() int 19 | ``` 20 | 21 | Parent is in the parent package. 22 | 23 | Generated by [gomarkdoc]() 24 | -------------------------------------------------------------------------------- /testData/nested/README-github.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # nested 4 | 5 | ```go 6 | import "github.com/princjef/gomarkdoc/testData/nested" 7 | ``` 8 | 9 | ## Index 10 | 11 | - [func Parent\(\) int](<#Parent>) 12 | 13 | 14 | 15 | ## func [Parent]() 16 | 17 | ```go 18 | func Parent() int 19 | ``` 20 | 21 | Parent is in the parent package. 22 | 23 | Generated by [gomarkdoc]() 24 | -------------------------------------------------------------------------------- /testData/nested/README-plain.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # nested 4 | 5 | import "github.com/princjef/gomarkdoc/testData/nested" 6 | 7 | ## Index 8 | 9 | - [func Parent\(\) int](<#Parent>) 10 | 11 | 12 | 13 | ## func Parent 14 | 15 | func Parent() int 16 | 17 | Parent is in the parent package. 18 | 19 | Generated by [gomarkdoc]() 20 | -------------------------------------------------------------------------------- /testData/nested/inner/README-azure-devops.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # inner 4 | 5 | ```go 6 | import "github.com/princjef/gomarkdoc/testData/nested/inner" 7 | ``` 8 | 9 | ## Index 10 | 11 | - [func Child\(\) int](<#Child>) 12 | 13 | 14 | 15 | ## func [Child]() 16 | 17 | ```go 18 | func Child() int 19 | ``` 20 | 21 | Child is in the child package. 22 | 23 | Generated by [gomarkdoc]() 24 | -------------------------------------------------------------------------------- /testData/nested/inner/README-github.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # inner 4 | 5 | ```go 6 | import "github.com/princjef/gomarkdoc/testData/nested/inner" 7 | ``` 8 | 9 | ## Index 10 | 11 | - [func Child\(\) int](<#Child>) 12 | 13 | 14 | 15 | ## func [Child]() 16 | 17 | ```go 18 | func Child() int 19 | ``` 20 | 21 | Child is in the child package. 22 | 23 | Generated by [gomarkdoc]() 24 | -------------------------------------------------------------------------------- /testData/nested/inner/README-plain.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # inner 4 | 5 | import "github.com/princjef/gomarkdoc/testData/nested/inner" 6 | 7 | ## Index 8 | 9 | - [func Child\(\) int](<#Child>) 10 | 11 | 12 | 13 | ## func Child 14 | 15 | func Child() int 16 | 17 | Child is in the child package. 18 | 19 | Generated by [gomarkdoc]() 20 | -------------------------------------------------------------------------------- /testData/nested/inner/child.go: -------------------------------------------------------------------------------- 1 | package inner 2 | 3 | // Child is in the child package. 4 | func Child() int { 5 | return 234 6 | } 7 | -------------------------------------------------------------------------------- /testData/nested/parent.go: -------------------------------------------------------------------------------- 1 | package nested 2 | 3 | // Parent is in the parent package. 4 | func Parent() int { 5 | return 123 6 | } 7 | -------------------------------------------------------------------------------- /testData/simple/.gomarkdoc.yml: -------------------------------------------------------------------------------- 1 | output: "{{.Dir}}/README.md" 2 | -------------------------------------------------------------------------------- /testData/simple/README-azure-devops.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # simple 4 | 5 | ```go 6 | import "github.com/princjef/gomarkdoc/testData/simple" 7 | ``` 8 | 9 | Package simple contains, some simple code to exercise basic scenarios for documentation purposes. 10 | 11 | ## Index 12 | 13 | - [type Num](<#Num>) 14 | - [func AddNums\(num1, num2 Num\) Num](<#AddNums>) 15 | - [func \(n Num\) Add\(num Num\) Num](<#Num.Add>) 16 | 17 | 18 | 19 | ## type [Num]() 20 | 21 | Num is a number. 22 | 23 | It is just a test type so that we can make sure this works. 24 | 25 | ```go 26 | type Num int 27 | ``` 28 | 29 | 30 | ### func [AddNums]() 31 | 32 | ```go 33 | func AddNums(num1, num2 Num) Num 34 | ``` 35 | 36 | AddNums adds two Nums together. 37 | 38 | 39 | ### func \(Num\) [Add]() 40 | 41 | ```go 42 | func (n Num) Add(num Num) Num 43 | ``` 44 | 45 | Add adds the other num to this one. 46 | 47 | Generated by [gomarkdoc]() 48 | -------------------------------------------------------------------------------- /testData/simple/README-github.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # simple 4 | 5 | ```go 6 | import "github.com/princjef/gomarkdoc/testData/simple" 7 | ``` 8 | 9 | Package simple contains, some simple code to exercise basic scenarios for documentation purposes. 10 | 11 | ## Index 12 | 13 | - [type Num](<#Num>) 14 | - [func AddNums\(num1, num2 Num\) Num](<#AddNums>) 15 | - [func \(n Num\) Add\(num Num\) Num](<#Num.Add>) 16 | 17 | 18 | 19 | ## type [Num]() 20 | 21 | Num is a number. 22 | 23 | It is just a test type so that we can make sure this works. 24 | 25 | ```go 26 | type Num int 27 | ``` 28 | 29 | 30 | ### func [AddNums]() 31 | 32 | ```go 33 | func AddNums(num1, num2 Num) Num 34 | ``` 35 | 36 | AddNums adds two Nums together. 37 | 38 | 39 | ### func \(Num\) [Add]() 40 | 41 | ```go 42 | func (n Num) Add(num Num) Num 43 | ``` 44 | 45 | Add adds the other num to this one. 46 | 47 | Generated by [gomarkdoc]() 48 | -------------------------------------------------------------------------------- /testData/simple/README-plain.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # simple 4 | 5 | import "github.com/princjef/gomarkdoc/testData/simple" 6 | 7 | Package simple contains, some simple code to exercise basic scenarios for documentation purposes. 8 | 9 | ## Index 10 | 11 | - [type Num](<#Num>) 12 | - [func AddNums\(num1, num2 Num\) Num](<#AddNums>) 13 | - [func \(n Num\) Add\(num Num\) Num](<#Num.Add>) 14 | 15 | 16 | 17 | ## type Num 18 | 19 | Num is a number. 20 | 21 | It is just a test type so that we can make sure this works. 22 | 23 | type Num int 24 | 25 | 26 | ### func AddNums 27 | 28 | func AddNums(num1, num2 Num) Num 29 | 30 | AddNums adds two Nums together. 31 | 32 | 33 | ### func \(Num\) Add 34 | 35 | func (n Num) Add(num Num) Num 36 | 37 | Add adds the other num to this one. 38 | 39 | Generated by [gomarkdoc]() 40 | -------------------------------------------------------------------------------- /testData/simple/main.go: -------------------------------------------------------------------------------- 1 | // Package simple contains, some simple code to exercise basic scenarios 2 | // for documentation purposes. 3 | package simple 4 | 5 | // Num is a number. 6 | // 7 | // It is just a test type so that we can make sure this works. 8 | type Num int 9 | 10 | // Add adds the other num to this one. 11 | func (n Num) Add(num Num) Num { 12 | return addInternal(n, num) 13 | } 14 | 15 | // AddNums adds two Nums together. 16 | func AddNums(num1, num2 Num) Num { 17 | return addInternal(num1, num2) 18 | } 19 | 20 | // addInternal is a private version of AddNums. 21 | func addInternal(num1, num2 Num) Num { 22 | return num1 + num2 23 | } 24 | -------------------------------------------------------------------------------- /testData/tags/.gomarkdoc.yml: -------------------------------------------------------------------------------- 1 | output: "{{.Dir}}/README.md" 2 | tags: 3 | - tagged 4 | -------------------------------------------------------------------------------- /testData/tags/README-azure-devops.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # tags 4 | 5 | ```go 6 | import "github.com/princjef/gomarkdoc/testData/tags" 7 | ``` 8 | 9 | Package tags contains code to demonstrate usage of build tags. 10 | 11 | ## Index 12 | 13 | - [func Tagged\(\) int](<#Tagged>) 14 | - [func Untagged\(\) int](<#Untagged>) 15 | 16 | 17 | 18 | ## func [Tagged]() 19 | 20 | ```go 21 | func Tagged() int 22 | ``` 23 | 24 | Tagged is only visible with tags. 25 | 26 | 27 | ## func [Untagged]() 28 | 29 | ```go 30 | func Untagged() int 31 | ``` 32 | 33 | Untagged is visible without tags. 34 | 35 | Generated by [gomarkdoc]() 36 | -------------------------------------------------------------------------------- /testData/tags/README-github.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # tags 4 | 5 | ```go 6 | import "github.com/princjef/gomarkdoc/testData/tags" 7 | ``` 8 | 9 | Package tags contains code to demonstrate usage of build tags. 10 | 11 | ## Index 12 | 13 | - [func Tagged\(\) int](<#Tagged>) 14 | - [func Untagged\(\) int](<#Untagged>) 15 | 16 | 17 | 18 | ## func [Tagged]() 19 | 20 | ```go 21 | func Tagged() int 22 | ``` 23 | 24 | Tagged is only visible with tags. 25 | 26 | 27 | ## func [Untagged]() 28 | 29 | ```go 30 | func Untagged() int 31 | ``` 32 | 33 | Untagged is visible without tags. 34 | 35 | Generated by [gomarkdoc]() 36 | -------------------------------------------------------------------------------- /testData/tags/README-plain.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # tags 4 | 5 | import "github.com/princjef/gomarkdoc/testData/tags" 6 | 7 | Package tags contains code to demonstrate usage of build tags. 8 | 9 | ## Index 10 | 11 | - [func Tagged\(\) int](<#Tagged>) 12 | - [func Untagged\(\) int](<#Untagged>) 13 | 14 | 15 | 16 | ## func Tagged 17 | 18 | func Tagged() int 19 | 20 | Tagged is only visible with tags. 21 | 22 | 23 | ## func Untagged 24 | 25 | func Untagged() int 26 | 27 | Untagged is visible without tags. 28 | 29 | Generated by [gomarkdoc]() 30 | -------------------------------------------------------------------------------- /testData/tags/tagged.go: -------------------------------------------------------------------------------- 1 | //go:build tagged 2 | // +build tagged 3 | 4 | package tags 5 | 6 | // Tagged is only visible with tags. 7 | func Tagged() int { 8 | return 7 9 | } 10 | -------------------------------------------------------------------------------- /testData/tags/untagged.go: -------------------------------------------------------------------------------- 1 | // Package tags contains code to demonstrate usage of build tags. 2 | package tags 3 | 4 | // Untagged is visible without tags. 5 | func Untagged() int { 6 | return 6 7 | } 8 | -------------------------------------------------------------------------------- /testData/unexported/.gomarkdoc.yml: -------------------------------------------------------------------------------- 1 | output: "{{.Dir}}/README.md" 2 | includeUnexported: true -------------------------------------------------------------------------------- /testData/unexported/README-azure-devops.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # unexported 4 | 5 | ```go 6 | import "github.com/princjef/gomarkdoc/testData/unexported" 7 | ``` 8 | 9 | Package unexported contains some simple code to exercise basic scenarios for documentation purposes. 10 | 11 | ## Index 12 | 13 | - [type Num](<#Num>) 14 | - [func AddNums\(num1, num2 Num\) Num](<#AddNums>) 15 | - [func addInternal\(num1, num2 Num\) Num](<#addInternal>) 16 | - [func \(n Num\) Add\(num Num\) Num](<#Num.Add>) 17 | 18 | 19 | 20 | ## type [Num]() 21 | 22 | Num is a number. 23 | 24 | It is just a test type so that we can make sure this works. 25 | 26 | ```go 27 | type Num int 28 | ``` 29 | 30 | 31 | ### func [AddNums]() 32 | 33 | ```go 34 | func AddNums(num1, num2 Num) Num 35 | ``` 36 | 37 | AddNums adds two Nums together. 38 | 39 | 40 | ### func [addInternal]() 41 | 42 | ```go 43 | func addInternal(num1, num2 Num) Num 44 | ``` 45 | 46 | addInternal is a private version of AddNums. 47 | 48 | 49 | ### func \(Num\) [Add]() 50 | 51 | ```go 52 | func (n Num) Add(num Num) Num 53 | ``` 54 | 55 | Add adds the other num to this one. 56 | 57 | Generated by [gomarkdoc]() 58 | -------------------------------------------------------------------------------- /testData/unexported/README-github.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # unexported 4 | 5 | ```go 6 | import "github.com/princjef/gomarkdoc/testData/unexported" 7 | ``` 8 | 9 | Package unexported contains some simple code to exercise basic scenarios for documentation purposes. 10 | 11 | ## Index 12 | 13 | - [type Num](<#Num>) 14 | - [func AddNums\(num1, num2 Num\) Num](<#AddNums>) 15 | - [func addInternal\(num1, num2 Num\) Num](<#addInternal>) 16 | - [func \(n Num\) Add\(num Num\) Num](<#Num.Add>) 17 | 18 | 19 | 20 | ## type [Num]() 21 | 22 | Num is a number. 23 | 24 | It is just a test type so that we can make sure this works. 25 | 26 | ```go 27 | type Num int 28 | ``` 29 | 30 | 31 | ### func [AddNums]() 32 | 33 | ```go 34 | func AddNums(num1, num2 Num) Num 35 | ``` 36 | 37 | AddNums adds two Nums together. 38 | 39 | 40 | ### func [addInternal]() 41 | 42 | ```go 43 | func addInternal(num1, num2 Num) Num 44 | ``` 45 | 46 | addInternal is a private version of AddNums. 47 | 48 | 49 | ### func \(Num\) [Add]() 50 | 51 | ```go 52 | func (n Num) Add(num Num) Num 53 | ``` 54 | 55 | Add adds the other num to this one. 56 | 57 | Generated by [gomarkdoc]() 58 | -------------------------------------------------------------------------------- /testData/unexported/README-plain.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # unexported 4 | 5 | import "github.com/princjef/gomarkdoc/testData/unexported" 6 | 7 | Package unexported contains some simple code to exercise basic scenarios for documentation purposes. 8 | 9 | ## Index 10 | 11 | - [type Num](<#Num>) 12 | - [func AddNums\(num1, num2 Num\) Num](<#AddNums>) 13 | - [func addInternal\(num1, num2 Num\) Num](<#addInternal>) 14 | - [func \(n Num\) Add\(num Num\) Num](<#Num.Add>) 15 | 16 | 17 | 18 | ## type Num 19 | 20 | Num is a number. 21 | 22 | It is just a test type so that we can make sure this works. 23 | 24 | type Num int 25 | 26 | 27 | ### func AddNums 28 | 29 | func AddNums(num1, num2 Num) Num 30 | 31 | AddNums adds two Nums together. 32 | 33 | 34 | ### func addInternal 35 | 36 | func addInternal(num1, num2 Num) Num 37 | 38 | addInternal is a private version of AddNums. 39 | 40 | 41 | ### func \(Num\) Add 42 | 43 | func (n Num) Add(num Num) Num 44 | 45 | Add adds the other num to this one. 46 | 47 | Generated by [gomarkdoc]() 48 | -------------------------------------------------------------------------------- /testData/unexported/main.go: -------------------------------------------------------------------------------- 1 | // Package unexported contains some simple code to exercise basic scenarios 2 | // for documentation purposes. 3 | package unexported 4 | 5 | // Num is a number. 6 | // 7 | // It is just a test type so that we can make sure this works. 8 | type Num int 9 | 10 | // Add adds the other num to this one. 11 | func (n Num) Add(num Num) Num { 12 | return addInternal(n, num) 13 | } 14 | 15 | // AddNums adds two Nums together. 16 | func AddNums(num1, num2 Num) Num { 17 | return addInternal(num1, num2) 18 | } 19 | 20 | // addInternal is a private version of AddNums. 21 | func addInternal(num1, num2 Num) Num { 22 | return num1 + num2 23 | } 24 | -------------------------------------------------------------------------------- /testData/untagged/.gomarkdoc.yml: -------------------------------------------------------------------------------- 1 | output: "{{.Dir}}/README.md" 2 | -------------------------------------------------------------------------------- /testData/untagged/README-azure-devops.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # untagged 4 | 5 | ```go 6 | import "github.com/princjef/gomarkdoc/testData/untagged" 7 | ``` 8 | 9 | Package untagged contains code to demonstrate usage of build tags. 10 | 11 | ## Index 12 | 13 | - [func Untagged\(\) int](<#Untagged>) 14 | 15 | 16 | 17 | ## func [Untagged]() 18 | 19 | ```go 20 | func Untagged() int 21 | ``` 22 | 23 | Untagged is visible without tags. 24 | 25 | Generated by [gomarkdoc]() 26 | -------------------------------------------------------------------------------- /testData/untagged/README-github.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # untagged 4 | 5 | ```go 6 | import "github.com/princjef/gomarkdoc/testData/untagged" 7 | ``` 8 | 9 | Package untagged contains code to demonstrate usage of build tags. 10 | 11 | ## Index 12 | 13 | - [func Untagged\(\) int](<#Untagged>) 14 | 15 | 16 | 17 | ## func [Untagged]() 18 | 19 | ```go 20 | func Untagged() int 21 | ``` 22 | 23 | Untagged is visible without tags. 24 | 25 | Generated by [gomarkdoc]() 26 | -------------------------------------------------------------------------------- /testData/untagged/README-plain.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # untagged 4 | 5 | import "github.com/princjef/gomarkdoc/testData/untagged" 6 | 7 | Package untagged contains code to demonstrate usage of build tags. 8 | 9 | ## Index 10 | 11 | - [func Untagged\(\) int](<#Untagged>) 12 | 13 | 14 | 15 | ## func Untagged 16 | 17 | func Untagged() int 18 | 19 | Untagged is visible without tags. 20 | 21 | Generated by [gomarkdoc]() 22 | -------------------------------------------------------------------------------- /testData/untagged/tagged.go: -------------------------------------------------------------------------------- 1 | //go:build tagged 2 | // +build tagged 3 | 4 | package untagged 5 | 6 | // Tagged is only visible with tags. 7 | func Tagged() int { 8 | return 7 9 | } 10 | -------------------------------------------------------------------------------- /testData/untagged/untagged.go: -------------------------------------------------------------------------------- 1 | // Package untagged contains code to demonstrate usage of build tags. 2 | package untagged 3 | 4 | // Untagged is visible without tags. 5 | func Untagged() int { 6 | return 6 7 | } 8 | --------------------------------------------------------------------------------