-
Notifications
You must be signed in to change notification settings - Fork 94
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- 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
Showing
74 changed files
with
426 additions
and
14,680 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.