diff --git a/commands/hugo.go b/commands/hugo.go index b965ba167..42e129cf1 100644 --- a/commands/hugo.go +++ b/commands/hugo.go @@ -981,8 +981,28 @@ func (c *commandeer) newWatcher(port int) error { } if !buildWatch && !c.Cfg.GetBool("disableLiveReload") { - // Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized - livereload.ForceRefresh() + + navigate := c.Cfg.GetBool("navigateToChanged") + + var p *hugolib.Page + + if navigate { + + // It is probably more confusing than useful + // to navigate to a new URL on RENAME etc. + // so for now we use the WRITE event only. + name := pickOneWritePath(dynamicEvents) + + if name != "" { + p = Hugo.GetContentPage(name) + } + } + + if p != nil { + livereload.NavigateToPath(p.RelPermalink()) + } else { + livereload.ForceRefresh() + } } } case err := <-watcher.Errors: @@ -1007,6 +1027,16 @@ func (c *commandeer) newWatcher(port int) error { return nil } +func pickOneWritePath(events []fsnotify.Event) string { + for _, ev := range events { + if ev.Op&fsnotify.Write == fsnotify.Write { + return ev.Name + } + } + + return "" +} + func (c *commandeer) isStatic(path string) bool { return strings.HasPrefix(path, c.PathSpec().GetStaticDirPath()) || (len(c.PathSpec().GetThemesDirPath()) > 0 && strings.HasPrefix(path, c.PathSpec().GetThemesDirPath())) } diff --git a/commands/server.go b/commands/server.go index 32a94f9c8..89d5c239e 100644 --- a/commands/server.go +++ b/commands/server.go @@ -33,6 +33,7 @@ import ( var ( disableLiveReload bool + navigateToChanged bool renderToDisk bool serverAppend bool serverInterface string @@ -87,6 +88,7 @@ func init() { serverCmd.Flags().BoolVarP(&serverWatch, "watch", "w", true, "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.Flags().BoolVar(&navigateToChanged, "navigateToChanged", false, "navigate to changed content file on live browser reload") serverCmd.Flags().BoolVar(&renderToDisk, "renderToDisk", false, "render to Destination path (default is render to memory & serve from there)") serverCmd.Flags().String("memstats", "", "log memory usage to this file") serverCmd.Flags().String("meminterval", "100ms", "interval to poll memory usage (requires --memstats), valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".") @@ -110,6 +112,10 @@ func server(cmd *cobra.Command, args []string) error { c.Set("disableLiveReload", disableLiveReload) } + if cmd.Flags().Changed("navigateToChanged") { + c.Set("navigateToChanged", navigateToChanged) + } + if serverWatch { c.Set("watch", true) } diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go index 4fe3f8771..c3829fb1d 100644 --- a/hugolib/hugo_sites.go +++ b/hugolib/hugo_sites.go @@ -19,6 +19,8 @@ import ( "strings" "sync" + "path/filepath" + "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/helpers" @@ -38,6 +40,27 @@ type HugoSites struct { *deps.Deps } +// GetContentPage finds a Page with content given the absolute filename. +// Returns nil if none found. +func (h *HugoSites) GetContentPage(filename string) *Page { + s := h.Sites[0] + contendDir := filepath.Join(s.PathSpec.AbsPathify(s.Cfg.GetString("contentDir"))) + if !strings.HasPrefix(filename, contendDir) { + return nil + } + + rel := strings.TrimPrefix(filename, contendDir) + rel = strings.TrimPrefix(rel, helpers.FilePathSeparator) + + pos := s.rawAllPages.findPagePosByFilePath(rel) + + if pos == -1 { + return nil + } + return s.rawAllPages[pos] + +} + // NewHugoSites creates a new collection of sites given the input sites, building // a language configuration based on those. func newHugoSites(cfg deps.DepsCfg, sites ...*Site) (*HugoSites, error) { diff --git a/hugolib/hugo_sites_build_test.go b/hugolib/hugo_sites_build_test.go index e7d791652..c343f6087 100644 --- a/hugolib/hugo_sites_build_test.go +++ b/hugolib/hugo_sites_build_test.go @@ -223,6 +223,12 @@ func doTestMultiSitesBuild(t *testing.T, configTemplate, configSuffix string) { require.NotNil(t, s.disabledKinds) } + gp1 := sites.GetContentPage(filepath.FromSlash("content/sect/doc1.en.md")) + require.NotNil(t, gp1) + require.Equal(t, "doc1", gp1.Title) + gp2 := sites.GetContentPage(filepath.FromSlash("content/sect/notfound.md")) + require.Nil(t, gp2) + enSite := sites.Sites[0] enSiteHome := enSite.getPage(KindHome) require.True(t, enSiteHome.IsTranslated()) diff --git a/livereload/livereload.go b/livereload/livereload.go index 04e3ac0f0..a5da891fc 100644 --- a/livereload/livereload.go +++ b/livereload/livereload.go @@ -37,12 +37,16 @@ package livereload import ( + "fmt" "net/http" "path/filepath" "github.com/gorilla/websocket" ) +// Prefix to signal to LiveReload that we need to navigate to another path. +const hugoNavigatePrefix = "__hugo_navigate" + var upgrader = &websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 1024} // Handler is a HandlerFunc handling the livereload @@ -69,6 +73,12 @@ func ForceRefresh() { RefreshPath("/x.js") } +// NavigateToPath tells livereload to navigate to the given path. +// This translates to `window.location.href = path` in the client. +func NavigateToPath(path string) { + RefreshPath(hugoNavigatePrefix + path) +} + // RefreshPath tells livereload to refresh only the given path. // If that path points to a CSS stylesheet or an image, only the changes // will be updated in the browser, not the entire page. @@ -81,9 +91,42 @@ func RefreshPath(s string) { // ServeJS serves the liverreload.js who's reference is injected into the page. func ServeJS(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/javascript") - w.Write(livereloadJS) + w.Write(liveReloadJS()) } -// livereloadJS contains the output of `uglifyjs livereload.js -m toplevel` -// Source: https://github.com/livereload/livereload-js/archive/v2.2.1.tar.gz -var livereloadJS = []byte(`(function e(t,n,o){function i(s,l){if(!n[s]){if(!t[s]){var c=typeof require=="function"&&require;if(!l&&c)return c(s,!0);if(r)return r(s,!0);var a=new Error("Cannot find module '"+s+"'");throw a.code="MODULE_NOT_FOUND",a}var u=n[s]={exports:{}};t[s][0].call(u.exports,function(e){var n=t[s][1][e];return i(n?n:e)},u,u.exports,e,t,n,o)}return n[s].exports}var r=typeof require=="function"&&require;for(var s=0;s tag");return}this.reloader=new r(this.window,this.console,s);this.connector=new t(this.options,this.WebSocket,s,{connecting:function(e){return function(){}}(this),socketConnected:function(e){return function(){}}(this),connected:function(e){return function(t){var n;if(typeof(n=e.listeners).connect==="function"){n.connect()}e.log("LiveReload is connected to "+e.options.host+":"+e.options.port+" (protocol v"+t+").");return e.analyze()}}(this),error:function(e){return function(e){if(e instanceof ProtocolError){if(typeof console!=="undefined"&&console!==null){return console.log(""+e.message+".")}}else{if(typeof console!=="undefined"&&console!==null){return console.log("LiveReload internal error: "+e.message)}}}}(this),disconnected:function(e){return function(t,n){var o;if(typeof(o=e.listeners).disconnect==="function"){o.disconnect()}switch(t){case"cannot-connect":return e.log("LiveReload cannot connect to "+e.options.host+":"+e.options.port+", will retry in "+n+" sec.");case"broken":return e.log("LiveReload disconnected from "+e.options.host+":"+e.options.port+", reconnecting in "+n+" sec.");case"handshake-timeout":return e.log("LiveReload cannot connect to "+e.options.host+":"+e.options.port+" (handshake timeout), will retry in "+n+" sec.");case"handshake-failed":return e.log("LiveReload cannot connect to "+e.options.host+":"+e.options.port+" (handshake failed), will retry in "+n+" sec.");case"manual":break;case"error":break;default:return e.log("LiveReload disconnected from "+e.options.host+":"+e.options.port+" ("+t+"), reconnecting in "+n+" sec.")}}}(this),message:function(e){return function(t){switch(t.command){case"reload":return e.performReload(t);case"alert":return e.performAlert(t)}}}(this)})}e.prototype.on=function(e,t){return this.listeners[e]=t};e.prototype.log=function(e){return this.console.log(""+e)};e.prototype.performReload=function(e){var t,n;this.log("LiveReload received reload request: "+JSON.stringify(e,null,2));return this.reloader.reload(e.path,{liveCSS:(t=e.liveCSS)!=null?t:true,liveImg:(n=e.liveImg)!=null?n:true,originalPath:e.originalPath||"",overrideURL:e.overrideURL||"",serverURL:"http://"+this.options.host+":"+this.options.port})};e.prototype.performAlert=function(e){return alert(e.message)};e.prototype.shutDown=function(){var e;this.connector.disconnect();this.log("LiveReload disconnected.");return typeof(e=this.listeners).shutdown==="function"?e.shutdown():void 0};e.prototype.hasPlugin=function(e){return!!this.pluginIdentifiers[e]};e.prototype.addPlugin=function(e){var t;if(this.hasPlugin(e.identifier)){return}this.pluginIdentifiers[e.identifier]=true;t=new e(this.window,{_livereload:this,_reloader:this.reloader,_connector:this.connector,console:this.console,Timer:s,generateCacheBustUrl:function(e){return function(t){return e.reloader.generateCacheBustUrl(t)}}(this)});this.plugins.push(t);this.reloader.addPlugin(t)};e.prototype.analyze=function(){var e,t,n,o,i,r;if(!(this.connector.protocol>=7)){return}n={};r=this.plugins;for(o=0,i=r.length;o1){s.set(o[0].replace(/-/g,"_"),o.slice(1).join("="))}}}return s}}return null}}).call(this)},{}],6:[function(e,t,n){(function(){var e,t,o,i,r=[].indexOf||function(e){for(var t=0,n=this.length;t=0){this.protocol=7}else if(r.call(l.protocols,e)>=0){this.protocol=6}else{throw new i("no supported protocols found")}}return this.handlers.connected(this.protocol)}else if(this.protocol===6){l=JSON.parse(n);if(!l.length){throw new i("protocol 6 messages must be arrays")}o=l[0],c=l[1];if(o!=="refresh"){throw new i("unknown protocol 6 command")}return this.handlers.message({command:"reload",path:c.path,liveCSS:(a=c.apply_css_live)!=null?a:true})}else{l=this._parseMessage(n,["reload","alert"]);return this.handlers.message(l)}}catch(u){s=u;if(s instanceof i){return this.handlers.error(s)}else{throw s}}};n.prototype._parseMessage=function(e,t){var n,o,s;try{o=JSON.parse(e)}catch(l){n=l;throw new i("unparsable JSON",e)}if(!o.command){throw new i('missing "command" key',e)}if(s=o.command,r.call(t,s)<0){throw new i("invalid command '"+o.command+"', only valid commands are: "+t.join(", ")+")",e)}return o};return n}()}).call(this)},{}],7:[function(e,t,n){(function(){var e,t,o,i,r,s,l;l=function(e){var t,n,o;if((n=e.indexOf("#"))>=0){t=e.slice(n);e=e.slice(0,n)}else{t=""}if((n=e.indexOf("?"))>=0){o=e.slice(n);e=e.slice(0,n)}else{o=""}return{url:e,params:o,hash:t}};i=function(e){var t;e=l(e).url;if(e.indexOf("file://")===0){t=e.replace(/^file:\/\/(localhost)?/,"")}else{t=e.replace(/^([^:]+:)?\/\/([^:\/]+)(:\d*)?\//,"/")}return decodeURIComponent(t)};s=function(e,t,n){var i,r,s,l,c;i={score:0};for(l=0,c=t.length;li.score){i={object:r,score:s}}}if(i.score>0){return i}else{return null}};o=function(e,t){var n,o,i,r;e=e.replace(/^\/+/,"").toLowerCase();t=t.replace(/^\/+/,"").toLowerCase();if(e===t){return 1e4}n=e.split("/").reverse();o=t.split("/").reverse();r=Math.min(n.length,o.length);i=0;while(i0};e=[{selector:"background",styleNames:["backgroundImage"]},{selector:"border",styleNames:["borderImage","webkitBorderImage","MozBorderImage"]}];n.Reloader=t=function(){function t(e,t,n){this.window=e;this.console=t;this.Timer=n;this.document=this.window.document;this.importCacheWaitPeriod=200;this.plugins=[]}t.prototype.addPlugin=function(e){return this.plugins.push(e)};t.prototype.analyze=function(e){return results};t.prototype.reload=function(e,t){var n,o,i,r,s;this.options=t;if((o=this.options).stylesheetReloadTimeout==null){o.stylesheetReloadTimeout=15e3}s=this.plugins;for(i=0,r=s.length;i tag");return}}this.reloader=new s(this.window,this.console,l);this.connector=new t(this.options,this.WebSocket,l,{connecting:function(e){return function(){}}(this),socketConnected:function(e){return function(){}}(this),connected:function(e){return function(t){var n;if(typeof(n=e.listeners).connect==="function"){n.connect()}e.log("LiveReload is connected to "+e.options.host+":"+e.options.port+" (protocol v"+t+").");return e.analyze()}}(this),error:function(e){return function(e){if(e instanceof r){if(typeof console!=="undefined"&&console!==null){return console.log(""+e.message+".")}}else{if(typeof console!=="undefined"&&console!==null){return console.log("LiveReload internal error: "+e.message)}}}}(this),disconnected:function(e){return function(t,n){var o;if(typeof(o=e.listeners).disconnect==="function"){o.disconnect()}switch(t){case"cannot-connect":return e.log("LiveReload cannot connect to "+e.options.host+":"+e.options.port+", will retry in "+n+" sec.");case"broken":return e.log("LiveReload disconnected from "+e.options.host+":"+e.options.port+", reconnecting in "+n+" sec.");case"handshake-timeout":return e.log("LiveReload cannot connect to "+e.options.host+":"+e.options.port+" (handshake timeout), will retry in "+n+" sec.");case"handshake-failed":return e.log("LiveReload cannot connect to "+e.options.host+":"+e.options.port+" (handshake failed), will retry in "+n+" sec.");case"manual":break;case"error":break;default:return e.log("LiveReload disconnected from "+e.options.host+":"+e.options.port+" ("+t+"), reconnecting in "+n+" sec.")}}}(this),message:function(e){return function(t){switch(t.command){case"reload":return e.performReload(t);case"alert":return e.performAlert(t)}}}(this)});this.initialized=true}e.prototype.on=function(e,t){return this.listeners[e]=t};e.prototype.log=function(e){return this.console.log(""+e)};e.prototype.performReload=function(e){var t,n;this.log("LiveReload received reload request: "+JSON.stringify(e,null,2));return this.reloader.reload(e.path,{liveCSS:(t=e.liveCSS)!=null?t:true,liveImg:(n=e.liveImg)!=null?n:true,originalPath:e.originalPath||"",overrideURL:e.overrideURL||"",serverURL:"http://"+this.options.host+":"+this.options.port})};e.prototype.performAlert=function(e){return alert(e.message)};e.prototype.shutDown=function(){var e;if(!this.initialized){return}this.connector.disconnect();this.log("LiveReload disconnected.");return typeof(e=this.listeners).shutdown==="function"?e.shutdown():void 0};e.prototype.hasPlugin=function(e){return!!this.pluginIdentifiers[e]};e.prototype.addPlugin=function(e){var t;if(!this.initialized){return}if(this.hasPlugin(e.identifier)){return}this.pluginIdentifiers[e.identifier]=true;t=new e(this.window,{_livereload:this,_reloader:this.reloader,_connector:this.connector,console:this.console,Timer:l,generateCacheBustUrl:function(e){return function(t){return e.reloader.generateCacheBustUrl(t)}}(this)});this.plugins.push(t);this.reloader.addPlugin(t)};e.prototype.analyze=function(){var e,t,n,o,i,r;if(!this.initialized){return}if(!(this.connector.protocol>=7)){return}n={};r=this.plugins;for(o=0,i=r.length;o1){s.set(o[0].replace(/-/g,"_"),o.slice(1).join("="))}}}return s}}return null}}).call(this)},{}],6:[function(e,t,n){(function(){var e,t,o,i,r=[].indexOf||function(e){for(var t=0,n=this.length;t=0){this.protocol=7}else if(r.call(l.protocols,e)>=0){this.protocol=6}else{throw new i("no supported protocols found")}}return this.handlers.connected(this.protocol)}else if(this.protocol===6){l=JSON.parse(n);if(!l.length){throw new i("protocol 6 messages must be arrays")}o=l[0],c=l[1];if(o!=="refresh"){throw new i("unknown protocol 6 command")}return this.handlers.message({command:"reload",path:c.path,liveCSS:(a=c.apply_css_live)!=null?a:true})}else{l=this._parseMessage(n,["reload","alert"]);return this.handlers.message(l)}}catch(e){s=e;if(s instanceof i){return this.handlers.error(s)}else{throw s}}};n.prototype._parseMessage=function(e,t){var n,o,s;try{o=JSON.parse(e)}catch(t){n=t;throw new i("unparsable JSON",e)}if(!o.command){throw new i('missing "command" key',e)}if(s=o.command,r.call(t,s)<0){throw new i("invalid command '"+o.command+"', only valid commands are: "+t.join(", ")+")",e)}return o};return n}()}).call(this)},{}],7:[function(e,t,n){(function(){var e,t,o,i,r,s,l;l=function(e){var t,n,o;if((n=e.indexOf("#"))>=0){t=e.slice(n);e=e.slice(0,n)}else{t=""}if((n=e.indexOf("?"))>=0){o=e.slice(n);e=e.slice(0,n)}else{o=""}return{url:e,params:o,hash:t}};i=function(e){var t;e=l(e).url;if(e.indexOf("file://")===0){t=e.replace(/^file:\/\/(localhost)?/,"")}else{t=e.replace(/^([^:]+:)?\/\/([^:\/]+)(:\d*)?\//,"/")}return decodeURIComponent(t)};s=function(e,t,n){var i,r,s,l,c;i={score:0};for(l=0,c=t.length;li.score){i={object:r,score:s}}}if(i.score>0){return i}else{return null}};o=function(e,t){var n,o,i,r;e=e.replace(/^\/+/,"").toLowerCase();t=t.replace(/^\/+/,"").toLowerCase();if(e===t){return 1e4}n=e.split("/").reverse();o=t.split("/").reverse();r=Math.min(n.length,o.length);i=0;while(i0};e=[{selector:"background",styleNames:["backgroundImage"]},{selector:"border",styleNames:["borderImage","webkitBorderImage","MozBorderImage"]}];n.Reloader=t=function(){function t(e,t,n){this.window=e;this.console=t;this.Timer=n;this.document=this.window.document;this.importCacheWaitPeriod=200;this.plugins=[]}t.prototype.addPlugin=function(e){return this.plugins.push(e)};t.prototype.analyze=function(e){return results};t.prototype.reload=function(e,t){var n,o,i,r,s;this.options=t;if((o=this.options).stylesheetReloadTimeout==null){o.stylesheetReloadTimeout=15e3}s=this.plugins;for(i=0,r=s.length;i