├── LICENSE ├── README.md ├── converter └── convert.go ├── go.mod ├── go.sum ├── md2html.go ├── templates ├── footer.html └── header.html └── utils └── files.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Kevin Wan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mdconv 2 | 3 | A tool to convert `markdown` files to `html` files. 4 | 5 | ## Usage 6 | 7 | `md2html -i -o ` 8 | 9 | ### Options supported 10 | 11 | - `-i` - specify the input `markdown` file 12 | - `-o` - specify the output `html` file 13 | - `-header` - specify the `html` header template file, default to use embedded header 14 | - `-footer` - specify the `html` footer template file, default to use embedded footer 15 | 16 | -------------------------------------------------------------------------------- /converter/convert.go: -------------------------------------------------------------------------------- 1 | package converter 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/kennygrant/sanitize" 10 | "github.com/kevwan/blackfriday" 11 | ) 12 | 13 | const headerMark = "#" 14 | 15 | func Convert(body []byte) []byte { 16 | toc := buildToc(body) 17 | content := buildContent(body) 18 | return append(toc, content...) 19 | } 20 | 21 | func MarkdownToHtml(header, footer string, body []byte) ([]byte, error) { 22 | var buffer bytes.Buffer 23 | buffer.WriteString(fmt.Sprintf(string(header), extractTitle(body))) 24 | buffer.Write(Convert(body)) 25 | buffer.WriteString(footer) 26 | 27 | return buffer.Bytes(), nil 28 | } 29 | 30 | func buildToc(body []byte) []byte { 31 | title := extractTitle(body) 32 | renderer := blackfriday.HtmlRenderer(blackfriday.HTML_TOC|blackfriday.HTML_OMIT_CONTENTS, title, "") 33 | return blackfriday.Markdown(body, renderer, getExtensions()) 34 | } 35 | 36 | func buildContent(body []byte) []byte { 37 | title := extractTitle(body) 38 | renderer := blackfriday.HtmlRenderer(blackfriday.HTML_TOC|blackfriday.HTML_OMIT_TOC, title, "") 39 | return blackfriday.Markdown(body, renderer, getExtensions()) 40 | } 41 | 42 | func extractTitle(body []byte) string { 43 | scanner := bufio.NewScanner(bytes.NewReader(body)) 44 | 45 | for scanner.Scan() { 46 | line := strings.TrimSpace(scanner.Text()) 47 | if !strings.HasPrefix(line, headerMark) { 48 | continue 49 | } 50 | 51 | for { 52 | if strings.HasPrefix(line, headerMark) { 53 | line = strings.TrimPrefix(line, headerMark) 54 | } else { 55 | break 56 | } 57 | } 58 | 59 | return strings.TrimSpace(sanitize.HTML(line)) 60 | } 61 | 62 | return "" 63 | } 64 | 65 | func getExtensions() int { 66 | extensions := 0 67 | extensions |= blackfriday.EXTENSION_NO_INTRA_EMPHASIS 68 | extensions |= blackfriday.EXTENSION_TABLES 69 | extensions |= blackfriday.EXTENSION_FENCED_CODE 70 | extensions |= blackfriday.EXTENSION_AUTOLINK 71 | extensions |= blackfriday.EXTENSION_STRIKETHROUGH 72 | extensions |= blackfriday.EXTENSION_SPACE_HEADERS 73 | return extensions 74 | } 75 | 76 | func stripchars(str, chr string) string { 77 | return strings.Map(func(r rune) rune { 78 | if strings.IndexRune(chr, r) < 0 { 79 | return r 80 | } 81 | return -1 82 | }, str) 83 | } 84 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kevwan/mdconv 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/kennygrant/sanitize v1.2.4 7 | github.com/kevwan/blackfriday v0.0.0-20161114141135-f788a0de6a70 8 | ) 9 | 10 | require ( 11 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect 12 | golang.org/x/net v0.0.0-20211029224645-99673261e6eb // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o= 2 | github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak= 3 | github.com/kevwan/blackfriday v0.0.0-20161114141135-f788a0de6a70 h1:yACwYIAUIkL/reREc917+nWT1M+g9py8wMzoyg+y4tQ= 4 | github.com/kevwan/blackfriday v0.0.0-20161114141135-f788a0de6a70/go.mod h1:rdyaZHyTZV14JMa89LyHyk8xD03K+15XJH3kLkskGOI= 5 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 6 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 7 | golang.org/x/net v0.0.0-20211029224645-99673261e6eb h1:pirldcYWx7rx7kE5r+9WsOXPXK0+WH5+uZ7uPmJ44uM= 8 | golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 9 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 10 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 11 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 12 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 13 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 14 | -------------------------------------------------------------------------------- /md2html.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "flag" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | "path" 11 | 12 | "github.com/kevwan/mdconv/converter" 13 | ) 14 | 15 | const ( 16 | embedDir = "templates" 17 | embedHeaderFile = "header.html" 18 | embedFooterFile = "footer.html" 19 | ) 20 | 21 | var ( 22 | input = flag.String("i", "", "markdown file") 23 | output = flag.String("o", "", "output html file") 24 | headerFile = flag.String("header", "", "the header template file, default to use embedded template") 25 | footerFile = flag.String("footer", "", "the footer template file, default to use embedded template") 26 | 27 | //go:embed templates/*.html 28 | embedded embed.FS 29 | ) 30 | 31 | func getContent(file, embeddedFile string) (string, error) { 32 | if len(file) > 0 { 33 | content, err := ioutil.ReadFile(file) 34 | if err != nil { 35 | return "", err 36 | } 37 | 38 | return string(content), nil 39 | } 40 | 41 | content, err := embedded.ReadFile(path.Join(embedDir, embeddedFile)) 42 | if err != nil { 43 | return "", err 44 | } 45 | 46 | return string(content), nil 47 | } 48 | 49 | func main() { 50 | flag.Parse() 51 | 52 | if len(*input) == 0 { 53 | flag.Usage() 54 | } 55 | 56 | header, err := getContent(*headerFile, embedHeaderFile) 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | 61 | footer, err := getContent(*footerFile, embedFooterFile) 62 | if err != nil { 63 | log.Fatal(err) 64 | } 65 | 66 | content, err := ioutil.ReadFile(*input) 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | 71 | html, err := converter.MarkdownToHtml(header, footer, content) 72 | if err != nil { 73 | log.Fatal(err) 74 | } 75 | 76 | if len(*output) > 0 { 77 | file, err := os.Create(*output) 78 | if err != nil { 79 | log.Fatal(err) 80 | } 81 | fmt.Fprint(file, string(html)) 82 | } else { 83 | fmt.Println(string(html)) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /templates/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /templates/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %s 9 | 10 | 11 | 310 | 311 | 312 | -------------------------------------------------------------------------------- /utils/files.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strings" 7 | ) 8 | 9 | func ChangeExt(filename, ext string) string { 10 | name := strings.TrimSuffix(filename, filepath.Ext(filename)) 11 | return fmt.Sprintf("%s.%s", name, ext) 12 | } 13 | --------------------------------------------------------------------------------