github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/publisher/htmlElementsCollector_test.go (about)

     1  // Copyright 2020 The Hugo Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package publisher
    15  
    16  import (
    17  	"bytes"
    18  	"fmt"
    19  	"io"
    20  	"math/rand"
    21  	"strings"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/gohugoio/hugo/config"
    26  	"github.com/gohugoio/hugo/media"
    27  	"github.com/gohugoio/hugo/minifiers"
    28  	"github.com/gohugoio/hugo/output"
    29  
    30  	qt "github.com/frankban/quicktest"
    31  )
    32  
    33  func TestClassCollector(t *testing.T) {
    34  	c := qt.New((t))
    35  	rnd := rand.New(rand.NewSource(time.Now().Unix()))
    36  
    37  	f := func(tags, classes, ids string) HTMLElements {
    38  		var tagss, classess, idss []string
    39  		if tags != "" {
    40  			tagss = strings.Split(tags, " ")
    41  		}
    42  		if classes != "" {
    43  			classess = strings.Split(classes, " ")
    44  		}
    45  		if ids != "" {
    46  			idss = strings.Split(ids, " ")
    47  		}
    48  		return HTMLElements{
    49  			Tags:    tagss,
    50  			Classes: classess,
    51  			IDs:     idss,
    52  		}
    53  	}
    54  
    55  	skipMinifyTest := map[string]bool{
    56  		"Script tags content should be skipped": true, // https://github.com/tdewolff/minify/issues/396
    57  	}
    58  
    59  	for _, test := range []struct {
    60  		name   string
    61  		html   string
    62  		expect HTMLElements
    63  	}{
    64  		{"basic", `<body class="b a"></body>`, f("body", "a b", "")},
    65  		{"duplicates", `<div class="b a b"></div><div class="b a b"></div>x'`, f("div", "a b", "")},
    66  		{"single quote", `<body class='b a'></body>`, f("body", "a b", "")},
    67  		{"no quote", `<body class=b id=myelement></body>`, f("body", "b", "myelement")},
    68  		{"short", `<i>`, f("i", "", "")},
    69  		{"invalid", `< body class="b a"></body><div></div>`, f("div", "", "")},
    70  		// https://github.com/gohugoio/hugo/issues/7318
    71  		{"thead", `<table class="cl1">
    72      <thead class="cl2"><tr class="cl3"><td class="cl4"></td></tr></thead>
    73      <tbody class="cl5"><tr class="cl6"><td class="cl7"></td></tr></tbody>
    74  </table>`, f("table tbody td thead tr", "cl1 cl2 cl3 cl4 cl5 cl6 cl7", "")},
    75  		{"thead uppercase", `<TABLE class="CL1">
    76      <THEAD class="CL2"><TR class="CL3"><TD class="CL4"></TD></TR></THEAD>
    77      <TBODY class="CL5"><TR class="CL6"><TD class="CL7"></TD></TR></TBODY>
    78  </TABLE>`, f("table tbody td thead tr", "CL1 CL2 CL3 CL4 CL5 CL6 CL7", "")},
    79  		// https://github.com/gohugoio/hugo/issues/7161
    80  		{"minified a href", `<a class="b a" href=/></a>`, f("a", "a b", "")},
    81  		{"AlpineJS bind 1", `<body>
    82      <div x-bind:class="{
    83          'class1': data.open,
    84          'class2 class3': data.foo == 'bar'
    85           }">
    86      </div>
    87  </body>`, f("body div", "class1 class2 class3", "")},
    88  		{"AlpineJS bind 2", `<div x-bind:class="{ 'bg-black':  filter.checked }" class="inline-block mr-1 mb-2 rounded  bg-gray-300 px-2 py-2">FOO</div>`,
    89  			f("div", "bg-black bg-gray-300 inline-block mb-2 mr-1 px-2 py-2 rounded", ""),
    90  		},
    91  		{"AlpineJS bind 3", `<div x-bind:class="{ 'text-gray-800':  !checked, 'text-white': checked }"></div>`, f("div", "text-gray-800 text-white", "")},
    92  		{"AlpineJS bind 4", `<div x-bind:class="{ 'text-gray-800':  !checked, 
    93  					 'text-white': checked }"></div>`, f("div", "text-gray-800 text-white", "")},
    94  		{"AlpineJS bind 5", `<a x-bind:class="{
    95                  'text-a': a && b,
    96                  'text-b': !a && b || c,
    97                  'pl-3': a === 1,
    98                   pl-2: b == 3,
    99                  'text-gray-600': (a > 1)
   100                  }" class="block w-36 cursor-pointer pr-3 no-underline capitalize"></a>`, f("a", "block capitalize cursor-pointer no-underline pl-2 pl-3 pr-3 text-a text-b text-gray-600 w-36", "")},
   101  		{"AlpineJS transition 1", `<div x-transition:enter-start="opacity-0 transform mobile:-translate-x-8 sm:-translate-y-8">`, f("div", "mobile:-translate-x-8 opacity-0 sm:-translate-y-8 transform", "")},
   102  		{"Vue bind", `<div v-bind:class="{ active: isActive }"></div>`, f("div", "active", "")},
   103  		// Issue #7746
   104  		{"Apostrophe inside attribute value", `<a class="missingclass" title="Plus d'information">my text</a><div></div>`, f("a div", "missingclass", "")},
   105  		// Issue #7567
   106  		{"Script tags content should be skipped", `<script><span>foo</span><span>bar</span></script><div class="foo"></div>`, f("div script", "foo", "")},
   107  		{"Style tags content should be skipped", `<style>p{color: red;font-size: 20px;}</style><div class="foo"></div>`, f("div style", "foo", "")},
   108  		{"Pre tags content should be skipped", `<pre class="preclass"><span>foo</span><span>bar</span></pre><div class="foo"></div>`, f("div pre", "foo preclass", "")},
   109  		{"Textarea tags content should be skipped", `<textarea class="textareaclass"><span>foo</span><span>bar</span></textarea><div class="foo"></div>`, f("div textarea", "foo textareaclass", "")},
   110  		{"DOCTYPE should beskipped", `<!DOCTYPE html>`, f("", "", "")},
   111  		{"Comments should be skipped", `<!-- example comment -->`, f("", "", "")},
   112  		{"Comments with elements before and after", `<div></div><!-- example comment --><span><span>`, f("div span", "", "")},
   113  		{"Self closing tag", `<div><hr/></div>`, f("div hr", "", "")},
   114  		// svg with self closing style tag.
   115  		{"SVG with self closing style tag", `<svg><style/><g><path class="foo"/></g></svg>`, f("g path style svg", "foo", "")},
   116  		// Issue #8530
   117  		{"Comment with single quote", `<!-- Hero Area Image d'accueil --><i class="foo">`, f("i", "foo", "")},
   118  		{"Uppercase tags", `<DIV></DIV>`, f("div", "", "")},
   119  		{"Predefined tags with distinct casing", `<script>if (a < b) { nothing(); }</SCRIPT><div></div>`, f("div script", "", "")},
   120  		// Issue #8417
   121  		{"Tabs inline", `<hr	id="a" class="foo"><div class="bar">d</div>`, f("div hr", "bar foo", "a")},
   122  		{"Tabs on multiple rows", `<form
   123  			id="a"
   124  			action="www.example.com"
   125  			method="post"
   126  ></form>
   127  <div id="b" class="foo">d</div>`, f("div form", "foo", "a b")},
   128  		{"Big input, multibyte runes", strings.Repeat(`神真美好 `, rnd.Intn(500)+1) + "<div id=\"神真美好\" class=\"foo\">" + strings.Repeat(`神真美好 `, rnd.Intn(100)+1) + "   <span>神真美好</span>", f("div span", "foo", "神真美好")},
   129  	} {
   130  
   131  		for _, variant := range []struct {
   132  			minify bool
   133  		}{
   134  			{minify: false},
   135  			{minify: true},
   136  		} {
   137  
   138  			c.Run(fmt.Sprintf("%s--minify-%t", test.name, variant.minify), func(c *qt.C) {
   139  				w := newHTMLElementsCollectorWriter(newHTMLElementsCollector())
   140  				if variant.minify {
   141  					if skipMinifyTest[test.name] {
   142  						c.Skip("skip minify test")
   143  					}
   144  					v := config.NewWithTestDefaults()
   145  					m, _ := minifiers.New(media.DefaultTypes, output.DefaultFormats, v)
   146  					m.Minify(media.HTMLType, w, strings.NewReader(test.html))
   147  
   148  				} else {
   149  					var buff bytes.Buffer
   150  					buff.WriteString(test.html)
   151  					io.Copy(w, &buff)
   152  				}
   153  				got := w.collector.getHTMLElements()
   154  				c.Assert(got, qt.DeepEquals, test.expect)
   155  			})
   156  		}
   157  	}
   158  
   159  }
   160  
   161  func TestEndsWithTag(t *testing.T) {
   162  	c := qt.New((t))
   163  
   164  	for _, test := range []struct {
   165  		name    string
   166  		s       string
   167  		tagName string
   168  		expect  bool
   169  	}{
   170  		{"empty", "", "div", false},
   171  		{"no match", "foo", "div", false},
   172  		{"no close", "foo<div>", "div", false},
   173  		{"no close 2", "foo/div>", "div", false},
   174  		{"no close 2", "foo//div>", "div", false},
   175  		{"no tag", "foo</>", "div", false},
   176  		{"match", "foo</div>", "div", true},
   177  		{"match space", "foo<  / div>", "div", true},
   178  		{"match space 2", "foo<  / div   \n>", "div", true},
   179  		{"match case", "foo</DIV>", "div", true},
   180  		{"self closing", `</defs><g><g><path fill="#010101" d=asdf"/>`, "div", false},
   181  	} {
   182  		c.Run(test.name, func(c *qt.C) {
   183  			got := isClosedByTag([]byte(test.s), []byte(test.tagName))
   184  			c.Assert(got, qt.Equals, test.expect)
   185  		})
   186  	}
   187  
   188  }
   189  
   190  func BenchmarkElementsCollectorWriter(b *testing.B) {
   191  	const benchHTML = `
   192  <!DOCTYPE html>
   193  <html>
   194  <head>
   195  <title>title</title>
   196  <style>
   197  	a {color: red;}
   198  	.c {color: blue;}
   199  </style>
   200  </head>
   201  <body id="i1" class="a b c d">
   202  <a class="c d e"></a>
   203  <hr>
   204  <a class="c d e"></a>
   205  <a class="c d e"></a>
   206  <hr>
   207  <a id="i2" class="c d e f"></a>
   208  <a id="i3" class="c d e"></a>
   209  <a class="c d e"></a>
   210  <p>To force<br> line breaks<br> in a text,<br> use the br<br> element.</p>
   211  <hr>
   212  <a class="c d e"></a>
   213  <a class="c d e"></a>
   214  <a class="c d e"></a>
   215  <a class="c d e"></a>
   216  <table>
   217    <thead class="ch">
   218    <tr>
   219      <th>Month</th>
   220      <th>Savings</th>
   221    </tr>
   222    </thead>
   223    <tbody class="cb">
   224    <tr>
   225      <td>January</td>
   226      <td>$100</td>
   227    </tr>
   228    <tr>
   229      <td>February</td>
   230      <td>$200</td>
   231    </tr>
   232    </tbody>
   233    <tfoot class="cf">
   234    <tr>
   235      <td></td>
   236      <td>$300</td>
   237    </tr>
   238    </tfoot>
   239  </table>
   240  </body>
   241  </html>
   242  `
   243  	for i := 0; i < b.N; i++ {
   244  		w := newHTMLElementsCollectorWriter(newHTMLElementsCollector())
   245  		fmt.Fprint(w, benchHTML)
   246  
   247  	}
   248  }
   249  
   250  func BenchmarkElementsCollectorWriterPre(b *testing.B) {
   251  	const benchHTML = `
   252  <pre class="preclass">
   253  <span>foo</span><span>bar</span>
   254  <!-- many more span elements -->
   255  <span class="foo">foo</span>
   256  <span class="bar">bar</span>
   257  <span class="baz">baz</span>
   258  <span class="qux">qux</span>
   259  <span class="quux">quux</span>
   260  <span class="quuz">quuz</span>
   261  <span class="corge">corge</span>
   262  </pre>
   263  <div class="foo"></div>
   264  
   265  `
   266  	w := newHTMLElementsCollectorWriter(newHTMLElementsCollector())
   267  	for i := 0; i < b.N; i++ {
   268  		fmt.Fprint(w, benchHTML)
   269  
   270  	}
   271  }