├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── cmd └── markdown-toc │ └── main.go ├── go.mod ├── internal └── cli │ └── cli.go ├── renovate.json ├── toc.go └── toc_test.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: CI 4 | on: 5 | pull_request: 6 | push: 7 | branches: 8 | - main 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: lint 15 | uses: reviewdog/action-golangci-lint@v2 16 | test: 17 | strategy: 18 | matrix: 19 | go_version: 20 | - 1.14.x 21 | - 1.15.x 22 | - 1.16.x 23 | - 1.17.x 24 | os: 25 | - ubuntu-latest 26 | runs-on: ${{ matrix.os }} 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: actions/setup-go@v2 30 | with: 31 | go-version: ${{ matrix.go_version }} 32 | - uses: actions/cache@v2 33 | with: 34 | path: ~/go/pkg/mod 35 | key: ${{ runner.os }}-go-${{ matrix.go_version }}-${{ hashFiles('**/go.sum') }} 36 | restore-keys: | 37 | ${{ runner.os }}-go-${{ matrix.go_version }} 38 | - name: test 39 | run: go test -v -race -cover -covermode=atomic ./... 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /markdown-toc 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 aereal 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | go build -o markdown-toc ./cmd/markdown-toc 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # markdown-toc 2 | 3 | 4 | 5 | 6 | ---- 7 | 8 | ## Usage 9 | 10 | ``` 11 | cat README.markdown 12 | => # README 13 | => 14 | => 15 | => 16 | => 17 | => ## Usage 18 | => ... 19 | => ## Install 20 | => ... 21 | => ## License 22 | 23 | markdown-toc README.markdown 24 | => # README 25 | => 26 | => 27 | => * [Usage](#usage) 28 | => * [Install](#install) 29 | => * [License](#license) 30 | => 31 | => 32 | => ## Usage 33 | => ... 34 | => ## Install 35 | => ... 36 | => ## License 37 | ``` 38 | 39 | ## Install 40 | 41 | ``` 42 | go install github.com/aereal/markdown-toc/cmd/markdown-toc 43 | ``` 44 | 45 | ## See also 46 | 47 | Inspired by [hail2u/node-gfmtoc][] 48 | 49 | [hail2u/node-gfmtoc]: https://github.com/hail2u/node-gfmtoc 50 | -------------------------------------------------------------------------------- /cmd/markdown-toc/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/aereal/markdown-toc/internal/cli" 7 | ) 8 | 9 | func main() { 10 | app := cli.NewCLI() 11 | os.Exit(app.Run(os.Args)) 12 | } 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aereal/markdown-toc 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /internal/cli/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "os" 8 | 9 | markdowntoc "github.com/aereal/markdown-toc" 10 | ) 11 | 12 | func NewCLI() *CLI { 13 | return &CLI{out: os.Stdout, err: os.Stderr} 14 | } 15 | 16 | type CLI struct { 17 | out, err io.Writer 18 | } 19 | 20 | func (cli *CLI) Run(args []string) int { 21 | flags := flag.NewFlagSet("markdown-toc", flag.ContinueOnError) 22 | flags.SetOutput(cli.err) 23 | 24 | if err := flags.Parse(args[1:]); err != nil { 25 | return 1 26 | } 27 | 28 | parsedArgs := flags.Args() 29 | if len(parsedArgs) < 1 { 30 | fmt.Fprintln(cli.err, "Usage: markdown-toc FILE.md") 31 | return 1 32 | } 33 | 34 | target := parsedArgs[0] 35 | f, err := os.Open(target) 36 | if err != nil { 37 | fmt.Fprintln(cli.err, err) 38 | return 1 39 | } 40 | defer f.Close() 41 | 42 | markdowntoc.InjectToc(f, cli.out) 43 | 44 | return 0 45 | } 46 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>aereal/renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /toc.go: -------------------------------------------------------------------------------- 1 | package markdowntoc 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "net/url" 8 | "strings" 9 | "unicode" 10 | ) 11 | 12 | const marker = "" 13 | 14 | func InjectToc(in io.Reader, out io.Writer) { 15 | reader := bufio.NewReader(in) 16 | f := func(c rune) bool { 17 | return !(unicode.IsSpace(c) || c == 35) 18 | } 19 | var toc []string 20 | var formatted []string 21 | tocIndex := -1 22 | i := 0 23 | for { 24 | lineBytes, _, err := reader.ReadLine() 25 | line := string(lineBytes) 26 | if strings.HasPrefix(line, "#") { 27 | pos := strings.IndexFunc(line, f) 28 | if pos == -1 { 29 | pos = 1 30 | } 31 | title := line[pos:] 32 | escapedTitle := url.QueryEscape(title) 33 | toc = append(toc, fmt.Sprintf("* [%s](#%s)", title, escapedTitle)) 34 | formatted = append(formatted, line+fmt.Sprintf(``, escapedTitle)) 35 | } else if line == marker { 36 | tocIndex = i 37 | } else { 38 | formatted = append(formatted, line) 39 | } 40 | i++ 41 | if err == io.EOF { 42 | break 43 | } 44 | } 45 | if tocIndex != -1 { 46 | toc = append([]string{marker}, toc...) 47 | formatted = append(formatted[:tocIndex], append(toc, formatted[tocIndex:]...)...) 48 | } 49 | fmt.Fprintln(out, strings.Join(formatted, "\n")) 50 | } 51 | -------------------------------------------------------------------------------- /toc_test.go: -------------------------------------------------------------------------------- 1 | package markdowntoc 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestInjectToc(t *testing.T) { 10 | input := strings.NewReader(` 11 | 12 | 13 | # h1 14 | 15 | h1 16 | 17 | # 姉 18 | `) 19 | 20 | expected := ` 21 | * [h1](#h1) 22 | * [姉](#%E5%A7%89) 23 | 24 | 25 | # h1 26 | 27 | h1 28 | 29 | # 姉 30 | ` 31 | output := new(bytes.Buffer) 32 | InjectToc(input, output) 33 | if got := output.String(); strings.TrimSpace(got) != strings.TrimSpace(expected) { 34 | t.Fatalf("\nExpected:\n%s\n----\nGot:\n%s----\n", expected, got) 35 | } 36 | } 37 | 38 | func TestEmptyToc(t *testing.T) { 39 | input := strings.NewReader(` 40 | 41 | 42 | # 43 | `) 44 | 45 | expected := ` 46 | * [](#) 47 | 48 | 49 | # 50 | ` 51 | output := new(bytes.Buffer) 52 | InjectToc(input, output) 53 | if got := output.String(); strings.TrimSpace(got) != strings.TrimSpace(expected) { 54 | t.Fatalf("\nExpected:\n%s\n----\nGot:\n%s----\n", expected, got) 55 | } 56 | } 57 | --------------------------------------------------------------------------------