├── .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 |
--------------------------------------------------------------------------------