mirror of
https://github.com/arnaucube/md-live-server.git
synced 2026-02-07 03:46:40 +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