mirror of
https://github.com/arnaucube/md-live-server.git
synced 2026-02-06 19:36:38 +01:00
server rendering markdown with live reload on file update working
This commit is contained in:
19
README.md
Normal file
19
README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# md-live-server
|
||||
Server that renders markdown files and live updates the page each time that the file is updated.
|
||||
|
||||

|
||||
|
||||
## 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
main.go
Normal file
134
main.go
Normal file
@@ -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
Executable file
BIN
md-live-server
Executable file
Binary file not shown.
BIN
screenshot00.png
Normal file
BIN
screenshot00.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
76
template.go
Normal file
76
template.go
Normal file
@@ -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
test.md
Normal file
33
test.md
Normal file
@@ -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
utils.go
Normal file
40
utils.go
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user