`
+ }
+
+ return &ReGrabber{
+ cfg: cfg,
+ re: regexp.MustCompile(re),
+ }
+}
+
+func (g *ReGrabber) Grab(html string) (*entity.Toc, error) {
+
+ listIndentation := utils.GenerateListIndentation(g.cfg.Indent)
+
+ toc := entity.Toc{}
+ minHeaderNum := 6
+ var groups []map[string]string
+ // doc.d("GrabToc: matching ...")
+ for _, match := range g.re.FindAllStringSubmatch(html, -1) {
+ // doc.d("GrabToc: match #" + strconv.Itoa(idx) + " ...")
+ group := make(map[string]string)
+ // fill map for groups
+ for i, name := range g.re.SubexpNames() {
+ if i == 0 || name == "" {
+ continue
+ }
+ // doc.d("GrabToc: process group: " + name + ": " + match[i] + " ...")
+ group[name] = utils.RemoveStuff(match[i])
+ }
+ // update minimum header number
+ n, _ := strconv.Atoi(group["num"])
+ if n < minHeaderNum {
+ minHeaderNum = n
+ }
+ groups = append(groups, group)
+ }
+
+ var tmpSection string
+ // doc.d("GrabToc: processing groups ...")
+ // doc.d("Including starting from level " + strconv.Itoa(doc.StartDepth))
+ for _, group := range groups {
+ // format result
+ n, _ := strconv.Atoi(group["num"])
+ if n <= g.cfg.StartDepth {
+ continue
+ }
+ if g.cfg.Depth > 0 && n > g.cfg.Depth {
+ continue
+ }
+
+ link, _ := url.QueryUnescape(group["href"])
+ if g.cfg.AbsPaths {
+ link = g.cfg.Path + link
+ }
+
+ tmpSection = utils.RemoveStuff(group["name"])
+ if g.cfg.Escape {
+ tmpSection = utils.EscapeSpecChars(tmpSection)
+ }
+ tocItem := strings.Repeat(listIndentation(), n-minHeaderNum-g.cfg.StartDepth) + "* " +
+ "[" + tmpSection + "]" +
+ "(" + link + ")"
+ //fmt.Println(tocItem)
+ toc = append(toc, tocItem)
+ }
+
+ return &toc, nil
+}
diff --git a/internal/adapters/tocgrabber_re_test.go b/internal/adapters/tocgrabber_re_test.go
new file mode 100644
index 0000000..2c7b8fe
--- /dev/null
+++ b/internal/adapters/tocgrabber_re_test.go
@@ -0,0 +1,324 @@
+package adapters
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/ekalinin/github-markdown-toc.go/internal/version"
+)
+
+const (
+ HTML_README_OTHER_LANG_0 = `
+ README in another language
+ `
+ HTML_README_OTHER_LANG_2023_10 = `
+
+ `
+ HTML_README_OTHER_LANG_2024_03 = `
+ README in another language
+ `
+)
+
+type reTest struct {
+ html string
+ version string
+}
+
+func checkTest(t *testing.T, tests []reTest, cfg GrabberCfg, expected []string) {
+ for _, d := range tests {
+ t.Run(fmt.Sprintf("v.%s", d.version), func(t *testing.T) {
+ grabber := NewReGrabber("", cfg, d.version)
+ toc, _ := grabber.Grab(d.html)
+ if len(*toc) != len(expected) {
+ t.Errorf("Rows differs. Got: %d, want: %d (got toc=%v)\n",
+ len(*toc), len(expected), *toc)
+ }
+ for i, got := range *toc {
+ want := expected[i]
+ if got != want {
+ t.Errorf("\nGot : %s\nExpected: %s\n", got, want)
+ }
+ }
+ })
+ }
+}
+
+func Test_ReGrabberOneRow(t *testing.T) {
+ // https://raw.githubusercontent.com/ekalinin/envirius/f939d3b6882bfb6ecb28ef7b6e62862f934ba945/README.md
+ // $ go run cmd/gh-md-toc/main.go --debug https://raw.githubusercontent.com/ekalinin/envirius/f939d3b6882bfb6ecb28ef7b6e62862f934ba945/README.md
+ // $ grep "README in another" /var/folders/5t/spm0zsl13zx4p0b4z5s01d04qb6th3/T/ghtoc-remote-txt91529502.debug.html
+ expected := []string{
+ "* [README in another language](#readme-in-another-language)",
+ }
+
+ tests := []reTest{
+ {
+ HTML_README_OTHER_LANG_0,
+ version.GH_V0,
+ },
+ {
+ HTML_README_OTHER_LANG_2023_10,
+ version.GH_2023_10,
+ },
+ {
+ HTML_README_OTHER_LANG_2024_03,
+ version.GH_2024_03,
+ },
+ }
+ checkTest(t, tests, DefaultCfg(), expected)
+}
+
+func Test_ReGrabberOneRowWithNewLines(t *testing.T) {
+ // https://raw.githubusercontent.com/ekalinin/envirius/f939d3b6882bfb6ecb28ef7b6e62862f934ba945/README.md
+ // $ go run cmd/gh-md-toc/main.go --debug https://raw.githubusercontent.com/ekalinin/envirius/f939d3b6882bfb6ecb28ef7b6e62862f934ba945/README.md
+ expected := []string{
+ "* [README in another language](#readme-in-another-language)",
+ }
+ tests := []reTest{
+ {
+ `
+
+ `,
+ version.GH_2023_10,
+ },
+ }
+ checkTest(t, tests, DefaultCfg(), expected)
+}
+
+func Test_ReGrabberMultilineOriginGithub(t *testing.T) {
+ // https://github.com/ekalinin/envirius/blob/master/README.md#how-to-add-a-plugin
+ // $ go run cmd/gh-md-toc/main.go --debug https://raw.githubusercontent.com/ekalinin/envirius/f939d3b6882bfb6ecb28ef7b6e62862f934ba945/README.md
+ expected := []string{
+ "* [How to add a plugin?](#how-to-add-a-plugin)",
+ " * [Mandatory elements](#mandatory-elements)",
+ " * [plug\\_list\\_versions](#plug_list_versions)",
+ }
+ tests := []reTest{
+ {
+ 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:
+ `,
+ version: version.GH_2023_10,
+ },
+ {
+ 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:
+ `,
+ version: version.GH_2024_03,
+ },
+ }
+ checkTest(t, tests, DefaultCfg(), expected)
+}
+
+const (
+ HTML_MULTILINE_2023_10 = `
+
+Blabla...
+
+Blabla...
+
+Blabla...
+
+Blabla...
+
+Blabla...
+ `
+ HTML_MULTILINE_2024_03 = `
+
+Blabla...
+The command foo2
is better
+Blabla...
+
+Blabla...
+The command bar2
is better
+Blabla...
+The command bar3
is the best
+Blabla...
+ `
+)
+
+func Test_ReGrabberBackquoted(t *testing.T) {
+ // https://github.com/ekalinin/github-markdown-toc/blob/656b34011a482544a9ebb4116332c044834bdbbf/tests/test%20directory/test_backquote.md
+ // $ go run cmd/gh-md-toc/main.go --debug https://raw.githubusercontent.com/ekalinin/github-markdown-toc/656b34011a482544a9ebb4116332c044834bdbbf/tests/test%20directory/test_backquote.md
+ expected := []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)",
+ " * [The command bar3 is the best](#the-command-bar3-is-the-best)",
+ }
+
+ tests := []reTest{
+ {
+ html: HTML_MULTILINE_2023_10,
+ version: version.GH_2023_10,
+ },
+ {
+ html: HTML_MULTILINE_2024_03,
+ version: version.GH_2024_03,
+ },
+ }
+ checkTest(t, tests, DefaultCfg(), expected)
+}
+
+func Test_ReGrabberDepth(t *testing.T) {
+ // https://github.com/ekalinin/github-markdown-toc/blob/656b34011a482544a9ebb4116332c044834bdbbf/tests/test%20directory/test_backquote.md
+ // $ go run cmd/gh-md-toc/main.go --debug https://raw.githubusercontent.com/ekalinin/github-markdown-toc/656b34011a482544a9ebb4116332c044834bdbbf/tests/test%20directory/test_backquote.md
+ expected := []string{
+ "* [The command foo1](#the-command-foo1)",
+ "* [The command bar1](#the-command-bar1)",
+ }
+
+ tests := []reTest{
+ {
+ html: HTML_MULTILINE_2023_10,
+ version: version.GH_2023_10,
+ },
+ {
+ html: HTML_MULTILINE_2024_03,
+ version: version.GH_2024_03,
+ },
+ }
+
+ cfg := DefaultCfg()
+ cfg.Depth = 1
+ checkTest(t, tests, cfg, expected)
+}
+
+func Test_ReGrabberStartDepth(t *testing.T) {
+ // https://github.com/ekalinin/github-markdown-toc/blob/656b34011a482544a9ebb4116332c044834bdbbf/tests/test%20directory/test_backquote.md
+ // $ go run cmd/gh-md-toc/main.go --debug https://raw.githubusercontent.com/ekalinin/github-markdown-toc/656b34011a482544a9ebb4116332c044834bdbbf/tests/test%20directory/test_backquote.md
+ expected := []string{
+ "* [The command foo2 is better](#the-command-foo2-is-better)",
+ "* [The command bar2 is better](#the-command-bar2-is-better)",
+ " * [The command bar3 is the best](#the-command-bar3-is-the-best)",
+ }
+
+ tests := []reTest{
+ {
+ html: HTML_MULTILINE_2023_10,
+ version: version.GH_2023_10,
+ },
+ {
+ html: HTML_MULTILINE_2024_03,
+ version: version.GH_2024_03,
+ },
+ }
+
+ cfg := DefaultCfg()
+ cfg.StartDepth = 1
+ checkTest(t, tests, cfg, expected)
+}
+
+func Test_ReGrabberAbsPath(t *testing.T) {
+ // https://github.com/ekalinin/github-markdown-toc/blob/656b34011a482544a9ebb4116332c044834bdbbf/tests/test%20directory/test_backquote.md
+ // $ go run cmd/gh-md-toc/main.go --debug https://raw.githubusercontent.com/ekalinin/github-markdown-toc/656b34011a482544a9ebb4116332c044834bdbbf/tests/test%20directory/test_backquote.md
+ link := "https://github.com/ekalinin/envirius/blob/master/README.md"
+ expected := []string{
+ "* [README in another language](" + link + "#readme-in-another-language)",
+ }
+
+ tests := []reTest{
+ {
+ html: HTML_README_OTHER_LANG_2023_10,
+ version: version.GH_2023_10,
+ },
+ {
+ html: HTML_README_OTHER_LANG_2024_03,
+ version: version.GH_2024_03,
+ },
+ }
+ cfg := DefaultCfg()
+ cfg.AbsPaths = true
+ cfg.Path = link
+ checkTest(t, tests, cfg, expected)
+}
+
+func Test_ReGrabberEscapedChars(t *testing.T) {
+ expected := []string{
+ "* [mod\\_\\*](#mod_)",
+ }
+
+ tests := []reTest{
+ {
+ html: `
+
+ `,
+ version: version.GH_2023_10,
+ },
+ {
+ html: `
+
+ `,
+ version: version.GH_2024_03,
+ },
+ }
+ checkTest(t, tests, DefaultCfg(), expected)
+}
+
+func Test_ReGrabberCustomSpaceIndentation(t *testing.T) {
+ /*
+ $ cat test.md
+ # Header Level1
+ ## Header Level2
+ ### Header Level3
+ $ go run cmd/gh-md-toc/main.go --debug test.md
+ $ cat test.md.debug.html
+ */
+ expected := []string{
+ "* [Header Level1](#header-level1)",
+ " * [Header Level2](#header-level2)",
+ " * [Header Level3](#header-level3)",
+ }
+
+ tests := []reTest{
+ {
+ html: `
+
+
+
+ `,
+ version: version.GH_2023_10,
+ },
+ {
+ html: `
+Header Level1
+Header Level2
+Header Level3
+ `,
+ version: version.GH_2024_03,
+ },
+ }
+ cfg := DefaultCfg()
+ cfg.Indent = 4
+ checkTest(t, tests, cfg, expected)
+}
diff --git a/internal/app/config.go b/internal/app/config.go
new file mode 100644
index 0000000..7f7f4ca
--- /dev/null
+++ b/internal/app/config.go
@@ -0,0 +1,49 @@
+package app
+
+import (
+ "github.com/ekalinin/github-markdown-toc.go/internal/adapters"
+ "github.com/ekalinin/github-markdown-toc.go/internal/controller"
+)
+
+// copy of controller.Config
+type Config struct {
+ Files []string
+ Serial bool
+ HideHeader bool
+ HideFooter bool
+ StartDepth int
+ Depth int
+ NoEscape bool
+ Indent int
+ Debug bool
+ GHToken string
+ GHUrl string
+ GHVersion string
+}
+
+func (c Config) ToControllerConfig() controller.Config {
+ return controller.Config{
+ Files: c.Files,
+ Serial: c.Serial,
+ HideHeader: c.HideHeader,
+ HideFooter: c.HideFooter,
+ StartDepth: c.StartDepth,
+ Depth: c.Depth,
+ NoEscape: c.NoEscape,
+ Indent: c.Indent,
+ Debug: c.Debug,
+ GHToken: c.GHToken,
+ GHUrl: c.GHUrl,
+ GHVersion: c.GHVersion,
+ }
+}
+
+func (c Config) ToGrabberConfig() adapters.GrabberCfg {
+ return adapters.GrabberCfg{
+ AbsPaths: len(c.Files) > 0,
+ StartDepth: c.StartDepth,
+ Depth: c.Depth,
+ Escape: !c.NoEscape,
+ Indent: c.Indent,
+ }
+}
diff --git a/internal/app/config_test.go b/internal/app/config_test.go
new file mode 100644
index 0000000..9f1fb69
--- /dev/null
+++ b/internal/app/config_test.go
@@ -0,0 +1,110 @@
+package app
+
+import (
+ "slices"
+ "testing"
+)
+
+func Test_ConfigToControllerConfig(t *testing.T) {
+ cfg := Config{
+ Files: []string{"f1", "f2"},
+ Serial: true,
+ HideHeader: true,
+ HideFooter: true,
+ StartDepth: 10,
+ Depth: 20,
+ NoEscape: true,
+ Indent: 15,
+ Debug: true,
+ GHToken: "t1",
+ GHUrl: "some-url",
+ GHVersion: "some-version",
+ }
+ cfgCtrl := cfg.ToControllerConfig()
+
+ if !slices.Equal(cfg.Files, cfgCtrl.Files) {
+ t.Errorf("Files are not the same. Got=%v, want=%v\n", cfgCtrl.Files, cfg.Files)
+ }
+
+ if cfg.Serial != cfgCtrl.Serial {
+ t.Errorf("Serial is not the same. Got=%v, want=%v\n", cfgCtrl.Serial, cfg.Serial)
+ }
+
+ if cfg.HideHeader != cfgCtrl.HideHeader {
+ t.Errorf("HideHeader is not the same. Got=%v, want=%v\n", cfgCtrl.HideHeader, cfg.HideHeader)
+ }
+
+ if cfg.HideFooter != cfgCtrl.HideFooter {
+ t.Errorf("HideFooter is not the same. Got=%v, want=%v\n", cfgCtrl.HideFooter, cfg.HideFooter)
+ }
+
+ if cfg.StartDepth != cfgCtrl.StartDepth {
+ t.Errorf("StartDepth is not the same. Got=%v, want=%v\n", cfgCtrl.StartDepth, cfg.StartDepth)
+ }
+
+ if cfg.Depth != cfgCtrl.Depth {
+ t.Errorf("Depth is not the same. Got=%v, want=%v\n", cfgCtrl.Depth, cfg.Depth)
+ }
+
+ if cfg.NoEscape != cfgCtrl.NoEscape {
+ t.Errorf("NoEscape is not the same. Got=%v, want=%v\n", cfgCtrl.NoEscape, cfg.NoEscape)
+ }
+
+ if cfg.Indent != cfgCtrl.Indent {
+ t.Errorf("Indent is not the same. Got=%v, want=%v\n", cfgCtrl.Indent, cfg.Indent)
+ }
+
+ if cfg.Debug != cfgCtrl.Debug {
+ t.Errorf("Debug is not the same. Got=%v, want=%v\n", cfgCtrl.Debug, cfg.Debug)
+ }
+
+ if cfg.GHToken != cfgCtrl.GHToken {
+ t.Errorf("GHToken is not the same. Got=%v, want=%v\n", cfgCtrl.GHToken, cfg.GHToken)
+ }
+
+ if cfg.GHUrl != cfgCtrl.GHUrl {
+ t.Errorf("GHUrl is not the same. Got=%v, want=%v\n", cfgCtrl.GHUrl, cfg.GHUrl)
+ }
+
+ if cfg.GHVersion != cfgCtrl.GHVersion {
+ t.Errorf("GHVersion is not the same. Got=%v, want=%v\n", cfgCtrl.GHVersion, cfg.GHVersion)
+ }
+}
+
+func Test_ConfigToGrabberConfig(t *testing.T) {
+ cfg := Config{
+ Files: []string{"f1", "f2"},
+ Serial: true,
+ HideHeader: true,
+ HideFooter: true,
+ StartDepth: 10,
+ Depth: 20,
+ NoEscape: true,
+ Indent: 15,
+ Debug: true,
+ GHToken: "t1",
+ GHUrl: "some-url",
+ GHVersion: "some-version",
+ }
+ cfgGrbr := cfg.ToGrabberConfig()
+
+ if cfg.StartDepth != cfgGrbr.StartDepth {
+ t.Errorf("StartDepth is not the same. Got=%v, want=%v\n", cfgGrbr.StartDepth, cfg.StartDepth)
+ }
+
+ if cfg.Depth != cfgGrbr.Depth {
+ t.Errorf("Depth is not the same. Got=%v, want=%v\n", cfgGrbr.Depth, cfg.Depth)
+ }
+
+ if !cfgGrbr.AbsPaths {
+ t.Errorf("AbsPaths should be true. Got=%v\n", cfgGrbr.AbsPaths)
+ }
+
+ if cfg.NoEscape == cfgGrbr.Escape {
+ t.Errorf("NoEscape is the same. Got=%v, want=%v\n", cfgGrbr.Escape, cfg.NoEscape)
+ }
+
+ if cfg.Indent != cfgGrbr.Indent {
+ t.Errorf("Indent is not the same. Got=%v, want=%v\n", cfgGrbr.Indent, cfg.Indent)
+ }
+}
diff --git a/internal/app/new.go b/internal/app/new.go
new file mode 100644
index 0000000..fddf1ba
--- /dev/null
+++ b/internal/app/new.go
@@ -0,0 +1,50 @@
+package app
+
+import (
+ "io"
+
+ "github.com/ekalinin/github-markdown-toc.go/internal/adapters"
+ "github.com/ekalinin/github-markdown-toc.go/internal/controller"
+ "github.com/ekalinin/github-markdown-toc.go/internal/core/usecase"
+)
+
+type Controller interface {
+ Process(stdout io.Writer) error
+}
+
+type App struct {
+ cfg Config
+ ctl Controller
+}
+
+func New(cfg Config) *App {
+ log := adapters.NewLogger(cfg.Debug)
+
+ log.Info("App.New: init configs ...", "app cfg", cfg)
+ ctlCfg := cfg.ToControllerConfig()
+ ucCfg := ctlCfg.ToUseCaseConfig()
+
+ log.Info("App.New: init adapters ...")
+ checker := adapters.NewFileCheck(log)
+ writer := adapters.NewFileWriter(log)
+ converter := adapters.NewHTMLConverter(cfg.GHToken, cfg.GHUrl, log)
+ grabberRe := adapters.NewReGrabber("", cfg.ToGrabberConfig(), cfg.GHVersion)
+ grabberJson := adapters.NewJsonGrabber(cfg.ToGrabberConfig())
+ getter := adapters.NewRemoteGetter(true)
+ temper := adapters.NewFileTemper()
+
+ log.Info("App.New: init usecases ...")
+ ucLocalMD, ucRemoteMD, ucRemoteHTML := usecase.New(
+ ucCfg, checker, writer, converter, grabberRe, grabberJson,
+ getter, temper, log,
+ )
+
+ log.Info("App.New: init controller ...")
+ ctl := controller.New(ctlCfg, ucLocalMD, ucRemoteMD, ucRemoteHTML, log)
+
+ log.Info("App.New: done.")
+ return &App{
+ ctl: ctl,
+ cfg: cfg,
+ }
+}
diff --git a/internal/app/run.go b/internal/app/run.go
new file mode 100644
index 0000000..16394ca
--- /dev/null
+++ b/internal/app/run.go
@@ -0,0 +1,25 @@
+package app
+
+import (
+ "io"
+
+ "github.com/ekalinin/github-markdown-toc.go/internal/utils"
+)
+
+func (a *App) Run(stdout io.Writer) error {
+
+ // do not show for stdin case (Files is empty)
+ if !a.cfg.HideHeader && len(a.cfg.Files) == 1 {
+ utils.ShowHeader(stdout)
+ }
+
+ if err := a.ctl.Process(stdout); err != nil {
+ return err
+ }
+
+ if !a.cfg.HideFooter {
+ utils.ShowFooter(stdout)
+ }
+
+ return nil
+}
diff --git a/internal/app/run_test.go b/internal/app/run_test.go
new file mode 100644
index 0000000..f687281
--- /dev/null
+++ b/internal/app/run_test.go
@@ -0,0 +1,65 @@
+package app
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "testing"
+)
+
+type TestController struct {
+ err error
+ body string
+}
+
+func (c TestController) Process(stdout io.Writer) error {
+ if c.err != nil {
+ return c.err
+ }
+ if len(c.body) > 0 {
+ if _, err := fmt.Fprint(stdout, c.body); err != nil {
+ return err
+ }
+
+ }
+ return nil
+}
+
+func Test_AppRun(t *testing.T) {
+ ctl := TestController{}
+ app := App{
+ cfg: Config{
+ HideHeader: false,
+ HideFooter: false,
+ Files: []string{"aaa"},
+ },
+ ctl: ctl,
+ }
+
+ var b bytes.Buffer
+ if err := app.Run(&b); err != nil {
+ t.Error(err)
+ }
+
+ want := "\nTable of Contents\n=================\n\n" +
+ "Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc.go)\n"
+ if got := b.String(); got != want {
+ t.Errorf("\nWant=%s\n Got=%s", want, got)
+ }
+}
+
+func Test_AppRunFail(t *testing.T) {
+ errWant := errors.New("Proccess failed!")
+ ctl := TestController{err: errWant}
+ app := App{
+ cfg: Config{},
+ ctl: ctl,
+ }
+
+ var b bytes.Buffer
+ err := app.Run(&b)
+ if err.Error() != errWant.Error() {
+ t.Errorf("\nWant=%s\n Got=%s", errWant.Error(), err.Error())
+ }
+}
diff --git a/internal/controller/config.go b/internal/controller/config.go
new file mode 100644
index 0000000..8f25c12
--- /dev/null
+++ b/internal/controller/config.go
@@ -0,0 +1,37 @@
+package controller
+
+import (
+ "github.com/ekalinin/github-markdown-toc.go/internal/core/usecase/config"
+)
+
+type Config struct {
+ Files []string
+ Serial bool
+ HideHeader bool
+ HideFooter bool
+ StartDepth int
+ Depth int
+ NoEscape bool
+ Indent int
+ Debug bool
+ GHToken string
+ GHUrl string
+ GHVersion string
+}
+
+func (c Config) ToUseCaseConfig() config.Config {
+ return config.Config{
+ Serial: c.Serial,
+ HideHeader: c.HideHeader,
+ HideFooter: c.HideFooter,
+ StartDepth: c.StartDepth,
+ Depth: c.Depth,
+ NoEscape: c.NoEscape,
+ Indent: c.Indent,
+ Debug: c.Debug,
+ GHToken: c.GHToken,
+ GHUrl: c.GHUrl,
+ GHVersion: c.GHVersion,
+ AbsPathInToc: len(c.Files) > 1,
+ }
+}
diff --git a/internal/controller/file.go b/internal/controller/file.go
new file mode 100644
index 0000000..911656e
--- /dev/null
+++ b/internal/controller/file.go
@@ -0,0 +1,57 @@
+package controller
+
+import (
+ "errors"
+ "io"
+
+ "github.com/ekalinin/github-markdown-toc.go/internal/core/entity"
+)
+
+func (ctl *Controller) getUseCase(file string) useCase {
+ switch t := entity.GetType(file); t {
+ case entity.TypeLocalMD:
+ ctl.log.Info("Controller.ProcessFiles: detect use-case", "use-case", entity.TypeLocalMD)
+ return ctl.ucLocalMd
+ case entity.TypeRemoteMD:
+ ctl.log.Info("Controller.ProcessFiles: detect use-case", "use-case", entity.TypeRemoteMD)
+ return ctl.ucRemoteMD
+ case entity.TypeRemoteHTML:
+ ctl.log.Info("Controller.ProcessFiles: detect use-case", "use-case", entity.TypeRemoteHTML)
+ return ctl.ucRemoteHTML
+ }
+ ctl.log.Info("Controller.ProcessFiles: use-case is null")
+ return nil
+}
+
+func (ctl *Controller) ProcessFiles(stdout io.Writer, files ...string) error {
+ ctl.log.Info("Controller.ProcessFiles: start", "files", files)
+ cnt := len(files)
+
+ ch := make(chan *entity.Toc, cnt)
+ for _, file := range files {
+ ctl.log.Info("Controller.ProcessFiles: processing", "file", file)
+ uc := ctl.getUseCase(file)
+ if uc == nil {
+ return errors.New("useCase is null")
+ }
+
+ if ctl.cfg.Serial {
+ ch <- uc.Do(file)
+ } else {
+ go func(ucc useCase, path string) {
+ ch <- ucc.Do(path)
+ }(uc, file)
+ }
+ }
+
+ for i := 0; i < cnt; i++ {
+ toc := <-ch
+ // #14, check if there's really TOC?
+ if toc != nil {
+ if err := toc.Print(stdout); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
diff --git a/internal/controller/new.go b/internal/controller/new.go
new file mode 100644
index 0000000..e3f4c48
--- /dev/null
+++ b/internal/controller/new.go
@@ -0,0 +1,38 @@
+package controller
+
+import (
+ "io"
+ "os"
+
+ "github.com/ekalinin/github-markdown-toc.go/internal/core/entity"
+ "github.com/ekalinin/github-markdown-toc.go/internal/core/ports"
+)
+
+type useCase interface {
+ Do(string) *entity.Toc
+}
+
+type Controller struct {
+ cfg Config
+ ucLocalMd useCase
+ ucRemoteMD useCase
+ ucRemoteHTML useCase
+ log ports.Logger
+}
+
+func New(cfg Config, ucLocalMD useCase, ucRemoteMD useCase, ucRemoteHTML useCase, log ports.Logger) *Controller {
+ return &Controller{
+ cfg: cfg,
+ ucLocalMd: ucLocalMD,
+ ucRemoteMD: ucRemoteMD,
+ ucRemoteHTML: ucRemoteHTML,
+ log: log,
+ }
+}
+
+func (ctl *Controller) Process(stdout io.Writer) error {
+ if len(ctl.cfg.Files) > 0 {
+ return ctl.ProcessFiles(stdout, ctl.cfg.Files...)
+ }
+ return ctl.ProcessSTDIN(stdout, os.Stdin)
+}
diff --git a/internal/controller/stdin.go b/internal/controller/stdin.go
new file mode 100644
index 0000000..ca23964
--- /dev/null
+++ b/internal/controller/stdin.go
@@ -0,0 +1,31 @@
+package controller
+
+import (
+ "fmt"
+ "io"
+ "os"
+)
+
+func (ctl *Controller) ProcessSTDIN(stdout io.Writer, stding *os.File) error {
+ bytes, err := io.ReadAll(stding)
+ if err != nil {
+ return err
+ }
+
+ file, err := os.CreateTemp(os.TempDir(), "ghtoc")
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if err := os.Remove(file.Name()); err != nil {
+ _, _ = fmt.Fprintln(stdout, "Error during file delete:", err)
+ }
+ }()
+
+ err = os.WriteFile(file.Name(), bytes, 0644)
+ if err != nil {
+ return err
+ }
+
+ return ctl.ProcessFiles(stdout, file.Name())
+}
diff --git a/internal/core/entity/toc.go b/internal/core/entity/toc.go
new file mode 100644
index 0000000..30eee49
--- /dev/null
+++ b/internal/core/entity/toc.go
@@ -0,0 +1,41 @@
+package entity
+
+import (
+ "fmt"
+ "io"
+)
+
+type TocPrinter interface {
+ Fprintln(w io.Writer, a ...any) (n int, err error)
+}
+
+type TocPrinterDefault struct {
+}
+
+func (p TocPrinterDefault) Fprintln(w io.Writer, a ...any) (n int, err error) {
+ return fmt.Fprintln(w, a...)
+}
+
+type Toc []string
+
+func (toc *Toc) Print(w io.Writer) error {
+ printer := TocPrinterDefault{}
+ return toc.CustomPrint(w, printer)
+}
+
+func (toc *Toc) CustomPrint(w io.Writer, p TocPrinter) error {
+ for _, tocItem := range *toc {
+ if _, err := p.Fprintln(w, tocItem); err != nil {
+ return err
+ }
+ }
+ if _, err := p.Fprintln(w); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (toc Toc) At(idx int) string {
+ ss := []string(toc)
+ return ss[idx]
+}
diff --git a/internal/core/entity/toc_test.go b/internal/core/entity/toc_test.go
new file mode 100644
index 0000000..03a04de
--- /dev/null
+++ b/internal/core/entity/toc_test.go
@@ -0,0 +1,68 @@
+package entity
+
+import (
+ "bytes"
+ "errors"
+ "io"
+ "testing"
+)
+
+func Test_TocPrint(t *testing.T) {
+ tests := []struct {
+ name string
+ toc *Toc
+ want string
+ }{
+ {"Print", &Toc{"hello", "there"}, "hello\nthere\n\n"},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var b bytes.Buffer
+ if err := tt.toc.Print(&b); err != nil {
+ t.Errorf("failed print, err=%v", err)
+ }
+ if got := b.String(); got != tt.want {
+ t.Errorf("Got=%s, want=%s", got, tt.want)
+ }
+ })
+ }
+}
+
+func Test_TocAt(t *testing.T) {
+ toc := Toc{"hello", "there"}
+ got := toc.At(1)
+ if got != "there" {
+ t.Errorf("got: %s, want: %s\n", got, "there")
+ }
+}
+
+type TestPrinter struct {
+ n int
+ err string
+}
+
+func (p TestPrinter) Fprintln(w io.Writer, a ...any) (n int, err error) {
+ if p.err != "" {
+ return 0, errors.New(p.err)
+ }
+ return p.n, nil
+}
+
+func Test_TocCustomPrintFail(t *testing.T) {
+ toc := Toc{"hello", "there"}
+ printer := TestPrinter{0, "failed"}
+
+ var b bytes.Buffer
+ got := toc.CustomPrint(&b, printer)
+
+ if got == nil {
+ t.Errorf("should fail first print")
+ }
+
+ toc = Toc{}
+ got = toc.CustomPrint(&b, printer)
+
+ if got == nil {
+ t.Errorf("should fail last print")
+ }
+}
diff --git a/internal/core/entity/type.go b/internal/core/entity/type.go
new file mode 100644
index 0000000..18d7dca
--- /dev/null
+++ b/internal/core/entity/type.go
@@ -0,0 +1,25 @@
+package entity
+
+import (
+ "net/url"
+ "strings"
+)
+
+type Type int
+
+const (
+ TypeLocalMD Type = iota
+ TypeRemoteMD
+ TypeRemoteHTML
+)
+
+func GetType(path string) Type {
+ u, err := url.Parse(path)
+ if err != nil || u.Scheme == "" {
+ return TypeLocalMD
+ }
+ if strings.Contains(path, "githubusercontent.com") {
+ return TypeRemoteMD
+ }
+ return TypeRemoteHTML
+}
diff --git a/internal/core/entity/type_test.go b/internal/core/entity/type_test.go
new file mode 100644
index 0000000..adfdf2d
--- /dev/null
+++ b/internal/core/entity/type_test.go
@@ -0,0 +1,22 @@
+package entity
+
+import "testing"
+
+func Test_GetType(t *testing.T) {
+ tests := []struct {
+ name string
+ path string
+ result Type
+ }{
+ {"LocalMD", "./README.md", TypeLocalMD},
+ {"RemoteMD", "https://raw.githubusercontent.com/ekalinin/github-markdown-toc.go/master/README.md", TypeRemoteMD},
+ {"RemoteHTML", "https://github.com/ekalinin/github-markdown-toc.go/blob/master/README.md", TypeRemoteHTML},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := GetType(tt.path); got != tt.result {
+ t.Errorf("Want=%d, got=%d", tt.result, got)
+ }
+ })
+ }
+}
diff --git a/internal/core/ports/ports.go b/internal/core/ports/ports.go
new file mode 100644
index 0000000..413f1e3
--- /dev/null
+++ b/internal/core/ports/ports.go
@@ -0,0 +1,39 @@
+package ports
+
+import (
+ "os"
+
+ "github.com/ekalinin/github-markdown-toc.go/internal/core/entity"
+)
+
+type FileChecker interface {
+ Exists(file string) bool
+}
+
+type FileWriter interface {
+ Write(file string, data []byte) error
+}
+
+type HTMLConverter interface {
+ Convert(file string) (string, error)
+}
+
+type TocGrabber interface {
+ Grab(html string) (*entity.Toc, error)
+}
+
+type Logger interface {
+ Info(format string, v ...any)
+}
+
+type RemoteGetter interface {
+ Get(path string) ([]byte, string, error)
+}
+
+type FileTemper interface {
+ CreateTemp(dir, pattern string) (*os.File, error)
+}
+
+type RemotePoster interface {
+ Post(url, token, path string) (string, error)
+}
diff --git a/internal/core/usecase/config/config.go b/internal/core/usecase/config/config.go
new file mode 100644
index 0000000..db41a89
--- /dev/null
+++ b/internal/core/usecase/config/config.go
@@ -0,0 +1,16 @@
+package config
+
+type Config struct {
+ Serial bool
+ HideHeader bool
+ HideFooter bool
+ StartDepth int
+ Depth int
+ NoEscape bool
+ Indent int
+ Debug bool
+ GHToken string
+ GHUrl string
+ GHVersion string
+ AbsPathInToc bool
+}
diff --git a/internal/core/usecase/localmd/localmd.go b/internal/core/usecase/localmd/localmd.go
new file mode 100644
index 0000000..808ad33
--- /dev/null
+++ b/internal/core/usecase/localmd/localmd.go
@@ -0,0 +1,67 @@
+package localmd
+
+import (
+ "github.com/ekalinin/github-markdown-toc.go/internal/core/entity"
+ "github.com/ekalinin/github-markdown-toc.go/internal/core/ports"
+ "github.com/ekalinin/github-markdown-toc.go/internal/core/usecase/config"
+)
+
+// - read file
+// - call gh api (md->html)
+// - grab toc from html
+type LocalMd struct {
+ cfg config.Config
+ checker ports.FileChecker
+ writer ports.FileWriter
+ converter ports.HTMLConverter
+ grabber ports.TocGrabber
+
+ log ports.Logger
+}
+
+func New(cfg config.Config, checker ports.FileChecker, writer ports.FileWriter,
+ converter ports.HTMLConverter, grabber ports.TocGrabber, log ports.Logger) *LocalMd {
+ return &LocalMd{
+ cfg: cfg,
+ checker: checker,
+ writer: writer,
+ converter: converter,
+ grabber: grabber,
+ log: log,
+ }
+}
+
+func (uc *LocalMd) Do(file string) *entity.Toc {
+ uc.log.Info("LocalMD: Start", "file", file)
+ if !uc.checker.Exists(file) {
+ uc.log.Info("LocalMD: local file is not exists.")
+ return nil
+ }
+
+ uc.log.Info("LocalMD: converting to html ...")
+ html, err := uc.converter.Convert(file)
+ if err != nil {
+ uc.log.Info("LocalMD: Failed to convert MD into HTML: %s", err)
+ return nil
+ }
+
+ if uc.cfg.Debug {
+ htmlFile := file + ".debug.html"
+ uc.log.Info("LocalMD: writing html", "file", htmlFile)
+ // TODO: move to port
+ if err := uc.writer.Write(htmlFile, []byte(html)); err != nil {
+ uc.log.Info("writing html file error: %s", err)
+ return nil
+ }
+ }
+
+ uc.log.Info("LocalMD: grabbing the TOC ...")
+ toc, err := uc.grabber.Grab(html)
+ if err != nil {
+ uc.log.Info("LocalMD: failed to grab TOC: %s", err)
+ return nil
+ }
+
+ uc.log.Info("LocalMD: done.")
+ return toc
+}
diff --git a/internal/core/usecase/new.go b/internal/core/usecase/new.go
new file mode 100644
index 0000000..6b73935
--- /dev/null
+++ b/internal/core/usecase/new.go
@@ -0,0 +1,26 @@
+package usecase
+
+import (
+ "github.com/ekalinin/github-markdown-toc.go/internal/core/ports"
+ "github.com/ekalinin/github-markdown-toc.go/internal/core/usecase/config"
+ "github.com/ekalinin/github-markdown-toc.go/internal/core/usecase/localmd"
+ "github.com/ekalinin/github-markdown-toc.go/internal/core/usecase/remotehtml"
+ "github.com/ekalinin/github-markdown-toc.go/internal/core/usecase/remotemd"
+)
+
+func New(cfg config.Config,
+ checker ports.FileChecker,
+ writer ports.FileWriter,
+ converter ports.HTMLConverter,
+ grabberRe ports.TocGrabber,
+ grabberJson ports.TocGrabber,
+ getter ports.RemoteGetter,
+ temper ports.FileTemper,
+ log ports.Logger) (*localmd.LocalMd, *remotemd.RemoteMd, *remotehtml.RemoteHTML) {
+
+ ucLocalMD := localmd.New(cfg, checker, writer, converter, grabberRe, log)
+ ucRemoteMD := remotemd.New(cfg, getter, ucLocalMD, temper, writer, log)
+ ucRemoteHTML := remotehtml.New(cfg, getter, writer, temper, grabberJson, log)
+
+ return ucLocalMD, ucRemoteMD, ucRemoteHTML
+}
diff --git a/internal/core/usecase/remotehtml/remotehtml.go b/internal/core/usecase/remotehtml/remotehtml.go
new file mode 100644
index 0000000..1dd2f7f
--- /dev/null
+++ b/internal/core/usecase/remotehtml/remotehtml.go
@@ -0,0 +1,64 @@
+package remotehtml
+
+import (
+ "github.com/ekalinin/github-markdown-toc.go/internal/core/entity"
+ "github.com/ekalinin/github-markdown-toc.go/internal/core/ports"
+ "github.com/ekalinin/github-markdown-toc.go/internal/core/usecase/config"
+)
+
+// - download json file
+// - grab toc from json ()
+type RemoteHTML struct {
+ cfg config.Config
+ getter ports.RemoteGetter
+ grabber ports.TocGrabber
+ writer ports.FileWriter
+ tempter ports.FileTemper
+ log ports.Logger
+}
+
+func New(cfg config.Config, getter ports.RemoteGetter, writer ports.FileWriter,
+ temper ports.FileTemper, grabber ports.TocGrabber, log ports.Logger) *RemoteHTML {
+ return &RemoteHTML{cfg, getter, grabber, writer, temper, log}
+}
+
+func (r *RemoteHTML) Do(url string) *entity.Toc {
+ r.log.Info("RemoteHTML: start, downloading remote file ...", "url", url)
+ jsonBody, ContentType, err := r.getter.Get(url)
+ if err != nil {
+ r.log.Info("RemoteHTML: download fail", "err", err)
+ return nil
+ }
+ r.log.Info("RemoteHTML: got file", "content-type=", ContentType)
+
+ if r.cfg.Debug {
+ tmpfile, err := r.tempter.CreateTemp("", "ghtoc-remote-json-*")
+ if err != nil {
+ r.log.Info("RemoteHTML: creating file failed", "err", err)
+ return nil
+ }
+ defer func() {
+ if err := tmpfile.Close(); err != nil {
+ r.log.Info("RemoteHTML: closing file failed", "err", err)
+ }
+ }()
+ path := tmpfile.Name()
+
+ jsonFile := path + ".debug.json"
+ r.log.Info("RemoteHTML: writing json file", "path", jsonFile)
+ if err := r.writer.Write(jsonFile, jsonBody); err != nil {
+ r.log.Info("RemoteHTML: writing json file failed", "err", err)
+ return nil
+ }
+ }
+
+ r.log.Info("RemoteHTML: grabbing the TOC ...")
+ toc, err := r.grabber.Grab(string(jsonBody))
+ if err != nil {
+ r.log.Info("RemoteHTML: failed to grab TOC", "err", err)
+ return nil
+ }
+
+ r.log.Info("RemoteHTML: done.")
+ return toc
+}
diff --git a/internal/core/usecase/remotemd/remotemd.go b/internal/core/usecase/remotemd/remotemd.go
new file mode 100644
index 0000000..516f29c
--- /dev/null
+++ b/internal/core/usecase/remotemd/remotemd.go
@@ -0,0 +1,69 @@
+package remotemd
+
+import (
+ "strings"
+
+ "github.com/ekalinin/github-markdown-toc.go/internal/core/entity"
+ "github.com/ekalinin/github-markdown-toc.go/internal/core/ports"
+ "github.com/ekalinin/github-markdown-toc.go/internal/core/usecase/config"
+ "github.com/ekalinin/github-markdown-toc.go/internal/core/usecase/localmd"
+)
+
+// - download remote file
+// - call localmd use case
+type RemoteMd struct {
+ cfg config.Config
+ ucLocalMD *localmd.LocalMd
+ getter ports.RemoteGetter
+ temper ports.FileTemper
+ writer ports.FileWriter
+ log ports.Logger
+}
+
+func New(cfg config.Config, getter ports.RemoteGetter, localMD *localmd.LocalMd,
+ temper ports.FileTemper, writer ports.FileWriter, log ports.Logger) *RemoteMd {
+ return &RemoteMd{cfg, localMD, getter, temper, writer, log}
+}
+
+func (r *RemoteMd) download(url string) (string, error) {
+ body, ContentType, err := r.getter.Get(url)
+ if err != nil {
+ return "", err
+ }
+
+ // if not a plain text - it's an error
+ if strings.Split(ContentType, ";")[0] != "text/plain" {
+ r.log.Info("RemoteMD: not a plain text, stop.", "content-type", ContentType)
+ return "", err
+ }
+
+ // if remote file's content is a plain text
+ // we need to convert it to html
+ tmpfile, err := r.temper.CreateTemp("", "ghtoc-remote-txt-*")
+ if err != nil {
+ r.log.Info("RemoteMD: creating tmp file failed.", "err", err)
+ return "", err
+ }
+ defer func() {
+ if err := tmpfile.Close(); err != nil {
+ r.log.Info("RemoteMD: closing file failed", "err", err)
+ }
+ }()
+
+ path := tmpfile.Name()
+ r.log.Info("RemoteMD: save content into tmp file", "path", path)
+ if err = r.writer.Write(tmpfile.Name(), body); err != nil {
+ r.log.Info("RemoteMD: writing file failed.", "err", err)
+ return "", err
+ }
+ return path, nil
+}
+
+func (r *RemoteMd) Do(url string) *entity.Toc {
+ filename, err := r.download(url)
+ if err != nil {
+ r.log.Info("RemoteMD: download fail", "err", err)
+ return nil
+ }
+ return r.ucLocalMD.Do(filename)
+}
diff --git a/internal/utils.go b/internal/utils/utils.go
similarity index 73%
rename from internal/utils.go
rename to internal/utils/utils.go
index 738a079..b55c9f8 100644
--- a/internal/utils.go
+++ b/internal/utils/utils.go
@@ -1,4 +1,4 @@
-package internal
+package utils
import (
"bytes"
@@ -8,18 +8,22 @@ import (
"net/http"
"os"
"strings"
+
+ "github.com/ekalinin/github-markdown-toc.go/internal/version"
)
// doHTTPReq executes a particular http request
func doHTTPReq(req *http.Request) ([]byte, string, error) {
- req.Header.Set("User-Agent", userAgent)
+ req.Header.Set("User-Agent", version.UserAgent)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return []byte{}, "", err
}
- defer resp.Body.Close()
+ defer func() {
+ _ = resp.Body.Close()
+ }()
body, err := io.ReadAll(resp.Body)
if err != nil {
return []byte{}, "", err
@@ -52,12 +56,14 @@ func HttpGetJson(urlPath string) ([]byte, string, error) {
}
// HttpPost executes HTTP POST with file content.
-func HttpPost(urlPath, filePath, token string) (string, error) {
- file, err := os.Open(filePath)
+func HttpPost(url, path, token string) (string, error) {
+ file, err := os.Open(path)
if err != nil {
return "", err
}
- defer file.Close()
+ defer func() {
+ _ = file.Close()
+ }()
body := &bytes.Buffer{}
_, err = io.Copy(body, file)
@@ -65,7 +71,7 @@ func HttpPost(urlPath, filePath, token string) (string, error) {
return "", err
}
- req, err := http.NewRequest("POST", urlPath, body)
+ req, err := http.NewRequest("POST", url, body)
if err != nil {
return "", err
}
@@ -81,9 +87,9 @@ func HttpPost(urlPath, filePath, token string) (string, error) {
// 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.ReplaceAll(s, "\n", "")
+ res = strings.ReplaceAll(res, "", "")
+ res = strings.ReplaceAll(res, "
", "")
res = strings.TrimSpace(res)
return res
@@ -102,20 +108,20 @@ func EscapeSpecChars(s string) string {
res := s
for _, c := range specChar {
- res = strings.Replace(res, c, "\\"+c, -1)
+ res = strings.ReplaceAll(res, c, "\\"+c)
}
return res
}
// ShowHeader shows header befor TOC.
func ShowHeader(w io.Writer) {
- fmt.Fprintln(w)
- fmt.Fprintln(w, "Table of Contents")
- fmt.Fprintln(w, "=================")
- fmt.Fprintln(w)
+ _, _ = fmt.Fprintln(w)
+ _, _ = fmt.Fprintln(w, "Table of Contents")
+ _, _ = fmt.Fprintln(w, "=================")
+ _, _ = fmt.Fprintln(w)
}
// ShowFooter shows footer after TOC.
func ShowFooter(w io.Writer) {
- fmt.Fprintln(w, "Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc.go)")
+ _, _ = fmt.Fprintln(w, "Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc.go)")
}
diff --git a/internal/utils_test.go b/internal/utils/utils_test.go
similarity index 53%
rename from internal/utils_test.go
rename to internal/utils/utils_test.go
index f473a1a..e6f2f27 100644
--- a/internal/utils_test.go
+++ b/internal/utils/utils_test.go
@@ -1,17 +1,25 @@
-package internal
+package utils
import (
+ "bytes"
"fmt"
"log"
"net/http"
"net/http/httptest"
"os"
"testing"
+
+ "github.com/ekalinin/github-markdown-toc.go/internal/version"
)
func TestHttpGet(t *testing.T) {
expected := "dummy data"
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ ua := r.Header.Get("User-Agent")
+ if ua != version.UserAgent {
+ t.Errorf("User-agent should be=%s, got=%s\n", version.UserAgent, ua)
+ }
+
_, err := fmt.Fprint(w, expected)
if err != nil {
println(err)
@@ -30,6 +38,41 @@ func TestHttpGet(t *testing.T) {
}
}
+func TestHttpGetJson(t *testing.T) {
+ expected := "dummy data"
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ ua := r.Header.Get("User-Agent")
+ if ua != version.UserAgent {
+ t.Errorf("User-agent should be=%s, got=%s\n", version.UserAgent, ua)
+ }
+ want := "application/json"
+ ctGot := r.Header.Get("Content-type")
+ if ctGot != want {
+ t.Errorf("Content-type should be=%s, got=%s\n", want, ctGot)
+ }
+ acGot := r.Header.Get("Accept")
+ if acGot != want {
+ t.Errorf("Accept should be=%s, got=%s\n", want, acGot)
+ }
+
+ _, err := fmt.Fprint(w, expected)
+ if err != nil {
+ println(err)
+ }
+ }))
+ defer srv.Close()
+
+ body, _, err := HttpGetJson(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) {
@@ -84,7 +127,9 @@ func TestHttpPost(t *testing.T) {
if err != nil {
t.Error("Should not be err", err)
}
- defer os.Remove(fileName)
+ defer func() {
+ _ = os.Remove(fileName)
+ }()
_, err = HttpPost(srv.URL, fileName, token)
if err != nil {
@@ -95,7 +140,7 @@ func TestHttpPost(t *testing.T) {
// 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")
+ _, _ = fmt.Fprintln(w, "Hello, client")
}))
defer srv.Close()
@@ -122,3 +167,54 @@ func Test_doHTTPReq_issue35(t *testing.T) {
t.Error("response header should be \"Hello, client\", but got:", resHeader)
}
}
+
+func Test_RemoveStuff(t *testing.T) {
+ tests := []struct {
+ name string
+ in string
+ want string
+ }{
+ {"All", "\n\nsome code
here\n", "some code here"},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := RemoveStuff(tt.in)
+ if got != tt.want {
+ t.Errorf("Got=%s, want=%s\n", got, tt.want)
+ }
+ })
+ }
+}
+
+func Test_GenerateListIndentations(t *testing.T) {
+ f := GenerateListIndentation(2)
+ if got := f(); got != " " {
+ t.Errorf("Got='%s', want=' '", got)
+ }
+}
+
+func Test_EscapeSpecChars(t *testing.T) {
+ in := `abc\*_{}`
+ want := "abc\\\\\\*\\_\\{\\}"
+ got := EscapeSpecChars(in)
+ if got != want {
+ t.Errorf("Got=%s, want=%s", got, want)
+ }
+}
+
+func Test_ShowHeaderFooter(t *testing.T) {
+ var b bytes.Buffer
+
+ ShowHeader(&b)
+ want := "\nTable of Contents\n=================\n\n"
+ if got := b.String(); got != want {
+ t.Errorf("\nWant=%s\n Got=%s", want, got)
+ }
+
+ b.Reset()
+ ShowFooter(&b)
+ want = "Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc.go)\n"
+ if got := b.String(); got != want {
+ t.Errorf("\nWant=%s\n Got=%s", want, got)
+ }
+}
diff --git a/internal/version.go b/internal/version/version.go
similarity index 71%
rename from internal/version.go
rename to internal/version/version.go
index f629ac3..27238e2 100644
--- a/internal/version.go
+++ b/internal/version/version.go
@@ -1,9 +1,9 @@
-package internal
+package version
const (
// Version is a current app version
Version = "1.4.0"
- userAgent = "github-markdown-toc.go v" + Version
+ UserAgent = "github-markdown-toc.go v" + Version
)
// Versions of GH layouts