Repos / s4g / 8606ea563e
commit 8606ea563e4331c6c56afa3aaa44498e7a83de6c
Author: Nhân <hi@imnhan.com>
Date:   Sat Jul 8 17:16:23 2023 +0700

    implement livereload
    
    Not ideal right now: if multiple tabs are open, only 1 will be reloaded.

diff --git a/Makefile b/Makefile
index 83c5cf1..920c8df 100644
--- a/Makefile
+++ b/Makefile
@@ -2,7 +2,7 @@ build:
 	go build -o dist/
 
 watch:
-	find . -name '*.go' -or -name '*.js' \
+	find . -name '*.go' -or -name '*.js' -or -name 'livereload.html' \
 	| entr -rc go run .
 
 # Cheating a little because the djot.js repo on github does not provide builds
diff --git a/livereload/livereload.go b/livereload/livereload.go
new file mode 100644
index 0000000..d440790
--- /dev/null
+++ b/livereload/livereload.go
@@ -0,0 +1,90 @@
+package livereload
+
+import (
+	"bytes"
+	_ "embed"
+	"io/fs"
+	"net/http"
+	"strings"
+	"sync"
+
+	"go.imnhan.com/webmaker2000/writablefs"
+)
+
+const endpoint = "/_livereload"
+
+//go:embed livereload.html
+var lrScript []byte
+
+var pleaseReload = []byte("1")
+var dontReload = []byte("0")
+
+var state struct {
+	shouldReload bool
+	mut          sync.RWMutex
+}
+
+func init() {
+	lrScript = bytes.ReplaceAll(lrScript, []byte("{{LR_ENDPOINT}}"), []byte(endpoint))
+	lrScript = bytes.ReplaceAll(lrScript, []byte("{{SHOULD_RELOAD}}"), pleaseReload)
+}
+
+// For html pages, insert a script tag to enable livereload
+func Middleware(fsys writablefs.FS, f http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		path := r.URL.Path
+
+		// Handle AJAX endpoint
+		if path == endpoint {
+			state.mut.RLock()
+			shouldReload := state.shouldReload
+			state.mut.RUnlock()
+
+			if shouldReload {
+				w.Write(pleaseReload)
+				state.mut.Lock()
+				state.shouldReload = false
+				state.mut.Unlock()
+			} else {
+				w.Write(dontReload)
+			}
+			return
+		}
+
+		// For non-html requests, fall through to default FileServer handler
+		if !strings.HasSuffix(path, ".html") && !strings.HasSuffix(path, "/") {
+			f.ServeHTTP(w, r)
+			return
+		}
+
+		if strings.HasSuffix(path, "/") {
+			path += "index.html"
+		}
+
+		// Filesystem access doesn't expect leading slash "/"
+		path = strings.TrimPrefix(path, "/")
+
+		originalContent, err := fs.ReadFile(fsys, path)
+		if err != nil {
+			http.NotFound(w, r)
+			return
+		}
+
+		w.Write(withLiveReload(originalContent))
+	})
+}
+
+func Trigger() {
+	state.mut.Lock()
+	state.shouldReload = true
+	state.mut.Unlock()
+}
+
+func withLiveReload(original []byte) []byte {
+	bodyEndPos := bytes.LastIndex(original, []byte("</body>"))
+	result := make([]byte, len(original)+len(lrScript))
+	copy(result, original[:bodyEndPos])
+	copy(result[bodyEndPos:], lrScript)
+	copy(result[bodyEndPos+len(lrScript):], original[bodyEndPos:])
+	return result
+}
diff --git a/livereload/livereload.html b/livereload/livereload.html
new file mode 100644
index 0000000..8f35245
--- /dev/null
+++ b/livereload/livereload.html
@@ -0,0 +1,11 @@
+<script>
+  setInterval(() => {
+    fetch("{{LR_ENDPOINT}}")
+      .then((resp) => resp.text())
+      .then((text) => {
+        if (text === "{{SHOULD_RELOAD}}") {
+          location.reload();
+        }
+      });
+  }, 500);
+</script>
diff --git a/main.go b/main.go
index d5d1826..abaadff 100644
--- a/main.go
+++ b/main.go
@@ -14,6 +14,8 @@
 
 	"github.com/BurntSushi/toml"
 	"go.imnhan.com/webmaker2000/djot"
