first reasonable version

This commit is contained in:
Barak Michener 2015-02-07 17:49:41 -05:00
parent 429ba99406
commit de8ddbb0a6
5 changed files with 809 additions and 19 deletions

View file

@ -1,4 +1,23 @@
livemd # livemd
======
Live Markdown Rendering In Go ## Overview
I wanted a simple tool that watched file updates as I worked on design docs.
I also didn't want to `npm install` anything. So I wrote a server in Go, with live Markdown updates over websockets.
## Libraries Used
* [github.com/russross/blackfriday](https://github.com/russross/blackfriday)
* [golang.org/x/net/websocket](https://golang.org/x/net/websocket)
* [gopkg.in/fsnotify.v1](https://github.com/go-fsnotify/fsnotify)
## Usage
```
go get github.com/barakmich/livemd
cd $PROJECT_DIR
livemd
```
## License
BSD

665
github_markdown.go Normal file

File diff suppressed because it is too large Load diff

116
livemd.go
View file

@ -1,7 +1,7 @@
package main package main
import ( import (
"fmt" "io/ioutil"
"log" "log"
"net/http" "net/http"
"os" "os"
@ -23,6 +23,28 @@ var rootTmpl *template.Template
var pageTmpl *template.Template var pageTmpl *template.Template
var path string var path string
type state int
const (
None state = iota
Open
Close
)
type Listener struct {
File string
Socket *websocket.Conn
State state
}
type Update struct {
File string
}
type BrowserMsg struct {
Markdown string
}
func init() { func init() {
var err error var err error
rootTmpl, err = template.New("root").Parse(rootTemplate) rootTmpl, err = template.New("root").Parse(rootTemplate)
@ -64,17 +86,18 @@ func AddWatch(w *fsnotify.Watcher) filepath.WalkFunc {
} }
} }
func WatcherEventLoop(w *fsnotify.Watcher, done chan bool) { func WatcherEventLoop(w *fsnotify.Watcher, updates chan Update, done chan bool) {
for { for {
select { select {
case event := <-w.Events: case event := <-w.Events:
log.Println("Event:", event) // log.Println("Event:", event)
// TODO(barakmich): On directory creation, stat path if directory, and watch it. // TODO(barakmich): On directory creation, stat path if directory, and watch it.
if HasMarkdownSuffix(event.Name) { if HasMarkdownSuffix(event.Name) {
subfile := strings.TrimPrefix(event.Name, path)
if event.Op == fsnotify.Write { if event.Op == fsnotify.Write {
updates <- Update{subfile}
} }
} }
case err := <-w.Errors: case err := <-w.Errors:
log.Println("Error:", err) log.Println("Error:", err)
done <- true done <- true
@ -82,28 +105,92 @@ func WatcherEventLoop(w *fsnotify.Watcher, done chan bool) {
} }
} }
func writeFileForListener(l Listener) {
var data []byte
file, err := os.Open(filepath.Join(path, l.File))
if err != nil {
data = []byte("Error: " + err.Error())
}
filebytes, err := ioutil.ReadAll(file)
if err != nil {
data = []byte("Error: " + err.Error())
}
data = blackfriday.MarkdownCommon(filebytes)
var msg BrowserMsg
msg.Markdown = string(data)
err = websocket.JSON.Send(l.Socket, msg)
if err != nil {
log.Println("Error sending message:", err)
}
}
func UpdateListeners(updates chan Update, listeners chan Listener) {
currentListeners := make([]Listener, 0)
for {
select {
case listener := <-listeners:
if listener.State == Open {
log.Println("New listener on", listener.File)
currentListeners = append(currentListeners, listener)
writeFileForListener(listener)
}
if listener.State == Close {
for i, l := range currentListeners {
if l.Socket == listener.Socket {
log.Println("Deregistering Listener")
currentListeners = append(currentListeners[:i], currentListeners[i+1:]...)
}
}
}
case update := <-updates:
log.Println("Update on", update.File)
for _, l := range currentListeners {
if update.File == l.File {
writeFileForListener(l)
}
}
}
}
}
func RootFunc(w http.ResponseWriter, r *http.Request) { func RootFunc(w http.ResponseWriter, r *http.Request) {
tocMutex.Lock() tocMutex.Lock()
localToc := toc[:] localToc := make([]string, len(toc))
copy(localToc, toc)
tocMutex.Unlock() tocMutex.Unlock()
log.Println(localToc)
for i, s := range localToc { for i, s := range localToc {
s = strings.TrimPrefix(s, path) chop := strings.TrimPrefix(s, path)
localToc[i] = "* " + s localToc[i] = "* [" + chop + "](/md" + chop + ")"
} }
tocMkd := strings.Join(localToc, "\n") tocMkd := strings.Join(localToc, "\n")
bytes := blackfriday.MarkdownCommon([]byte(tocMkd)) bytes := blackfriday.MarkdownCommon([]byte(tocMkd))
rootTmpl.Execute(w, string(bytes)) rootTmpl.Execute(w, string(bytes))
} }
func CSSFunc(css string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(css))
}
}
func PageFunc(w http.ResponseWriter, r *http.Request) { func PageFunc(w http.ResponseWriter, r *http.Request) {
subpath := strings.TrimPrefix(r.RequestURI, "/md") subpath := strings.TrimPrefix(r.RequestURI, "/md")
log.Println("New watcher on ", subpath) log.Println("New watcher on ", subpath)
pageTmpl.Execute(w, subpath) pageTmpl.Execute(w, subpath)
} }
func HandleListener(ws *websocket.Conn) { func HandleListener(listeners chan Listener) func(ws *websocket.Conn) {
fmt.Println("WEBSOCKET!", ws.Request().RequestURI) return func(ws *websocket.Conn) {
ws.Close() subpath := strings.TrimPrefix(ws.Request().RequestURI, "/ws")
listeners <- Listener{subpath, ws, Open}
var closeMessage string
err := websocket.Message.Receive(ws, &closeMessage)
if err != nil && err.Error() != "EOF" {
log.Println("Error before close:", err)
}
listeners <- Listener{subpath, ws, Close}
}
} }
func main() { func main() {
@ -119,7 +206,8 @@ func main() {
defer watcher.Close() defer watcher.Close()
done := make(chan bool) done := make(chan bool)
go WatcherEventLoop(watcher, done) updates := make(chan Update)
go WatcherEventLoop(watcher, updates, done)
log.Println("Watching directory", path) log.Println("Watching directory", path)
err = filepath.Walk(path, AddWatch(watcher)) err = filepath.Walk(path, AddWatch(watcher))
@ -127,10 +215,12 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
fmt.Println(toc) listeners := make(chan Listener)
go UpdateListeners(updates, listeners)
http.HandleFunc("/", RootFunc) http.HandleFunc("/", RootFunc)
http.HandleFunc("/md/", PageFunc) http.HandleFunc("/md/", PageFunc)
http.Handle("/ws/", websocket.Handler(HandleListener)) http.HandleFunc("/github.css", CSSFunc(githubCss))
http.Handle("/ws/", websocket.Handler(HandleListener(listeners)))
http.ListenAndServe(":8080", nil) http.ListenAndServe(":8080", nil)
} }

View file

@ -3,24 +3,33 @@ package main
var pageTemplate = ` var pageTemplate = `
<html> <html>
<head> <head>
<title>LiveMarkdown</title> <title>LiveMarkdown {{.}}</title>
<link rel="stylesheet" type="text/css" href="/github.css">
<script type="text/javascript" src="https://code.jquery.com/jquery-2.1.3.min.js"></script> <script type="text/javascript" src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
<script type="text/javascript"> <script type="text/javascript">
var ws = new WebSocket("ws://" + location.host + "/ws" + "{{.}}") var ws = new WebSocket("ws://" + location.host + "/ws" + "{{.}}")
ws.onopen = function() { ws.onopen = function() {
$("body").Text("wooot") $("#content").html("wooot")
} }
ws.onmessage = function(evt) { ws.onmessage = function(evt) {
$("body").Text(evt) var data = JSON.parse(evt.data)
$("#content").html(data.Markdown)
} }
ws.onclose = function() { ws.onclose = function() {
} }
</script> </script>
<style>
body {
padding: 10px 20px;
}
</style>
</head> </head>
<body> <body>
<div id="content" class="markdown-body">
</div>
</body> </body>
</html> </html>
` `

View file

@ -4,8 +4,15 @@ var rootTemplate = `
<html> <html>
<head> <head>
<title>LiveMarkdown</title> <title>LiveMarkdown</title>
<link rel="stylesheet" type="text/css" href="/github.css">
<style>
body {
padding: 10px 20px;
}
</style>
</head> </head>
<body> <body>
<h1> LiveMD: Listing of files </h1>
{{.}} {{.}}
</body> </body>
</html> </html>