@ -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 |
@ -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 |
|||
} |
|||
} |
@ -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> |
|||
` |
@ -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 | |
|||
|
|||
|
@ -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 |
|||
} |