shithub: hugo

Download patch

ref: be1ee220327073d7304621b4386df9d6252bb837
parent: 60ed5bda2b3d1c6f2d67287a9d7a434f588cdb14
author: spf13 <[email protected]>
date: Fri May 16 13:49:27 EDT 2014

Proper integration of live reload with automatic injection

--- a/commands/hugo.go
+++ b/commands/hugo.go
@@ -15,6 +15,7 @@
 
 import (
 	"fmt"
+	"net/http"
 	"os"
 	"path/filepath"
 	"runtime"
@@ -26,6 +27,7 @@
 	"github.com/spf13/cobra"
 	"github.com/spf13/hugo/helpers"
 	"github.com/spf13/hugo/hugolib"
+	"github.com/spf13/hugo/livereload"
 	"github.com/spf13/hugo/utils"
 	"github.com/spf13/hugo/watcher"
 	jww "github.com/spf13/jwalterweatherman"
@@ -46,6 +48,7 @@
 		build()
 	},
 }
+
 var hugoCmdV *cobra.Command
 
 var BuildWatch, Draft, UglyUrls, Verbose, Logging, VerboseLog, DisableRSS, DisableSitemap bool
@@ -66,7 +69,7 @@
 }
 
 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(&DisableSitemap, "disableSitemap", false, "Do not build Sitemap file")
 	HugoCmd.PersistentFlags().StringVarP(&Source, "source", "s", "", "filesystem path to read files relative from")
@@ -73,12 +76,12 @@
 	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().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")
-	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().BoolVar(&UglyUrls, "uglyUrls", false, "if true, use /filename.html instead of /filename/")
+	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().BoolVar(&Logging, "log", false, "Enable Logging")
-	HugoCmd.PersistentFlags().StringVar(&LogFile, "logfile", "", "Log File path (if set, logging enabled automatically)")
-	HugoCmd.PersistentFlags().BoolVar(&VerboseLog, "verboselog", false, "verbose logging")
+	HugoCmd.PersistentFlags().StringVar(&LogFile, "logFile", "", "Log File path (if set, logging enabled automatically)")
+	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.Flags().BoolVarP(&BuildWatch, "watch", "w", false, "watch filesystem for changes and recreate as needed")
 	hugoCmdV = HugoCmd
@@ -94,6 +97,7 @@
 
 	viper.RegisterAlias("taxonomies", "indexes")
 
+	viper.SetDefault("Watch", false)
 	viper.SetDefault("MetadataFormat", "toml")
 	viper.SetDefault("DisableRSS", false)
 	viper.SetDefault("DisableSitemap", false)
@@ -112,12 +116,13 @@
 	viper.SetDefault("Sitemap", hugolib.Sitemap{Priority: -1})
 	viper.SetDefault("PygmentsStyle", "monokai")
 	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)
 	}
 
-	if hugoCmdV.PersistentFlags().Lookup("uglyurls").Changed {
+	if hugoCmdV.PersistentFlags().Lookup("uglyUrls").Changed {
 		viper.Set("UglyUrls", UglyUrls)
 	}
 
@@ -133,7 +138,7 @@
 		viper.Set("Verbose", Verbose)
 	}
 
