From fcff0fc8a2fdd0d47739e0d9cbc58bae977fce65 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Sun, 26 Feb 2023 18:10:04 +0300 Subject: [PATCH] Improvements --- .github/ISSUE_TEMPLATE.md | 5 +- .github/workflows/ci.yml | 2 +- README.md | 2 +- cli/cli.go | 465 ++++++++++++++++++++++++++++++++++++++ cli/support/support.go | 166 ++++++++++++++ go.mod | 5 +- go.sum | 2 + mdtoc.go | 385 +------------------------------ 8 files changed, 647 insertions(+), 385 deletions(-) create mode 100644 cli/cli.go create mode 100644 cli/support/support.go diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 52bcc40..27cb74c 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -4,10 +4,7 @@ _Before opening an issue, search for similar bug reports or feature requests on **System info:** -* **Version used (`mdtoc -v`):** -* **OS (e.g. from `/etc/*-release`):** -* **Kernel (`uname -a`):** -* **Go version (`go version`):** +* **Verbose version info (`mdtoc -vv`):** * **Install tools:** **Steps to reproduce:** diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fde3e7f..fc6f8db 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - go: [ '1.19.x' ] + go: [ '1.19.x', '1.20.x' ] steps: - name: Set up Go diff --git a/README.md b/README.md index 61f449b..97cd22e 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ #### From source -To build the MDToc from scratch, make sure you have a working Go 1.16+ workspace ([instructions](https://golang.org/doc/install)), then: +To build the MDToc from scratch, make sure you have a working Go 1.18+ workspace ([instructions](https://golang.org/doc/install)), then: ``` go install github.com/essentialkaos/mdtoc diff --git a/cli/cli.go b/cli/cli.go new file mode 100644 index 0000000..f67be37 --- /dev/null +++ b/cli/cli.go @@ -0,0 +1,465 @@ +package cli + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2023 ESSENTIAL KAOS // +// Apache License, Version 2.0 // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "bufio" + "fmt" + "os" + "regexp" + "strings" + + "github.com/essentialkaos/ek/v12/fmtc" + "github.com/essentialkaos/ek/v12/fmtutil" + "github.com/essentialkaos/ek/v12/fsutil" + "github.com/essentialkaos/ek/v12/mathutil" + "github.com/essentialkaos/ek/v12/options" + "github.com/essentialkaos/ek/v12/strutil" + "github.com/essentialkaos/ek/v12/usage" + "github.com/essentialkaos/ek/v12/usage/completion/bash" + "github.com/essentialkaos/ek/v12/usage/completion/fish" + "github.com/essentialkaos/ek/v12/usage/completion/zsh" + "github.com/essentialkaos/ek/v12/usage/man" + "github.com/essentialkaos/ek/v12/usage/update" + + "github.com/essentialkaos/mdtoc/cli/support" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// App info +const ( + APP = "MDToc" + VER = "1.2.5" + DESC = "Utility for generating table of contents for markdown files" +) + +// Options +const ( + OPT_MIN_LEVEL = "m:min-level" + OPT_MAX_LEVEL = "M:max-level" + OPT_FLAT = "f:flat" + OPT_HTML = "H:html" + OPT_NO_COLOR = "nc:no-color" + OPT_HELP = "h:help" + OPT_VER = "v:version" + + OPT_VERB_VER = "vv:verbose-version" + OPT_COMPLETION = "completion" + OPT_GENERATE_MAN = "generate-man" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// Header contains info about header +type Header struct { + Level int // Header level 1-7 + Text string // Header text + Link string // Link +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +var optMap = options.Map{ + OPT_MIN_LEVEL: {Type: options.INT, Value: 1, Min: 1, Max: 6}, + OPT_MAX_LEVEL: {Type: options.INT, Value: 6, Min: 1, Max: 6}, + OPT_FLAT: {Type: options.BOOL}, + OPT_HTML: {Type: options.BOOL}, + OPT_NO_COLOR: {Type: options.BOOL}, + OPT_HELP: {Type: options.BOOL}, + OPT_VER: {Type: options.BOOL}, + + OPT_VERB_VER: {Type: options.BOOL}, + OPT_COMPLETION: {}, + OPT_GENERATE_MAN: {Type: options.BOOL}, +} + +var anchorRegExp = regexp.MustCompile(`[\s\d\w-]`) +var badgeRegExp = regexp.MustCompile(`\[!\[[^\]]*\]\((.*?)\s*("(?:.*[^"])")?\s*\)\]\((.*?)\s*("(?:.*[^"])")?\s*\)`) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// Init is main function +func Init(gitRev string, gomod []byte) { + args, errs := options.Parse(optMap) + + if len(errs) != 0 { + for _, err := range errs { + printError(err.Error()) + } + + os.Exit(1) + } + + configureUI() + + switch { + case options.Has(OPT_COMPLETION): + os.Exit(genCompletion()) + case options.Has(OPT_GENERATE_MAN): + os.Exit(genMan()) + case options.GetB(OPT_VER): + showAbout(gitRev) + os.Exit(0) + case options.GetB(OPT_VERB_VER): + support.ShowSupportInfo(APP, VER, gitRev, gomod) + os.Exit(0) + case options.GetB(OPT_HELP): + showUsage() + os.Exit(0) + } + + var file string + + if len(args) == 0 { + file = findProperReadme() + + if file == "" { + showUsage() + os.Exit(0) + } + } else { + file = args.Get(0).Clean().String() + } + + checkFile(file) + process(file) +} + +// configureUI configures user interface +func configureUI() { + if options.GetB(OPT_NO_COLOR) { + fmtc.DisableColors = true + } + + fmtutil.SeparatorFullscreen = true + fmtutil.SeparatorSymbol = "–" + fmtutil.SeparatorColorTag = "{s-}" +} + +// findProperReadme tries to find readme file in current directory +func findProperReadme() string { + file := fsutil.ProperPath("FRS", []string{"README.md", "readme.md"}) + return file +} + +// checkFile checks markdown file before processing +func checkFile(file string) { + if !fsutil.IsExist(file) { + printErrorAndExit("Can't read file %s - file does not exist", file) + } + + if !fsutil.IsRegular(file) { + printErrorAndExit("Can't read file %s - is not a file", file) + } + + if !fsutil.IsReadable(file) { + printErrorAndExit("Can't read file %s - file is not readable", file) + } + + if !fsutil.IsNonEmpty(file) { + printErrorAndExit("Can't read file %s - file is empty", file) + } +} + +// process starts file processing +func process(file string) { + headers := extractHeaders(file) + + if len(headers) == 0 { + printWarn("Headers not found in given file") + return + } + + printTOC(headers) +} + +// extractHeaders extracts headers from markdown file +func extractHeaders(file string) []*Header { + fd, err := os.Open(file) + + if err != nil { + printErrorAndExit("File reading error: %v", err) + } + + defer fd.Close() + + reader := bufio.NewReader(fd) + scanner := bufio.NewScanner(reader) + + var headers []*Header + + for scanner.Scan() { + line := scanner.Text() + + if !strings.HasPrefix(line, "#") { + continue + } + + headers = append(headers, parseHeader(line)) + } + + return headers +} + +// printTOC collects headers and print ToC for given markdown file +func printTOC(headers []*Header) { + var toc string + + switch { + case !options.GetB(OPT_FLAT): + toc = renderTOC(headers) + case options.GetB(OPT_FLAT) && options.GetB(OPT_HTML): + toc = renderFlatHTMLTOC(headers) + case options.GetB(OPT_FLAT) && !options.GetB(OPT_HTML): + toc = renderFlatTOC(headers) + } + + if toc == "" { + printWarn("Suitable headers not found in given file") + return + } + + fmtutil.Separator(false) + fmtc.Println(toc) + fmtutil.Separator(false) +} + +// renderTOC renders headers as default (vertical) markdown ToC +func renderTOC(headers []*Header) string { + var toc []string + + minLevel := getMinLevel(headers) + + for _, header := range headers { + if !isSuitableHeader(header) { + continue + } + + toc = append(toc, fmtc.Sprintf( + "%s [%s](%s)", + getMarkdownListPrefix(header.Level, minLevel), + header.Text, header.Link, + )) + } + + return strings.Join(toc, "\n") +} + +// renderFlatTOC renders headers as flat (horizontal) markdown ToC +func renderFlatTOC(headers []*Header) string { + var toc []string + + for _, header := range headers { + if !isSuitableHeader(header) { + continue + } + + toc = append(toc, fmtc.Sprintf("[%s](%s)", header.Text, header.Link)) + } + + if len(toc) == 0 { + return "" + } + + return strings.Join(toc, " • ") +} + +// renderFlatTOC renders headers as flat (horizontal) HTML ToC +func renderFlatHTMLTOC(headers []*Header) string { + var toc []string + + for _, header := range headers { + if !isSuitableHeader(header) { + continue + } + + toc = append(toc, fmtc.Sprintf("%s", header.Link, header.Text)) + } + + if len(toc) == 0 { + return "" + } + + return "

