]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
livereloadinject: Use more robust injection method
authorDomino Goupil <1er@live.ca>
Wed, 18 Oct 2023 21:46:36 +0000 (17:46 -0400)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sun, 29 Oct 2023 17:37:05 +0000 (18:37 +0100)
transform/livereloadinject/livereloadinject.go
transform/livereloadinject/livereloadinject_test.go

index a29a64ebbbf55a475453d5f02a731c6e5135a085..43c03348d233ac7fadc0d50634236ee963e935a8 100644 (file)
 package livereloadinject
 
 import (
-       "bytes"
        "fmt"
        "html"
        "net/url"
+       "regexp"
        "strings"
 
        "github.com/gohugoio/hugo/common/loggers"
@@ -25,42 +25,27 @@ import (
        "github.com/gohugoio/hugo/transform"
 )
 
-const warnMessage = `"head" or "body" tag is required in html to append livereload script. ` +
-       "As a fallback, Hugo injects it somewhere but it might not work properly."
-
-var warnScript = fmt.Sprintf(`<script data-no-instant defer>console.warn('%s');</script>`, warnMessage)
-
-type tag struct {
-       markup       []byte
-       appendScript bool
-       warnRequired bool
-}
-
-var tags = []tag{
-       {markup: []byte("<head"), appendScript: true},
-       {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},
+var ignoredSyntax = regexp.MustCompile(`(?s)^(?:\s+|<!--.*?-->|<\?.*?\?>)*`)
+var tagsBeforeHead = []*regexp.Regexp{
+       regexp.MustCompile(`(?is)^<!doctype\s[^>]*>`),
+       regexp.MustCompile(`(?is)^<html(?:\s[^>]*)?>`),
+       regexp.MustCompile(`(?is)^<head(?:\s[^>]*)?>`),
 }
 
-// New creates a function that can be used
-// to inject a script tag for the livereload JavaScript in a HTML document.
+// New creates a function that can be used to inject a script tag for
+// the livereload JavaScript at the start of an HTML document's head.
 func New(baseURL url.URL) transform.Transformer {
        return func(ft transform.FromTo) error {
                b := ft.From().Bytes()
-               idx := -1
-               var match tag
-               // We used to insert the livereload script right before the closing body.
-               // This does not work when combined with tools such as Turbolinks.
-               // So we try to inject the script as early as possible.
-               for _, t := range tags {
-                       idx = bytes.Index(b, t.markup)
-                       if idx != -1 {
-                               match = t
-                               break
-                       }
+
+               // We find the start of the head by reading past (in order)
+               // the doctype declaration, HTML start tag and head start tag,
+               // all of which are optional, and any whitespace, comments, or
+               // XML instructions in-between.
+               idx := 0
+               for _, tag := range tagsBeforeHead {
+                       idx += len(ignoredSyntax.Find(b[idx:]))
+                       idx += len(tag.Find(b[idx:]))
                }
 
                path := strings.TrimSuffix(baseURL.Path, "/")
@@ -72,23 +57,9 @@ func New(baseURL url.URL) transform.Transformer {
                c := make([]byte, len(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)))
 
-               i := 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:]...)...)
+               c = append(c[:idx], append(script, c[idx:]...)...)
 
                if _, err := ft.To().Write(c); err != nil {
                        loggers.Log().Warnf("Failed to inject LiveReload script:", err)
index cba2d383451e1677a17956ee861fbe2e337c18df..dc87402081501310ca7a4cd7939005bbaaed47a7 100644 (file)
@@ -43,32 +43,80 @@ func TestLiveReloadInject(t *testing.T) {
                return out.String()
        }
 
-       c.Run("Head lower", func(c *qt.C) {
-               c.Assert(apply("<html><head>foo"), qt.Equals, "<html><head>"+expectBase+"foo")
+       c.Run("Inject after head tag", func(c *qt.C) {
+               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.Assert(apply("<html><HEAD>foo"), qt.Equals, "<html><HEAD>"+expectBase+"foo")
+       c.Run("Inject after head tag when doctype and html omitted", func(c *qt.C) {
+               c.Assert(apply("<head>after"), qt.Equals, "<head>"+expectBase+"after")
        })
 
-       c.Run("Body lower", func(c *qt.C) {
-               c.Assert(apply("foo</body>"), qt.Equals, "foo"+expectBase+"</body>")
+       c.Run("Inject after html when head omitted", func(c *qt.C) {
+               c.Assert(apply("<html>after"), qt.Equals, "<html>"+expectBase+"after")
        })
 
-       c.Run("Body upper", func(c *qt.C) {
-               c.Assert(apply("foo</BODY>"), qt.Equals, "foo"+expectBase+"</BODY>")
+       c.Run("Inject after doctype when head and html omitted", func(c *qt.C) {
+               c.Assert(apply("<!doctype html>after"), qt.Equals, "<!doctype html>"+expectBase+"after")
        })
 
-       c.Run("Html upper", func(c *qt.C) {
-               c.Assert(apply("<html>foo"), qt.Equals, "<html>"+expectBase+warnScript+"foo")
+       c.Run("Inject before other elements if all else omitted", func(c *qt.C) {
+               c.Assert(apply("<title>after</title>"), qt.Equals, expectBase+"<title>after</title>")
        })
 
-       c.Run("Html upper with attr", func(c *qt.C) {
-               c.Assert(apply(`<html lang="en">foo`), qt.Equals, `<html lang="en">`+expectBase+warnScript+"foo")
+       c.Run("Inject before text content if all else omitted", func(c *qt.C) {
+               c.Assert(apply("after"), qt.Equals, expectBase+"after")
        })
 
-       c.Run("No match", func(c *qt.C) {
-               c.Assert(apply("<h1>No match</h1>"), qt.Equals, "<h1>No match</h1>"+expectBase+warnScript)
+       c.Run("Inject after HeAd tag MiXed CaSe", func(c *qt.C) {
+               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>")
        })
 }