-	if hugoCmdV.PersistentFlags().Lookup("logfile").Changed {
+	if hugoCmdV.PersistentFlags().Lookup("logFile").Changed {
 		viper.Set("LogFile", LogFile)
 	}
 	if BaseUrl != "" {
@@ -323,8 +328,7 @@
 					fmt.Print("Static file changed, syncing\n\n")
 					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
-					wsHub.broadcast <- []byte(`{"command":"reload","path":"/x.js","originalPath":"","liveCSS":true}`)
+					livereload.ForceRefresh()
 				}
 
 				if dynamic_changed {
@@ -331,8 +335,7 @@
 					fmt.Print("Change detected, rebuilding site\n\n")
 					utils.StopOnErr(buildSite(true))
 
-					// Tell livereload a js file changed to force a hard refresh
-					wsHub.broadcast <- []byte(`{"command":"reload","path":"/x.js","originalPath":"","liveCSS":true}`)
+					livereload.ForceRefresh()
 				}
 			case err := <-watcher.Error:
 				if err != nil {
@@ -343,6 +346,12 @@
 	}()
 
 	if port > 0 {
+		if !viper.GetBool("DisableLiveReload") {
+			livereload.Initialize()
+			http.HandleFunc("/livereload.js", livereload.ServeJS)
+			http.HandleFunc("/livereload", livereload.Handler)
+		}
+
 		go serve(port)
 	}
 
--- a/commands/server.go
+++ b/commands/server.go
@@ -14,7 +14,6 @@
 package commands
 
 import (
-	"bytes"
 	"fmt"
 	"net"
 	"net/http"
@@ -22,9 +21,6 @@
 	"strconv"
 	"strings"
 
-	"github.com/gorilla/websocket"
-	//"code.google.com/p/go.net/websocket"
-
 	"github.com/spf13/cobra"
 	"github.com/spf13/hugo/helpers"
 	jww "github.com/spf13/jwalterweatherman"
@@ -34,12 +30,9 @@
 var serverPort int
 var serverWatch bool
 var serverAppend bool
+var disableLiveReload bool
 
-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, "append-port", "", true, "append port to baseurl")
-}
+//var serverCmdV *cobra.Command
 
 var serverCmd = &cobra.Command{
 	Use:   "server",
@@ -47,9 +40,17 @@
 	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
 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) {
 	InitializeConfig()
 
@@ -57,6 +58,14 @@
 		BaseUrl = "http://localhost"
 	}
 
+	if cmd.Flags().Lookup("disableLiveReload").Changed {
+		viper.Set("DisableLiveReload", disableLiveReload)
+	}
+
+	if serverWatch {
+		viper.Set("Watch", true)
+	}
+
 	if !strings.HasPrefix(BaseUrl, "http://") {
 		BaseUrl = "http://" + BaseUrl
 	}
@@ -96,111 +105,13 @@
 
 func serve(port int) {
 	jww.FEEDBACK.Println("Serving pages from " + helpers.AbsPathify(viper.GetString("PublishDir")))
-
 	jww.FEEDBACK.Printf("Web Server is available at %s\n", viper.GetString("BaseUrl"))
-
 	fmt.Println("Press ctrl+c to stop")
 
 	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)
 	if err != nil {
 		jww.ERROR.Printf("Error: %s\n", err.Error())
 		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()
 }
--- a/docs/layouts/chrome/includes.html
+++ b/docs/layouts/chrome/includes.html
@@ -3,6 +3,5 @@
     <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 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>hljs.initHighlightingOnLoad();</script>
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -864,6 +864,10 @@
 		transformLinks = append(transformLinks, absURL...)
 	}
 
+	if viper.GetBool("watch") && !viper.GetBool("DisableLiveReload") {
+		transformLinks = append(transformLinks, transform.LiveReloadInject)
+	}
+
 	transformer := transform.NewChain(transformLinks...)
 
 	var renderBuffer *bytes.Buffer
