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: ` -

README in another language

- `, - AbsPaths: false, - Depth: 0, - Indent: 2, - } - toc := *doc.GrabToc() - if toc[0] != tocExpected[0] { - t.Error("Res :", toc, "\nExpected :", tocExpected) - } -} - -func TestGrabTocOneRowWithNewLines(t *testing.T) { - tocExpected := []string{ - "* [README in another language](#readme-in-another-language)", - } - doc := &GHDoc{ - html: ` -

- - README in another language -

- `, AbsPaths: false, - Depth: 0, - Escape: true, - Indent: 2, - } - toc := *doc.GrabToc() - if toc[0] != tocExpected[0] { - t.Error("Res :", toc, "\nExpected :", tocExpected) - } -} - -func TestGrabTocMultilineOriginGithub(t *testing.T) { - - tocExpected := []string{ - "* [How to add a plugin?](#how-to-add-a-plugin)", - " * [Mandatory elements](#mandatory-elements)", - " * [plug\\_list\\_versions](#plug_list_versions)", - } - doc := &GHDoc{ - html: ` -

How to add a plugin?

- -

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.

- -

Mandatory elements

- -

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:

- -

plug_list_versions

- -

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: ` -

-The command foo1 -

- -

Blabla...

- -

-The command foo2 is better

- -

Blabla...

- -

-The command bar1 -

- -

Blabla...

- -

-The command bar2 is better

- -

Blabla...

- `, 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: ` -

-The command foo1 -

- -

Blabla...

- -

-The command foo2 is better

- -

Blabla...

- -

-The command bar1 -

- -

Blabla...

- -

-The command bar2 is better

- -

Blabla...

- `, 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: ` -

-The command foo1 -

- -

Blabla...

- -

-The command foo2 is better

- -

Blabla...

- -

-The command foo3 is even betterer

- -

Blabla...

- -

-The command bar1 -

- -

Blabla...

- -

-The command bar2 is better

- -

Blabla...

- -

-The command bar3 is even betterer

- -

Blabla...

- `, 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: ` -

README in another language

- `, AbsPaths: true, - Path: link, - Depth: 0, - Indent: 2, - } - toc := *doc.GrabToc() - if toc[0] != tocExpected[0] { - t.Error("Res :", toc, "\nExpected :", tocExpected) - } -} - -func TestEscapedChars(t *testing.T) { - tocExpected := []string{ - "* [mod\\_\\*](#mod_)", - } - - doc := &GHDoc{ - html: ` -

- - mod_* -

`, - AbsPaths: false, - Escape: true, - Depth: 0, - Indent: 2, - } - toc := *doc.GrabToc() - - if toc[0] != tocExpected[0] { - t.Error("Res :", toc, "\nExpected :", tocExpected) - } -} - -func TestCustomSpaceIndentation(t *testing.T) { - tocExpected := []string{ - "* [Header Level1](#header-level1)", - " * [Header Level2](#header-level2)", - " * [Header Level3](#header-level3)", - } - - doc := &GHDoc{ - html: ` -

-Header Level1 -

-

-Header Level2 -

-

-Header Level3 -

- `, - AbsPaths: false, - Depth: 0, - Indent: 4, - } - 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 TestMinHeaderNumber(t *testing.T) { - tocExpected := []string{ - "* [foo](#foo)", - " * [bar](#bar)", - } - - doc := &GHDoc{ - html: ` -

- - foo -

-

- - bar -

- `, - AbsPaths: false, - Depth: 0, - Indent: 2, - } - toc := *doc.GrabToc() - - if toc[0] != tocExpected[0] { - t.Error("Res :", toc, "\nExpected :", tocExpected) - } -} - -func TestGHTocPrint(t *testing.T) { - toc := GHToc{"one", "two"} - want := "one\ntwo\n\n" - var got bytes.Buffer - toc.Print(&got) - - if got.String() != want { - t.Error("\nGot :", got.String(), "\nWant:", want) - } -} - -func TestNewGHDocWithDebug(t *testing.T) { - noMatterN := 1 - noMatterS := "test" - noMatterB := false - var got bytes.Buffer - - doc := NewGHDoc(noMatterS, noMatterB, noMatterN, noMatterN, - noMatterB, noMatterS, noMatterN, true) - doc.logger = log.New(&got, "", 0) - - want := "test" - doc.d(want) - if got.String() != want+"\n" { - t.Error("\nGot :", got.String(), "\nWant:", want) - } -} - -func TestGHDocConvert2HTML(t *testing.T) { - remotePath := "https://github.com/some/readme.md" - token := "some-gh-token" - doc := NewGHDoc(remotePath, true, 0, 0, - true, token, 4, false) - - // mock for getting remote raw README text - htmlResponse := []byte("raw md text") - doc.httpGetter = func(urlPath string) ([]byte, string, error) { - if urlPath != remotePath { - t.Error("Wrong urlPath. \nGot :", urlPath, "\nWant:", remotePath) - } - return htmlResponse, "text/plain;utf-8", nil - } - - // mock for converting md to txt - ghURL := "https://api.github.com/markdown/raw" - htmlBody := `

header>

some text` - doc.httpPoster = func(urlPath, filePath, token string) (string, error) { - if urlPath != ghURL { - if urlPath != remotePath { - t.Error("Wrong urlPath. \nGot :", urlPath, "\nWant:", ghURL) - } - } - return htmlBody, nil - } - if err := doc.Convert2HTML(); err != nil { - t.Error("Got error:", err) - } - if doc.html != htmlBody { - t.Error("Wrong html. \nGot :", doc.html, "\nWant:", htmlBody) - } -} - -func TestGHDocConvert2HTMLNonPlainText(t *testing.T) { - remotePath := "https://github.com/some/readme.md" - token := "some-gh-token" - doc := NewGHDoc(remotePath, true, 0, 0, - true, token, 4, false) - - // mock for getting remote raw README text - htmlResponse := []byte("raw md text") - doc.httpGetter = func(_ string) ([]byte, string, error) { - return htmlResponse, "text/html;utf-8", nil - } - // should not call converter to HTML - doc.httpPoster = func(urlPath, filePath, token string) (string, error) { - t.Error("Should not call httpPost (via convertMd2Html)") - return "", nil - } - if err := doc.Convert2HTML(); err != nil { - t.Error("Got error:", err) - } - if doc.html != string(htmlResponse) { - t.Error("Wrong html. \nGot :", doc.html, "\nWant:", string(htmlResponse)) - } -} - -func TestGHDocConvert2HTMLErrorConvert(t *testing.T) { - remotePath := "https://github.com/some/readme.md" - token := "some-gh-token" - errGet := errors.New("error from http get") - doc := NewGHDoc(remotePath, true, 0, 0, - true, token, 4, false) - - // mock for getting remote raw README text - doc.httpGetter = func(urlPath string) ([]byte, string, error) { - return nil, "", errGet - } - - err := doc.Convert2HTML() - if err == nil { - t.Error("Should get error from http get!") - } - - if !errors.Is(err, errGet) { - t.Error("Wrong error. \nGot :", err, "\nWant:", errGet) - } -} - -func TestGHDocConvert2HTMLLocalFileNotExists(t *testing.T) { - localPath := "/some/readme.md" - token := "some-gh-token" - doc := NewGHDoc(localPath, true, 0, 0, - true, token, 4, false) - - // should not be called - doc.httpGetter = func(_ string) ([]byte, string, error) { - t.Error("Should not call httpGet") - return nil, "", nil - } - - err := doc.Convert2HTML() - if err == nil { - t.Error("Should get error from file checking.") - } - - if !errors.Is(err, os.ErrNotExist) { - t.Error("Wrong error. \nGot :", err, "\nWant:", os.ErrNotExist) - } -} - -// Cover the changes of `ioutil.*` to `os.*` in Convert2HTML. -func TestGHDocConvert2HTML_issue35(t *testing.T) { - remotePath := "https://github.com/some/readme.md" - token := "some-gh-token" - - // enable debug - doc := NewGHDoc(remotePath, true, 0, 0, true, token, 4, true) - - // mock for getting remote raw README text - htmlResponse := []byte("raw md text") - doc.httpGetter = func(urlPath string) ([]byte, string, error) { - return htmlResponse, "text/plain;utf-8", nil - } - - // mock for converting md to txt - htmlBody := `

header>

some text` - doc.httpPoster = func(urlPath, filePath, token string) (string, error) { - return htmlBody, nil - } - - if err := doc.Convert2HTML(); err != nil { - t.Error("Got error:", err) - } - - if doc.html != htmlBody { - t.Error("Wrong html. \nGot :", doc.html, "\nWant:", htmlBody) - } -} - -func TestGrabToc_issue35(t *testing.T) { - // As of 2022-08-25, GitHub API returns the HTML in the below format. - doc := &GHDoc{ - html: ` -

One

-

Uno

-

Two

-

Dos

-

Three

-

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 = ` -

Document Title

-` - -const singleH2 = ` -

- - Interesting Section -

-` - -const multipleSections = ` -

Document Title

-Hi -

First Section

-Some Text -

First Subsection

-

Second Section

-

Second Subsection

-` - -func TestFindHeaders(t *testing.T) { - t.Run("single H1", func(t *testing.T) { - results := findHeadersInString(singleH1) - assert.Len(t, results, 1) - assert.Equal( - t, - Header{Depth: 0, Href: "#document-title", Name: "Document Title"}, - results[0], - ) - }) - t.Run("single H2", func(t *testing.T) { - results := findHeadersInString(singleH2) - assert.Len(t, results, 1) - assert.Equal( - t, - Header{Depth: 1, Href: "#interesting-section", Name: "Interesting Section"}, - results[0], - ) - }) - t.Run("multiple sections", func(t *testing.T) { - results := findHeadersInString(multipleSections) - assert.Len(t, results, 5) - assert.Equal( - t, - Header{Depth: 0, Href: "#document-title", Name: "Document Title"}, - results[0], - ) - assert.Equal( - t, - Header{Depth: 1, Href: "#first-section", Name: "First Section"}, - results[1], - ) - assert.Equal( - t, - Header{Depth: 2, Href: "#first-subsection", Name: "First Subsection"}, - results[2], - ) - assert.Equal( - t, - Header{Depth: 1, Href: "#second-section", Name: "Second Section"}, - results[3], - ) - assert.Equal( - t, - Header{Depth: 3, Href: "#second-subsection", Name: "Second Subsection"}, - results[4], - ) - }) -} - -func TestFindAttribute(t *testing.T) { - worldGreeting := html.Attribute{Namespace: "", Key: "greeting", Val: "Hello, World!"} - spaceGreeting := html.Attribute{Namespace: "outer-space", Key: "greeting", Val: "Hello, Space!"} - attrs := []html.Attribute{spaceGreeting, worldGreeting} - t.Run("attribute exists", func(t *testing.T) { - attr, ok := findAttribute(attrs, "", "greeting") - assert.True(t, ok) - assert.Equal(t, worldGreeting, attr) - - attr, ok = findAttribute(attrs, "outer-space", "greeting") - assert.True(t, ok) - assert.Equal(t, spaceGreeting, attr) - }) - t.Run("attribute does not exist", func(t *testing.T) { - _, ok := findAttribute(attrs, "", "doesnotexist") - assert.False(t, ok) - }) -} - -func TestGetHxDepth(t *testing.T) { - assert.Equal(t, HxDepth(0), getHxDepth(atom.H1)) - assert.Equal(t, HxDepth(1), getHxDepth(atom.H2)) - assert.Equal(t, HxDepth(2), getHxDepth(atom.H3)) - assert.Equal(t, HxDepth(3), getHxDepth(atom.H4)) - assert.Equal(t, HxDepth(4), getHxDepth(atom.H5)) - assert.Equal(t, HxDepth(5), getHxDepth(atom.H6)) - assert.Equal(t, InvalidDepth, getHxDepth(atom.A)) -} diff --git a/markdown/tools/github_markdown_toc/internal_test.go b/markdown/tools/github_markdown_toc/internal_test.go deleted file mode 100644 index 87503051..00000000 --- a/markdown/tools/github_markdown_toc/internal_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package ghtoc - -import ( - "fmt" - "log" - "net/http" - "net/http/httptest" - "os" - "testing" -) - -func TestHttpGet(t *testing.T) { - expected := "dummy data" - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := fmt.Fprint(w, expected) - if err != nil { - println(err) - } - })) - defer srv.Close() - - body, _, err := httpGet(srv.URL) - got := string(body) - - if err != nil { - t.Error("Should not be err", err) - } - if got != expected { - t.Error("\nGot :", got, "\nWant:", expected) - } -} - -func TestHttpGetForbidden(t *testing.T) { - txt := "please, do not try" - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusForbidden) - _, err := fmt.Fprint(w, txt) - if err != nil { - println(err) - } - })) - defer srv.Close() - - _, _, err := httpGet(srv.URL) - if err == nil { - t.Error("Should not not be nil") - } -} - -func createTmp(content string) (string, error) { - tmpFile, err := os.CreateTemp("", "example.*.txt") - if err != nil { - log.Fatal(err) - } - - if _, err := tmpFile.Write([]byte(content)); err != nil { - if err := tmpFile.Close(); err != nil { - return "", err - } - log.Fatal(err) - } - if err := tmpFile.Close(); err != nil { - log.Fatal(err) - } - - return tmpFile.Name(), nil -} - -func TestHttpPost(t *testing.T) { - token := "xxx-token-yyy" - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { - t.Error("Should be POST") - } - tokenPassed := r.Header.Get("Authorization") - tokenWanted := "token " + token - if tokenPassed != tokenWanted { - t.Error("Should pass token", tokenWanted, ", but passed: ", tokenPassed) - } - })) - defer srv.Close() - - fileName, err := createTmp("#some title") - if err != nil { - t.Error("Should not be err", err) - } - defer os.Remove(fileName) - - _, err = httpPost(srv.URL, fileName, token) - if err != nil { - t.Error("Should not be err", err) - } -} - -// Cover the changes of ioutil.ReadAll to io.ReadAll in doHTTPReq. -func Test_doHTTPReq_issue35(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "Hello, client") - })) - defer srv.Close() - - dummyURL := srv.URL - - req, err := http.NewRequest("POST", dummyURL, nil) - if err != nil { - t.Fatal(err) - } - - resBody, resHeader, err := doHTTPReq(req) - - // Require no error - if err != nil { - t.Fatal("doHTTPReq should not be err:", err.Error()) - } - - // Assert response body - if string(resBody) != "Hello, client\n" { - t.Error("response body should be \"Hello, client\", but got:", string(resBody)) - } - // Assert response header - if resHeader != "text/plain; charset=utf-8" { - t.Error("response header should be \"Hello, client\", but got:", resHeader) - } -} diff --git a/markdown/tools/github_markdown_toc/internals.go b/markdown/tools/github_markdown_toc/internals.go deleted file mode 100644 index fa15e0e5..00000000 --- a/markdown/tools/github_markdown_toc/internals.go +++ /dev/null @@ -1,105 +0,0 @@ -package ghtoc - -import ( - "bytes" - "errors" - "io" - "net/http" - "os" - "strings" -) - -const ( - // Version is a current app version - Version = "1.2.0" - userAgent = "github-markdown-toc.go v" + Version -) - -// doHTTPReq executes a particular http request -func doHTTPReq(req *http.Request) ([]byte, string, error) { - req.Header.Set("User-Agent", userAgent) - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return []byte{}, "", err - } - - defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - return []byte{}, "", err - } - - if resp.StatusCode == http.StatusForbidden { - return []byte{}, resp.Header.Get("Content-type"), errors.New(string(body)) - } - - return body, resp.Header.Get("Content-type"), nil -} - -// Executes HTTP GET request -func httpGet(urlPath string) ([]byte, string, error) { - req, err := http.NewRequest("GET", urlPath, nil) - if err != nil { - return []byte{}, "", err - } - return doHTTPReq(req) -} - -// httpPost executes HTTP POST with file content -func httpPost(urlPath, filePath, token string) (string, error) { - file, err := os.Open(filePath) - if err != nil { - return "", err - } - defer file.Close() - - body := &bytes.Buffer{} - _, err = io.Copy(body, file) - if err != nil { - return "", err - } - - req, err := http.NewRequest("POST", urlPath, body) - if err != nil { - return "", err - } - - if token != "" { - req.Header.Add("Authorization", "token "+token) - } - req.Header.Set("Content-Type", "text/plain;charset=utf-8") - - resp, _, err := doHTTPReq(req) - return string(resp), err -} - -// removeStuff trims spaces, removes new lines and code tag from a string -func removeStuff(s string) string { - res := strings.Replace(s, "\n", "", -1) - res = strings.Replace(res, "", "", -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}"