├── .gitignore
├── OWNERS
├── code-of-conduct.md
├── testdata
├── invalid_noend.md
├── invalid_nostart.md
├── empty_toc.md
├── invalid_endbeforestart.md
├── capital_toc.md
├── include_prefix.md
├── code.md
└── weird_headings.md
├── .github
├── ISSUE_TEMPLATE
│ ├── feature.md
│ └── bug-report.md
├── SECURITY.md
├── dependabot.yml
├── workflows
│ ├── snapshot.yml
│ └── release.yml
└── PULL_REQUEST_TEMPLATE.md
├── OWNERS_ALIASES
├── SECURITY_CONTACTS
├── hack
├── boilerplate
│ ├── boilerplate.go.txt
│ ├── boilerplate.generatego.txt
│ ├── boilerplate.bzl.txt
│ ├── boilerplate.generatebzl.txt
│ ├── boilerplate.py.txt
│ ├── boilerplate.sh.txt
│ ├── boilerplate.Dockerfile.txt
│ ├── boilerplate.Makefile.txt
│ ├── boilerplate_test.py
│ └── boilerplate.py
├── verify-go-mod.sh
├── verify-golangci-lint.sh
├── test-go.sh
└── verify-boilerplate.sh
├── go.mod
├── .mdtoc-bom-config.yaml
├── .goreleaser.yml
├── CONTRIBUTING.md
├── README.md
├── .golangci.yml
├── go.sum
├── mdtoc.go
├── Makefile
├── mdtoc_test.go
├── pkg
└── mdtoc
│ └── mdtoc.go
└── LICENSE
/.gitignore:
--------------------------------------------------------------------------------
1 | ./mdtoc
2 | coverage*
3 | dist/
4 | output/
5 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request
3 | about: Suggest a feature for mdtoc
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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.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.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.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.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: Report a bug encountered while using mdtoc
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 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module sigs.k8s.io/mdtoc
2 |
3 | go 1.24.0
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.10.2
9 | github.com/stretchr/testify v1.11.1
10 | sigs.k8s.io/release-utils v0.12.2
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.9 // indirect
22 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
23 | gopkg.in/yaml.v3 v3.0.1 // indirect
24 | )
25 |
--------------------------------------------------------------------------------
/.mdtoc-bom-config.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | namespace: https://sigs.k8s.io/mdtoc
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 |
--------------------------------------------------------------------------------
/.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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
16 |
17 | - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.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@8af7b2a5596dff526de9db59b2c4b8457e9f52a1 # v0.4.0
24 |
25 | - name: Install GoReleaser
26 | uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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/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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
22 | with:
23 | fetch-depth: 1
24 |
25 | - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
26 | with:
27 | go-version-file: './go.mod'
28 | check-latest: true
29 |
30 | - name: Install cosign
31 | uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
32 |
33 | - name: Install bom
34 | uses: kubernetes-sigs/release-actions/setup-bom@8af7b2a5596dff526de9db59b2c4b8457e9f52a1 # v0.4.0
35 |
36 | - name: Install GoReleaser
37 | uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
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@8af7b2a5596dff526de9db59b2c4b8457e9f52a1 # v0.4.0
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@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
25 | github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
26 | github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
27 | github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
28 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
29 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
30 | go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
31 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
32 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
33 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
34 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
35 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
36 | sigs.k8s.io/release-utils v0.12.2 h1:H06v3FuLElAkf7Ikkd9ll8hnhdtQ+OgktJAni3iIAl8=
37 | sigs.k8s.io/release-utils v0.12.2/go.mod h1:Ab9Lb/FpGUw4lUXj1QYbUcF2TRzll+GS7Md54W1G7sA=
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 |
--------------------------------------------------------------------------------
/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 mdtoc 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 mdtoc 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------