Proper integration of live reload with automatic injection

This commit is contained in:
spf13 committed 2014-05-16 17:49:27 -04:00
1 parent 60ed5bda2b
commit be1ee22032
8 files changed
+224 -121

No files matched your search

+21 -12
View File
@@ -15,6 +15,7 @@ package commands
import (
"fmt"
"net/http"
"os"
"path/filepath"
"runtime"
@@ -26,6 +27,7 @@ import (
"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 @@ Complete documentation is available at http://hugo.spf13.com`,
build()
},
}
var hugoCmdV *cobra.Command
var BuildWatch, Draft, UglyUrls, Verbose, Logging, VerboseLog, DisableRSS, DisableSitemap bool
@@ -66,19 +69,19 @@ func AddCommands() {
}
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")
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 @@ func InitializeConfig() {
viper.RegisterAlias("taxonomies", "indexes")
viper.SetDefault("Watch", false)
viper.SetDefault("MetadataFormat", "toml")
viper.SetDefault("DisableRSS", false)
viper.SetDefault("DisableSitemap", false)
@@ -112,12 +116,13 @@ func InitializeConfig() {
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 @@ func InitializeConfig() {
viper.Set("Verbose", Verbose)
}
if hugoCmdV.PersistentFlags().Lookup("logfile").Changed {
if hugoCmdV.PersistentFlags().Lookup("logFile").Changed {
viper.Set("LogFile", LogFile)
}
if BaseUrl != "" {
@@ -323,16 +328,14 @@ func NewWatcher(port int) error {
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 {
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 @@ func NewWatcher(port int) error {
}()
if port > 0 {
if !viper.GetBool("DisableLiveReload") {
livereload.Initialize()
http.HandleFunc("/livereload.js", livereload.ServeJS)
http.HandleFunc("/livereload", livereload.Handler)
}
go serve(port)
}
+19 -108
View File
@@ -14,7 +14,6 @@
package commands
import (
"bytes"
"fmt"
"net"
"net/http"
@@ -22,9 +21,6 @@ import (
"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 @@ import (
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,7 +40,15 @@ var serverCmd = &cobra.Command{
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) {
@@ -57,6 +58,14 @@ func server(cmd *cobra.Command, args []string) {
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 server(cmd *cobra.Command, args []string) {
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()
}
-1
View File
@@ -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>
+4
View File
@@ -864,6 +864,10 @@ func (s *Site) render(d interface{}, out string, layouts ...string) (err error)
transformLinks = append(transformLinks, absURL...)
}
if viper.GetBool("watch") && !viper.GetBool("DisableLiveReload") {
transformLinks = append(transformLinks, transform.LiveReloadInject)
}
transformer := transform.NewChain(transformLinks...)
var renderBuffer *bytes.Buffer
+56
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
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
View File
@@ -0,0 +1,49 @@
// 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 (
"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.socketConnected();this._disconnectionReason="handshake-failed";t={command:"hello",protocols:[f,l]};t.ver=v;if(this.options.ext){t.ext=this.options.ext}if(this.options.extver){t.extver=this.options.extver}if(this.options.snipver){t.snipver=this.options.snipver}this._sendCommand(t);return this._handshakeTimeout.start(this.options.handshake_timeout)};e.prototype._onclose=function(e){this.protocol=0;this.handlers.disconnected(this._disconnectionReason,this._nextDelay);return this._scheduleReconnection()};e.prototype._onerror=function(e){};e.prototype._onmessage=function(e){return this.protocolParser.process(e.data)};return e}();var g;g={bind:function(e,t,n){if(e.addEventListener){return e.addEventListener(t,n,false)}else if(e.attachEvent){e[t]=1;return e.attachEvent("onpropertychange",function(e){if(e.propertyName===t){return n()}})}else{throw new Error("Attempt to attach custom event "+t+" to something whLine truncated
+19
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
}