@@ -30,12 +30,12 @@ import (
30
30
// directory. metrics reports whether print the metrics. If asm is -1 or
31
31
// greater, serve prints the assembly code of the served file and the value of
32
32
// asm determines the maximum length, in runes, of disassembled Text
33
- // instructions
33
+ // instructions. If disableLiveReload is true, it disables LiveReload.
34
34
//
35
35
// asm > 0: at most asm runes; leading and trailing white space are removed
36
36
// asm == 0: no text
37
37
// asm == -1: all text
38
- func serve (asm int , metrics bool ) error {
38
+ func serve (asm int , metrics bool , disableLiveReload bool ) error {
39
39
40
40
fsys , err := newTemplateFS ("." )
41
41
if err != nil {
@@ -59,6 +59,9 @@ func serve(asm int, metrics bool) error {
59
59
templatesDependencies : map [string ]map [string ]struct {}{},
60
60
asm : asm ,
61
61
}
62
+ if ! disableLiveReload {
63
+ srv .liveReloads = map [* liveReload ]struct {}{}
64
+ }
62
65
if metrics {
63
66
srv .metrics .active = true
64
67
srv .metrics .header = true
@@ -68,27 +71,35 @@ func serve(asm int, metrics bool) error {
68
71
select {
69
72
case name := <- fsys .Changed ():
70
73
srv .Lock ()
74
+ invalidated := map [string ]bool {}
71
75
if _ , ok := srv .templates [name ]; ok {
72
76
delete (srv .templates , name )
73
77
for _ , dependents := range srv .templatesDependencies {
74
78
delete (dependents , name )
75
79
}
80
+ invalidated [name ] = true
76
81
} else {
77
- var invalidatedFiles []string
78
82
for dependency , dependents := range srv .templatesDependencies {
79
83
if dependency == name {
80
84
for d := range dependents {
81
85
delete (srv .templates , d )
82
- invalidatedFiles = append ( invalidatedFiles , d )
86
+ invalidated [ d ] = true
83
87
}
84
88
}
85
89
}
86
- for _ , invalidated := range invalidatedFiles {
90
+ for invalidated := range invalidated {
87
91
for _ , dependents := range srv .templatesDependencies {
88
92
delete (dependents , invalidated )
89
93
}
90
94
}
91
95
}
96
+ if len (invalidated ) > 0 {
97
+ for r := range srv .liveReloads {
98
+ if invalidated [r .file + ".html" ] || invalidated [r .file + ".md" ] {
99
+ go func () { r .reload () }()
100
+ }
101
+ }
102
+ }
92
103
srv .Unlock ()
93
104
case err := <- fsys .Errors :
94
105
srv .logf ("%v" , err )
@@ -120,13 +131,83 @@ type server struct {
120
131
sync.Mutex
121
132
templates map [string ]* scriggo.Template
122
133
templatesDependencies map [string ]map [string ]struct {}
134
+ liveReloads map [* liveReload ]struct {}
123
135
metrics struct {
124
136
active bool
125
137
header bool
126
138
}
127
139
}
128
140
129
141
func (srv * server ) ServeHTTP (w http.ResponseWriter , r * http.Request ) {
142
+ if r .Header .Get ("Accept" ) == "text/event-stream" {
143
+ srv .serveLiveReload (w , r )
144
+ return
145
+ }
146
+ srv .serveTemplate (w , r )
147
+ }
148
+
149
+ type liveReload struct {
150
+ file string // file path without extension
151
+
152
+ sync.Mutex
153
+ w io.Writer
154
+ }
155
+
156
+ var reload = []byte ("data: reload\n \n " )
157
+
158
+ func (lr * liveReload ) reload () {
159
+ lr .Lock ()
160
+ _ , _ = lr .w .Write (reload )
161
+ lr .w .(http.Flusher ).Flush ()
162
+ lr .Unlock ()
163
+ }
164
+
165
+ func (srv * server ) serveLiveReload (w http.ResponseWriter , r * http.Request ) {
166
+
167
+ if _ , ok := w .(http.Flusher ); ! ok || srv .liveReloads == nil {
168
+ http .Error (w , "Live reload is not supported" , http .StatusUnsupportedMediaType )
169
+ return
170
+ }
171
+
172
+ file := r .URL .Path [1 :]
173
+ if file == "" || strings .HasSuffix (file , "/" ) {
174
+ file += "index"
175
+ }
176
+ if ext := path .Ext (file ); ext != "" {
177
+ file = file [:len (file )- len (ext )]
178
+ }
179
+
180
+ lr := & liveReload {
181
+ file : file ,
182
+ w : w ,
183
+ }
184
+
185
+ w .Header ().Set ("Content-Type" , "text/event-stream" )
186
+ w .Header ().Set ("Cache-Control" , "no-cache" )
187
+ w .Header ().Set ("Connection" , "keep-alive" )
188
+ w .WriteHeader (200 )
189
+
190
+ srv .Lock ()
191
+ srv .liveReloads [lr ] = struct {}{}
192
+ _ , ok := srv .templates [file + ".html" ]
193
+ if ! ok {
194
+ _ , ok = srv .templates [file + ".md" ]
195
+ }
196
+ srv .Unlock ()
197
+
198
+ if ! ok {
199
+ lr .reload ()
200
+ }
201
+
202
+ <- r .Context ().Done ()
203
+
204
+ srv .Lock ()
205
+ delete (srv .liveReloads , lr )
206
+ srv .Unlock ()
207
+
208
+ }
209
+
210
+ func (srv * server ) serveTemplate (w http.ResponseWriter , r * http.Request ) {
130
211
131
212
name := r .URL .Path [1 :]
132
213
if name == "" || strings .HasSuffix (name , "/" ) {
@@ -221,10 +302,32 @@ func (srv *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
221
302
}
222
303
runTime := time .Since (start )
223
304
305
+ s := b .Bytes ()
306
+ i := indexEndBody (s )
307
+ if i == - 1 {
308
+ i = len (s )
309
+ }
224
310
w .Header ().Set ("Content-Type" , "text/html; charset=utf-8" )
225
- _ , err = b .WriteTo (w )
226
- if err != nil {
227
- srv .logf ("%s" , err )
311
+ if srv .liveReloads == nil {
312
+ _ , _ = w .Write (s )
313
+ } else {
314
+ _ , _ = w .Write (s [:i ])
315
+ _ , _ = io .WriteString (w , `<script>
316
+ // Scriggo LiveReload.
317
+ (function () {
318
+ if (typeof EventSource !== 'function') return;
319
+ const es = new EventSource('` )
320
+ jsStringEscape (w , r .URL .Path )
321
+ _ , _ = io .WriteString (w , `');
322
+ es.onmessage = function(e) {
323
+ if (e.data === 'reload') {
324
+ es.close();
325
+ location.reload();
326
+ }
327
+ };
328
+ })();
329
+ </script>` )
330
+ _ , _ = w .Write (s [i :])
228
331
}
229
332
230
333
if srv .metrics .active {
@@ -269,3 +372,54 @@ func (srv *server) logf(format string, a ...interface{}) {
269
372
println ()
270
373
srv .metrics .header = true
271
374
}
375
+
376
+ // indexEndBody locates the closing "</body>" tag in s, ignoring case and
377
+ // permitting spaces before '>'. If the tag is found, the function returns its
378
+ // index, unless the tag's line contains only whitespaces before it; in that
379
+ // case, it returns the index of the newline ('\n') at the start of the line.
380
+ // If no closing tag is found, it returns -1.
381
+ func indexEndBody (s []byte ) int {
382
+ for {
383
+ i := bytes .LastIndex (s , []byte ("</" ))
384
+ if i == - 1 {
385
+ return - 1
386
+ }
387
+ if isBodyTag (s [i + 2 :]) {
388
+ for j := i - 1 ; j >= 0 ; j -- {
389
+ switch s [j ] {
390
+ case ' ' , '\t' , '\r' :
391
+ continue
392
+ case '\n' :
393
+ if j > 0 && s [j - 1 ] == '\r' {
394
+ j --
395
+ }
396
+ return j
397
+ }
398
+ break
399
+ }
400
+ return i
401
+ }
402
+ s = s [:i ]
403
+ }
404
+ }
405
+
406
+ // isBodyTag checks if s starts with "body>", ignoring case and allowing spaces
407
+ // before '>'.
408
+ func isBodyTag (s []byte ) bool {
409
+ if len (s ) < 5 {
410
+ return false
411
+ }
412
+ if ! bytes .EqualFold (s [:4 ], []byte ("body" )) {
413
+ return false
414
+ }
415
+ for i := 4 ; i < len (s ); i ++ {
416
+ switch s [i ] {
417
+ case '>' :
418
+ return true
419
+ case ' ' , '\t' , '\n' , '\r' :
420
+ continue
421
+ }
422
+ break
423
+ }
424
+ return false
425
+ }
0 commit comments