--- /dev/null
+++ b/livereload/connection.go
@@ -1,0 +1,56 @@
+// Copyright © 2014 Steve Francia <[email protected]>.
+//
+// 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()
+}
--- /dev/null
+++ b/livereload/hub.go
@@ -1,0 +1,56 @@
+// Copyright © 2014 Steve Francia <[email protected]>.
+//
+// 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)
+				}
+			}
+		}
+	}
+}
--- /dev/null
+++ b/livereload/livereload.go
@@ -1,0 +1,49 @@
+// Copyright © 2014 Steve Francia <[email protected]>.
+//
+// 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 (
+	"net/http"
+
+	"github.com/gorilla/websocket"
+)
+
+var upgrader = &websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 1024}
+
+func Handler(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()
+}
+
+func Initialize() {
+	go wsHub.run()
+}
+
+func ForceRefresh() {
+	// Tell livereload a js file changed to force a hard refresh
+	wsHub.broadcast <- []byte(`{"command":"reload","path":"/x.js","originalPath":"","liveCSS":true}`)
+}
+
+func ServeJS(w http.ResponseWriter, r *http.Request) {
+	w.Write(livereloadJS)
+}
+
+var livereloadJS []byte = []byte(`(function(){var e={},t={},n={},r={},i={},s={},o={},u={},a={};var f,l,c,h,p=[].indexOf||function(e){for(var t=0,n=this.length;t<n;t++){if(t in this&&this[t]===e)return t}return-1};e.PROTOCOL_6=f="http://livereload.com/protocols/official-6";e.PROTOCOL_7=l="http://livereload.com/protocols/official-7";e.ProtocolError=h=function(){function e(e,t){this.message="LiveReload protocol error ("+e+') after receiving data: "'+t+'".'}return e}();e.Parser=c=function(){function e(e){this.handlers=e;this.reset()}e.prototype.reset=function(){return this.protocol=null};e.prototype.process=function(e){var t,n,r,i,s;try{if(this.protocol==null){if(e.match(/^!!ver:([\d.]+)$/)){this.protocol=6}else if(r=this._parseMessage(e,["hello"])){if(!r.protocols.length){throw new h("no protocols specified in handshake message")}else if(p.call(r.protocols,l)>=0){this.protocol=7}else if(p.call(r.protocols,f)>=0){this.protocol=6}else{throw new h("no supported protocols found")}}return this.handlers.connected(this.protocol)}else if(this.protocol===6){r=JSON.parse(e);if(!r.length){throw new h("protocol 6 messages must be arrays")}t=r[0],i=r[1];if(t!=="refresh"){throw new h("unknown protocol 6 command")}return this.handlers.message({command:"reload",path:i.path,liveCSS:(s=i.apply_css_live)!=null?s:true})}else{r=this._parseMessage(e,["reload","alert"]);return this.handlers.message(r)}}catch(o){n=o;if(n instanceof h){return this.handlers.error(n)}else{throw n}}};e.prototype._parseMessage=function(e,t){var n,r,i;try{r=JSON.parse(e)}catch(s){n=s;throw new h("unparsable JSON",e)}if(!r.command){throw new h('missing "command" key',e)}if(i=r.command,p.call(t,i)<0){throw new h("invalid command '"+r.command+"', only valid commands are: "+t.join(", ")+")",e)}return r};return e}();var d,f,l,c,v,m;m=e,c=m.Parser,f=m.PROTOCOL_6,l=m.PROTOCOL_7;v="2.0.8";t.Connector=d=function(){function e(e,t,n,r){var i=this;this.options=e;this.WebSocket=t;this.Timer=n;this.handlers=r;this._uri="ws://"+this.options.host+":"+this.options.port+"/livereload";this._nextDelay=this.options.mindelay;this._connectionDesired=false;this.protocol=0;this.protocolParser=new c({connected:function(e){i.protocol=e;i._handshakeTimeout.stop();i._nextDelay=i.options.mindelay;i._disconnectionReason="broken";return i.handlers.connected(e)},error:function(e){i.handlers.error(e);return i._closeOnError()},message:function(e){return i.handlers.message(e)}});this._handshakeTimeout=new n(function(){if(!i._isSocketConnected()){return}i._disconnectionReason="handshake-timeout";return i.socket.close()});this._reconnectTimer=new n(function(){if(!i._connectionDesired){return}return i.connect()});this.connect()}e.prototype._isSocketConnected=function(){return this.socket&&this.socket.readyState===this.WebSocket.OPEN};e.prototype.connect=function(){var e=this;this._connectionDesired=true;if(this._isSocketConnected()){return}this._reconnectTimer.stop();this._disconnectionReason="cannot-connect";this.protocolParser.reset();this.handlers.connecting();this.socket=new this.WebSocket(this._uri);this.socket.onopen=function(t){return e._onopen(t)};this.socket.onclose=function(t){return e._onclose(t)};this.socket.onmessage=function(t){return e._onmessage(t)};return this.socket.onerror=function(t){return e._onerror(t)}};e.prototype.disconnect=function(){this._connectionDesired=false;this._reconnectTimer.stop();if(!this._isSocketConnected()){return}this._disconnectionReason="manual";return this.socket.close()};e.prototype._scheduleReconnection=function(){if(!this._connectionDesired){return}if(!this._reconnectTimer.running){this._reconnectTimer.start(this._nextDelay);return this._nextDelay=Math.min(this.options.maxdelay,this._nextDelay*2)}};e.prototype.sendCommand=function(e){if(this.protocol==null){return}return this._sendCommand(e)};e.prototype._sendCommand=function(e){return this.socket.send(JSON.stringify(e))};e.prototype._closeOnError=function(){this._handshakeTimeout.stop();this._disconnectionReason="error";return this.socket.close()};e.prototype._onopen=function(e){var t;this.handlers.socketConne
\ No newline at end of file
--- /dev/null
+++ b/transform/livereloadinject.go
@@ -1,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
+}