Proper integration of live reload with automatic injection

This commit is contained in:
spf13 2014-05-16 17:49:27 -04:00
parent 60ed5bda2b
commit be1ee22032
8 changed files with 224 additions and 121 deletions

View file

@ -15,6 +15,7 @@ package commands
import ( import (
"fmt" "fmt"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
@ -26,6 +27,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/hugo/helpers" "github.com/spf13/hugo/helpers"
"github.com/spf13/hugo/hugolib" "github.com/spf13/hugo/hugolib"
"github.com/spf13/hugo/livereload"
"github.com/spf13/hugo/utils" "github.com/spf13/hugo/utils"
"github.com/spf13/hugo/watcher" "github.com/spf13/hugo/watcher"
jww "github.com/spf13/jwalterweatherman" jww "github.com/spf13/jwalterweatherman"
@ -46,6 +48,7 @@ Complete documentation is available at http://hugo.spf13.com`,
build() build()
}, },
} }
var hugoCmdV *cobra.Command var hugoCmdV *cobra.Command
var BuildWatch, Draft, UglyUrls, Verbose, Logging, VerboseLog, DisableRSS, DisableSitemap bool var BuildWatch, Draft, UglyUrls, Verbose, Logging, VerboseLog, DisableRSS, DisableSitemap bool
@ -66,19 +69,19 @@ func AddCommands() {
} }
func init() { func init() {
HugoCmd.PersistentFlags().BoolVarP(&Draft, "build-drafts", "D", false, "include content marked as draft") HugoCmd.PersistentFlags().BoolVarP(&Draft, "buildDrafts", "D", false, "include content marked as draft")
HugoCmd.PersistentFlags().BoolVar(&DisableRSS, "disableRSS", false, "Do not build RSS files") HugoCmd.PersistentFlags().BoolVar(&DisableRSS, "disableRSS", false, "Do not build RSS files")
HugoCmd.PersistentFlags().BoolVar(&DisableSitemap, "disableSitemap", false, "Do not build Sitemap file") HugoCmd.PersistentFlags().BoolVar(&DisableSitemap, "disableSitemap", false, "Do not build Sitemap file")
HugoCmd.PersistentFlags().StringVarP(&Source, "source", "s", "", "filesystem path to read files relative from") HugoCmd.PersistentFlags().StringVarP(&Source, "source", "s", "", "filesystem path to read files relative from")
HugoCmd.PersistentFlags().StringVarP(&Destination, "destination", "d", "", "filesystem path to write files to") HugoCmd.PersistentFlags().StringVarP(&Destination, "destination", "d", "", "filesystem path to write files to")
HugoCmd.PersistentFlags().StringVarP(&Theme, "theme", "t", "", "theme to use (located in /themes/THEMENAME/)") HugoCmd.PersistentFlags().StringVarP(&Theme, "theme", "t", "", "theme to use (located in /themes/THEMENAME/)")
HugoCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output") HugoCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")
HugoCmd.PersistentFlags().BoolVar(&UglyUrls, "uglyurls", false, "if true, use /filename.html instead of /filename/") HugoCmd.PersistentFlags().BoolVar(&UglyUrls, "uglyUrls", false, "if true, use /filename.html instead of /filename/")
HugoCmd.PersistentFlags().StringVarP(&BaseUrl, "base-url", "b", "", "hostname (and path) to the root eg. http://spf13.com/") HugoCmd.PersistentFlags().StringVarP(&BaseUrl, "baseUrl", "b", "", "hostname (and path) to the root eg. http://spf13.com/")
HugoCmd.PersistentFlags().StringVar(&CfgFile, "config", "", "config file (default is path/config.yaml|json|toml)") HugoCmd.PersistentFlags().StringVar(&CfgFile, "config", "", "config file (default is path/config.yaml|json|toml)")
HugoCmd.PersistentFlags().BoolVar(&Logging, "log", false, "Enable Logging") HugoCmd.PersistentFlags().BoolVar(&Logging, "log", false, "Enable Logging")
HugoCmd.PersistentFlags().StringVar(&LogFile, "logfile", "", "Log File path (if set, logging enabled automatically)") HugoCmd.PersistentFlags().StringVar(&LogFile, "logFile", "", "Log File path (if set, logging enabled automatically)")
HugoCmd.PersistentFlags().BoolVar(&VerboseLog, "verboselog", false, "verbose logging") HugoCmd.PersistentFlags().BoolVar(&VerboseLog, "verboseLog", false, "verbose logging")
HugoCmd.PersistentFlags().BoolVar(&nitro.AnalysisOn, "stepAnalysis", false, "display memory and timing of different steps of the program") HugoCmd.PersistentFlags().BoolVar(&nitro.AnalysisOn, "stepAnalysis", false, "display memory and timing of different steps of the program")
HugoCmd.Flags().BoolVarP(&BuildWatch, "watch", "w", false, "watch filesystem for changes and recreate as needed") HugoCmd.Flags().BoolVarP(&BuildWatch, "watch", "w", false, "watch filesystem for changes and recreate as needed")
hugoCmdV = HugoCmd hugoCmdV = HugoCmd
@ -94,6 +97,7 @@ func InitializeConfig() {
viper.RegisterAlias("taxonomies", "indexes") viper.RegisterAlias("taxonomies", "indexes")
viper.SetDefault("Watch", false)
viper.SetDefault("MetadataFormat", "toml") viper.SetDefault("MetadataFormat", "toml")
viper.SetDefault("DisableRSS", false) viper.SetDefault("DisableRSS", false)
viper.SetDefault("DisableSitemap", false) viper.SetDefault("DisableSitemap", false)
@ -112,12 +116,13 @@ func InitializeConfig() {
viper.SetDefault("Sitemap", hugolib.Sitemap{Priority: -1}) viper.SetDefault("Sitemap", hugolib.Sitemap{Priority: -1})
viper.SetDefault("PygmentsStyle", "monokai") viper.SetDefault("PygmentsStyle", "monokai")
viper.SetDefault("PygmentsUseClasses", false) viper.SetDefault("PygmentsUseClasses", false)
viper.SetDefault("DisableLiveReload", false)
if hugoCmdV.PersistentFlags().Lookup("build-drafts").Changed { if hugoCmdV.PersistentFlags().Lookup("buildDrafts").Changed {
viper.Set("BuildDrafts", Draft) viper.Set("BuildDrafts", Draft)
} }
if hugoCmdV.PersistentFlags().Lookup("uglyurls").Changed { if hugoCmdV.PersistentFlags().Lookup("uglyUrls").Changed {
viper.Set("UglyUrls", UglyUrls) viper.Set("UglyUrls", UglyUrls)
} }
@ -133,7 +138,7 @@ func InitializeConfig() {
viper.Set("Verbose", Verbose) viper.Set("Verbose", Verbose)
} }
if hugoCmdV.PersistentFlags().Lookup("logfile").Changed { if hugoCmdV.PersistentFlags().Lookup("logFile").Changed {
viper.Set("LogFile", LogFile) viper.Set("LogFile", LogFile)
} }
if BaseUrl != "" { if BaseUrl != "" {
@ -323,16 +328,14 @@ func NewWatcher(port int) error {
fmt.Print("Static file changed, syncing\n\n") fmt.Print("Static file changed, syncing\n\n")
utils.StopOnErr(copyStatic(), fmt.Sprintf("Error copying static files to %s", helpers.AbsPathify(viper.GetString("PublishDir")))) utils.StopOnErr(copyStatic(), fmt.Sprintf("Error copying static files to %s", helpers.AbsPathify(viper.GetString("PublishDir"))))
// Tell livereload a js file changed to force a hard refresh livereload.ForceRefresh()
wsHub.broadcast <- []byte(`{"command":"reload","path":"/x.js","originalPath":"","liveCSS":true}`)
} }
if dynamic_changed { if dynamic_changed {
fmt.Print("Change detected, rebuilding site\n\n") fmt.Print("Change detected, rebuilding site\n\n")
utils.StopOnErr(buildSite(true)) utils.StopOnErr(buildSite(true))
// Tell livereload a js file changed to force a hard refresh livereload.ForceRefresh()
wsHub.broadcast <- []byte(`{"command":"reload","path":"/x.js","originalPath":"","liveCSS":true}`)
} }
case err := <-watcher.Error: case err := <-watcher.Error:
if err != nil { if err != nil {
@ -343,6 +346,12 @@ func NewWatcher(port int) error {
}() }()
if port > 0 { if port > 0 {
if !viper.GetBool("DisableLiveReload") {
livereload.Initialize()
http.HandleFunc("/livereload.js", livereload.ServeJS)
http.HandleFunc("/livereload", livereload.Handler)
}
go serve(port) go serve(port)
} }

View file

@ -14,7 +14,6 @@
package commands package commands
import ( import (
"bytes"
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
@ -22,9 +21,6 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/gorilla/websocket"
//"code.google.com/p/go.net/websocket"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/hugo/helpers" "github.com/spf13/hugo/helpers"
jww "github.com/spf13/jwalterweatherman" jww "github.com/spf13/jwalterweatherman"
@ -34,12 +30,9 @@ import (
var serverPort int var serverPort int
var serverWatch bool var serverWatch bool
var serverAppend bool var serverAppend bool
var disableLiveReload bool
func init() { //var serverCmdV *cobra.Command
serverCmd.Flags().IntVarP(&serverPort, "port", "p", 1313, "port to run the server on")
serverCmd.Flags().BoolVarP(&serverWatch, "watch", "w", false, "watch filesystem for changes and recreate as needed")
serverCmd.Flags().BoolVarP(&serverAppend, "append-port", "", true, "append port to baseurl")
}
var serverCmd = &cobra.Command{ var serverCmd = &cobra.Command{
Use: "server", Use: "server",
@ -47,7 +40,15 @@ var serverCmd = &cobra.Command{
Long: `Hugo is able to run it's own high performance web server. Long: `Hugo is able to run it's own high performance web server.
Hugo will render all the files defined in the source directory and Hugo will render all the files defined in the source directory and
Serve them up.`, Serve them up.`,
Run: server, //Run: server,
}
func init() {
serverCmd.Flags().IntVarP(&serverPort, "port", "p", 1313, "port to run the server on")
serverCmd.Flags().BoolVarP(&serverWatch, "watch", "w", false, "watch filesystem for changes and recreate as needed")
serverCmd.Flags().BoolVarP(&serverAppend, "appendPort", "", true, "append port to baseurl")
serverCmd.Flags().BoolVar(&disableLiveReload, "disableLiveReload", false, "watch without enabling live browser reload on rebuild")
serverCmd.Run = server
} }
func server(cmd *cobra.Command, args []string) { func server(cmd *cobra.Command, args []string) {
@ -57,6 +58,14 @@ func server(cmd *cobra.Command, args []string) {
BaseUrl = "http://localhost" BaseUrl = "http://localhost"
} }
if cmd.Flags().Lookup("disableLiveReload").Changed {
viper.Set("DisableLiveReload", disableLiveReload)
}
if serverWatch {
viper.Set("Watch", true)
}
if !strings.HasPrefix(BaseUrl, "http://") { if !strings.HasPrefix(BaseUrl, "http://") {
BaseUrl = "http://" + BaseUrl BaseUrl = "http://" + BaseUrl
} }
@ -96,111 +105,13 @@ func server(cmd *cobra.Command, args []string) {
func serve(port int) { func serve(port int) {
jww.FEEDBACK.Println("Serving pages from " + helpers.AbsPathify(viper.GetString("PublishDir"))) jww.FEEDBACK.Println("Serving pages from " + helpers.AbsPathify(viper.GetString("PublishDir")))
jww.FEEDBACK.Printf("Web Server is available at %s\n", viper.GetString("BaseUrl")) jww.FEEDBACK.Printf("Web Server is available at %s\n", viper.GetString("BaseUrl"))
fmt.Println("Press ctrl+c to stop") fmt.Println("Press ctrl+c to stop")
http.Handle("/", http.FileServer(http.Dir(helpers.AbsPathify(viper.GetString("PublishDir"))))) http.Handle("/", http.FileServer(http.Dir(helpers.AbsPathify(viper.GetString("PublishDir")))))
go wsHub.run()
http.HandleFunc("/livereload", wsHandler)
err := http.ListenAndServe(":"+strconv.Itoa(port), nil) err := http.ListenAndServe(":"+strconv.Itoa(port), nil)
if err != nil { if err != nil {
jww.ERROR.Printf("Error: %s\n", err.Error()) jww.ERROR.Printf("Error: %s\n", err.Error())
os.Exit(1) os.Exit(1)
} }
} }
type hub struct {
// Registered connections.
connections map[*connection]bool
// Inbound messages from the connections.
broadcast chan []byte
// Register requests from the connections.
register chan *connection
// Unregister requests from connections.
unregister chan *connection
}
var wsHub = hub{
broadcast: make(chan []byte),
register: make(chan *connection),
unregister: make(chan *connection),
connections: make(map[*connection]bool),
}
func (h *hub) run() {
for {
select {
case c := <-h.register:
h.connections[c] = true
case c := <-h.unregister:
delete(h.connections, c)
close(c.send)
case m := <-h.broadcast:
for c := range h.connections {
select {
case c.send <- m:
default:
delete(h.connections, c)
close(c.send)
}
}
}
}
}
type connection struct {
// The websocket connection.
ws *websocket.Conn
// Buffered channel of outbound messages.
send chan []byte
}
func (c *connection) reader() {
for {
_, message, err := c.ws.ReadMessage()
if err != nil {
break
}
fmt.Println(string(message))
switch true {
case bytes.Contains(message, []byte(`"command":"hello"`)):
wsHub.broadcast <- []byte(`{
"command": "hello",
"protocols": [ "http://livereload.com/protocols/official-7" ],
"serverName": "Hugo"
}`)
}
}
c.ws.Close()
}
func (c *connection) writer() {
for message := range c.send {
err := c.ws.WriteMessage(websocket.TextMessage, message)
if err != nil {
break
}
}
c.ws.Close()
}
var upgrader = &websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 1024}
func wsHandler(w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
c := &connection{send: make(chan []byte, 256), ws: ws}
wsHub.register <- c
defer func() { wsHub.unregister <- c }()
go c.writer()
c.reader()
}

View file

@ -3,6 +3,5 @@
<link href="/static/css/hugofont.css" rel="stylesheet"/> <link href="/static/css/hugofont.css" rel="stylesheet"/>
<link href='http://fonts.googleapis.com/css?family=Arbutus+Slab&family=Cabin:600&family=Source+Code+Pro' rel='stylesheet' type='text/css'> <link href='http://fonts.googleapis.com/css?family=Arbutus+Slab&family=Cabin:600&family=Source+Code+Pro' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="/static/css/monokai_sublime.css"> <link rel="stylesheet" href="/static/css/monokai_sublime.css">
<script src="/static/js/livereload.js?host=localhost&port=1313"></script>
<script src="/static/js/highlight.pack.js"></script> <script src="/static/js/highlight.pack.js"></script>
<script>hljs.initHighlightingOnLoad();</script> <script>hljs.initHighlightingOnLoad();</script>

View file

@ -864,6 +864,10 @@ func (s *Site) render(d interface{}, out string, layouts ...string) (err error)
transformLinks = append(transformLinks, absURL...) transformLinks = append(transformLinks, absURL...)
} }
if viper.GetBool("watch") && !viper.GetBool("DisableLiveReload") {
transformLinks = append(transformLinks, transform.LiveReloadInject)
}
transformer := transform.NewChain(transformLinks...) transformer := transform.NewChain(transformLinks...)
var renderBuffer *bytes.Buffer var renderBuffer *bytes.Buffer

56
livereload/connection.go Normal file
View file

@ -0,0 +1,56 @@
// Copyright © 2014 Steve Francia <spf@spf13.com>.
//
// Licensed under the Simple Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://opensource.org/licenses/Simple-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package livereload
import (
"bytes"
"github.com/gorilla/websocket"
)
type connection struct {
// The websocket connection.
ws *websocket.Conn
// Buffered channel of outbound messages.
send chan []byte
}
func (c *connection) reader() {
for {
_, message, err := c.ws.ReadMessage()
if err != nil {
break
}
switch true {
case bytes.Contains(message, []byte(`"command":"hello"`)):
wsHub.broadcast <- []byte(`{
"command": "hello",
"protocols": [ "http://livereload.com/protocols/official-7" ],
"serverName": "Hugo"
}`)
}
}
c.ws.Close()
}
func (c *connection) writer() {
for message := range c.send {
err := c.ws.WriteMessage(websocket.TextMessage, message)
if err != nil {
break
}
}
c.ws.Close()
}

56
livereload/hub.go Normal file
View file

@ -0,0 +1,56 @@
// Copyright © 2014 Steve Francia <spf@spf13.com>.
//
// Licensed under the Simple Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://opensource.org/licenses/Simple-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package livereload
type hub struct {
// Registered connections.
connections map[*connection]bool
// Inbound messages from the connections.
broadcast chan []byte
// Register requests from the connections.
register chan *connection
// Unregister requests from connections.
unregister chan *connection
}
var wsHub = hub{
broadcast: make(chan []byte),
register: make(chan *connection),
unregister: make(chan *connection),
connections: make(map[*connection]bool),
}
func (h *hub) run() {
for {
select {
case c := <-h.register:
h.connections[c] = true
case c := <-h.unregister:
delete(h.connections, c)
close(c.send)
case m := <-h.broadcast:
for c := range h.connections {
select {
case c.send <- m:
default:
delete(h.connections, c)
close(c.send)
}
}
}
}
}

49
livereload/livereload.go Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,19 @@
package transform
import "bytes"
func LiveReloadInject(content []byte) []byte {
match := []byte("</body>")
replace := []byte(`<script>document.write('<script src="http://'
+ (location.host || 'localhost').split(':')[0]
+ ':1313/livereload.js?mindelay=10"></'
+ 'script>')</script></body>`)
newcontent := bytes.Replace(content, match, replace, -1)
if len(newcontent) == len(content) {
match := []byte("</BODY>")
newcontent = bytes.Replace(content, match, replace, -1)
}
return newcontent
}