├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature.md ├── PULL_REQUEST_TEMPLATE.md ├── SECURITY.md ├── dependabot.yml └── workflows │ ├── release.yml │ └── snapshot.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── .mdtoc-bom-config.yaml ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── OWNERS ├── OWNERS_ALIASES ├── README.md ├── SECURITY_CONTACTS ├── code-of-conduct.md ├── go.mod ├── go.sum ├── hack ├── boilerplate │ ├── boilerplate.Dockerfile.txt │ ├── boilerplate.Makefile.txt │ ├── boilerplate.bzl.txt │ ├── boilerplate.generatebzl.txt │ ├── boilerplate.generatego.txt │ ├── boilerplate.go.txt │ ├── boilerplate.py │ ├── boilerplate.py.txt │ ├── boilerplate.sh.txt │ └── boilerplate_test.py ├── test-go.sh ├── verify-boilerplate.sh ├── verify-go-mod.sh └── verify-golangci-lint.sh ├── mdtoc.go ├── mdtoc_test.go ├── pkg └── mdtoc │ └── mdtoc.go └── testdata ├── capital_toc.md ├── code.md ├── empty_toc.md ├── include_prefix.md ├── invalid_endbeforestart.md ├── invalid_noend.md ├── invalid_nostart.md └── weird_headings.md /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report a bug encountered while using zeitgeist 4 | labels: kind/bug, sig/release, area/release-eng 5 | 6 | --- 7 | 8 | 15 | 16 | #### What happened: 17 | 18 | #### What you expected to happen: 19 | 20 | #### How to reproduce it (as minimally and precisely as possible): 21 | 22 | #### Anything else we need to know?: 23 | 24 | #### Environment: 25 | 26 | - Cloud provider or hardware configuration: 27 | - OS (e.g: `cat /etc/os-release`): 28 | - Kernel (e.g. `uname -a`): 29 | - Others: 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest a feature for zeitgeist 4 | labels: kind/feature, sig/release, area/release-eng 5 | 6 | --- 7 | 8 | 9 | #### What would you like to be added: 10 | 11 | #### Why is this needed: 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 15 | 16 | #### What type of PR is this? 17 | 18 | 33 | 34 | #### What this PR does / why we need it: 35 | 36 | #### Which issue(s) this PR fixes: 37 | 38 | 48 | 49 | #### Special notes for your reviewer: 50 | 51 | #### Does this PR introduce a user-facing change? 52 | 53 | 63 | 64 | ```release-note 65 | 66 | ``` 67 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Information about supported Kubernetes versions can be found on the 6 | [Kubernetes version and version skew support policy] page on the Kubernetes 7 | website. 8 | 9 | ## Reporting a Vulnerability 10 | 11 | Instructions for reporting a vulnerability can be found on the 12 | [Kubernetes Security and Disclosure Information] page. 13 | 14 | [Kubernetes version and version skew support policy]: https://kubernetes.io/docs/setup/release/version-skew-policy/#supported-versions 15 | [Kubernetes Security and Disclosure Information]: https://kubernetes.io/docs/reference/issues-security/security/#report-a-vulnerability 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: gomod 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | open-pull-requests-limit: 10 9 | labels: 10 | - "area/dependency" 11 | - "release-note-none" 12 | - "ok-to-test" 13 | groups: 14 | all: 15 | update-types: 16 | - "minor" 17 | - "patch" 18 | 19 | - package-ecosystem: "github-actions" 20 | directory: "/" 21 | schedule: 22 | interval: "weekly" 23 | open-pull-requests-limit: 10 24 | labels: 25 | - "area/dependency" 26 | - "release-note-none" 27 | - "ok-to-test" 28 | groups: 29 | all: 30 | update-types: 31 | - "minor" 32 | - "patch" 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | 12 | permissions: 13 | id-token: write 14 | contents: write 15 | 16 | env: 17 | COSIGN_YES: "true" 18 | 19 | steps: 20 | - name: Check out code onto GOPATH 21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | with: 23 | fetch-depth: 1 24 | 25 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 26 | with: 27 | go-version-file: './go.mod' 28 | check-latest: true 29 | 30 | - name: Install cosign 31 | uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2 32 | 33 | - name: Install bom 34 | uses: kubernetes-sigs/release-actions/setup-bom@a30d93cf2aa029e1e4c8a6c79f766aebf429fddb # v0.3.1 35 | 36 | - name: Install GoReleaser 37 | uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 38 | with: 39 | install-only: true 40 | 41 | - name: Get TAG 42 | id: get_tag 43 | run: echo "TAG=${GITHUB_REF#refs/*/}" >> "$GITHUB_OUTPUT" 44 | 45 | - name: Run goreleaser 46 | run: make goreleaser 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | 50 | attestation: 51 | runs-on: ubuntu-latest 52 | 53 | permissions: 54 | id-token: write 55 | contents: write 56 | 57 | needs: 58 | - release 59 | 60 | steps: 61 | - name: Check out code onto GOPATH 62 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 63 | with: 64 | fetch-depth: 1 65 | 66 | - name: Set tag output 67 | id: tag 68 | run: echo "tag_name=${GITHUB_REF#refs/*/}" >> "$GITHUB_OUTPUT" 69 | 70 | - name: Install tejolote 71 | uses: kubernetes-sigs/release-actions/setup-tejolote@a30d93cf2aa029e1e4c8a6c79f766aebf429fddb # v0.3.1 72 | 73 | - run: | 74 | tejolote attest --artifacts github://kubernetes-sigs/mdtoc/${{ steps.tag.outputs.tag_name }} github://kubernetes-sigs/mdtoc/"${GITHUB_RUN_ID}" --output mdtoc.intoto.json --sign 75 | 76 | - name: Release 77 | uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2 78 | with: 79 | files: mdtoc.intoto.json 80 | tag_name: "${{ steps.tag.outputs.tag_name }}" 81 | token: ${{ secrets.GITHUB_TOKEN }} 82 | env: 83 | GITHUB_REPOSITORY: kubernetes-sigs/mdtoc 84 | -------------------------------------------------------------------------------- /.github/workflows/snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Snapshot 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | pull_request: 8 | 9 | jobs: 10 | snapshot: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Check out code onto GOPATH 15 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 16 | 17 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 18 | with: 19 | go-version-file: './go.mod' 20 | check-latest: true 21 | 22 | - name: Install bom 23 | uses: kubernetes-sigs/release-actions/setup-bom@a30d93cf2aa029e1e4c8a6c79f766aebf429fddb # v0.3.1 24 | 25 | - name: Install GoReleaser 26 | uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 27 | with: 28 | install-only: true 29 | 30 | - name: Run goreleaser snapshot 31 | run: make snapshot 32 | 33 | - name: check binary 34 | run: ./dist/mdtoc-amd64-linux -v 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ./mdtoc 2 | coverage* 3 | dist/ 4 | output/ 5 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | run: 3 | concurrency: 6 4 | timeout: 5m 5 | issues: 6 | linters: 7 | disable-all: true 8 | enable: 9 | - asasalint 10 | - asciicheck 11 | - bidichk 12 | - bodyclose 13 | - canonicalheader 14 | - containedctx 15 | - contextcheck 16 | - copyloopvar 17 | - cyclop 18 | - decorder 19 | - dogsled 20 | - dupl 21 | - dupword 22 | - durationcheck 23 | - errcheck 24 | - errchkjson 25 | - errname 26 | - errorlint 27 | - exhaustive 28 | - exptostd 29 | - fatcontext 30 | - forcetypeassert 31 | - funlen 32 | - gci 33 | - ginkgolinter 34 | - gocheckcompilerdirectives 35 | - gochecksumtype 36 | - gocognit 37 | - goconst 38 | - gocritic 39 | - gocyclo 40 | - godot 41 | - godox 42 | - gofmt 43 | - gofumpt 44 | - goheader 45 | - goimports 46 | - gomoddirectives 47 | - gomodguard 48 | - goprintffuncname 49 | - gosec 50 | - gosimple 51 | - gosmopolitan 52 | - govet 53 | - grouper 54 | - iface 55 | - importas 56 | - inamedparam 57 | - ineffassign 58 | - interfacebloat 59 | - intrange 60 | - loggercheck 61 | - maintidx 62 | - makezero 63 | - mirror 64 | - misspell 65 | - mnd 66 | - musttag 67 | - nakedret 68 | - nestif 69 | - nilerr 70 | - nilnesserr 71 | - nilnil 72 | - nlreturn 73 | - noctx 74 | - nolintlint 75 | - nosprintfhostport 76 | - paralleltest 77 | - perfsprint 78 | - prealloc 79 | - predeclared 80 | - promlinter 81 | - protogetter 82 | - reassign 83 | - recvcheck 84 | - revive 85 | - rowserrcheck 86 | - sloglint 87 | - spancheck 88 | - sqlclosecheck 89 | - staticcheck 90 | - stylecheck 91 | - tagalign 92 | - tagliatelle 93 | - testableexamples 94 | - testifylint 95 | - thelper 96 | - tparallel 97 | - typecheck 98 | - unconvert 99 | - unparam 100 | - unused 101 | - usestdlibvars 102 | - usetesting 103 | - wastedassign 104 | - whitespace 105 | - wrapcheck 106 | - wsl 107 | - zerologlint 108 | # - depguard 109 | # - err113 110 | # - exhaustruct 111 | # - forbidigo 112 | # - gochecknoglobals 113 | # - gochecknoinits 114 | # - ireturn 115 | # - lll 116 | # - nlreturn 117 | # - nonamedreturns 118 | # - testpackage 119 | # - varnamelen 120 | linters-settings: 121 | gci: 122 | sections: 123 | - standard 124 | - default 125 | - localmodule 126 | cyclop: 127 | max-complexity: 15 128 | godox: 129 | keywords: 130 | - BUG 131 | - FIXME 132 | - HACK 133 | gocritic: 134 | enable-all: true 135 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: mdtoc 2 | 3 | env: 4 | - CGO_ENABLED=0 5 | - COSIGN_YES=true 6 | 7 | before: 8 | hooks: 9 | - go mod tidy 10 | # - /bin/bash -c 'if [ -n "$(git --no-pager diff --exit-code go.mod go.sum)" ]; then exit 1; fi' 11 | 12 | gomod: 13 | proxy: true 14 | 15 | builds: 16 | - id: mdtoc 17 | dir: . 18 | no_unique_dist_dir: true 19 | binary: mdtoc-{{ .Arch }}-{{ .Os }} 20 | goos: 21 | - darwin 22 | - linux 23 | - windows 24 | goarch: 25 | - amd64 26 | - arm64 27 | - arm 28 | goarm: 29 | - '7' 30 | ignore: 31 | - goos: windows 32 | goarch: arm 33 | flags: 34 | - -trimpath 35 | ldflags: 36 | - "{{ .Env.LDFLAGS }}" 37 | 38 | archives: 39 | - format: binary 40 | name_template: "{{ .Binary }}" 41 | allow_different_binary_count: true 42 | 43 | signs: 44 | # Keyless 45 | - id: mdtoc-keyless 46 | signature: "${artifact}.sig" 47 | certificate: "${artifact}.pem" 48 | cmd: cosign 49 | args: ["sign-blob", "--output-signature", "${artifact}.sig", "--output-certificate", "${artifact}.pem", "${artifact}"] 50 | artifacts: all 51 | 52 | sboms: 53 | - id: mdtoc 54 | cmd: bom 55 | args: 56 | - generate 57 | - "--output" 58 | - "mdtoc-bom.json.spdx" 59 | - "-d" 60 | - "../" 61 | - "-c" 62 | - "../.mdtoc-bom-config.yaml" 63 | - "--format" 64 | - "json" 65 | artifacts: any 66 | documents: 67 | - "mdtoc-bom.json.spdx" 68 | 69 | checksum: 70 | name_template: 'checksums.txt' 71 | 72 | snapshot: 73 | version_template: "{{ .Tag }}-next" 74 | 75 | release: 76 | github: 77 | owner: kubernetes-sigs 78 | name: mdtoc 79 | prerelease: auto 80 | 81 | changelog: 82 | disable: true 83 | -------------------------------------------------------------------------------- /.mdtoc-bom-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | namespace: https://sigs.k8s.io/zeitgeist 3 | license: Apache-2.0 4 | name: mdtoc 5 | creator: 6 | person: The Kubernetes Authors 7 | tool: mdtoc 8 | 9 | artifacts: 10 | - type: file 11 | source: mdtoc-amd64-windows.exe 12 | license: Apache-2.0 13 | gomodules: true 14 | 15 | - type: file 16 | source: mdtoc-arm64-windows.exe 17 | license: Apache-2.0 18 | gomodules: true 19 | 20 | - type: file 21 | source: mdtoc-amd64-darwin 22 | license: Apache-2.0 23 | gomodules: true 24 | 25 | - type: file 26 | source: mdtoc-amd64-linux 27 | license: Apache-2.0 28 | gomodules: true 29 | 30 | - type: file 31 | source: mdtoc-arm-linux 32 | license: Apache-2.0 33 | gomodules: true 34 | 35 | - type: file 36 | source: mdtoc-arm64-darwin 37 | license: Apache-2.0 38 | gomodules: true 39 | 40 | - type: file 41 | source: mdtoc-arm64-linux 42 | license: Apache-2.0 43 | gomodules: true 44 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Welcome to Kubernetes. We are excited about the prospect of you joining our 4 | [community](https://git.k8s.io/community)! The Kubernetes community abides by 5 | the CNCF [code of conduct](code-of-conduct.md). Here is an excerpt: 6 | 7 | _As contributors and maintainers of this project, and in the interest of 8 | fostering an open and welcoming community, we pledge to respect all people who 9 | contribute through reporting issues, posting feature requests, updating 10 | documentation, submitting pull requests or patches, and other activities._ 11 | 12 | ## Getting Started 13 | 14 | We have full documentation on how to get started contributing here: 15 | 16 | 20 | 21 | - [Contributor License Agreement](https://git.k8s.io/community/CLA.md) 22 | Kubernetes projects require that you sign a Contributor License Agreement 23 | (CLA) before we can accept your pull requests 24 | - [Kubernetes Contributor 25 | Guide](https://git.k8s.io/community/contributors/guide) - Main contributor 26 | documentation, or you can just jump directly to the [contributing 27 | section](https://git.k8s.io/community/contributors/guide#contributing) 28 | - [Contributor Cheat 29 | Sheet](https://git.k8s.io/community/contributors/guide/contributor-cheatsheet) 30 | - Common resources for existing developers 31 | 32 | ## Mentorship 33 | 34 | - [Mentoring Initiatives](https://git.k8s.io/community/mentoring) - We have a 35 | diverse set of mentorship programs available that are always looking for 36 | volunteers! 37 | 38 | 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # If you update this file, please follow 16 | # https://suva.sh/posts/well-documented-makefiles 17 | 18 | .DEFAULT_GOAL:=help 19 | SHELL:=/usr/bin/env bash 20 | 21 | COLOR:=\\033[36m 22 | NOCOLOR:=\\033[0m 23 | 24 | # Set version variables for LDFLAGS 25 | GIT_VERSION ?= $(shell git describe --tags --always --dirty) 26 | GIT_HASH ?= $(shell git rev-parse HEAD) 27 | DATE_FMT = +%Y-%m-%dT%H:%M:%SZ 28 | SOURCE_DATE_EPOCH ?= $(shell git log -1 --pretty=%ct) 29 | ifdef SOURCE_DATE_EPOCH 30 | BUILD_DATE ?= $(shell date -u -d "@$(SOURCE_DATE_EPOCH)" "$(DATE_FMT)" 2>/dev/null || date -u -r "$(SOURCE_DATE_EPOCH)" "$(DATE_FMT)" 2>/dev/null || date -u "$(DATE_FMT)") 31 | else 32 | BUILD_DATE ?= $(shell date "$(DATE_FMT)") 33 | endif 34 | GIT_TREESTATE = "clean" 35 | DIFF = $(shell git diff --quiet >/dev/null 2>&1; if [ $$? -eq 1 ]; then echo "1"; fi) 36 | ifeq ($(DIFF), 1) 37 | GIT_TREESTATE = "dirty" 38 | endif 39 | 40 | LDFLAGS=-buildid= -X sigs.k8s.io/release-utils/version.gitVersion=$(GIT_VERSION) \ 41 | -X sigs.k8s.io/release-utils/version.gitCommit=$(GIT_HASH) \ 42 | -X sigs.k8s.io/release-utils/version.gitTreeState=$(GIT_TREESTATE) \ 43 | -X sigs.k8s.io/release-utils/version.buildDate=$(BUILD_DATE) 44 | 45 | 46 | ##@ Build 47 | 48 | build: ## Build mdtoc 49 | # build local version 50 | go build -trimpath -ldflags "$(LDFLAGS)" -o ./output/mdtoc . 51 | 52 | ##@ Verify 53 | 54 | .PHONY: verify verify-boilerplate verify-dependencies verify-go-mod verify-golangci-lint 55 | 56 | verify: verify-boilerplate verify-dependencies verify-go-mod verify-golangci-lint ## Runs verification scripts to ensure correct execution 57 | 58 | verify-boilerplate: ## Runs the file header check 59 | ./hack/verify-boilerplate.sh 60 | 61 | verify-go-mod: ## Runs the go module linter 62 | ./hack/verify-go-mod.sh 63 | 64 | verify-golangci-lint: ## Runs all golang linters 65 | ./hack/verify-golangci-lint.sh 66 | 67 | ##@ Tests 68 | 69 | .PHONY: test 70 | test: ## Runs unit tests to ensure correct executionx 71 | ./hack/test-go.sh 72 | 73 | ##@ Dependencies 74 | 75 | .SILENT: update-deps update-deps-go 76 | .PHONY: update-deps update-deps-go 77 | 78 | update-deps: update-deps-go ## Update all dependencies for this repo 79 | echo -e "${COLOR}Commit/PR the following changes:${NOCOLOR}" 80 | git status --short 81 | 82 | update-deps-go: GO111MODULE=on 83 | update-deps-go: ## Update all golang dependencies for this repo 84 | go get -u -t ./... 85 | go mod tidy 86 | go mod verify 87 | $(MAKE) test 88 | 89 | ## Release 90 | 91 | .PHONY: goreleaser 92 | goreleaser: ## Build zeitgeist binaries with goreleaser 93 | LDFLAGS="$(LDFLAGS)" GIT_HASH=$(GIT_HASH) GIT_VERSION=$(GIT_VERSION) \ 94 | goreleaser release --clean 95 | 96 | .PHONY: snapshot 97 | snapshot: ## Build zeitgeist binaries with goreleaser in snapshot mode 98 | LDFLAGS="$(LDFLAGS)" GIT_HASH=$(GIT_HASH) GIT_VERSION=$(GIT_VERSION) \ 99 | goreleaser release --clean --snapshot --skip=sign,publish 100 | 101 | ##@ Helpers 102 | 103 | .PHONY: help 104 | 105 | help: ## Display this help 106 | @awk \ 107 | -v "col=${COLOR}" -v "nocol=${NOCOLOR}" \ 108 | ' \ 109 | BEGIN { \ 110 | FS = ":.*##" ; \ 111 | printf "\nUsage:\n make %s%s\n", col, nocol \ 112 | } \ 113 | /^[a-zA-Z_-]+:.*?##/ { \ 114 | printf " %s%-15s%s %s\n", col, $$1, nocol, $$2 \ 115 | } \ 116 | /^##@/ { \ 117 | printf "\n%s%s%s\n", col, substr($$0, 5), nocol \ 118 | } \ 119 | ' $(MAKEFILE_LIST) 120 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | # See the OWNERS docs at https://go.k8s.io/owners 2 | 3 | approvers: 4 | - tallclair 5 | - kubernetes/enhancements-admins 6 | - sig-release-leads 7 | -------------------------------------------------------------------------------- /OWNERS_ALIASES: -------------------------------------------------------------------------------- 1 | # See the OWNERS docs at https://go.k8s.io/owners#owners_aliases 2 | 3 | aliases: 4 | kubernetes/enhancements-admins: 5 | - justaugustus 6 | - johnbelamaric 7 | - jeremyrickard 8 | sig-release-leads: 9 | - cpanato # SIG Technical Lead 10 | - jeremyrickard # SIG Chair 11 | - justaugustus # SIG Chair 12 | - puerco # SIG Technical Lead 13 | - saschagrunert # SIG Chair 14 | - Verolop # SIG Technical Lead 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Markdown Table of Contents Generator 2 | 3 | `mdtoc` is a utility for generating a table-of-contents for markdown files. 4 | 5 | Only github-flavored markdown is currently supported, but I am open to accepting patches to add 6 | other formats. 7 | 8 | # Table of Contents 9 | 10 | Generated with `mdtoc --inplace README.md` 11 | 12 | 13 | - [Usage](#usage) 14 | - [Installation](#installation) 15 | - [Community, discussion, contribution, and support](#community-discussion-contribution-and-support) 16 | - [Code of conduct](#code-of-conduct) 17 | 18 | 19 | ## Usage 20 | 21 | Usage: `mdtoc [OPTIONS] [FILE]...` 22 | Generate a table of contents for a markdown file (github flavor). 23 | 24 | TOC may be wrapped in a pair of tags to allow in-place updates: 25 | ``` 26 | 27 | generated TOC goes here 28 | 29 | ``` 30 | 31 | TOC indentation is normalized, so the shallowest header has indentation 0. 32 | 33 | **Options:** 34 | 35 | `--dryrun` - Whether to check for changes to TOC, rather than overwriting. 36 | Requires `--inplace` flag. Exit code 1 if there are changes. 37 | 38 | `--inplace` - Whether to edit the file in-place, or output to STDOUT. Requires 39 | toc tags to be present. 40 | 41 | `--skip-prefix` - Whether to ignore any headers before the opening toc 42 | tag. (default true) 43 | 44 | For example, with `--skip-prefix=false` the TOC for this file becomes: 45 | 46 | ``` 47 | - [Markdown Table of Contents Generator](#markdown-table-of-contents-generator) 48 | - [Table of Contents](#table-of-contents) 49 | - [Usage](#usage) 50 | - [Installation](#installation) 51 | ``` 52 | 53 | ## Installation 54 | 55 | On linux, simply download and run the [standalone release 56 | binary](https://github.com/kubernetes-sigs/mdtoc/releases) 57 | 58 | ```sh 59 | # Optional: Verify the file integrity - check the release notes for the expected value. 60 | $ sha256sum $BINARY 61 | $ chmod +x $BINARY 62 | ``` 63 | 64 | Or, if you have a go development environment set up: 65 | 66 | ``` 67 | go install sigs.k8s.io/mdtoc@latest 68 | ``` 69 | 70 | ## Community, discussion, contribution, and support 71 | 72 | Learn how to engage with the Kubernetes community on the [community page](http://kubernetes.io/community/). 73 | 74 | You can reach the maintainers of this project at: 75 | 76 | - [Slack](http://slack.k8s.io/) 77 | - [Mailing List](https://groups.google.com/forum/#!forum/kubernetes-dev) 78 | 79 | ### Code of conduct 80 | 81 | Participation in the Kubernetes community is governed by the [Kubernetes Code of Conduct](code-of-conduct.md). 82 | 83 | [owners]: https://git.k8s.io/community/contributors/guide/owners.md 84 | [Creative Commons 4.0]: https://git.k8s.io/website/LICENSE 85 | -------------------------------------------------------------------------------- /SECURITY_CONTACTS: -------------------------------------------------------------------------------- 1 | # Defined below are the security contacts for this repo. 2 | # 3 | # They are the contact point for the Product Security Committee to reach out 4 | # to for triaging and handling of incoming issues. 5 | # 6 | # The below names agree to abide by the 7 | # [Embargo Policy](https://git.k8s.io/security/private-distributors-list.md#embargo-policy) 8 | # and will be removed and replaced if they violate that agreement. 9 | # 10 | # DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE 11 | # INSTRUCTIONS AT https://kubernetes.io/security/ 12 | 13 | tallclair 14 | kubernetes/enhancements-admins 15 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Kubernetes Community Code of Conduct 2 | 3 | Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module sigs.k8s.io/mdtoc 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0 7 | github.com/mmarkdown/mmark v2.0.40+incompatible 8 | github.com/spf13/cobra v1.9.1 9 | github.com/stretchr/testify v1.10.0 10 | sigs.k8s.io/release-utils v0.11.1 11 | ) 12 | 13 | require ( 14 | github.com/BurntSushi/toml v1.4.0 // indirect 15 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 18 | github.com/kr/text v0.2.0 // indirect 19 | github.com/pmezard/go-difflib v1.0.0 // indirect 20 | github.com/rogpeppe/go-internal v1.13.1 // indirect 21 | github.com/spf13/pflag v1.0.6 // indirect 22 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 23 | gopkg.in/yaml.v3 v3.0.1 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= 2 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 3 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ= 4 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= 5 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 6 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0 h1:4gjrh/PN2MuWCCElk8/I4OCKRKWCCo2zEct3VKCbibU= 10 | github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= 11 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 12 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 13 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 14 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 15 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 16 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 17 | github.com/mmarkdown/mmark v2.0.40+incompatible h1:vMeUeDzBK3H+/mU0oMVfMuhSXJlIA+DE/DMPQNAj5C4= 18 | github.com/mmarkdown/mmark v2.0.40+incompatible/go.mod h1:Uvmoz7tvsWpr7bMVxIpqZPyN3FbOtzDmnsJDFp7ltJs= 19 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 20 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 21 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 22 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 23 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 24 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 25 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 26 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 27 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 28 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 29 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 30 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 31 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 32 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 33 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 34 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 35 | sigs.k8s.io/release-utils v0.11.1 h1:hzvXGpHgHJfLOJB6TRuu14bzWc3XEglHmXHJqwClSZE= 36 | sigs.k8s.io/release-utils v0.11.1/go.mod h1:ybR2V/uQAOGxYfzYtBenSYeXWkBGNP2qnEiX77ACtpc= 37 | -------------------------------------------------------------------------------- /hack/boilerplate/boilerplate.Dockerfile.txt: -------------------------------------------------------------------------------- 1 | # Copyright YEAR The Kubernetes Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /hack/boilerplate/boilerplate.Makefile.txt: -------------------------------------------------------------------------------- 1 | # Copyright YEAR The Kubernetes Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /hack/boilerplate/boilerplate.bzl.txt: -------------------------------------------------------------------------------- 1 | # Copyright YEAR The Kubernetes Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /hack/boilerplate/boilerplate.generatebzl.txt: -------------------------------------------------------------------------------- 1 | # Copyright The Kubernetes Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /hack/boilerplate/boilerplate.generatego.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | -------------------------------------------------------------------------------- /hack/boilerplate/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright YEAR The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | -------------------------------------------------------------------------------- /hack/boilerplate/boilerplate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2015 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from __future__ import print_function 18 | 19 | import argparse 20 | import datetime 21 | import difflib 22 | import glob 23 | import os 24 | import re 25 | import sys 26 | 27 | parser = argparse.ArgumentParser() 28 | parser.add_argument( 29 | "filenames", 30 | help="list of files to check, all files if unspecified", 31 | nargs='*') 32 | 33 | rootdir = os.path.dirname(__file__) + "/../../" 34 | rootdir = os.path.abspath(rootdir) 35 | parser.add_argument( 36 | "--rootdir", default=rootdir, help="root directory to examine") 37 | 38 | default_boilerplate_dir = os.path.join(rootdir, "hack/boilerplate") 39 | parser.add_argument( 40 | "--boilerplate-dir", default=default_boilerplate_dir) 41 | 42 | parser.add_argument( 43 | "-v", "--verbose", 44 | help="give verbose output regarding why a file does not pass", 45 | action="store_true") 46 | 47 | args = parser.parse_args() 48 | 49 | verbose_out = sys.stderr if args.verbose else open("/dev/null", "w") 50 | 51 | 52 | def get_refs(): 53 | refs = {} 54 | 55 | for path in glob.glob(os.path.join(args.boilerplate_dir, "boilerplate.*.txt")): 56 | extension = os.path.basename(path).split(".")[1] 57 | 58 | ref_file = open(path, 'r') 59 | ref = ref_file.read().splitlines() 60 | ref_file.close() 61 | refs[extension] = ref 62 | 63 | return refs 64 | 65 | 66 | def is_generated_file(filename, data, regexs): 67 | for d in skipped_ungenerated_files: 68 | if d in filename: 69 | return False 70 | 71 | p = regexs["generated"] 72 | return p.search(data) 73 | 74 | 75 | def file_passes(filename, refs, regexs): 76 | try: 77 | f = open(filename, 'r') 78 | except Exception as exc: 79 | print("Unable to open %s: %s" % (filename, exc), file=verbose_out) 80 | return False 81 | 82 | data = f.read() 83 | f.close() 84 | 85 | # determine if the file is automatically generated 86 | generated = is_generated_file(filename, data, regexs) 87 | 88 | basename = os.path.basename(filename) 89 | extension = file_extension(filename) 90 | if generated: 91 | if extension == "go": 92 | extension = "generatego" 93 | elif extension == "bzl": 94 | extension = "generatebzl" 95 | 96 | if extension != "": 97 | ref = refs[extension] 98 | else: 99 | ref = refs[basename] 100 | 101 | # remove extra content from the top of files 102 | if extension == "go" or extension == "generatego": 103 | p = regexs["go_build_constraints"] 104 | (data, found) = p.subn("", data, 1) 105 | elif extension in ["sh", "py"]: 106 | p = regexs["shebang"] 107 | (data, found) = p.subn("", data, 1) 108 | 109 | data = data.splitlines() 110 | 111 | # if our test file is smaller than the reference it surely fails! 112 | if len(ref) > len(data): 113 | print('File %s smaller than reference (%d < %d)' % 114 | (filename, len(data), len(ref)), 115 | file=verbose_out) 116 | return False 117 | 118 | # trim our file to the same number of lines as the reference file 119 | data = data[:len(ref)] 120 | 121 | p = regexs["year"] 122 | for d in data: 123 | if p.search(d): 124 | if generated: 125 | print('File %s has the YEAR field, but it should not be in generated file' % 126 | filename, file=verbose_out) 127 | else: 128 | print('File %s has the YEAR field, but missing the year of date' % 129 | filename, file=verbose_out) 130 | return False 131 | 132 | if not generated: 133 | # Replace all occurrences of the regex "2014|2015|2016|2017|2018" with "YEAR" 134 | p = regexs["date"] 135 | for i, d in enumerate(data): 136 | (data[i], found) = p.subn('YEAR', d) 137 | if found != 0: 138 | break 139 | 140 | # if we don't match the reference at this point, fail 141 | if ref != data: 142 | print("Header in %s does not match reference, diff:" % 143 | filename, file=verbose_out) 144 | if args.verbose: 145 | print(file=verbose_out) 146 | for line in difflib.unified_diff(ref, data, 'reference', filename, lineterm=''): 147 | print(line, file=verbose_out) 148 | print(file=verbose_out) 149 | return False 150 | 151 | return True 152 | 153 | 154 | def file_extension(filename): 155 | return os.path.splitext(filename)[1].split(".")[-1].lower() 156 | 157 | 158 | skipped_dirs = ['Godeps', 'third_party', '_gopath', '_output', '.git', 'cluster/env.sh', 159 | "vendor", "test/e2e/generated/bindata.go", "hack/boilerplate/test", 160 | "staging/src/k8s.io/kubectl/pkg/generated/bindata.go"] 161 | 162 | # list all the files contain 'DO NOT EDIT', but are not generated 163 | skipped_ungenerated_files = [ 164 | 'hack/lib/swagger.sh', 'hack/boilerplate/boilerplate.py'] 165 | 166 | 167 | def normalize_files(files): 168 | newfiles = [] 169 | for pathname in files: 170 | if any(x in pathname for x in skipped_dirs): 171 | continue 172 | newfiles.append(pathname) 173 | for i, pathname in enumerate(newfiles): 174 | if not os.path.isabs(pathname): 175 | newfiles[i] = os.path.join(args.rootdir, pathname) 176 | return newfiles 177 | 178 | 179 | def get_files(extensions): 180 | files = [] 181 | if len(args.filenames) > 0: 182 | files = args.filenames 183 | else: 184 | for root, dirs, walkfiles in os.walk(args.rootdir): 185 | # don't visit certain dirs. This is just a performance improvement 186 | # as we would prune these later in normalize_files(). But doing it 187 | # cuts down the amount of filesystem walking we do and cuts down 188 | # the size of the file list 189 | for d in skipped_dirs: 190 | if d in dirs: 191 | dirs.remove(d) 192 | 193 | for name in walkfiles: 194 | pathname = os.path.join(root, name) 195 | files.append(pathname) 196 | 197 | files = normalize_files(files) 198 | outfiles = [] 199 | for pathname in files: 200 | basename = os.path.basename(pathname) 201 | extension = file_extension(pathname) 202 | if extension in extensions or basename in extensions: 203 | outfiles.append(pathname) 204 | return outfiles 205 | 206 | 207 | def get_dates(): 208 | years = datetime.datetime.now().year 209 | return '(%s)' % '|'.join((str(year) for year in range(2014, years+1))) 210 | 211 | 212 | def get_regexs(): 213 | regexs = {} 214 | # Search for "YEAR" which exists in the boilerplate, but shouldn't in the real thing 215 | regexs["year"] = re.compile('YEAR') 216 | # get_dates return 2014, 2015, 2016, 2017, or 2018 until the current year as a regex like: "(2014|2015|2016|2017|2018)"; 217 | # company holder names can be anything 218 | regexs["date"] = re.compile(get_dates()) 219 | # strip // +build \n\n build constraints 220 | regexs["go_build_constraints"] = re.compile( 221 | r"^(// \+build.*\n)+\n", re.MULTILINE) 222 | # strip #!.* from scripts 223 | regexs["shebang"] = re.compile(r"^(#!.*\n)\n*", re.MULTILINE) 224 | # Search for generated files 225 | regexs["generated"] = re.compile('DO NOT EDIT') 226 | return regexs 227 | 228 | 229 | def main(): 230 | regexs = get_regexs() 231 | refs = get_refs() 232 | filenames = get_files(refs.keys()) 233 | 234 | for filename in filenames: 235 | if not file_passes(filename, refs, regexs): 236 | print(filename, file=sys.stdout) 237 | 238 | return 0 239 | 240 | 241 | if __name__ == "__main__": 242 | sys.exit(main()) 243 | -------------------------------------------------------------------------------- /hack/boilerplate/boilerplate.py.txt: -------------------------------------------------------------------------------- 1 | # Copyright YEAR The Kubernetes Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /hack/boilerplate/boilerplate.sh.txt: -------------------------------------------------------------------------------- 1 | # Copyright YEAR The Kubernetes Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /hack/boilerplate/boilerplate_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2016 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import boilerplate 18 | import unittest 19 | from io import StringIO 20 | import os 21 | import sys 22 | 23 | class TestBoilerplate(unittest.TestCase): 24 | """ 25 | Note: run this test from the hack/boilerplate directory. 26 | 27 | $ python -m unittest boilerplate_test 28 | """ 29 | 30 | def test_boilerplate(self): 31 | os.chdir("test/") 32 | 33 | class Args(object): 34 | def __init__(self): 35 | self.filenames = [] 36 | self.rootdir = "." 37 | self.boilerplate_dir = "../" 38 | self.verbose = True 39 | 40 | # capture stdout 41 | old_stdout = sys.stdout 42 | sys.stdout = StringIO.StringIO() 43 | 44 | boilerplate.args = Args() 45 | ret = boilerplate.main() 46 | 47 | output = sorted(sys.stdout.getvalue().split()) 48 | 49 | sys.stdout = old_stdout 50 | 51 | self.assertEquals( 52 | output, ['././fail.go', '././fail.py']) 53 | -------------------------------------------------------------------------------- /hack/test-go.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2020 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -euo pipefail 18 | 19 | # Default timeout is 1800s 20 | TEST_TIMEOUT=1800 21 | 22 | for arg in "$@" 23 | do 24 | case $arg in 25 | -t=*|--timeout=*) 26 | TEST_TIMEOUT="${arg#*=}" 27 | shift 28 | ;; 29 | -t|--timeout) 30 | TEST_TIMEOUT="$2" 31 | shift 32 | shift 33 | esac 34 | done 35 | 36 | REPO_ROOT=$(git rev-parse --show-toplevel) 37 | cd "${REPO_ROOT}" 38 | 39 | GO111MODULE=on go test -v -timeout="${TEST_TIMEOUT}s" -count=1 -cover -coverprofile coverage.out ./... 40 | go tool cover -html coverage.out -o coverage.html 41 | -------------------------------------------------------------------------------- /hack/verify-boilerplate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2014 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | KUBE_ROOT=$(dirname "${BASH_SOURCE[0]}")/.. 22 | 23 | boilerDir="${KUBE_ROOT}/hack/boilerplate" 24 | boiler="${boilerDir}/boilerplate.py" 25 | 26 | files_need_boilerplate=() 27 | while IFS=$'\n' read -r line; do 28 | files_need_boilerplate+=( "$line" ) 29 | done < <("${boiler}" "$@") 30 | 31 | # Run boilerplate check 32 | if [[ ${#files_need_boilerplate[@]} -gt 0 ]]; then 33 | for file in "${files_need_boilerplate[@]}"; do 34 | echo "Boilerplate header is wrong for: ${file}" >&2 35 | done 36 | 37 | exit 1 38 | fi 39 | -------------------------------------------------------------------------------- /hack/verify-go-mod.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2020 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | go mod tidy 22 | git diff --exit-code 23 | -------------------------------------------------------------------------------- /hack/verify-golangci-lint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2020 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | VERSION=v1.64.5 22 | URL_BASE=https://raw.githubusercontent.com/golangci/golangci-lint 23 | URL=$URL_BASE/$VERSION/install.sh 24 | 25 | if [[ ! -f .golangci.yml ]]; then 26 | echo 'ERROR: missing .golangci.yml in repo root' >&2 27 | exit 1 28 | fi 29 | 30 | if ! command -v golangci-lint; then 31 | curl -sfL $URL | sh -s $VERSION 32 | PATH=$PATH:bin 33 | fi 34 | 35 | golangci-lint version 36 | golangci-lint linters 37 | golangci-lint run "$@" 38 | -------------------------------------------------------------------------------- /mdtoc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "log" 23 | "os" 24 | 25 | "github.com/spf13/cobra" 26 | "sigs.k8s.io/release-utils/version" 27 | 28 | "sigs.k8s.io/mdtoc/pkg/mdtoc" 29 | ) 30 | 31 | var cmd = &cobra.Command{ 32 | Use: os.Args[0] + " [FILE]...", 33 | Long: "Generate a table of contents for a markdown file (GitHub flavor).\n\n" + 34 | "TOC may be wrapped in a pair of tags to allow in-place updates:\n" + 35 | "", 36 | RunE: run, 37 | } 38 | 39 | type utilityOptions struct { 40 | mdtoc.Options 41 | Inplace bool 42 | } 43 | 44 | var defaultOptions utilityOptions 45 | 46 | func init() { 47 | cmd.PersistentFlags().BoolVarP(&defaultOptions.Dryrun, "dryrun", "d", false, "Whether to check for changes to TOC, rather than overwriting. Requires --inplace flag.") 48 | cmd.PersistentFlags().BoolVarP(&defaultOptions.Inplace, "inplace", "i", false, "Whether to edit the file in-place, or output to STDOUT. Requires toc tags to be present.") 49 | cmd.PersistentFlags().BoolVarP(&defaultOptions.SkipPrefix, "skip-prefix", "s", true, "Whether to ignore any headers before the opening toc tag.") 50 | cmd.PersistentFlags().IntVarP(&defaultOptions.MaxDepth, "max-depth", "m", mdtoc.MaxHeaderDepth, "Limit the depth of headers that will be included in the TOC.") 51 | cmd.PersistentFlags().BoolVarP(&defaultOptions.Version, "version", "v", false, "Show MDTOC version.") 52 | } 53 | 54 | func main() { 55 | if err := cmd.Execute(); err != nil { 56 | log.Fatal(err) 57 | } 58 | } 59 | 60 | func run(_ *cobra.Command, args []string) error { 61 | if defaultOptions.Version { 62 | v := version.GetVersionInfo() 63 | v.Name = "mdtoc" 64 | v.Description = "is a utility for generating a table-of-contents for markdown files" 65 | v.ASCIIName = "true" 66 | v.FontName = "banner" 67 | fmt.Fprintln(os.Stdout, v.String()) 68 | 69 | return nil 70 | } 71 | 72 | if err := validateArgs(defaultOptions, args); err != nil { 73 | return fmt.Errorf("validate args: %w", err) 74 | } 75 | 76 | if defaultOptions.Inplace { 77 | var retErr error 78 | 79 | for _, file := range args { 80 | if err := mdtoc.WriteTOC(file, defaultOptions.Options); err != nil { 81 | retErr = errors.Join(retErr, fmt.Errorf("%s: %w", file, err)) 82 | } 83 | } 84 | 85 | return retErr 86 | } 87 | 88 | toc, err := mdtoc.GetTOC(args[0], defaultOptions.Options) 89 | if err != nil { 90 | return fmt.Errorf("get toc: %w", err) 91 | } 92 | 93 | fmt.Println(toc) 94 | 95 | return nil 96 | } 97 | 98 | func validateArgs(opts utilityOptions, args []string) error { 99 | if len(args) < 1 { 100 | return errors.New("must specify at least 1 file") 101 | } 102 | 103 | if !opts.Inplace && len(args) > 1 { 104 | return errors.New("non-inplace updates require exactly 1 file") 105 | } 106 | 107 | if opts.Dryrun && !opts.Inplace { 108 | return errors.New("--dryrun requires --inplace") 109 | } 110 | 111 | return nil 112 | } 113 | -------------------------------------------------------------------------------- /mdtoc_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "os" 21 | "path/filepath" 22 | "strings" 23 | "testing" 24 | 25 | "github.com/stretchr/testify/assert" 26 | "github.com/stretchr/testify/require" 27 | 28 | "sigs.k8s.io/mdtoc/pkg/mdtoc" 29 | ) 30 | 31 | type testcase struct { 32 | file string 33 | includePrefix bool 34 | completeTOC bool 35 | validTOCTags bool 36 | expectedTOC string 37 | } 38 | 39 | var testcases = []testcase{{ 40 | file: testdata("weird_headings.md"), 41 | includePrefix: false, 42 | completeTOC: true, 43 | validTOCTags: true, 44 | }, { 45 | file: testdata("empty_toc.md"), 46 | includePrefix: false, 47 | completeTOC: false, 48 | validTOCTags: true, 49 | expectedTOC: "- [Only Heading](#only-heading)\n", 50 | }, { 51 | file: testdata("capital_toc.md"), 52 | includePrefix: false, 53 | completeTOC: true, 54 | validTOCTags: true, 55 | }, { 56 | file: testdata("include_prefix.md"), 57 | includePrefix: true, 58 | completeTOC: true, 59 | validTOCTags: true, 60 | }, { 61 | file: testdata("invalid_nostart.md"), 62 | validTOCTags: false, 63 | }, { 64 | file: testdata("invalid_noend.md"), 65 | validTOCTags: false, 66 | }, { 67 | file: testdata("invalid_endbeforestart.md"), 68 | validTOCTags: false, 69 | }, { 70 | file: "README.md", 71 | includePrefix: false, 72 | completeTOC: true, 73 | validTOCTags: true, 74 | }, { 75 | file: testdata("code.md"), 76 | includePrefix: true, 77 | completeTOC: true, 78 | validTOCTags: true, 79 | }} 80 | 81 | func testdata(subpath string) string { 82 | return filepath.Join("testdata", subpath) 83 | } 84 | 85 | func TestDryRun(t *testing.T) { 86 | t.Parallel() 87 | 88 | for _, test := range testcases { 89 | t.Run(test.file, func(t *testing.T) { 90 | t.Parallel() 91 | 92 | opts := utilityOptions{ 93 | Options: mdtoc.Options{ 94 | Dryrun: true, 95 | SkipPrefix: !test.includePrefix, 96 | MaxDepth: mdtoc.MaxHeaderDepth, 97 | }, 98 | Inplace: true, 99 | } 100 | require.NoError(t, validateArgs(opts, []string{test.file}), test.file) 101 | 102 | err := mdtoc.WriteTOC(test.file, opts.Options) 103 | 104 | if test.completeTOC { 105 | assert.NoError(t, err, test.file) 106 | } else { 107 | assert.Error(t, err, test.file) 108 | } 109 | }) 110 | } 111 | } 112 | 113 | func TestInplace(t *testing.T) { 114 | t.Parallel() 115 | 116 | for _, test := range testcases { 117 | t.Run(test.file, func(t *testing.T) { 118 | t.Parallel() 119 | 120 | original, err := os.ReadFile(test.file) 121 | require.NoError(t, err, test.file) 122 | 123 | // Create a copy of the test.file to modify. 124 | escapedFile := strings.ReplaceAll(test.file, string(filepath.Separator), "_") 125 | tmpFile, err := os.CreateTemp(t.TempDir(), escapedFile) 126 | require.NoError(t, err, test.file) 127 | 128 | defer os.Remove(tmpFile.Name()) 129 | _, err = tmpFile.Write(original) 130 | require.NoError(t, err, test.file) 131 | require.NoError(t, tmpFile.Close(), test.file) 132 | 133 | opts := utilityOptions{ 134 | Options: mdtoc.Options{ 135 | SkipPrefix: !test.includePrefix, 136 | Dryrun: false, 137 | MaxDepth: mdtoc.MaxHeaderDepth, 138 | }, 139 | } 140 | assert.NoError(t, validateArgs(opts, []string{tmpFile.Name()}), test.file) 141 | 142 | err = mdtoc.WriteTOC(tmpFile.Name(), opts.Options) 143 | if test.validTOCTags { 144 | require.NoError(t, err, test.file) 145 | } else { 146 | require.Error(t, err, test.file) 147 | } 148 | 149 | updated, err := os.ReadFile(tmpFile.Name()) 150 | require.NoError(t, err, test.file) 151 | 152 | if test.completeTOC || !test.validTOCTags { // Invalid tags should not modify contents. 153 | assert.Equal(t, string(original), string(updated), test.file) 154 | } else { 155 | assert.NotEqual(t, string(original), string(updated), test.file) 156 | } 157 | }) 158 | } 159 | } 160 | 161 | func TestOutput(t *testing.T) { 162 | t.Parallel() 163 | 164 | for _, test := range testcases { 165 | // Ignore the invalid cases, they're only for inplace tests. 166 | if !test.validTOCTags { 167 | continue 168 | } 169 | 170 | t.Run(test.file, func(t *testing.T) { 171 | t.Parallel() 172 | 173 | opts := utilityOptions{ 174 | Options: mdtoc.Options{ 175 | Dryrun: false, 176 | SkipPrefix: !test.includePrefix, 177 | MaxDepth: mdtoc.MaxHeaderDepth, 178 | }, 179 | } 180 | require.NoError(t, validateArgs(opts, []string{test.file}), test.file) 181 | 182 | toc, err := mdtoc.GetTOC(test.file, opts.Options) 183 | require.NoError(t, err, test.file) 184 | 185 | if test.expectedTOC != "" { 186 | assert.Equal(t, test.expectedTOC, toc, test.file) 187 | } 188 | }) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /pkg/mdtoc/mdtoc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package mdtoc 18 | 19 | import ( 20 | "bytes" 21 | "errors" 22 | "fmt" 23 | "math" 24 | "os" 25 | "regexp" 26 | "strings" 27 | 28 | "github.com/gomarkdown/markdown/ast" 29 | "github.com/gomarkdown/markdown/html" 30 | "github.com/gomarkdown/markdown/parser" 31 | "github.com/mmarkdown/mmark/mparser" 32 | ) 33 | 34 | const ( 35 | // StartTOC is the opening tag for the table of contents. 36 | StartTOC = "" 37 | // EndTOC is the tag that marks the end of the TOC. 38 | EndTOC = "" 39 | // MaxHeaderDepth is the default maximum header depth for ToC generation. 40 | MaxHeaderDepth = 6 41 | ) 42 | 43 | var ( 44 | startTOCRegex = regexp.MustCompile("(?i)" + StartTOC) 45 | endTOCRegex = regexp.MustCompile("(?i)" + EndTOC) 46 | ) 47 | 48 | // Options set for the toc generator. 49 | type Options struct { 50 | Dryrun bool 51 | SkipPrefix bool 52 | Version bool 53 | MaxDepth int 54 | } 55 | 56 | // parse parses a raw markdown document to an AST. 57 | func parse(b []byte) ast.Node { 58 | p := parser.NewWithExtensions(parser.CommonExtensions) 59 | p.Opts = parser.Options{ 60 | // mparser is required for parsing the --- title blocks 61 | ParserHook: mparser.Hook, 62 | } 63 | 64 | return p.Parse(b) 65 | } 66 | 67 | // GenerateTOC parses a document and returns its TOC. 68 | func GenerateTOC(doc []byte, opts Options) (string, error) { 69 | anchors := make(anchorGen) 70 | 71 | md := parse(doc) 72 | 73 | baseLvl := headingBase(md) 74 | toc := &bytes.Buffer{} 75 | htmlRenderer := html.NewRenderer(html.RendererOptions{}) 76 | 77 | walkHeadings(md, func(heading *ast.Heading) { 78 | if opts.MaxDepth > 0 && heading.Level > opts.MaxDepth { 79 | return 80 | } 81 | 82 | anchor := anchors.mkAnchor(asText(heading)) 83 | content := headingBody(htmlRenderer, heading) 84 | fmt.Fprintf(toc, "%s- [%s](#%s)\n", strings.Repeat(" ", heading.Level-baseLvl), content, anchor) 85 | }) 86 | 87 | return toc.String(), nil 88 | } 89 | 90 | type headingFn func(heading *ast.Heading) 91 | 92 | // walkHeadings runs the heading function on each heading in the parsed markdown document. 93 | func walkHeadings(doc ast.Node, headingFn headingFn) { 94 | ast.WalkFunc(doc, func(node ast.Node, entering bool) ast.WalkStatus { 95 | if !entering { 96 | return ast.GoToNext // Don't care about closing the heading section. 97 | } 98 | 99 | heading, ok := node.(*ast.Heading) 100 | if !ok { 101 | return ast.GoToNext // Ignore non-heading nodes. 102 | } 103 | 104 | if heading.IsTitleblock { 105 | return ast.GoToNext // Ignore title blocks (the --- section) 106 | } 107 | 108 | headingFn(heading) 109 | 110 | return ast.GoToNext 111 | }) 112 | } 113 | 114 | // anchorGen is used to generate heading anchor IDs, using the github-flavored markdown syntax. 115 | type anchorGen map[string]int 116 | 117 | func (a anchorGen) mkAnchor(text string) string { 118 | text = strings.ToLower(text) 119 | text = punctuation.ReplaceAllString(text, "") 120 | text = strings.ReplaceAll(text, " ", "-") 121 | idx := a[text] 122 | a[text] = idx + 1 123 | 124 | if idx > 0 { 125 | return fmt.Sprintf("%s-%d", text, idx) 126 | } 127 | 128 | return text 129 | } 130 | 131 | // Locate the case-insensitive TOC tags. 132 | func findTOCTags(raw []byte) (start, end int) { 133 | if ind := startTOCRegex.FindIndex(raw); len(ind) > 0 { 134 | start = ind[0] 135 | } else { 136 | start = -1 137 | } 138 | 139 | if ind := endTOCRegex.FindIndex(raw); len(ind) > 0 { 140 | end = ind[0] 141 | } else { 142 | end = -1 143 | } 144 | 145 | return 146 | } 147 | 148 | func asText(node ast.Node) (text string) { 149 | ast.WalkFunc(node, func(node ast.Node, entering bool) ast.WalkStatus { 150 | if !entering { 151 | return ast.GoToNext // Don't care about closing the heading section. 152 | } 153 | 154 | switch node.(type) { 155 | case *ast.Text, *ast.Code: 156 | text += string(node.AsLeaf().Literal) 157 | } 158 | 159 | return ast.GoToNext 160 | }) 161 | 162 | return text 163 | } 164 | 165 | // Renders the heading body as HTML. 166 | func headingBody(renderer *html.Renderer, heading *ast.Heading) string { 167 | var buf bytes.Buffer 168 | 169 | for _, child := range heading.Children { 170 | ast.WalkFunc(child, func(node ast.Node, entering bool) ast.WalkStatus { 171 | return renderer.RenderNode(&buf, node, entering) 172 | }) 173 | } 174 | 175 | return strings.TrimSpace(buf.String()) 176 | } 177 | 178 | // headingBase finds the minimum heading level. This is useful for normalizing indentation, such as 179 | // when a top-level heading is skipped in the prefix. 180 | func headingBase(doc ast.Node) int { 181 | baseLvl := math.MaxInt32 182 | 183 | walkHeadings(doc, func(heading *ast.Heading) { 184 | if baseLvl > heading.Level { 185 | baseLvl = heading.Level 186 | } 187 | }) 188 | 189 | return baseLvl 190 | } 191 | 192 | // Match punctuation that is filtered out from anchor IDs. 193 | var punctuation = regexp.MustCompile(`[^\w\- ]`) 194 | 195 | // WriteTOC writes the TOC generator on file with options. 196 | // Returns the generated toc, and any error. 197 | func WriteTOC(file string, opts Options) error { 198 | raw, err := os.ReadFile(file) 199 | if err != nil { 200 | return fmt.Errorf("unable to read %s: %w", file, err) 201 | } 202 | 203 | start, end := findTOCTags(raw) 204 | 205 | if start == -1 { 206 | return errors.New("missing opening TOC tag") 207 | } 208 | 209 | if end == -1 { 210 | return errors.New("missing closing TOC tag") 211 | } 212 | 213 | if end < start { 214 | return errors.New("TOC closing tag before start tag") 215 | } 216 | 217 | var doc []byte 218 | doc = raw 219 | // skipPrefix is only used when toc tags are present. 220 | if opts.SkipPrefix && start != -1 && end != -1 { 221 | doc = raw[end:] 222 | } 223 | 224 | toc, err := GenerateTOC(doc, opts) 225 | if err != nil { 226 | return fmt.Errorf("failed to generate toc: %w", err) 227 | } 228 | 229 | realStart := start + len(StartTOC) 230 | 231 | oldTOC := string(raw[realStart:end]) 232 | if strings.TrimSpace(oldTOC) == strings.TrimSpace(toc) { 233 | // No changes required. 234 | return nil 235 | } else if opts.Dryrun { 236 | return fmt.Errorf("changes found:\n%s", toc) 237 | } 238 | 239 | err = atomicWrite(file, 240 | string(raw[:realStart])+"\n", 241 | toc, 242 | string(raw[end:]), 243 | ) 244 | 245 | return err 246 | } 247 | 248 | // GetTOC generates the TOC from a file with options. 249 | // Returns the generated toc, and any error. 250 | func GetTOC(file string, opts Options) (string, error) { 251 | doc, err := os.ReadFile(file) 252 | if err != nil { 253 | return "", fmt.Errorf("unable to read %s: %w", file, err) 254 | } 255 | 256 | start, end := findTOCTags(doc) 257 | startPos := 0 258 | 259 | // skipPrefix is only used when toc tags are present. 260 | if opts.SkipPrefix && start != -1 && end != -1 { 261 | startPos = end 262 | } 263 | 264 | toc, err := GenerateTOC(doc[startPos:], opts) 265 | if err != nil { 266 | return toc, fmt.Errorf("failed to generate toc: %w", err) 267 | } 268 | 269 | return toc, err 270 | } 271 | 272 | // atomicWrite writes the chunks sequentially to the filePath. 273 | // A temporary file is used so no changes are made to the original in the case of an error. 274 | func atomicWrite(filePath string, chunks ...string) error { 275 | tmpPath := filePath + "_tmp" 276 | 277 | const perms = 0o600 278 | 279 | tmp, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, perms) 280 | if err != nil { 281 | return fmt.Errorf("unable to open tepmorary file %s: %w", tmpPath, err) 282 | } 283 | 284 | // Cleanup 285 | defer func() { 286 | tmp.Close() 287 | os.Remove(tmpPath) 288 | }() 289 | 290 | for _, chunk := range chunks { 291 | if _, err := tmp.WriteString(chunk); err != nil { 292 | return fmt.Errorf("write temp string: %w", err) 293 | } 294 | } 295 | 296 | if err := tmp.Close(); err != nil { 297 | return fmt.Errorf("close temp file: %w", err) 298 | } 299 | 300 | if err := os.Rename(tmp.Name(), filePath); err != nil { 301 | return fmt.Errorf("rename temp file: %w", err) 302 | } 303 | 304 | return nil 305 | } 306 | -------------------------------------------------------------------------------- /testdata/capital_toc.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: this is a title block 3 | description: we should ignore it 4 | --- 5 | 6 | # TOC 7 | 8 | Some other tools use a similar tag scheme, but capitalized. 9 | 10 | 11 | 12 | - [Only Heading](#only-heading) 13 | 14 | 15 | 16 | # Only Heading 17 | -------------------------------------------------------------------------------- /testdata/code.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: this is a title block 3 | description: we should ignore it 4 | --- 5 | 6 | # Expected TOC 7 | 8 | Manually verified by uploading to github. 9 | 10 | 11 | - [Expected TOC](#expected-toc) 12 | - [H1 H1 H1](#h1-h1-h1) 13 | - [H2 H2](#h2-h2) 14 | 15 | 16 | # H1 `H1` H1 17 | 18 | filler 19 | 20 | # H2 `H2` 21 | 22 | filler 23 | -------------------------------------------------------------------------------- /testdata/empty_toc.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: this is a title block 3 | description: we should ignore it 4 | --- 5 | 6 | # TOC 7 | 8 | 9 | 10 | # Only Heading 11 | -------------------------------------------------------------------------------- /testdata/include_prefix.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: this is a title block 3 | description: we should ignore it 4 | --- 5 | 6 | # Expected TOC 7 | 8 | Manually verified by uploading to github. 9 | 10 | 11 | - [Expected TOC](#expected-toc) 12 | - [H1](#h1) 13 | - [H2](#h2) 14 | 15 | 16 | # H1 17 | filler 18 | 19 | # H2 20 | filler 21 | -------------------------------------------------------------------------------- /testdata/invalid_endbeforestart.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: this is a title block 3 | description: we should ignore it 4 | --- 5 | 6 | # TOC 7 | 8 | 9 | 10 | 11 | # Only Heading 12 | -------------------------------------------------------------------------------- /testdata/invalid_noend.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: this is a title block 3 | description: we should ignore it 4 | --- 5 | 6 | # TOC 7 | 8 | 9 | 10 | # Only Heading 11 | -------------------------------------------------------------------------------- /testdata/invalid_nostart.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: this is a title block 3 | description: we should ignore it 4 | --- 5 | 6 | # TOC 7 | 8 | 9 | 10 | # Only Heading 11 | -------------------------------------------------------------------------------- /testdata/weird_headings.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: this is a title block 3 | description: we should ignore it 4 | --- 5 | 6 | # Expected TOC 7 | 8 | Manually verified by uploading to github. 9 | 10 | 11 | - [H1](#h1) 12 | - [H2: with punctuation!](#h2-with-punctuation) 13 | - [H3 duplicate](#h3-duplicate) 14 | - [H3 duplicate](#h3-duplicate-1) 15 | - [H3 duplicate](#h3-duplicate-2) 16 | - [H3 duplicate](#h3-duplicate-3) 17 | - [H3 duplicate](#h3-duplicate-4) 18 | - [H4 no fillers](#h4-no-fillers) 19 | - [H5 no fillers](#h5-no-fillers) 20 | - [H4.2 no fillers](#h42-no-fillers) 21 | - [H4.3 no fillers](#h43-no-fillers) 22 | - [H4.4 no fillers](#h44-no-fillers) 23 | - [H4.5 no fillers](#h45-no-fillers) 24 | - [H6: Let's get weird](#h6-lets-get-weird) 25 | - [!@#$&*^ headers!](#-headers) 26 | - [TEST THIS!](#test-this) 27 | - [-and-then-we-said_go](#-and-then-we-said_go) 28 | - [-------](#-------) 29 | - [be bold](#be-bold) 30 | - [don't go!](#dont-go) 31 | - [bring it all out: !@#$%^&*(){}/=?+;:"'`,.<> ok](#bring-it-all-out--ok) 32 | 33 | 34 | # H1 35 | filler 36 | 37 | ## H2: with punctuation! 38 | filler 39 | 40 | ### H3 duplicate 41 | filler 42 | 43 | ### H3 duplicate 44 | filler 45 | 46 | ### H3 duplicate 47 | filler 48 | 49 | ### H3 duplicate 50 | filler 51 | 52 | ### H3 duplicate 53 | filler 54 | 55 | #### H4 no fillers 56 | ##### H5 no fillers 57 | #### H4.2 no fillers 58 | #### H4.3 no fillers 59 | #### H4.4 no fillers 60 | #### H4.5 no fillers 61 | 62 | ## H6: Let's get weird 63 | ### !@#$&*^ headers! 64 | ### TEST THIS! 65 | ### -and-then-we-said_go 66 | ### ------- 67 | ### **be _bold_** 68 | ### don't go! {#forth} 69 | ### bring it all out: !@#$%^&*(){}/=?+;:"'`,.<> ok 70 | 71 | /fin 72 | --------------------------------------------------------------------------------