Skip to content

Commit

Permalink
1.0.0
Browse files Browse the repository at this point in the history
- used mod instead of dep (removed vendor dir)
- refactoring (most of the logic now in the GHDoc)
- handle "API rate limit exceeded" error
- added bash/zsh autocomplete section into README
  • Loading branch information
ekalinin committed Nov 14, 2018
1 parent 82bb51b commit 09cbee6
Show file tree
Hide file tree
Showing 74 changed files with 426 additions and 14,680 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Table of Contents
* [Depth](#depth)
* [No Escape](#no-escape)
* [Github token](#github-token)
* [Bash/ZSH auto\-complete](#bashzsh-auto-complete)
* [LICENSE](#license)

Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc.go)
Expand Down Expand Up @@ -334,6 +335,21 @@ Table of Contents
* [LICENSE](#license)
```
Bash/ZSH auto-complete
----------------------
Just add a simple command into your `~/.bashrc` or `~/.zshrc`:
```bash
# for zsh
eval "$(gh-md-toc --completion-script-zsh)"
# for bash
eval "$(gh-md-toc --completion-script-bash)"
```
LICENSE
=======
Expand Down
173 changes: 173 additions & 0 deletions ghdoc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package main

import (
"fmt"
"io/ioutil"
"log"
"net/url"
"os"
"regexp"
"strconv"
"strings"
)

// GHToc GitHub TOC
type GHToc []string

// Print print TOC to the console
func (toc *GHToc) Print() {
for _, tocItem := range *toc {
fmt.Println(tocItem)
}
fmt.Println()
}

// GHDoc GitHub document
type GHDoc struct {
Path string
AbsPaths bool
Depth int
Escape bool
GhToken string
Indent int
Debug bool
html string
}

// NewGHDoc create GHDoc
func NewGHDoc(Path string, AbsPaths bool, Depth int, Escape bool, Token string, Indent int, Debug bool) *GHDoc {
return &GHDoc{Path, AbsPaths, Depth, Escape, Token, Indent, Debug, ""}
}

func (doc *GHDoc) d(msg string) {
if doc.Debug {
log.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
}

// Convert2HTML downloads remote file
func (doc *GHDoc) Convert2HTML() error {
doc.d("Convert2HTML: start.")
defer doc.d("Convert2HTML: done.")

if doc.IsRemoteFile() {
htmlBody, ContentType, err := httpGet(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 := ioutil.TempFile("", "ghtoc-remote-txt")
if err != nil {
return err
}
defer tmpfile.Close()
doc.Path = tmpfile.Name()
if err = ioutil.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 := ConvertMd2Html(doc.Path, doc.GhToken)
doc.d("Convert2HTML: converted to html, size: " + strconv.Itoa(len(htmlBody)))
if err != nil {
return err
}
// doc.d("Convert2HTML: " + htmlBody)
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.")

re := `(?si)<h(?P<num>[1-6])>\s*` +
`<a\s*id="user-content-[^"]*"\s*class="anchor"\s*` +
`href="(?P<href>[^"]*)"[^>]*>\s*` +
`.*?</a>(?P<name>.*?)</h`
r := regexp.MustCompile(re)
listIndentation := generateListIndentation(doc.Indent)

toc := GHToc{}
minHeaderNum := 6
var groups []map[string]string
doc.d("GrabToc: matching ...")
for idx, match := range r.FindAllStringSubmatch(doc.html, -1) {
doc.d("GrabToc: match #" + strconv.Itoa(idx) + " ...")
group := make(map[string]string)
// fill map for groups
for i, name := range r.SubexpNames() {
if i == 0 || name == "" {
continue
}
doc.d("GrabToc: process group: " + name + " ...")
group[name] = removeStuf(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 ...")
for _, group := range groups {
// format result
n, _ := strconv.Atoi(group["num"])
if doc.Depth > 0 && n > doc.Depth {
continue
}

link := group["href"]
if doc.AbsPaths {
link = doc.Path + link
}

tmpSection = removeStuf(group["name"])
if doc.Escape {
tmpSection = EscapeSpecChars(tmpSection)
}
tocItem := strings.Repeat(listIndentation(), n-minHeaderNum) + "* " +
"[" + tmpSection + "]" +
"(" + link + ")"
//fmt.Println(tocItem)
toc = append(toc, tocItem)
}

return &toc
}

// 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()
}
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module github.com/ekalinin/github-markdown-toc.go

require (
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf
gopkg.in/alecthomas/kingpin.v2 v2.2.4
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
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=
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=
111 changes: 111 additions & 0 deletions internals.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package main

import (
"bytes"
"errors"
"io"
"io/ioutil"
"net/http"
"os"
"strings"
)

// check checks if there whas an error and do panic if it was
func check(e error) {
if e != nil {
panic(e)
}
}

// doHTTPReq executes a particullar 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 := ioutil.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 string, filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()

body := &bytes.Buffer{}
io.Copy(body, file)

req, err := http.NewRequest("POST", urlPath, body)
if err != nil {
return "", err
}

req.Header.Set("Content-Type", "text/plain")

resp, _, err := doHTTPReq(req)
return string(resp), err
}

// removeStuf trims spaces, removes new lines and code tag from a string
func removeStuf(s string) string {
res := strings.Replace(s, "\n", "", -1)
res = strings.Replace(res, "<code>", "", -1)
res = strings.Replace(res, "</code>", "", -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
}

// ConvertMd2Html Sends Markdown to the github converter
// and returns html
func ConvertMd2Html(localpath string, token string) (string, error) {
url := "https://api.github.com/markdown/raw"
if token != "" {
url += "?access_token=" + token
}
return httpPost(url, localpath)
}
Loading

0 comments on commit 09cbee6

Please sign in to comment.