+	"go.imnhan.com/webmaker2000/livereload"
+	"go.imnhan.com/webmaker2000/writablefs"
 )
 
 const DJOT_EXT = ".dj"
@@ -32,7 +34,7 @@ func main() {
 		panic(err)
 	}
 
-	fsys := WriteDirFS(absolutePath)
+	fsys := writablefs.WriteDirFS(absolutePath)
 
 	regenerate(fsys)
 
@@ -45,18 +47,19 @@ func main() {
 	closeWatcher := WatchLocalFS(fsys, func() {
 		fmt.Println("Change detected. Regenerating...")
 		regenerate(fsys)
+		livereload.Trigger()
 	})
 	defer closeWatcher()
 
 	println("Serving local website at http://localhost:" + port)
-	http.Handle("/", http.FileServer(http.FS(fsys)))
+	http.Handle("/", livereload.Middleware(fsys, http.FileServer(http.FS(fsys))))
 	err = http.ListenAndServe("127.0.0.1:"+port, nil)
 	if err != nil {
 		panic(err)
 	}
 }
 
-func regenerate(fsys WritableFS) {
+func regenerate(fsys writablefs.FS) {
 	defer timer("Took %s")()
 	site := readSiteMetadata(fsys)
 	articles := findArticles(fsys)
@@ -117,7 +120,7 @@ type SiteMetadata struct {
 	}
 }
 
-func readSiteMetadata(fsys WritableFS) SiteMetadata {
+func readSiteMetadata(fsys writablefs.FS) SiteMetadata {
 	sm := SiteMetadata{
 		HomePath:     "/",
 		ShowFooter:   true,
@@ -131,7 +134,7 @@ func readSiteMetadata(fsys WritableFS) SiteMetadata {
 }
 
 type Article struct {
-	Fs       WritableFS
+	Fs       writablefs.FS
 	Path     string
 	WebPath  string
 	DjotBody string
@@ -192,7 +195,7 @@ func (a *Article) WriteHtmlFile(
 }
 
 func WriteHomePage(
-	fsys WritableFS,
+	fsys writablefs.FS,
 	site SiteMetadata,
 	articlesInFeed, articlesInNav []Article,
 	startYear int,
@@ -229,7 +232,7 @@ func WriteHomePage(
 	fsys.WriteFile("index.html", buf.Bytes())
 }
 
-func findArticles(fsys WritableFS) (result []Article) {
+func findArticles(fsys writablefs.FS) (result []Article) {
 
 	fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
 		if d.IsDir() || !strings.HasSuffix(d.Name(), DJOT_EXT) {
diff --git a/watcher.go b/watcher.go
index 36d9dc9..2430b32 100644
--- a/watcher.go
+++ b/watcher.go
@@ -8,6 +8,7 @@
 	"time"
 
 	"github.com/fsnotify/fsnotify"
+	"go.imnhan.com/webmaker2000/writablefs"
 )
 
 var WATCHED_EXTS = []string{DJOT_EXT, SITE_EXT, ".tmpl"}
@@ -17,7 +18,7 @@
 // Watches for relevant changes in FS, debounces by debounceInterval,
 // then executes callback.
 // Returns cleanup function.
-func WatchLocalFS(fsys WritableFS, callback func()) (Close func() error) {
+func WatchLocalFS(fsys writablefs.FS, callback func()) (Close func() error) {
 	watcher, err := fsnotify.NewWatcher()
 	if err != nil {
 		panic(err)
diff --git a/writablefs.go b/writablefs/writablefs.go
similarity index 86%
rename from writablefs.go
rename to writablefs/writablefs.go
index 3930b1b..2451f8a 100644
--- a/writablefs.go
+++ b/writablefs/writablefs.go
@@ -1,4 +1,4 @@
-package main
+package writablefs
 
 import (
 	"io/fs"
@@ -6,14 +6,14 @@
 	"path/filepath"
 )
 
-type WritableFS interface {
+type FS interface {
 	fs.FS
 	WriteFile(path string, content []byte) error
 	Path() string
 }
 
 // Like os.DirFS but is writable
-func WriteDirFS(path string) WritableFS {
+func WriteDirFS(path string) FS {
 	return writeDirFS(path)
 }