mirror of
				https://github.com/gohugoio/hugo.git
				synced 2024-05-11 05:54:58 +00:00 
			
		
		
		
	livereloadinject: Use more robust injection method
This commit is contained in:
		
				
					committed by
					
						
						Bjørn Erik Pedersen
					
				
			
			
				
	
			
			
			
						parent
						
							a349aafb7f
						
					
				
				
					commit
					9dc608084b
				
			@@ -14,10 +14,10 @@
 | 
				
			|||||||
package livereloadinject
 | 
					package livereloadinject
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"bytes"
 | 
					 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"html"
 | 
						"html"
 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
 | 
						"regexp"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/gohugoio/hugo/common/loggers"
 | 
						"github.com/gohugoio/hugo/common/loggers"
 | 
				
			||||||
@@ -25,42 +25,27 @@ import (
 | 
				
			|||||||
	"github.com/gohugoio/hugo/transform"
 | 
						"github.com/gohugoio/hugo/transform"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const warnMessage = `"head" or "body" tag is required in html to append livereload script. ` +
 | 
					var ignoredSyntax = regexp.MustCompile(`(?s)^(?:\s+|<!--.*?-->|<\?.*?\?>)*`)
 | 
				
			||||||
	"As a fallback, Hugo injects it somewhere but it might not work properly."
 | 
					var tagsBeforeHead = []*regexp.Regexp{
 | 
				
			||||||
 | 
						regexp.MustCompile(`(?is)^<!doctype\s[^>]*>`),
 | 
				
			||||||
var warnScript = fmt.Sprintf(`<script data-no-instant defer>console.warn('%s');</script>`, warnMessage)
 | 
						regexp.MustCompile(`(?is)^<html(?:\s[^>]*)?>`),
 | 
				
			||||||
 | 
						regexp.MustCompile(`(?is)^<head(?:\s[^>]*)?>`),
 | 
				
			||||||
type tag struct {
 | 
					 | 
				
			||||||
	markup       []byte
 | 
					 | 
				
			||||||
	appendScript bool
 | 
					 | 
				
			||||||
	warnRequired bool
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var tags = []tag{
 | 
					// New creates a function that can be used to inject a script tag for
 | 
				
			||||||
	{markup: []byte("<head"), appendScript: true},
 | 
					// the livereload JavaScript at the start of an HTML document's head.
 | 
				
			||||||
	{markup: []byte("<HEAD"), appendScript: true},
 | 
					 | 
				
			||||||
	{markup: []byte("</body>")},
 | 
					 | 
				
			||||||
	{markup: []byte("</BODY>")},
 | 
					 | 
				
			||||||
	{markup: []byte("<html"), appendScript: true, warnRequired: true},
 | 
					 | 
				
			||||||
	{markup: []byte("<HTML"), appendScript: true, warnRequired: true},
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// New creates a function that can be used
 | 
					 | 
				
			||||||
// to inject a script tag for the livereload JavaScript in a HTML document.
 | 
					 | 
				
			||||||
func New(baseURL url.URL) transform.Transformer {
 | 
					func New(baseURL url.URL) transform.Transformer {
 | 
				
			||||||
	return func(ft transform.FromTo) error {
 | 
						return func(ft transform.FromTo) error {
 | 
				
			||||||
		b := ft.From().Bytes()
 | 
							b := ft.From().Bytes()
 | 
				
			||||||
		idx := -1
 | 
					
 | 
				
			||||||
		var match tag
 | 
							// We find the start of the head by reading past (in order)
 | 
				
			||||||
		// We used to insert the livereload script right before the closing body.
 | 
							// the doctype declaration, HTML start tag and head start tag,
 | 
				
			||||||
		// This does not work when combined with tools such as Turbolinks.
 | 
							// all of which are optional, and any whitespace, comments, or
 | 
				
			||||||
		// So we try to inject the script as early as possible.
 | 
							// XML instructions in-between.
 | 
				
			||||||
		for _, t := range tags {
 | 
							idx := 0
 | 
				
			||||||
			idx = bytes.Index(b, t.markup)
 | 
							for _, tag := range tagsBeforeHead {
 | 
				
			||||||
			if idx != -1 {
 | 
								idx += len(ignoredSyntax.Find(b[idx:]))
 | 
				
			||||||
				match = t
 | 
								idx += len(tag.Find(b[idx:]))
 | 
				
			||||||
				break
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		path := strings.TrimSuffix(baseURL.Path, "/")
 | 
							path := strings.TrimSuffix(baseURL.Path, "/")
 | 
				
			||||||
@@ -72,23 +57,9 @@ func New(baseURL url.URL) transform.Transformer {
 | 
				
			|||||||
		c := make([]byte, len(b))
 | 
							c := make([]byte, len(b))
 | 
				
			||||||
		copy(c, b)
 | 
							copy(c, b)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if idx == -1 {
 | 
					 | 
				
			||||||
			idx = len(b)
 | 
					 | 
				
			||||||
			match = tag{warnRequired: true}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		script := []byte(fmt.Sprintf(`<script src="%s" data-no-instant defer></script>`, html.EscapeString(src)))
 | 
							script := []byte(fmt.Sprintf(`<script src="%s" data-no-instant defer></script>`, html.EscapeString(src)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		i := idx
 | 
							c = append(c[:idx], append(script, c[idx:]...)...)
 | 
				
			||||||
		if match.appendScript {
 | 
					 | 
				
			||||||
			i += bytes.Index(b[i:], []byte(">")) + 1
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if match.warnRequired {
 | 
					 | 
				
			||||||
			script = append(script, []byte(warnScript)...)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		c = append(c[:i], append(script, c[i:]...)...)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if _, err := ft.To().Write(c); err != nil {
 | 
							if _, err := ft.To().Write(c); err != nil {
 | 
				
			||||||
			loggers.Log().Warnf("Failed to inject LiveReload script:", err)
 | 
								loggers.Log().Warnf("Failed to inject LiveReload script:", err)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -43,32 +43,80 @@ func TestLiveReloadInject(t *testing.T) {
 | 
				
			|||||||
		return out.String()
 | 
							return out.String()
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	c.Run("Head lower", func(c *qt.C) {
 | 
						c.Run("Inject after head tag", func(c *qt.C) {
 | 
				
			||||||
		c.Assert(apply("<html><head>foo"), qt.Equals, "<html><head>"+expectBase+"foo")
 | 
							c.Assert(apply("<!doctype html><html><head>after"), qt.Equals, "<!doctype html><html><head>"+expectBase+"after")
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	c.Run("Head upper", func(c *qt.C) {
 | 
						c.Run("Inject after head tag when doctype and html omitted", func(c *qt.C) {
 | 
				
			||||||
		c.Assert(apply("<html><HEAD>foo"), qt.Equals, "<html><HEAD>"+expectBase+"foo")
 | 
							c.Assert(apply("<head>after"), qt.Equals, "<head>"+expectBase+"after")
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	c.Run("Body lower", func(c *qt.C) {
 | 
						c.Run("Inject after html when head omitted", func(c *qt.C) {
 | 
				
			||||||
		c.Assert(apply("foo</body>"), qt.Equals, "foo"+expectBase+"</body>")
 | 
							c.Assert(apply("<html>after"), qt.Equals, "<html>"+expectBase+"after")
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	c.Run("Body upper", func(c *qt.C) {
 | 
						c.Run("Inject after doctype when head and html omitted", func(c *qt.C) {
 | 
				
			||||||
		c.Assert(apply("foo</BODY>"), qt.Equals, "foo"+expectBase+"</BODY>")
 | 
							c.Assert(apply("<!doctype html>after"), qt.Equals, "<!doctype html>"+expectBase+"after")
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	c.Run("Html upper", func(c *qt.C) {
 | 
						c.Run("Inject before other elements if all else omitted", func(c *qt.C) {
 | 
				
			||||||
		c.Assert(apply("<html>foo"), qt.Equals, "<html>"+expectBase+warnScript+"foo")
 | 
							c.Assert(apply("<title>after</title>"), qt.Equals, expectBase+"<title>after</title>")
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	c.Run("Html upper with attr", func(c *qt.C) {
 | 
						c.Run("Inject before text content if all else omitted", func(c *qt.C) {
 | 
				
			||||||
		c.Assert(apply(`<html lang="en">foo`), qt.Equals, `<html lang="en">`+expectBase+warnScript+"foo")
 | 
							c.Assert(apply("after"), qt.Equals, expectBase+"after")
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	c.Run("No match", func(c *qt.C) {
 | 
						c.Run("Inject after HeAd tag MiXed CaSe", func(c *qt.C) {
 | 
				
			||||||
		c.Assert(apply("<h1>No match</h1>"), qt.Equals, "<h1>No match</h1>"+expectBase+warnScript)
 | 
							c.Assert(apply("<HeAd>AfTer"), qt.Equals, "<HeAd>"+expectBase+"AfTer")
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.Run("Inject after HtMl tag MiXed CaSe", func(c *qt.C) {
 | 
				
			||||||
 | 
							c.Assert(apply("<HtMl>AfTer"), qt.Equals, "<HtMl>"+expectBase+"AfTer")
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.Run("Inject after doctype mixed case", func(c *qt.C) {
 | 
				
			||||||
 | 
							c.Assert(apply("<!DocType HtMl>AfTer"), qt.Equals, "<!DocType HtMl>"+expectBase+"AfTer")
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.Run("Inject after html tag with attributes", func(c *qt.C) {
 | 
				
			||||||
 | 
							c.Assert(apply(`<html lang="en">after`), qt.Equals, `<html lang="en">`+expectBase+"after")
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.Run("Inject after html tag with newline", func(c *qt.C) {
 | 
				
			||||||
 | 
							c.Assert(apply("<html\n>after"), qt.Equals, "<html\n>"+expectBase+"after")
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.Run("Skip comments and whitespace", func(c *qt.C) {
 | 
				
			||||||
 | 
							c.Assert(
 | 
				
			||||||
 | 
								apply(" <!--x--> <!doctype html>\n<?xml instruction ?> <head>after"),
 | 
				
			||||||
 | 
								qt.Equals,
 | 
				
			||||||
 | 
								" <!--x--> <!doctype html>\n<?xml instruction ?> <head>"+expectBase+"after",
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.Run("Do not search inside comment", func(c *qt.C) {
 | 
				
			||||||
 | 
							c.Assert(apply("<html><!--<head>-->"), qt.Equals, "<html><!--<head>-->"+expectBase)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.Run("Do not search inside scripts", func(c *qt.C) {
 | 
				
			||||||
 | 
							c.Assert(apply("<html><script>`<head>`</script>"), qt.Equals, "<html>"+expectBase+"<script>`<head>`</script>")
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.Run("Do not search inside templates", func(c *qt.C) {
 | 
				
			||||||
 | 
							c.Assert(apply("<html><template><head></template>"), qt.Not(qt.Equals), "<html><template><head>"+expectBase+"</template>")
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.Run("Search from the start of the input", func(c *qt.C) {
 | 
				
			||||||
 | 
							c.Assert(apply("<head>after<head>"), qt.Equals, "<head>"+expectBase+"after<head>")
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.Run("Do not mistake header for head", func(c *qt.C) {
 | 
				
			||||||
 | 
							c.Assert(apply("<html><header>"), qt.Equals, "<html>"+expectBase+"<header>")
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.Run("Do not mistake custom elements for head", func(c *qt.C) {
 | 
				
			||||||
 | 
							c.Assert(apply("<html><head-custom>"), qt.Equals, "<html>"+expectBase+"<head-custom>")
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user