" + strings.Join(toc, " • ") + "

" +} + +// isSuitableHeader returns true if header complies defined levels +func isSuitableHeader(header *Header) bool { + if header.Level < options.GetI(OPT_MIN_LEVEL) || header.Level > options.GetI(OPT_MAX_LEVEL) { + return false + } + + return true +} + +// parseHeader parses header text and return header struct +func parseHeader(text string) *Header { + header := &Header{} + + headerText := strings.TrimRight(text, " ") + headerText = removeBadges(headerText) + + header.Text, header.Level = parseHeaderText(headerText) + header.Link = makeLink(headerText) + + return header +} + +// makeLink converts header text to anchor link name +func makeLink(text string) string { + result := text + + result = strings.TrimLeft(result, "# ") + result = strings.Replace(result, " ", "-", -1) + result = strings.ToLower(result) + result = strings.Join(anchorRegExp.FindAllString(result, -1), "") + + return "#" + result +} + +// parseHeaderText parses text and return level and header +func parseHeaderText(text string) (string, int) { + level := strutil.PrefixSize(text, '#') + header := strings.TrimLeft(text, "# ") + header = strings.TrimRight(header, " ") + header = removeMarkdownTags(header) + + return header, level +} + +// removeMarkdownTags removes markdown tags from header +func removeMarkdownTags(header string) string { + for _, r := range "`_*~" { + if strings.Count(header, string(r))%2 == 0 { + header = strings.Replace(header, string(r), "", -1) + } + } + + return header +} + +// getMarkdownListPrefix returns list prefix for given level +func getMarkdownListPrefix(level, minLevel int) string { + return strings.Repeat(" ", level-minLevel) + "*" +} + +// getMinLevel returns minimal header level +func getMinLevel(headers []*Header) int { + result := 6 + + for _, header := range headers { + if !isSuitableHeader(header) { + continue + } + + result = mathutil.Min(result, header.Level) + } + + return result +} + +// removeBadges removes badges from header +func removeBadges(text string) string { + return badgeRegExp.ReplaceAllString(text, "") +} + +// printError prints error message to console +func printError(f string, a ...interface{}) { + fmtc.Fprintf(os.Stderr, "{r}"+f+"{!}\n", a...) +} + +// printError prints warning message to console +func printWarn(f string, a ...interface{}) { + fmtc.Fprintf(os.Stderr, "{y}"+f+"{!}\n", a...) +} + +// printErrorAndExit prints error mesage and exit with exit code 1 +func printErrorAndExit(f string, a ...interface{}) { + printError(f, a...) + os.Exit(1) +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// showUsage prints usage info +func showUsage() { + genUsage().Render() +} + +// showAbout prints info about version +func showAbout(gitRev string) { + genAbout(gitRev).Render() +} + +// genCompletion generates completion for different shells +func genCompletion() int { + info := genUsage() + + switch options.GetS(OPT_COMPLETION) { + case "bash": + fmt.Printf(bash.Generate(info, "mdtoc")) + case "fish": + fmt.Printf(fish.Generate(info, "mdtoc")) + case "zsh": + fmt.Printf(zsh.Generate(info, optMap, "mdtoc")) + default: + return 1 + } + + return 0 +} + +// genMan generates man page +func genMan() int { + fmt.Println( + man.Generate( + genUsage(), + genAbout(""), + ), + ) + + return 0 +} + +// genUsage generates usage info +func genUsage() *usage.Info { + info := usage.NewInfo("", "file") + + info.AddOption(OPT_FLAT, "Print flat (horizontal) ToC") + info.AddOption(OPT_HTML, "Render HTML ToC instead Markdown (works with {g}--flat{!})") + info.AddOption(OPT_MIN_LEVEL, "Minimal header level", "1-6") + info.AddOption(OPT_MAX_LEVEL, "Maximum header level", "1-6") + info.AddOption(OPT_NO_COLOR, "Disable colors in output") + info.AddOption(OPT_HELP, "Show this help message") + info.AddOption(OPT_VER, "Show version") + + info.AddExample("readme.md", "Generate table of contents for readme.md") + info.AddExample("-m 2 -M 4 readme.md", "Generate table of contents for readme.md with 2-4 level headers") + + return info +} + +// genAbout generates info about version +func genAbout(gitRev string) *usage.About { + about := &usage.About{ + App: APP, + Version: VER, + Desc: DESC, + Year: 2006, + Owner: "ESSENTIAL KAOS", + License: "Apache License, Version 2.0 ", + UpdateChecker: usage.UpdateChecker{"essentialkaos/mdtoc", update.GitHubChecker}, + } + + if gitRev != "" { + about.Build = "git:" + gitRev + } + + return about +} diff --git a/cli/support/support.go b/cli/support/support.go new file mode 100644 index 0000000..6fb1347 --- /dev/null +++ b/cli/support/support.go @@ -0,0 +1,166 @@ +package support + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2023 ESSENTIAL KAOS // +// Apache License, Version 2.0 // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "fmt" + "os" + "runtime" + "strings" + + "github.com/essentialkaos/ek/v12/fmtc" + "github.com/essentialkaos/ek/v12/fmtutil" + "github.com/essentialkaos/ek/v12/fsutil" + "github.com/essentialkaos/ek/v12/hash" + "github.com/essentialkaos/ek/v12/strutil" + "github.com/essentialkaos/ek/v12/system" + + "github.com/essentialkaos/depsy" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// ShowSupportInfo prints verbose info about application, system, dependencies and +// important environment +func ShowSupportInfo(app, ver, gitRev string, gomod []byte) { + fmtutil.SeparatorTitleColorTag = "{s-}" + fmtutil.SeparatorFullscreen = false + fmtutil.SeparatorColorTag = "{s-}" + fmtutil.SeparatorSize = 80 + + showApplicationInfo(app, ver, gitRev) + showOSInfo() + showDepsInfo(gomod) + + fmtutil.Separator(false) +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// showApplicationInfo shows verbose information about application +func showApplicationInfo(app, ver, gitRev string) { + fmtutil.Separator(false, "APPLICATION INFO") + + printInfo(7, "Name", app) + printInfo(7, "Version", ver) + + printInfo(7, "Go", fmtc.Sprintf( + "%s {s}(%s/%s){!}", + strings.TrimLeft(runtime.Version(), "go"), + runtime.GOOS, runtime.GOARCH, + )) + + if gitRev != "" { + if !fmtc.DisableColors && fmtc.IsTrueColorSupported() { + printInfo(7, "Git SHA", gitRev+getHashColorBullet(gitRev)) + } else { + printInfo(7, "Git SHA", gitRev) + } + } + + bin, _ := os.Executable() + binSHA := hash.FileHash(bin) + + if binSHA != "" { + binSHA = strutil.Head(binSHA, 7) + if !fmtc.DisableColors && fmtc.IsTrueColorSupported() { + printInfo(7, "Bin SHA", binSHA+getHashColorBullet(binSHA)) + } else { + printInfo(7, "Bin SHA", binSHA) + } + } +} + +// showOSInfo shows verbose information about system +func showOSInfo() { + osInfo, err := system.GetOSInfo() + + if err == nil { + fmtutil.Separator(false, "OS INFO") + + printInfo(12, "Name", osInfo.Name) + printInfo(12, "Pretty Name", osInfo.PrettyName) + printInfo(12, "Version", osInfo.VersionID) + printInfo(12, "ID", osInfo.ID) + printInfo(12, "ID Like", osInfo.IDLike) + printInfo(12, "Version ID", osInfo.VersionID) + printInfo(12, "Version Code", osInfo.VersionCodename) + printInfo(12, "CPE", osInfo.CPEName) + } + + systemInfo, err := system.GetSystemInfo() + + if err != nil { + return + } else { + if osInfo == nil { + fmtutil.Separator(false, "SYSTEM INFO") + printInfo(12, "Name", systemInfo.OS) + } + } + + printInfo(12, "Arch", systemInfo.Arch) + printInfo(12, "Kernel", systemInfo.Kernel) + + containerEngine := "No" + + switch { + case fsutil.IsExist("/.dockerenv"): + containerEngine = "Yes (Docker)" + case fsutil.IsExist("/run/.containerenv"): + containerEngine = "Yes (Podman)" + } + + fmtc.NewLine() + + printInfo(12, "Container", containerEngine) +} + +// showDepsInfo shows information about all dependencies +func showDepsInfo(gomod []byte) { + deps := depsy.Extract(gomod, false) + + if len(deps) == 0 { + return + } + + fmtutil.Separator(false, "DEPENDENCIES") + + for _, dep := range deps { + if dep.Extra == "" { + fmtc.Printf(" {s}%8s{!} %s\n", dep.Version, dep.Path) + } else { + fmtc.Printf(" {s}%8s{!} %s {s-}(%s){!}\n", dep.Version, dep.Path, dep.Extra) + } + } +} + +// getHashColorBullet return bullet with color from hash +func getHashColorBullet(v string) string { + if len(v) > 6 { + v = strutil.Head(v, 6) + } + + return fmtc.Sprintf(" {#" + strutil.Head(v, 6) + "}● {!}") +} + +// printInfo formats and prints info record +func printInfo(size int, name, value string) { + name = name + ":" + size++ + + if value == "" { + fm := fmt.Sprintf(" {*}%%-%ds{!} {s-}—{!}\n", size) + fmtc.Printf(fm, name) + } else { + fm := fmt.Sprintf(" {*}%%-%ds{!} %%s\n", size) + fmtc.Printf(fm, name, value) + } +} + +// ////////////////////////////////////////////////////////////////////////////////// // diff --git a/go.mod b/go.mod index 99e0ed2..cf76efe 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,9 @@ module github.com/essentialkaos/mdtoc go 1.17 -require github.com/essentialkaos/ek/v12 v12.60.0 +require ( + github.com/essentialkaos/depsy v1.0.0 + github.com/essentialkaos/ek/v12 v12.60.0 +) require golang.org/x/sys v0.5.0 // indirect diff --git a/go.sum b/go.sum index 59aeae2..57bcd24 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/essentialkaos/check v1.3.0 h1:ria+8o22RCLdt2D/1SHQsEH5Mmy5S+iWHaGHrrbPUc0= github.com/essentialkaos/check v1.3.0/go.mod h1:PhxzfJWlf5L/skuyhzBLIvjMB5Xu9TIyDIsqpY5MvB8= +github.com/essentialkaos/depsy v1.0.0 h1:FikBtTnNhk+xFO/hFr+CfiKs6QnA3wMD6tGL0XTEUkc= +github.com/essentialkaos/depsy v1.0.0/go.mod h1:XVsB2eVUonEzmLKQP3ig2P6v2+WcHVgJ10zm0JLqFMM= github.com/essentialkaos/ek/v12 v12.60.0 h1:Z0wGjnSAyJLHkbhlO27E/GfRqNFD11zPotEha7ygOzg= github.com/essentialkaos/ek/v12 v12.60.0/go.mod h1:QFEIBoGPE5ezTV08JYWlWLL5t8fwcdOe3/e7bhTJNW0= github.com/essentialkaos/go-linenoise/v3 v3.4.0/go.mod h1:t1kNLY2bSMQCy1JXOefD2BDLs/TTPMtTv3DFNV5uDSI= diff --git a/mdtoc.go b/mdtoc.go index 1e3188b..61271b3 100644 --- a/mdtoc.go +++ b/mdtoc.go @@ -8,392 +8,21 @@ package main // ////////////////////////////////////////////////////////////////////////////////// // import ( - "bufio" - "os" - "regexp" - "strings" + _ "embed" - "github.com/essentialkaos/ek/v12/fmtc" - "github.com/essentialkaos/ek/v12/fmtutil" - "github.com/essentialkaos/ek/v12/fsutil" - "github.com/essentialkaos/ek/v12/mathutil" - "github.com/essentialkaos/ek/v12/options" - "github.com/essentialkaos/ek/v12/strutil" - "github.com/essentialkaos/ek/v12/usage" - "github.com/essentialkaos/ek/v12/usage/update" + CLI "github.com/essentialkaos/mdtoc/cli" ) // ////////////////////////////////////////////////////////////////////////////////// // -// App info -const ( - APP = "MDToc" - VER = "1.2.4" - DESC = "Utility for generating table of contents for markdown files" -) - -// Options -const ( - OPT_MIN_LEVEL = "m:min-level" - OPT_MAX_LEVEL = "M:max-level" - OPT_FLAT = "f:flat" - OPT_HTML = "H:html" - OPT_NO_COLOR = "nc:no-color" - OPT_HELP = "h:help" - OPT_VER = "v:version" -) - -// ////////////////////////////////////////////////////////////////////////////////// // - -// Header contains info about header -type Header struct { - Level int // Header level 1-7 - Text string // Header text - Link string // Link -} +//go:embed go.mod +var gomod []byte -// ////////////////////////////////////////////////////////////////////////////////// // - -var optMap = options.Map{ - OPT_MIN_LEVEL: {Type: options.INT, Value: 1, Min: 1, Max: 6}, - OPT_MAX_LEVEL: {Type: options.INT, Value: 6, Min: 1, Max: 6}, - OPT_FLAT: {Type: options.BOOL}, - OPT_HTML: {Type: options.BOOL}, - OPT_NO_COLOR: {Type: options.BOOL}, - OPT_HELP: {Type: options.BOOL, Alias: "u:usage"}, - OPT_VER: {Type: options.BOOL, Alias: "ver"}, -} - -var anchorRegExp = regexp.MustCompile(`[\s\d\w-]`) -var badgeRegExp = regexp.MustCompile(`\[!\[[^\]]*\]\((.*?)\s*("(?:.*[^"])")?\s*\)\]\((.*?)\s*("(?:.*[^"])")?\s*\)`) +// gitrev is short hash of the latest git commit +var gitrev string // ////////////////////////////////////////////////////////////////////////////////// // func main() { - args, errs := options.Parse(optMap) - - if len(errs) != 0 { - for _, err := range errs { - printError(err.Error()) - } - - os.Exit(1) - } - - configureUI() - - if options.GetB(OPT_VER) { - showAbout() - return - } - - if options.GetB(OPT_HELP) { - showUsage() - return - } - - var file string - - if len(args) == 0 { - file = findProperReadme() - - if file == "" { - showUsage() - return - } - } else { - file = args.Get(0).Clean().String() - } - - checkFile(file) - process(file) -} - -// configureUI configures user interface -func configureUI() { - if options.GetB(OPT_NO_COLOR) { - fmtc.DisableColors = true - } - - fmtutil.SeparatorFullscreen = true - fmtutil.SeparatorSymbol = "–" - fmtutil.SeparatorColorTag = "{s-}" -} - -// findProperReadme tries to find readme file in current directory -func findProperReadme() string { - file := fsutil.ProperPath("FRS", []string{"README.md", "readme.md"}) - return file -} - -// checkFile checks markdown file before processing -func checkFile(file string) { - if !fsutil.IsExist(file) { - printErrorAndExit("Can't read file %s - file does not exist", file) - } - - if !fsutil.IsRegular(file) { - printErrorAndExit("Can't read file %s - is not a file", file) - } - - if !fsutil.IsReadable(file) { - printErrorAndExit("Can't read file %s - file is not readable", file) - } - - if !fsutil.IsNonEmpty(file) { - printErrorAndExit("Can't read file %s - file is empty", file) - } -} - -// process starts file processing -func process(file string) { - headers := extractHeaders(file) - - if len(headers) == 0 { - printWarn("Headers not found in given file") - return - } - - printTOC(headers) -} - -// extractHeaders extracts headers from markdown file -func extractHeaders(file string) []*Header { - fd, err := os.Open(file) - - if err != nil { - printErrorAndExit("File reading error: %v", err) - } - - defer fd.Close() - - reader := bufio.NewReader(fd) - scanner := bufio.NewScanner(reader) - - var headers []*Header - - for scanner.Scan() { - line := scanner.Text() - - if !strings.HasPrefix(line, "#") { - continue - } - - headers = append(headers, parseHeader(line)) - } - - return headers -} - -// printTOC collects headers and print ToC for given markdown file -func printTOC(headers []*Header) { - var toc string - - switch { - case !options.GetB(OPT_FLAT): - toc = renderTOC(headers) - case options.GetB(OPT_FLAT) && options.GetB(OPT_HTML): - toc = renderFlatHTMLTOC(headers) - case options.GetB(OPT_FLAT) && !options.GetB(OPT_HTML): - toc = renderFlatTOC(headers) - } - - if toc == "" { - printWarn("Suitable headers not found in given file") - return - } - - fmtutil.Separator(false) - fmtc.Println(toc) - fmtutil.Separator(false) -} - -// renderTOC renders headers as default (vertical) markdown ToC -func renderTOC(headers []*Header) string { - var toc []string - - minLevel := getMinLevel(headers) - - for _, header := range headers { - if !isSuitableHeader(header) { - continue - } - - toc = append(toc, fmtc.Sprintf( - "%s [%s](%s)", - getMarkdownListPrefix(header.Level, minLevel), - header.Text, header.Link, - )) - } - - return strings.Join(toc, "\n") -} - -// renderFlatTOC renders headers as flat (horizontal) markdown ToC -func renderFlatTOC(headers []*Header) string { - var toc []string - - for _, header := range headers { - if !isSuitableHeader(header) { - continue - } - - toc = append(toc, fmtc.Sprintf("[%s](%s)", header.Text, header.Link)) - } - - if len(toc) == 0 { - return "" - } - - return strings.Join(toc, " • ") -} - -// renderFlatTOC renders headers as flat (horizontal) HTML ToC -func renderFlatHTMLTOC(headers []*Header) string { - var toc []string - - for _, header := range headers { - if !isSuitableHeader(header) { - continue - } - - toc = append(toc, fmtc.Sprintf("%s", header.Link, header.Text)) - } - - if len(toc) == 0 { - return "" - } - - return "

