diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..c338e56d --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,4 @@ +run: + skip-dirs: + - .*/usr/local/Cellar/ + - .*/go/pkg/mod/ diff --git a/BUILD.bazel b/BUILD.bazel index 6aaa77cc..6b0968f5 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -163,8 +163,8 @@ _RUNTIME_PKGS = [ "//markdown", "//markdown/private", "//markdown/tools", - "//markdown/tools/github_markdown_toc", - "//markdown/tools/github_markdown_toc/cmd/gh-md-toc", + "//markdown/tools/markdown_toc/cmd/generate_toc", + "//markdown/tools/markdown_toc/mdtoc", "//shlib/lib", "//shlib/rules", "//shlib/rules/private", diff --git a/MODULE.bazel b/MODULE.bazel index 6c6c92d5..88758ef2 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -33,10 +33,9 @@ go_deps.from_file(go_mod = "//:go.mod") use_repo( go_deps, "com_github_creasty_defaults", + "com_github_gomarkdown_markdown", "com_github_stretchr_testify", - "in_gopkg_alecthomas_kingpin_v2", "in_gopkg_yaml_v3", - "org_golang_x_net", ) # MARK: - Dev Dependencies diff --git a/go.mod b/go.mod index 08f1dff3..193c0b70 100644 --- a/go.mod +++ b/go.mod @@ -4,16 +4,12 @@ go 1.19 require ( github.com/creasty/defaults v1.7.0 + github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386 github.com/stretchr/testify v1.8.4 - golang.org/x/net v0.15.0 - gopkg.in/alecthomas/kingpin.v2 v2.2.4 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/alecthomas/assert v1.0.0 // indirect - github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect - github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect ) diff --git a/go.sum b/go.sum index dc3991f2..5195d65d 100644 --- a/go.sum +++ b/go.sum @@ -1,26 +1,13 @@ -github.com/alecthomas/assert v1.0.0 h1:3XmGh/PSuLzDbK3W2gUbRXwgW5lqPkuqvRgeQ30FI5o= -github.com/alecthomas/assert v1.0.0/go.mod h1:va/d2JC+M7F6s+80kl/R3G7FUiW6JzUO+hPhLyJ36ZY= -github.com/alecthomas/colour v0.1.0 h1:nOE9rJm6dsZ66RGWYSFrXw461ZIt9A6+nHgL7FRrDUk= -github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/creasty/defaults v1.7.0 h1:eNdqZvc5B509z18lD8yc212CAqJNvfT1Jq6L8WowdBA= github.com/creasty/defaults v1.7.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386 h1:EcQR3gusLHN46TAD+G+EbaaqJArt5vHhNpXAa12PQf4= +github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -gopkg.in/alecthomas/kingpin.v2 v2.2.4 h1:CC8tJ/xljioKrK6ii3IeWVXU4Tw7VB+LbjZBJaBxN50= -gopkg.in/alecthomas/kingpin.v2 v2.2.4/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/go_deps.bzl b/go_deps.bzl index cc8d0ba3..ec6fb949 100644 --- a/go_deps.bzl +++ b/go_deps.bzl @@ -6,41 +6,7 @@ load("@bazel_gazelle//:deps.bzl", "go_repository") def bazel_starlib_go_dependencies(): """Load dependencies for `bazel-starlib`.""" - go_repository( - name = "com_github_alecthomas_assert", - build_external = "external", - importpath = "github.com/alecthomas/assert", - sum = "h1:3XmGh/PSuLzDbK3W2gUbRXwgW5lqPkuqvRgeQ30FI5o=", - version = "v1.0.0", - ) - go_repository( - name = "com_github_alecthomas_colour", - build_external = "external", - importpath = "github.com/alecthomas/colour", - sum = "h1:nOE9rJm6dsZ66RGWYSFrXw461ZIt9A6+nHgL7FRrDUk=", - version = "v0.1.0", - ) - go_repository( - name = "com_github_alecthomas_repr", - build_external = "external", - importpath = "github.com/alecthomas/repr", - sum = "h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48=", - version = "v0.0.0-20210801044451-80ca428c5142", - ) - go_repository( - name = "com_github_alecthomas_template", - build_external = "external", - importpath = "github.com/alecthomas/template", - sum = "h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=", - version = "v0.0.0-20160405071501-a0175ee3bccc", - ) - go_repository( - name = "com_github_alecthomas_units", - build_external = "external", - importpath = "github.com/alecthomas/units", - sum = "h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=", - version = "v0.0.0-20151022065526-2efee857e7cf", - ) + go_repository( name = "com_github_creasty_defaults", build_external = "external", @@ -56,12 +22,13 @@ def bazel_starlib_go_dependencies(): version = "v1.1.1", ) go_repository( - name = "com_github_mattn_go_isatty", + name = "com_github_gomarkdown_markdown", build_external = "external", - importpath = "github.com/mattn/go-isatty", - sum = "h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=", - version = "v0.0.14", + importpath = "github.com/gomarkdown/markdown", + sum = "h1:EcQR3gusLHN46TAD+G+EbaaqJArt5vHhNpXAa12PQf4=", + version = "v0.0.0-20230922112808-5421fefb8386", ) + go_repository( name = "com_github_pmezard_go_difflib", build_external = "external", @@ -69,13 +36,7 @@ def bazel_starlib_go_dependencies(): sum = "h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=", version = "v1.0.0", ) - go_repository( - name = "com_github_sergi_go_diff", - build_external = "external", - importpath = "github.com/sergi/go-diff", - sum = "h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=", - version = "v1.2.0", - ) + go_repository( name = "com_github_stretchr_objx", build_external = "external", @@ -90,13 +51,7 @@ def bazel_starlib_go_dependencies(): sum = "h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=", version = "v1.8.4", ) - go_repository( - name = "in_gopkg_alecthomas_kingpin_v2", - build_external = "external", - importpath = "gopkg.in/alecthomas/kingpin.v2", - sum = "h1:CC8tJ/xljioKrK6ii3IeWVXU4Tw7VB+LbjZBJaBxN50=", - version = "v2.2.4", - ) + go_repository( name = "in_gopkg_check_v1", build_external = "external", @@ -111,40 +66,3 @@ def bazel_starlib_go_dependencies(): sum = "h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=", version = "v3.0.1", ) - go_repository( - name = "org_golang_x_crypto", - build_external = "external", - importpath = "golang.org/x/crypto", - sum = "h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=", - version = "v0.13.0", - ) - - go_repository( - name = "org_golang_x_net", - build_external = "external", - importpath = "golang.org/x/net", - sum = "h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=", - version = "v0.15.0", - ) - - go_repository( - name = "org_golang_x_sys", - build_external = "external", - importpath = "golang.org/x/sys", - sum = "h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=", - version = "v0.12.0", - ) - go_repository( - name = "org_golang_x_term", - build_external = "external", - importpath = "golang.org/x/term", - sum = "h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=", - version = "v0.12.0", - ) - go_repository( - name = "org_golang_x_text", - build_external = "external", - importpath = "golang.org/x/text", - sum = "h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=", - version = "v0.13.0", - ) diff --git a/markdown/tools/BUILD.bazel b/markdown/tools/BUILD.bazel index d92f5ab8..7209ffc3 100644 --- a/markdown/tools/BUILD.bazel +++ b/markdown/tools/BUILD.bazel @@ -23,7 +23,8 @@ sh_binary( srcs = ["update_markdown_toc.sh"], data = [ ":update_markdown_doc", - "//markdown/tools/github_markdown_toc/cmd/gh-md-toc", + # "//markdown/tools/github_markdown_toc/cmd/gh-md-toc", + "//markdown/tools/markdown_toc/cmd/generate_toc", ], visibility = ["//visibility:public"], deps = [ diff --git a/markdown/tools/github_markdown_toc/LICENSE b/markdown/tools/github_markdown_toc/LICENSE deleted file mode 100644 index 66fe00f0..00000000 --- a/markdown/tools/github_markdown_toc/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 Eugene Kalinin - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/markdown/tools/github_markdown_toc/Makefile b/markdown/tools/github_markdown_toc/Makefile deleted file mode 100644 index b1ef7ec9..00000000 --- a/markdown/tools/github_markdown_toc/Makefile +++ /dev/null @@ -1,40 +0,0 @@ -EXEC=gh-md-toc -CMD_SRC=cmd/${EXEC}/main.go -BUILD_DIR=build -BUILD_OS="windows darwin linux" -BUILD_ARCH="amd64" - -clean: - @rm -f ${EXEC} - @rm -f ${BUILD_DIR}/* - @go clean - -lint: - @golint - @golangci-lint run - -# make run ARGS="--help" -run: - @go run ${CMD_SRC} $(ARGS) - -build: clean lint - go build -race -o ${EXEC} ${CMD_SRC} - -test: clean lint - @go test -cover -o ${EXEC} - -release: test buildall - @git tag `grep "version" main.go | grep -o -E '[0-9]\.[0-9]\.[0-9]{1,2}'` - @git push --tags origin master - -buildall: clean - @mkdir -p ${BUILD_DIR} - @for os in "${BUILD_OS}" ; do \ - for arch in "${BUILD_ARCH}" ; do \ - echo " * build $$os for $$arch"; \ - GOOS=$$os GOARCH=$$arch go build -o ${BUILD_DIR}/${EXEC} ${CMD_SRC}; \ - cd ${BUILD_DIR}; \ - tar czf ${EXEC}.$$os.$$arch.tgz ${EXEC}; \ - cd - ; \ - done done - @rm ${BUILD_DIR}/${EXEC} diff --git a/markdown/tools/github_markdown_toc/README.md b/markdown/tools/github_markdown_toc/README.md deleted file mode 100644 index 2510d414..00000000 --- a/markdown/tools/github_markdown_toc/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# GitHub Markdown TOC - -This utility is based upon [the gh-md-toc utility created by -eklanin](https://github.com/ekalinin/github-markdown-toc.go). The original implementation used -regular expressions to parse the HTML generated from GitHub's markdown rendering service. -Unfortunately, subtle changes in the output from this service can cause the regular expressions to -not work properly. I created [a pull request that replaced the regular experession logic with HTML -parsing using `golang.org/x/net/html`](https://github.com/ekalinin/github-markdown-toc.go/pull/38). -As of this writing, the pull request has not been merged. - -After another outage due to the fragility of the regular expression logic on 2023-08-29, I opted to -fork the code with the HTML parsing logic and incorporate it into this repository. I preserved the -original license on the code. However, any changes to this utility may not be compatible with the -original code base. diff --git a/markdown/tools/github_markdown_toc/cmd/gh-md-toc/main.go b/markdown/tools/github_markdown_toc/cmd/gh-md-toc/main.go deleted file mode 100644 index 2218c8d5..00000000 --- a/markdown/tools/github_markdown_toc/cmd/gh-md-toc/main.go +++ /dev/null @@ -1,92 +0,0 @@ -package main - -import ( - "fmt" - "io" - "os" - - "gopkg.in/alecthomas/kingpin.v2" - - ghtoc "github.com/cgrindel/bazel-starlib/markdown/tools/github_markdown_toc" -) - -var ( - pathsDesc = "Local path or URL of the document to grab TOC. Read MD from stdin if not entered." - paths = kingpin.Arg("path", pathsDesc).Strings() - serial = kingpin.Flag("serial", "Grab TOCs in the serial mode").Bool() - hideHeader = kingpin.Flag("hide-header", "Hide TOC header").Bool() - hideFooter = kingpin.Flag("hide-footer", "Hide TOC footer").Bool() - startDepth = kingpin.Flag("start-depth", "Start including from this level. Defaults to 0 (include all levels)").Default("0").Int() - depth = kingpin.Flag("depth", "How many levels of headings to include. Defaults to 0 (all)").Default("0").Int() - noEscape = kingpin.Flag("no-escape", "Do not escape chars in sections").Bool() - token = kingpin.Flag("token", "GitHub personal token").String() - indent = kingpin.Flag("indent", "Indent space of generated list").Default("2").Int() - debug = kingpin.Flag("debug", "Show debug info").Bool() -) - -// check if there was an error (and panic if it was) -func check(e error) { - if e != nil { - panic(e) - } -} - -// Entry point -func main() { - kingpin.Version(ghtoc.Version) - kingpin.Parse() - - if *token == "" { - *token = os.Getenv("GH_TOC_TOKEN") - } - - pathsCount := len(*paths) - - // read file paths | urls from args - absPathsInToc := pathsCount > 1 - ch := make(chan *ghtoc.GHToc, pathsCount) - - for _, p := range *paths { - ghdoc := ghtoc.NewGHDoc(p, absPathsInToc, *startDepth, *depth, !*noEscape, *token, *indent, *debug) - getFn := func(ch chan *ghtoc.GHToc, ghdoc *ghtoc.GHDoc) { ch <- ghdoc.GetToc() } - if *serial { - getFn(ch, ghdoc) - } else { - go getFn(ch, ghdoc) - } - } - - if !*hideHeader && pathsCount == 1 { - fmt.Println() - fmt.Println("Table of Contents") - fmt.Println("=================") - fmt.Println() - } - - for i := 1; i <= pathsCount; i++ { - toc := <-ch - // #14, check if there's really TOC? - if toc != nil { - check(toc.Print(os.Stdout)) - } - } - - // read md from stdin - if pathsCount == 0 { - bytes, err := io.ReadAll(os.Stdin) - check(err) - - file, err := os.CreateTemp(os.TempDir(), "ghtoc") - check(err) - defer os.Remove(file.Name()) - - check(os.WriteFile(file.Name(), bytes, 0644)) - check(ghtoc.NewGHDoc(file.Name(), false, *startDepth, *depth, !*noEscape, *token, *indent, *debug). - GetToc(). - Print(os.Stdout)) - } - - if !*hideFooter { - fmt.Println("Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc.go)") - } -} diff --git a/markdown/tools/github_markdown_toc/ghdoc.go b/markdown/tools/github_markdown_toc/ghdoc.go deleted file mode 100644 index 8179c666..00000000 --- a/markdown/tools/github_markdown_toc/ghdoc.go +++ /dev/null @@ -1,206 +0,0 @@ -package ghtoc - -import ( - "fmt" - "io" - "log" - "net/url" - "os" - "strconv" - "strings" -) - -// GHToc GitHub TOC -type GHToc []string - -// Print TOC to the console -func (toc *GHToc) Print(w io.Writer) error { - for _, tocItem := range *toc { - if _, err := fmt.Fprintln(w, tocItem); err != nil { - return err - } - } - if _, err := fmt.Fprintln(w); err != nil { - return err - } - return nil -} - -type httpGetter func(urlPath string) ([]byte, string, error) -type httpPoster func(urlPath, filePath, token string) (string, error) - -// GHDoc GitHub document -type GHDoc struct { - Path string - AbsPaths bool - StartDepth int - Depth int - Escape bool - GhToken string - Indent int - Debug bool - html string - logger *log.Logger - httpGetter httpGetter - httpPoster httpPoster -} - -// NewGHDoc create GHDoc -func NewGHDoc(Path string, AbsPaths bool, StartDepth int, Depth int, Escape bool, Token string, Indent int, Debug bool) *GHDoc { - return &GHDoc{ - Path: Path, - AbsPaths: AbsPaths, - StartDepth: StartDepth, - Depth: Depth, - Escape: Escape, - GhToken: Token, - Indent: Indent, - Debug: Debug, - html: "", - logger: log.New(os.Stderr, "", log.LstdFlags), - httpGetter: httpGet, - httpPoster: httpPost, - } -} - -func (doc *GHDoc) d(msg string) { - if doc.Debug { - doc.logger.Println(msg) - } -} - -// IsRemoteFile checks if path is for remote file or not -func (doc *GHDoc) IsRemoteFile() bool { - u, err := url.Parse(doc.Path) - if err != nil || u.Scheme == "" { - doc.d("IsRemoteFile: false") - return false - } - doc.d("IsRemoteFile: true") - return true -} - -func (doc *GHDoc) convertMd2Html(localPath string, token string) (string, error) { - ghURL := "https://api.github.com/markdown/raw" - return doc.httpPoster(ghURL, localPath, token) -} - -// Convert2HTML downloads remote file -func (doc *GHDoc) Convert2HTML() error { - doc.d("Convert2HTML: start.") - defer doc.d("Convert2HTML: done.") - - if doc.IsRemoteFile() { - htmlBody, ContentType, err := doc.httpGetter(doc.Path) - doc.d("Convert2HTML: remote file. content-type: " + ContentType) - if err != nil { - return err - } - - // if not a plain text - return the result (should be html) - if strings.Split(ContentType, ";")[0] != "text/plain" { - doc.html = string(htmlBody) - return nil - } - - // if remote file's content is a plain text - // we need to convert it to html - tmpfile, err := os.CreateTemp("", "ghtoc-remote-txt") - if err != nil { - return err - } - defer tmpfile.Close() - doc.Path = tmpfile.Name() - if err = os.WriteFile(tmpfile.Name(), htmlBody, 0644); err != nil { - return err - } - } - doc.d("Convert2HTML: local file: " + doc.Path) - if _, err := os.Stat(doc.Path); os.IsNotExist(err) { - return err - } - htmlBody, err := doc.convertMd2Html(doc.Path, doc.GhToken) - doc.d("Convert2HTML: converted to html, size: " + strconv.Itoa(len(htmlBody))) - if err != nil { - return err - } - if doc.Debug { - htmlFile := doc.Path + ".debug.html" - doc.d("Convert2HTML: write html file: " + htmlFile) - if err := os.WriteFile(htmlFile, []byte(htmlBody), 0644); err != nil { - return err - } - } - doc.html = htmlBody - return nil -} - -// GrabToc gets TOC from html -func (doc *GHDoc) GrabToc() *GHToc { - doc.d("GrabToc: start, html size: " + strconv.Itoa(len(doc.html))) - defer doc.d("GrabToc: done.") - - listIndentation := generateListIndentation(doc.Indent) - - minDepth := doc.StartDepth - var maxDepth int - if doc.Depth > 0 { - maxDepth = doc.Depth - 1 - } else { - maxDepth = int(MaxHxDepth) - } - - hdrs := findHeadersInString(doc.html) - - // Determine the min depth represented by the slice of headers. For example, if a document only - // has H2 tags and no H1 tags. We want the H2 TOC entries to not have an indent. - minHxDepth := MaxHxDepth - for _, hdr := range hdrs { - if hdr.Depth < minHxDepth { - minHxDepth = hdr.Depth - } - } - - // Populate the toc with entries - toc := GHToc{} - for _, hdr := range hdrs { - hDepth := int(hdr.Depth) - if hDepth >= minDepth && hDepth <= maxDepth { - indentDepth := int(hdr.Depth) - int(minHxDepth) - doc.StartDepth - indent := strings.Repeat(listIndentation(), indentDepth) - toc = append(toc, doc.tocEntry(indent, hdr)) - } - } - - return &toc -} - -func (doc *GHDoc) tocEntry(indent string, hdr Header) string { - return indent + "* " + - "[" + doc.tocName(hdr.Name) + "]" + - "(" + doc.tocLink(hdr.Href) + ")" -} - -func (doc *GHDoc) tocName(name string) string { - if doc.Escape { - return EscapeSpecChars(name) - } - return name -} - -func (doc *GHDoc) tocLink(href string) string { - link, _ := url.QueryUnescape(href) - if doc.AbsPaths { - link = doc.Path + link - } - return link -} - -// GetToc return GHToc for a document -func (doc *GHDoc) GetToc() *GHToc { - if err := doc.Convert2HTML(); err != nil { - log.Fatal(err) - return nil - } - return doc.GrabToc() -} diff --git a/markdown/tools/github_markdown_toc/ghdoc_test.go b/markdown/tools/github_markdown_toc/ghdoc_test.go deleted file mode 100644 index 19136958..00000000 --- a/markdown/tools/github_markdown_toc/ghdoc_test.go +++ /dev/null @@ -1,546 +0,0 @@ -package ghtoc - -import ( - "bytes" - "errors" - "log" - "os" - "testing" -) - -func TestIsUrl(t *testing.T) { - doc1 := &GHDoc{ - Path: "https://github.com/ekalinin/envirius/blob/master/README.md", - } - if !doc1.IsRemoteFile() { - t.Error("This is url: ", doc1.Path) - } - - doc2 := &GHDoc{ - Path: "./README.md", - } - if doc2.IsRemoteFile() { - t.Error("This is not url: ", doc2.Path) - } -} - -func TestGrabTocOneRow(t *testing.T) { - tocExpected := []string{ - "* [README in another language](#readme-in-another-language)", - } - doc := &GHDoc{ - html: ` -
All plugins are in the directory -nv-plugins. -If you need to add support for a new language you should add it as plugin -inside this directory.
- -If you create a plugin which builds all stuff from source then In a simplest -case you need to implement 2 functions in the plugin's body:
- -This function should return list of available versions of the plugin. -For example:
- `, AbsPaths: false, - Escape: true, - Depth: 0, - Indent: 2, - } - toc := *doc.GrabToc() - for i := 0; i <= len(tocExpected)-1; i++ { - if toc[i] != tocExpected[i] { - t.Error("Res :", toc[i], "\nExpected :", tocExpected[i]) - } - } -} - -func TestGrabTocBackquoted(t *testing.T) { - tocExpected := []string{ - "* [The command foo1](#the-command-foo1)", - " * [The command foo2 is better](#the-command-foo2-is-better)", - "* [The command bar1](#the-command-bar1)", - " * [The command bar2 is better](#the-command-bar2-is-better)", - } - - doc := &GHDoc{ - html: ` -foo1
-Blabla...
- -foo2
is betterBlabla...
- -bar1
-Blabla...
- -bar2
is betterBlabla...
- `, AbsPaths: false, - Depth: 0, - Indent: 2, - } - toc := *doc.GrabToc() - for i := 0; i <= len(tocExpected)-1; i++ { - if toc[i] != tocExpected[i] { - t.Error("Res :", toc[i], "\nExpected :", tocExpected[i]) - } - } -} - -func TestGrabTocDepth(t *testing.T) { - tocExpected := []string{ - "* [The command foo1](#the-command-foo1)", - "* [The command bar1](#the-command-bar1)", - } - - doc := &GHDoc{ - html: ` -foo1
-Blabla...
- -foo2
is betterBlabla...
- -bar1
-Blabla...
- -bar2
is betterBlabla...
- `, AbsPaths: false, - Escape: true, - Depth: 1, - Indent: 2, - } - toc := *doc.GrabToc() - for i := 0; i <= len(tocExpected)-1; i++ { - if toc[i] != tocExpected[i] { - t.Error("Res :", toc[i], "\nExpected :", tocExpected[i]) - } - } -} - -func TestGrabTocStartDepth(t *testing.T) { - tocExpected := []string{ - "* [The command foo2 is better](#the-command-foo2-is-better)", - " * [The command foo3 is even betterer](#the-command-foo3-is-even-betterer)", - "* [The command bar2 is better](#the-command-bar2-is-better)", - " * [The command bar3 is even betterer](#the-command-bar3-is-even-betterer)", - } - - doc := &GHDoc{ - html: ` -foo1
-Blabla...
- -foo2
is betterBlabla...
- -foo3
is even bettererBlabla...
- -bar1
-Blabla...
- -bar2
is betterBlabla...
- -bar3
is even bettererBlabla...
- `, AbsPaths: false, - Escape: true, - StartDepth: 1, - Indent: 2, - } - toc := *doc.GrabToc() - for i := 0; i <= len(tocExpected)-1; i++ { - if toc[i] != tocExpected[i] { - t.Error("Res :", toc[i], "\nExpected :", tocExpected[i]) - } - } -} - -func TestGrabTocWithAbspath(t *testing.T) { - link := "https://github.com/ekalinin/envirius/blob/master/README.md" - tocExpected := []string{ - "* [README in another language](" + link + "#readme-in-another-language)", - } - doc := &GHDoc{ - html: ` -Uno
-Dos
-Tres
`, - AbsPaths: false, - Depth: 0, - Indent: 2, - } - - tocExpected := []string{ - "* [One](#one)", - " * [Two](#two)", - " * [Three](#three)", - } - toc := *doc.GrabToc() - - // Require not empty - if len(toc) == 0 { - t.Fatal("returned ToC is empty. GrabToc could not parse the HTML") - } - - // Assert equal - for i, tocActual := range toc { - if tocExpected[i] != tocActual { - t.Error("Res :", tocActual, "\nExpected :", tocExpected) - } - } -} diff --git a/markdown/tools/github_markdown_toc/headerfinder.go b/markdown/tools/github_markdown_toc/headerfinder.go deleted file mode 100644 index 32c5bc86..00000000 --- a/markdown/tools/github_markdown_toc/headerfinder.go +++ /dev/null @@ -1,122 +0,0 @@ -package ghtoc - -import ( - "io" - "strings" - - "golang.org/x/net/html" - "golang.org/x/net/html/atom" -) - -// HxDepth represents the header depth with H1 being 0. -type HxDepth int - -// InvalidDepth designates that the data atom is not a valid Hx. -const InvalidDepth HxDepth = -1 - -// MaxHxDepth is the maximum HxDepth value. -// H6 is the last Hx tag (5 = 6 - 1) -const MaxHxDepth HxDepth = 5 - -// Header represents an HTML header -type Header struct { - Depth HxDepth - Href string - Name string -} - -func findHeadersInString(str string) []Header { - r := strings.NewReader(str) - return findHeaders(r) -} - -func findHeaders(r io.Reader) []Header { - hdrs := make([]Header, 0) - tokenizer := html.NewTokenizer(r) - for { - tt := tokenizer.Next() - switch tt { - case html.ErrorToken: - return hdrs - case html.StartTagToken: - t := tokenizer.Token() - if hdr, ok := createHeader(tokenizer, t); ok { - hdrs = append(hdrs, hdr) - } - } - } -} - -func getHxDepth(dataAtom atom.Atom) HxDepth { - hxAtoms := []atom.Atom{ - atom.H1, - atom.H2, - atom.H3, - atom.H4, - atom.H5, - atom.H6, - } - for depth, hxAtom := range hxAtoms { - if dataAtom == hxAtom { - return HxDepth(depth) - } - } - return InvalidDepth -} - -func createHeader(tokenizer *html.Tokenizer, token html.Token) (Header, bool) { - hxDepth := getHxDepth(token.DataAtom) - if hxDepth == InvalidDepth { - return Header{}, false - } - - var href string - var nameParts []string - // Start at 1 because we are inside the Hx tag - tokenDepth := 1 - afterAnchor := false - for { - tokenizer.Next() - t := tokenizer.Token() - switch t.Type { - case html.ErrorToken: - return Header{}, false - case html.StartTagToken: - tokenDepth++ - if t.DataAtom == atom.A { - if hrefAttr, ok := findAttribute(t.Attr, "", "href"); ok { - href = hrefAttr.Val - } else { - // Expected to find href attribute - return Header{}, false - } - } - case html.EndTagToken: - switch t.DataAtom { - case token.DataAtom: - // If we encountered the matching end tag for the Hx, then we are done - return Header{ - Depth: hxDepth, - Name: removeStuff(strings.Join(nameParts, " ")), - Href: href, - }, true - case atom.A: - afterAnchor = true - } - tokenDepth-- - case html.TextToken: - if afterAnchor { - nameParts = append(nameParts, removeStuff(t.Data)) - } - } - } -} - -func findAttribute(attrs []html.Attribute, namespace, key string) (html.Attribute, bool) { - for _, attr := range attrs { - if attr.Namespace == namespace && attr.Key == key { - return attr, true - } - } - return html.Attribute{}, false -} diff --git a/markdown/tools/github_markdown_toc/headerfinder_test.go b/markdown/tools/github_markdown_toc/headerfinder_test.go deleted file mode 100644 index c8ada93c..00000000 --- a/markdown/tools/github_markdown_toc/headerfinder_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package ghtoc - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "golang.org/x/net/html" - "golang.org/x/net/html/atom" -) - -const singleH1 = ` -", "", -1)
- res = strings.Replace(res, "
", "", -1)
- res = strings.TrimSpace(res)
-
- return res
-}
-
-// generate func of custom spaces indentation
-func generateListIndentation(spaces int) func() string {
- return func() string {
- return strings.Repeat(" ", spaces)
- }
-}
-
-// Public
-
-// EscapeSpecChars Escapes special characters
-func EscapeSpecChars(s string) string {
- specChar := []string{"\\", "`", "*", "_", "{", "}", "#", "+", "-", ".", "!"}
- res := s
-
- for _, c := range specChar {
- res = strings.Replace(res, c, "\\"+c, -1)
- }
- return res
-}
diff --git a/markdown/tools/github_markdown_toc/cmd/gh-md-toc/BUILD.bazel b/markdown/tools/markdown_toc/cmd/generate_toc/BUILD.bazel
similarity index 65%
rename from markdown/tools/github_markdown_toc/cmd/gh-md-toc/BUILD.bazel
rename to markdown/tools/markdown_toc/cmd/generate_toc/BUILD.bazel
index 1e24efa5..1ad6c3c8 100644
--- a/markdown/tools/github_markdown_toc/cmd/gh-md-toc/BUILD.bazel
+++ b/markdown/tools/markdown_toc/cmd/generate_toc/BUILD.bazel
@@ -1,27 +1,26 @@
load("@cgrindel_bazel_starlib//bzlformat:defs.bzl", "bzlformat_pkg")
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
-filegroup(
- name = "all_files",
- srcs = glob(["*"]),
- visibility = ["//:__subpackages__"],
-)
+bzlformat_pkg(name = "bzlformat")
go_library(
- name = "gh-md-toc_lib",
+ name = "generate_toc_lib",
srcs = ["main.go"],
- importpath = "github.com/cgrindel/bazel-starlib/markdown/tools/github_markdown_toc/cmd/gh-md-toc",
+ importpath = "github.com/cgrindel/bazel-starlib/markdown/tools/markdown_toc/cmd/generate_toc",
visibility = ["//visibility:private"],
- deps = [
- "//markdown/tools/github_markdown_toc",
- "@in_gopkg_alecthomas_kingpin_v2//:kingpin_v2",
- ],
+ deps = ["//markdown/tools/markdown_toc/mdtoc"],
)
go_binary(
- name = "gh-md-toc",
- embed = [":gh-md-toc_lib"],
+ name = "generate_toc",
+ embed = [":generate_toc_lib"],
visibility = ["//visibility:public"],
)
-bzlformat_pkg(name = "bzlformat")
+# MARK: - Collect Files for Integation Tests
+
+filegroup(
+ name = "all_files",
+ srcs = glob(["*"]),
+ visibility = ["//:__subpackages__"],
+)
diff --git a/markdown/tools/markdown_toc/cmd/generate_toc/main.go b/markdown/tools/markdown_toc/cmd/generate_toc/main.go
new file mode 100644
index 00000000..873fa260
--- /dev/null
+++ b/markdown/tools/markdown_toc/cmd/generate_toc/main.go
@@ -0,0 +1,53 @@
+package main
+
+import (
+ "flag"
+ "io"
+ "log"
+ "os"
+
+ "github.com/cgrindel/bazel-starlib/markdown/tools/markdown_toc/mdtoc"
+)
+
+func main() {
+ var outputPath string
+ var startLevel int
+ flag.StringVar(&outputPath, "output", "", "path for the TOC output")
+ flag.IntVar(&startLevel, "start-level", 1, "starting heading level to render")
+ flag.Parse()
+
+ if startLevel < 1 {
+ log.Fatalf("Invalid start index %d", startLevel)
+ }
+
+ inputPath := flag.Arg(0)
+ var err error
+ var input []byte
+ if inputPath != "" {
+ input, err = os.ReadFile(inputPath)
+ if err != nil {
+ log.Fatalf("Failed to read input file at %s.\n", inputPath)
+ }
+ } else {
+ input, err = io.ReadAll(os.Stdin)
+ if err != nil {
+ log.Fatalln("Failed to read input from stdin.")
+ }
+ }
+ var outW io.Writer
+ if outputPath != "" {
+ outFile, err := os.Create(outputPath)
+ if err != nil {
+ log.Fatalf("Failed opening output file %s; %s", outputPath, err)
+ }
+ defer outFile.Close()
+ outW = outFile
+ } else {
+ outW = os.Stdout
+ }
+
+ toc := mdtoc.NewFromBytes(input)
+ if err = toc.FprintAtStartLevel(outW, startLevel); err != nil {
+ log.Fatalf("Failed printing TOC; %s", err)
+ }
+}
diff --git a/markdown/tools/github_markdown_toc/BUILD.bazel b/markdown/tools/markdown_toc/mdtoc/BUILD.bazel
similarity index 54%
rename from markdown/tools/github_markdown_toc/BUILD.bazel
rename to markdown/tools/markdown_toc/mdtoc/BUILD.bazel
index 5d4d3fb2..2c65d824 100644
--- a/markdown/tools/github_markdown_toc/BUILD.bazel
+++ b/markdown/tools/markdown_toc/mdtoc/BUILD.bazel
@@ -1,40 +1,38 @@
load("@cgrindel_bazel_starlib//bzlformat:defs.bzl", "bzlformat_pkg")
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
-filegroup(
- name = "all_files",
- srcs = glob(["*"]),
- visibility = ["//:__subpackages__"],
-)
+bzlformat_pkg(name = "bzlformat")
go_library(
- name = "github_markdown_toc",
+ name = "mdtoc",
srcs = [
- "ghdoc.go",
- "headerfinder.go",
- "internals.go",
+ "heading.go",
+ "table_of_contents.go",
],
- importpath = "github.com/cgrindel/bazel-starlib/markdown/tools/github_markdown_toc",
+ importpath = "github.com/cgrindel/bazel-starlib/markdown/tools/markdown_toc/mdtoc",
visibility = ["//visibility:public"],
deps = [
- "@org_golang_x_net//html",
- "@org_golang_x_net//html/atom",
+ "@com_github_gomarkdown_markdown//ast",
+ "@com_github_gomarkdown_markdown//parser",
],
)
go_test(
- name = "github_markdown_toc_test",
+ name = "mdtoc_test",
srcs = [
- "ghdoc_test.go",
- "headerfinder_test.go",
- "internal_test.go",
+ "heading_test.go",
+ "table_of_contents_test.go",
],
- embed = [":github_markdown_toc"],
deps = [
+ ":mdtoc",
"@com_github_stretchr_testify//assert",
- "@org_golang_x_net//html",
- "@org_golang_x_net//html/atom",
],
)
-bzlformat_pkg(name = "bzlformat")
+# MARK: - Collect Files for Integation Tests
+
+filegroup(
+ name = "all_files",
+ srcs = glob(["*"]),
+ visibility = ["//:__subpackages__"],
+)
diff --git a/markdown/tools/markdown_toc/mdtoc/heading.go b/markdown/tools/markdown_toc/mdtoc/heading.go
new file mode 100644
index 00000000..1fb42e64
--- /dev/null
+++ b/markdown/tools/markdown_toc/mdtoc/heading.go
@@ -0,0 +1,78 @@
+package mdtoc
+
+import (
+ "fmt"
+ "regexp"
+ "strings"
+
+ "github.com/gomarkdown/markdown/ast"
+)
+
+const indent = " "
+
+var anchorRejectedChars *regexp.Regexp
+
+func init() {
+ // Duplicate what Commonmark (Ruby wrapper for comrak) does.
+ // https://github.com/kivikakk/comrak/blob/main/src/html.rs#L107-L111
+ anchorRejectedChars = regexp.MustCompile(`[^\p{L}\p{M}\p{N}\p{Pc} -]`)
+}
+
+type Heading struct {
+ Title string
+ Text string // Title text without any modifiers
+ Level int
+}
+
+func NewHeading() *Heading {
+ return &Heading{Level: 1}
+}
+
+type HeadingWithable = func(h *Heading)
+
+func NewHeadingWith(withable HeadingWithable) *Heading {
+ heading := NewHeading()
+ withable(heading)
+ return heading
+}
+
+func newHeadingFromNode(headingNode *ast.Heading) *Heading {
+ var titleParts []string
+ var textParts []string
+ for _, childNode := range headingNode.Children {
+ ast.WalkFunc(childNode, func(node ast.Node, entering bool) ast.WalkStatus {
+ if !entering {
+ return ast.GoToNext
+ }
+ switch tnode := node.(type) {
+ case *ast.Text:
+ literal := string(tnode.Literal)
+ titleParts = append(titleParts, literal)
+ textParts = append(textParts, literal)
+ case *ast.Code:
+ literal := string(tnode.Literal)
+ title := fmt.Sprintf("`%s`", string(literal))
+ titleParts = append(titleParts, title)
+ textParts = append(textParts, literal)
+ }
+
+ return ast.GoToNext
+ })
+ }
+ return &Heading{
+ Title: strings.Join(titleParts, ""),
+ Text: strings.Join(textParts, ""),
+ Level: headingNode.Level,
+ }
+}
+
+func (h *Heading) AnchorID() string {
+ id := strings.ToLower(h.Text)
+ id = anchorRejectedChars.ReplaceAllString(id, "")
+ id = strings.ReplaceAll(id, " ", "-")
+ return id
+}
+
+func (h *Heading) MarkdownLink() string {
+ return fmt.Sprintf("[%s](#%s)", h.Title, h.AnchorID())
+}
diff --git a/markdown/tools/markdown_toc/mdtoc/heading_test.go b/markdown/tools/markdown_toc/mdtoc/heading_test.go
new file mode 100644
index 00000000..da7beec6
--- /dev/null
+++ b/markdown/tools/markdown_toc/mdtoc/heading_test.go
@@ -0,0 +1,49 @@
+package mdtoc_test
+
+import (
+ "testing"
+
+ "github.com/cgrindel/bazel-starlib/markdown/tools/markdown_toc/mdtoc"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestHeading(t *testing.T) {
+ t.Run("anchor ID", func(t *testing.T) {
+ tests := []struct {
+ msg string
+ h *mdtoc.Heading
+ exp string
+ }{
+ {
+ msg: "simple",
+ h: mdtoc.NewHeadingWith(func(h *mdtoc.Heading) {
+ h.Title = "Other Configuration"
+ h.Text = "Other Configuration"
+ }),
+ exp: "other-configuration",
+ },
+ {
+ msg: "code in title",
+ h: mdtoc.NewHeadingWith(func(h *mdtoc.Heading) {
+ h.Title = "`MODULE.bazel` Snippet"
+ h.Text = "MODULE.bazel Snippet"
+ }),
+ exp: "modulebazel-snippet",
+ },
+ }
+ for _, tt := range tests {
+ actual := tt.h.AnchorID()
+ assert.Equal(t, tt.exp, actual, tt.msg)
+ }
+ })
+
+ t.Run("markdown link", func(t *testing.T) {
+ heading := mdtoc.NewHeadingWith(func(h *mdtoc.Heading) {
+ h.Title = "Other Configuration"
+ h.Text = "Other Configuration"
+ })
+ expected := "[Other Configuration](#other-configuration)"
+ actual := heading.MarkdownLink()
+ assert.Equal(t, expected, actual)
+ })
+}
diff --git a/markdown/tools/markdown_toc/mdtoc/table_of_contents.go b/markdown/tools/markdown_toc/mdtoc/table_of_contents.go
new file mode 100644
index 00000000..5ac85084
--- /dev/null
+++ b/markdown/tools/markdown_toc/mdtoc/table_of_contents.go
@@ -0,0 +1,86 @@
+package mdtoc
+
+import (
+ "fmt"
+ "io"
+ "strings"
+
+ "github.com/gomarkdown/markdown/ast"
+ "github.com/gomarkdown/markdown/parser"
+)
+
+const singleIndent = " "
+
+type MarkdownBullet = string
+
+const (
+ AsteriskMarkdownBullet MarkdownBullet = "*"
+ HyphenMarkdownBullet MarkdownBullet = "-"
+)
+
+type TableOfContents struct {
+ Headings []*Heading
+ MarkdownBullet MarkdownBullet
+}
+
+func New() *TableOfContents {
+ return &TableOfContents{
+ Headings: nil,
+ MarkdownBullet: AsteriskMarkdownBullet,
+ }
+}
+
+type TableOfContentsWithable = func(toc *TableOfContents)
+
+func NewWith(withable TableOfContentsWithable) *TableOfContents {
+ toc := New()
+ withable(toc)
+ return toc
+}
+
+func NewFromBytes(b []byte) *TableOfContents {
+ // Cannot use parser.AutoHeadingIDs as it does not format the heading IDs as
+ // Commonmrker/comrak.
+ extensions := parser.CommonExtensions | parser.AutoHeadingIDs
+ p := parser.NewWithExtensions(extensions)
+ doc := p.Parse(b)
+ return newFromAST(doc)
+}
+
+func newFromAST(node ast.Node) *TableOfContents {
+ toc := New()
+ ast.WalkFunc(node, func(node ast.Node, entering bool) ast.WalkStatus {
+ if !entering {
+ return ast.GoToNext
+ }
+ if headingNode, ok := node.(*ast.Heading); ok {
+ heading := newHeadingFromNode(headingNode)
+ toc.Headings = append(toc.Headings, heading)
+ return ast.SkipChildren
+ }
+ return ast.GoToNext
+ })
+ return toc
+}
+
+func (toc *TableOfContents) Fprint(w io.Writer) (err error) {
+ return toc.FprintAtStartLevel(w, 1)
+}
+
+func (toc *TableOfContents) FprintAtStartLevel(w io.Writer, startLevel int) (err error) {
+ if startLevel < 1 {
+ return fmt.Errorf("invalid start level: %d", startLevel)
+ }
+ for _, h := range toc.Headings {
+ if h.Level < startLevel {
+ continue
+ }
+ indentCnt := h.Level - startLevel
+ indent := strings.Repeat(singleIndent, indentCnt)
+ _, err := fmt.Fprintf(w, "%s%s %s\n", indent, toc.MarkdownBullet, h.MarkdownLink())
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/markdown/tools/markdown_toc/mdtoc/table_of_contents_test.go b/markdown/tools/markdown_toc/mdtoc/table_of_contents_test.go
new file mode 100644
index 00000000..ccc0de63
--- /dev/null
+++ b/markdown/tools/markdown_toc/mdtoc/table_of_contents_test.go
@@ -0,0 +1,155 @@
+package mdtoc_test
+
+import (
+ "bytes"
+ "fmt"
+ "testing"
+
+ "github.com/cgrindel/bazel-starlib/markdown/tools/markdown_toc/mdtoc"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestNewFromBytes(t *testing.T) {
+ tests := []struct {
+ msg string
+ in string
+ exp *mdtoc.TableOfContents
+ }{
+ {
+ msg: "Level 1 heading",
+ in: "# Foo Bar",
+ exp: mdtoc.NewWith(func(toc *mdtoc.TableOfContents) {
+ toc.Headings = []*mdtoc.Heading{
+ mdtoc.NewHeadingWith(func(h *mdtoc.Heading) {
+ h.Title = "Foo Bar"
+ h.Text = "Foo Bar"
+ }),
+ }
+ }),
+ },
+ {
+ msg: "Level 2 heading",
+ in: "## Foo Bar",
+ exp: mdtoc.NewWith(func(toc *mdtoc.TableOfContents) {
+ toc.Headings = []*mdtoc.Heading{
+ mdtoc.NewHeadingWith(func(h *mdtoc.Heading) {
+ h.Title = "Foo Bar"
+ h.Text = "Foo Bar"
+ h.Level = 2
+ }),
+ }
+ }),
+ },
+ {
+ msg: "Code in title",
+ in: "# `MODULE.bazel` Snippet",
+ exp: mdtoc.NewWith(func(toc *mdtoc.TableOfContents) {
+ toc.Headings = []*mdtoc.Heading{
+ mdtoc.NewHeadingWith(func(h *mdtoc.Heading) {
+ h.Title = "`MODULE.bazel` Snippet"
+ h.Text = "MODULE.bazel Snippet"
+ }),
+ }
+ }),
+ },
+ }
+ for _, tt := range tests {
+ actual := mdtoc.NewFromBytes([]byte(tt.in))
+ assert.Equal(t, tt.exp, actual, tt.msg)
+ }
+}
+
+const multiLevelMarkdown = `
+# Heading 1
+
+Some text
+
+## Heading 2a
+
+More Text
+
+### Heading 3
+
+## Heading 2b
+`
+
+func TestTableOfContents(t *testing.T) {
+ t.Run("print at start level", func(t *testing.T) {
+ tests := []struct {
+ msg string
+ md string
+ level int
+ bullet mdtoc.MarkdownBullet
+ exp string
+ expErr error
+ }{
+ {
+ msg: "start at level 1",
+ md: multiLevelMarkdown,
+ level: 1,
+ bullet: mdtoc.HyphenMarkdownBullet,
+ exp: `- [Heading 1](#heading-1)
+ - [Heading 2a](#heading-2a)
+ - [Heading 3](#heading-3)
+ - [Heading 2b](#heading-2b)
+`,
+ },
+ {
+ msg: "start at level 2",
+ md: multiLevelMarkdown,
+ level: 2,
+ bullet: mdtoc.HyphenMarkdownBullet,
+ exp: `- [Heading 2a](#heading-2a)
+ - [Heading 3](#heading-3)
+- [Heading 2b](#heading-2b)
+`,
+ },
+ {
+ msg: "start at level 3",
+ md: multiLevelMarkdown,
+ level: 3,
+ bullet: mdtoc.HyphenMarkdownBullet,
+ exp: `- [Heading 3](#heading-3)
+`,
+ },
+ {
+ msg: "start at level 4",
+ md: multiLevelMarkdown,
+ level: 4,
+ bullet: mdtoc.AsteriskMarkdownBullet,
+ exp: ``,
+ },
+ {
+ msg: "asteris bullet",
+ md: multiLevelMarkdown,
+ level: 1,
+ bullet: mdtoc.AsteriskMarkdownBullet,
+ exp: `* [Heading 1](#heading-1)
+ * [Heading 2a](#heading-2a)
+ * [Heading 3](#heading-3)
+ * [Heading 2b](#heading-2b)
+`,
+ },
+ {
+ msg: "invalid level",
+ md: multiLevelMarkdown,
+ level: 0,
+ bullet: mdtoc.AsteriskMarkdownBullet,
+ expErr: fmt.Errorf("invalid start level: 0"),
+ },
+ }
+ for _, tt := range tests {
+ var b bytes.Buffer
+ toc := mdtoc.NewFromBytes([]byte(tt.md))
+ toc.MarkdownBullet = tt.bullet
+ err := toc.FprintAtStartLevel(&b, tt.level)
+ if tt.expErr == nil {
+ assert.NoError(t, err)
+ actual := b.String()
+ assert.Equal(t, tt.exp, actual, tt.msg)
+ } else {
+ assert.Equal(t, tt.expErr, err)
+ }
+ }
+ })
+}
diff --git a/markdown/tools/update_markdown_toc.sh b/markdown/tools/update_markdown_toc.sh
index 5f7868e2..6a74d65c 100755
--- a/markdown/tools/update_markdown_toc.sh
+++ b/markdown/tools/update_markdown_toc.sh
@@ -23,41 +23,21 @@ update_markdown_doc_sh_location=cgrindel_bazel_starlib/markdown/tools/update_mar
update_markdown_doc_sh="$(rlocation "${update_markdown_doc_sh_location}")" || \
(echo >&2 "Failed to locate ${update_markdown_doc_sh_location}" && exit 1)
-gh_md_toc_location=cgrindel_bazel_starlib/markdown/tools/github_markdown_toc/cmd/gh-md-toc/gh-md-toc_/gh-md-toc
-gh_md_toc="$(rlocation "${gh_md_toc_location}")" || \
- (echo >&2 "Failed to locate ${gh_md_toc_location}" && exit 1)
+generate_toc_location=cgrindel_bazel_starlib/markdown/tools/markdown_toc/cmd/generate_toc/generate_toc_/generate_toc
+generate_toc="$(rlocation "${generate_toc_location}")" || \
+ (echo >&2 "Failed to locate ${generate_toc_location}" && exit 1)
+
# MARK - Process args
remove_toc_header_entry=true
toc_header="Table of Contents"
-
-gh_md_toc_cmd=( "${gh_md_toc}" --hide-header --hide-footer --start-depth=1 )
+generate_toc_cmd=( "${generate_toc}" --start-level=2 )
args=()
while (("$#")); do
case "${1}" in
- "--no_escape")
- gh_md_toc_args+=( --no-escape )
- shift 1
- ;;
- "--debug")
- gh_md_toc_args+=( --debug )
- shift 1
- ;;
- "--start_depth")
- gh_md_toc_args+=( "--start-depth=${2}" )
- shift 2
- ;;
- "--depth")
- gh_md_toc_args+=( "--depth=${2}" )
- shift 2
- ;;
- "--indent")
- gh_md_toc_args+=( "--indent=${2}" )
- shift 2
- ;;
"--no_remove_toc_header_entry")
remove_toc_header_entry=false
shift 1
@@ -66,6 +46,9 @@ while (("$#")); do
toc_header="${2}"
shift 2
;;
+ --*)
+ fail "Unexpected flag ${1}"
+ ;;
*)
args+=("${1}")
shift 1
@@ -90,8 +73,8 @@ cleanup() {
trap cleanup EXIT
# Generate the TOC
-gh_md_toc_cmd+=( "${in_path}" )
-"${gh_md_toc_cmd[@]}" > "${toc_path}"
+generate_toc_cmd+=( "${in_path}" )
+"${generate_toc_cmd[@]}" > "${toc_path}"
# MARK - Clean up the TOC
diff --git a/tests/markdown_tests/tools_tests/markdown_toc_tests/BUILD.bazel b/tests/markdown_tests/tools_tests/markdown_toc_tests/BUILD.bazel
new file mode 100644
index 00000000..659a35fe
--- /dev/null
+++ b/tests/markdown_tests/tools_tests/markdown_toc_tests/BUILD.bazel
@@ -0,0 +1,15 @@
+load("@cgrindel_bazel_starlib//bzlformat:defs.bzl", "bzlformat_pkg")
+
+bzlformat_pkg(name = "bzlformat")
+
+sh_test(
+ name = "generate_toc_test",
+ srcs = ["generate_toc_test.sh"],
+ data = [
+ "//markdown/tools/markdown_toc/cmd/generate_toc",
+ ],
+ deps = [
+ "//shlib/lib:assertions",
+ "@bazel_tools//tools/bash/runfiles",
+ ],
+)
diff --git a/tests/markdown_tests/tools_tests/markdown_toc_tests/generate_toc_test.sh b/tests/markdown_tests/tools_tests/markdown_toc_tests/generate_toc_test.sh
new file mode 100755
index 00000000..929af60f
--- /dev/null
+++ b/tests/markdown_tests/tools_tests/markdown_toc_tests/generate_toc_test.sh
@@ -0,0 +1,77 @@
+#!/usr/bin/env bash
+
+# --- begin runfiles.bash initialization v3 ---
+# Copy-pasted from the Bazel Bash runfiles library v3.
+set -uo pipefail; set +e; f=bazel_tools/tools/bash/runfiles/runfiles.bash
+source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \
+ source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \
+ source "$0.runfiles/$f" 2>/dev/null || \
+ source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
+ source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
+ { echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e
+# --- end runfiles.bash initialization v3 ---
+
+assertions_sh_location=cgrindel_bazel_starlib/shlib/lib/assertions.sh
+assertions_sh="$(rlocation "${assertions_sh_location}")" || \
+ (echo >&2 "Failed to locate ${assertions_sh_location}" && exit 1)
+# shellcheck source=SCRIPTDIR/../../../../shlib/lib/assertions.sh
+source "${assertions_sh}"
+
+generate_toc_location=cgrindel_bazel_starlib/markdown/tools/markdown_toc/cmd/generate_toc/generate_toc_/generate_toc
+generate_toc="$(rlocation "${generate_toc_location}")" || \
+ (echo >&2 "Failed to locate ${generate_toc_location}" && exit 1)
+
+
+# MARK - Setup
+
+input_file="input.md"
+cat >"${input_file}" <<-EOF
+# Heading 1
+
+Some text
+
+## Heading 2a
+
+More Text
+
+### Heading 3
+
+## Heading 2b
+EOF
+
+# MARK - Test read stdin, write stdout, default start level
+
+msg="read stdin, write stdout, default start level "
+output="$( "${generate_toc}" < "${input_file}" )"
+assert_match "Heading 1" "${output}" "${msg}"
+assert_match "Heading 2" "${output}" "${msg}"
+
+# MARK - Test read stdin, write stdout, start level 2
+
+msg="read stdin, write stdout, start level 2"
+output="$( "${generate_toc}" --start-level 2 < "${input_file}" )"
+assert_no_match "Heading 1" "${output}" "${msg}"
+assert_match "Heading 2" "${output}" "${msg}"
+
+# MARK - Test read file, write stdout, default start level
+
+msg="read file, write stdout, default start level "
+output="$( "${generate_toc}" "${input_file}" )"
+assert_match "Heading 1" "${output}" "${msg}"
+assert_match "Heading 2" "${output}" "${msg}"
+
+# MARK - Test read file, write file, default start level
+
+msg="read file, write stdout, default start level "
+output_file="output.md"
+"${generate_toc}" --output "${output_file}" "${input_file}"
+output="$( < "${output_file}" )"
+assert_match "Heading 1" "${output}" "${msg}"
+assert_match "Heading 2" "${output}" "${msg}"
+
+# MARK - Test invalid start level
+
+msg="invalid start level"
+fail_result=false
+"${generate_toc}" --start-level 0 < "${input_file}" || fail_result=true
+assert_equal "true" "${fail_result}" "${msg}"