Browse Source

server rendering markdown with live reload on file update working

master
arnaucube 5 years ago
parent
commit
a0b3d071e5
7 changed files with 302 additions and 0 deletions
  1. +19
    -0
      README.md
  2. +134
    -0
      main.go
  3. BIN
      md-live-server
  4. BIN
      screenshot00.png
  5. +76
    -0
      template.go
  6. +33
    -0
      test.md
  7. +40
    -0
      utils.go

+ 19
- 0
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

+ 134
- 0
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
}
}

BIN
md-live-server


BIN
screenshot00.png

Before After
Width: 1736  |  Height: 946  |  Size: 87 KiB

+ 76
- 0
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>
`

+ 33
- 0
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 |

+ 40
- 0
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
}

Loading…
Cancel
Save