" + strings.Join(toc, " • ") + "

" -} - -// isSuitableHeader returns true if header complies defined levels -func isSuitableHeader(header *Header) bool { - if header.Level < options.GetI(OPT_MIN_LEVEL) || header.Level > options.GetI(OPT_MAX_LEVEL) { - return false - } - - return true -} - -// parseHeader parses header text and return header struct -func parseHeader(text string) *Header { - header := &Header{} - - headerText := strings.TrimRight(text, " ") - headerText = removeBadges(headerText) - - header.Text, header.Level = parseHeaderText(headerText) - header.Link = makeLink(headerText) - - return header -} - -// makeLink converts header text to anchor link name -func makeLink(text string) string { - result := text - - result = strings.TrimLeft(result, "# ") - result = strings.Replace(result, " ", "-", -1) - result = strings.ToLower(result) - result = strings.Join(anchorRegExp.FindAllString(result, -1), "") - - return "#" + result -} - -// parseHeaderText parses text and return level and header -func parseHeaderText(text string) (string, int) { - level := strutil.PrefixSize(text, '#') - header := strings.TrimLeft(text, "# ") - header = strings.TrimRight(header, " ") - header = removeMarkdownTags(header) - - return header, level -} - -// removeMarkdownTags removes markdown tags from header -func removeMarkdownTags(header string) string { - for _, r := range "`_*~" { - if strings.Count(header, string(r))%2 == 0 { - header = strings.Replace(header, string(r), "", -1) - } - } - - return header -} - -// getMarkdownListPrefix returns list prefix for given level -func getMarkdownListPrefix(level, minLevel int) string { - return strings.Repeat(" ", level-minLevel) + "*" -} - -// getMinLevel returns minimal header level -func getMinLevel(headers []*Header) int { - result := 6 - - for _, header := range headers { - if !isSuitableHeader(header) { - continue - } - - result = mathutil.Min(result, header.Level) - } - - return result -} - -// removeBadges removes badges from header -func removeBadges(text string) string { - return badgeRegExp.ReplaceAllString(text, "") -} - -// printError prints error message to console -func printError(f string, a ...interface{}) { - fmtc.Fprintf(os.Stderr, "{r}"+f+"{!}\n", a...) -} - -// printError prints warning message to console -func printWarn(f string, a ...interface{}) { - fmtc.Fprintf(os.Stderr, "{y}"+f+"{!}\n", a...) -} - -// printErrorAndExit prints error mesage and exit with exit code 1 -func printErrorAndExit(f string, a ...interface{}) { - printError(f, a...) - os.Exit(1) -} - -// ////////////////////////////////////////////////////////////////////////////////// // - -// showUsage shows usage info -func showUsage() { - info := usage.NewInfo("", "file") - - info.AddOption(OPT_FLAT, "Print flat (horizontal) ToC") - info.AddOption(OPT_HTML, "Render HTML ToC instead Markdown (works with {g}--flat{!})") - info.AddOption(OPT_MIN_LEVEL, "Minimal header level", "1-6") - info.AddOption(OPT_MAX_LEVEL, "Maximum header level", "1-6") - info.AddOption(OPT_NO_COLOR, "Disable colors in output") - info.AddOption(OPT_HELP, "Show this help message") - info.AddOption(OPT_VER, "Show version") - - info.AddExample("readme.md", "Generate table of contents for readme.md") - info.AddExample("-m 2 -M 4 readme.md", "Generate table of contents for readme.md with 2-4 level headers") - - info.Render() -} - -// showAbout shows info about version -func showAbout() { - about := &usage.About{ - App: APP, - Version: VER, - Desc: DESC, - Year: 2006, - Owner: "ESSENTIAL KAOS", - License: "Apache License, Version 2.0 ", - UpdateChecker: usage.UpdateChecker{"essentialkaos/mdtoc", update.GitHubChecker}, - } - - about.Render() + CLI.Init(gitrev, gomod) }