arnaucube
arnaucube/md-live-serverPublic Self-hosted repository
server rendering markdown with live reload on file update working
arnaucube arnaucube@gmail.com
Tue, 07 May 2019 21:07:45 +0200
commit

a0b3d071e55be62ff318d132cd7619d8964151cd

parent

6e3b9cbf68657320f2512e8476ba71bf70ef23ec

7 files changed, 302 insertions(+), 0 deletions(-)

jump to
A README.md

@@ -0,0 +1,19 @@

+# md-live-server +Server that renders markdown files and live updates the page each time that the file is updated. + +![screenshot00](https://raw.githubusercontent.com/arnaucube/md-live-server/master/screenshot00.png 'screenshot00') + +## Usage +Put the binary file `md-live-server` in your `$PATH`, and then go to the directory where are the markdown files that want to live render, and just use: +``` +> md-live-server +``` +And then go to the browser at `http://127.0.0.1:8080` + +## Features +- [x] server rendering .md files +- [x] live reload when .md file changes +- [x] directory files list on `/` endpoint +- [ ] on error return error page, instead of panic server +- [ ] graphviz +- [ ] colour `<code>` with syntax highlighting
A main.go

@@ -0,0 +1,134 @@

+package main + +import ( + "html/template" + "log" + "net/http" + "strings" + "time" + + "github.com/fsnotify/fsnotify" + "github.com/gorilla/mux" + "github.com/gorilla/websocket" +) + +var ( + upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + } +) + +type PageModel struct { + Title string + Content template.HTML + LastMod time.Time +} + +func main() { + router := mux.NewRouter() + router.HandleFunc("/", getDir).Methods("GET") + router.HandleFunc("/{path}", getPage).Methods("GET") + router.HandleFunc("/ws/{path}", serveWs) + + log.Println("md-live-server web server running") + log.Print("port: 8080") + log.Fatal(http.ListenAndServe(":8080", router)) +} + +func getDir(w http.ResponseWriter, r *http.Request) { + elements := readDir("./") + var content string + content = `<ul>` + for _, elem := range elements { + content += ` + <li><a href="` + elem + `">` + elem + `</a></li> + ` + } + content += `</ul>` + var page PageModel + page.Title = "dir" + page.Content = template.HTML(content) + + tmplPage := template.Must(template.New("t").Parse(dirTemplate)) + tmplPage.Execute(w, page) +} + +func getPage(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + path := vars["path"] + path = strings.Replace(path, "%", "/", -1) + log.Println(path) + + if strings.Split(path, ".")[1] != "md" { + http.ServeFile(w, r, path) + } + + content, err := fileToHTML(path) + check(err) + + var page PageModel + page.Title = path + page.Content = template.HTML(content) + + tmplPage := template.Must(template.New("t").Parse(htmlTemplate)) + tmplPage.Execute(w, page) +} + +func serveWs(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + path := vars["path"] + path = strings.Replace(path, "%", "/", -1) + log.Println("websocket", path) + + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + if _, ok := err.(websocket.HandshakeError); !ok { + log.Println(err) + } + return + } + + // watch file + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.Fatal(err) + } + defer watcher.Close() + + done := make(chan bool) + go func() { + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + log.Println("event:", event) + if event.Op&fsnotify.Write == fsnotify.Write { + log.Println("modified file:", event.Name) + writer(ws, path) + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + log.Println("error:", err) + } + } + }() + + err = watcher.Add(path) + if err != nil { + log.Fatal(err) + } + <-done +} + +func writer(ws *websocket.Conn, path string) { + content, err := fileToHTML(path) + check(err) + if err := ws.WriteMessage(websocket.TextMessage, []byte(content)); err != nil { + return + } +}
A template.go

@@ -0,0 +1,76 @@

+package main + +// html templates are here instead of an .html file to avoid depending on external files +// in this way, everything is inside the binary + +const dirTemplate = ` +<!DOCTYPE html> +<html> +<title>{{.Title}}</title> +<style> +body { + font-family: Arial, Helvetica, sans-serif; + background:#000000; + color:#cccccc; +} +</style> +<body> + +{{.Content}} + +</body> +</html> +` + +const htmlTemplate = ` +<!DOCTYPE html> +<html> +<title>{{.Title}}</title> +<style> +body { + font-family: Arial, Helvetica, sans-serif; + background:#000000; + color:#cccccc; +} +pre, code{ + padding-left: 3px; + padding-right: 3px; + background: #333333; + border-radius: 3px; +} +pre>code { + color: #c0fffa; +} +h1:after{ + content:' '; + display:block; + border:1px solid #cccccc; +} +h2:after{ + content:' '; + display:block; + border:0.7px solid #cccccc; +} +</style> +<body> + +{{.Content}} + +<script> +(function() { + var conn = new WebSocket("ws://127.0.0.1:8080/ws/{{.Title}}"); + conn.onclose = function(evt) { + console.log('Connection closed'); + alert('Connection closed'); + } + conn.onmessage = function(evt) { + console.log('file updated'); + // location.reload(); + console.log("evt", evt); + document.getElementsByTagName("BODY")[0].innerHTML = evt.data; + } +})(); +</script> +</body> +</html> +`
A test.md

@@ -0,0 +1,33 @@

+# test 01 + +- 1 this +- 2 is + - 2.1 a + - 2.1.1 + - 2.2 list +- 3 sample + +## second header +[link](https://arnaucube.com) + +### third header +Asdf asdf asdf + +- Lorem ipsum bla bla `ipsum` bla bla + +### Some code +```go +func main() { + fmt.Println("hello world") +} +``` + +- a table: + +| Tables | Are | Cool | +| ------------- |:-------------:| -----:| +| col 3 is | right-aligned | $1600 | +| col 2 is | centered | $12 | +| zebra stripes | are neat | $1 | + +
A utils.go

@@ -0,0 +1,40 @@

+package main + +import ( + "io/ioutil" + "os" + "path/filepath" + + "github.com/fatih/color" + blackfriday "gopkg.in/russross/blackfriday.v2" +) + +func check(err error) { + if err != nil { + color.Red(err.Error()) + } +} + +func readDir(dirpath string) []string { + var elems []string + _ = filepath.Walk(dirpath, func(path string, f os.FileInfo, err error) error { + elems = append(elems, path) + return nil + }) + return elems +} + +func readFile(path string) string { + dat, err := ioutil.ReadFile(path) + if err != nil { + color.Red(path) + } + check(err) + return string(dat) +} + +func fileToHTML(path string) (string, error) { + mdcontent := readFile(path) + htmlcontent := string(blackfriday.Run([]byte(mdcontent))) + return htmlcontent, nil +}