Skip to content

Commit

Permalink
HTML: support Go templates, fixes #35
Browse files Browse the repository at this point in the history
  • Loading branch information
tdewolff committed Oct 30, 2023
1 parent cb84f81 commit 24bb939
Show file tree
Hide file tree
Showing 5 changed files with 53 additions and 12 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
github.com/fsnotify/fsnotify v1.7.0
github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2
github.com/tdewolff/argp v0.0.0-20231030173501-fa6c54897951
github.com/tdewolff/parse/v2 v2.7.1
github.com/tdewolff/parse/v2 v2.7.2
github.com/tdewolff/test v1.0.10
)

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2 h1:JAEbJn3j/FrhdWA9jW8
github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs=
github.com/tdewolff/argp v0.0.0-20231030173501-fa6c54897951 h1:QiakK8TMHgZfKZ2enwnyYzT3fOtzIwZ6mDkVC3scOrQ=
github.com/tdewolff/argp v0.0.0-20231030173501-fa6c54897951/go.mod h1:fF+gnKbmf3iMG+ErLiF+orMU/InyZIEnKVVigUjfriw=
github.com/tdewolff/parse/v2 v2.7.1 h1:gdImkv0sIupYr/cXAu5s+CxfVpxMdYZX2Qr+5Q+RdF8=
github.com/tdewolff/parse/v2 v2.7.1/go.mod h1:9p2qMIHpjRSTr1qnFxQr+igogyTUTlwvf9awHSm84h8=
github.com/tdewolff/parse/v2 v2.7.2 h1:9NdxF0nk/+lPI0YADDonSlpiY15hGcVUhXRj9hnK8sM=
github.com/tdewolff/parse/v2 v2.7.2/go.mod h1:9p2qMIHpjRSTr1qnFxQr+igogyTUTlwvf9awHSm84h8=
github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/tdewolff/test v1.0.10 h1:uWiheaLgLcNFqHcdWveum7PQfMnIUTf9Kl3bFxrIoew=
github.com/tdewolff/test v1.0.10/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
Expand Down
16 changes: 9 additions & 7 deletions html/buffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import (
// Token is a single token unit with an attribute value (if given) and hash of the data.
type Token struct {
html.TokenType
Hash Hash
Data []byte
Text []byte
AttrVal []byte
Traits traits
Offset int
Hash Hash
Data []byte
Text []byte
AttrVal []byte
Traits traits
Offset int
HasTemplate bool
}

// TokenBuffer is a buffer that allows for token look-ahead.
Expand All @@ -40,10 +41,11 @@ func (z *TokenBuffer) read(t *Token) {
t.Offset = z.r.Offset()
t.TokenType, t.Data = z.l.Next()
t.Text = z.l.Text()
t.HasTemplate = z.l.HasTemplate()
if t.TokenType == html.AttributeToken {
t.Offset += 1 + len(t.Text) + 1
t.AttrVal = z.l.AttrVal()
if len(t.AttrVal) > 1 && (t.AttrVal[0] == '"' || t.AttrVal[0] == '\'') {
if 1 < len(t.AttrVal) && (t.AttrVal[0] == '"' || t.AttrVal[0] == '\'') {
t.Offset++
t.AttrVal = t.AttrVal[1 : len(t.AttrVal)-1] // quotes will be readded in attribute loop if necessary
}
Expand Down
17 changes: 15 additions & 2 deletions html/html.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ var (

////////////////////////////////////////////////////////////////

var GoTemplateDelims = [2]string{"{{", "}}"}
var HandlebarsTemplateDelims = [2]string{"{{", "}}"}
var MustacheTemplateDelims = [2]string{"{{", "}}"}
var EJSTemplateDelims = [2]string{"<%", "%>"}
var ASPTemplateDelims = [2]string{"<%", "%>"}
var PHPTemplateDelims = [2]string{"<?", "?>"}

// Minifier is an HTML minifier.
type Minifier struct {
KeepComments bool
Expand All @@ -50,6 +57,7 @@ type Minifier struct {
KeepEndTags bool
KeepQuotes bool
KeepWhitespace bool
TemplateDelims [2]string
}

// Minify minifies HTML data, it reads from r and writes to w.
Expand All @@ -71,7 +79,7 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
z := parse.NewInput(r)
defer z.Restore()

l := html.NewLexer(z)
l := html.NewTemplateLexer(z, o.TemplateDelims)
tb := NewTokenBuffer(z, l)
for {
t := *tb.Shift()
Expand Down Expand Up @@ -127,7 +135,9 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
}
case html.TextToken:
// CSS and JS minifiers for inline code
if rawTagHash != 0 {
if t.HasTemplate {
w.Write(t.Data)
} else if rawTagHash != 0 {
if rawTagHash == Style || rawTagHash == Script || rawTagHash == Iframe {
var mimetype []byte
var params map[string]string
Expand Down Expand Up @@ -372,6 +382,9 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
break
} else if attr.Text == nil {
continue // removed attribute
} else if attr.HasTemplate {
w.Write(attr.Data)
continue // don't minify attributes that contain templates
}

val := attr.AttrVal
Expand Down
26 changes: 26 additions & 0 deletions html/html_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func TestHTML(t *testing.T) {
expected string
}{
{`html`, `html`},
//{"<title>title</title> <body>", `<title>title</title>`},
{`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd">`, `<!doctype html>`},
{`<!-- comment -->`, ``},
{`<!--# SSI Tag -->`, `<!--# SSI Tag -->`},
Expand Down Expand Up @@ -67,6 +68,7 @@ func TestHTML(t *testing.T) {
{`<input type="text" value="">`, `<input>`},
{`<a rel="noopener">`, `<a rel=noopener>`},
{`<a rel=" noopener external ">`, `<a rel="noopener external">`},
{`<input accept="image/png, image/jpeg">`, `<input accept=image/png,image/jpeg>`},

// increase coverage
{`<script style="css">js</script>`, `<script style=css>js</script>`},
Expand Down Expand Up @@ -352,6 +354,30 @@ func TestHTMLURL(t *testing.T) {
}
}

func TestHTMLTemplates(t *testing.T) {
htmlTests := []struct {
html string
expected string
}{
{`a<p> {{ printf " ! " }} </p>b`, `a<p> {{ printf " ! " }} </p>b`},
{`a<span> {{ printf " ! " }} </span>b`, `a<span> {{ printf " ! " }} </span>b`},
{`<a href={{ .Link }} />`, `<a href={{ .Link }}>`},
{`<input type="file" accept="{{ .Accept }}, image/jpeg">`, `<input type=file accept="{{ .Accept }}, image/jpeg">`},
{`<option value="0" {{ if eq .Type 0 }}selected{{ end }}>Foo</option>`, `<option value=0 {{ if eq .Type 0 }}selected{{ end }}>Foo</option>`},
}

m := minify.New()
htmlMinifier := &Minifier{KeepEndTags: true, TemplateDelims: GoTemplateDelims}
for _, tt := range htmlTests {
t.Run(tt.html, func(t *testing.T) {
r := bytes.NewBufferString(tt.html)
w := &bytes.Buffer{}
err := htmlMinifier.Minify(m, w, r, nil)
test.Minify(t, tt.html, err, w.String(), tt.expected)
})
}
}

func TestSpecialTagClosing(t *testing.T) {
m := minify.New()
m.AddFunc("text/html", Minify)
Expand Down

0 comments on commit 24bb939

Please sign in to comment.