diff --git a/README.md b/README.md new file mode 100644 index 0000000..bdfd353 --- /dev/null +++ b/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 `` with syntax highlighting diff --git a/main.go b/main.go new file mode 100644 index 0000000..b599b1c --- /dev/null +++ b/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 = `` + 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 + } +} diff --git a/md-live-server b/md-live-server new file mode 100755 index 0000000..470fd89 Binary files /dev/null and b/md-live-server differ diff --git a/screenshot00.png b/screenshot00.png new file mode 100644 index 0000000..b6d84f7 Binary files /dev/null and b/screenshot00.png differ diff --git a/template.go b/template.go new file mode 100644 index 0000000..a230caa --- /dev/null +++ b/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 = ` + + +{{.Title}} + + + +{{.Content}} + + + +` + +const htmlTemplate = ` + + +{{.Title}} + + + +{{.Content}} + + + + +` diff --git a/test.md b/test.md new file mode 100644 index 0000000..0969b3b --- /dev/null +++ b/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 | + + diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..d3fbfda --- /dev/null +++ b/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 +}