github.com/gohugoio/hugo@v0.88.1/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  
    27  	"github.com/gohugoio/hugo/media"
    28  	"github.com/gohugoio/hugo/minifiers"
    29  	"github.com/gohugoio/hugo/output"
    30  
    31  	qt "github.com/frankban/quicktest"
    32  )
    33  
    34  func TestClassCollector(t *testing.T) {
    35  	c := qt.New((t))
    36  	rnd := rand.New(rand.NewSource(time.Now().Unix()))
    37  
    38  	f := func(tags, classes, ids string) HTMLElements {
    39  		var tagss, classess, idss []string
    40  		if tags != "" {
    41  			tagss = strings.Split(tags, " ")
    42  		}
    43  		if classes != "" {
    44  			classess = strings.Split(classes, " ")
    45  		}
    46  		if ids != "" {
    47  			idss = strings.Split(ids, " ")
    48  		}
    49  		return HTMLElements{
    50  			Tags:    tagss,
    51  			Classes: classess,
    52  			IDs:     idss,
    53  		}
    54  	}
    55  
    56  	skipMinifyTest := map[string]bool{
    57  		"Script tags content should be skipped": true, // https://github.com/tdewolff/minify/issues/396
    58  	}
    59  
    60  	for _, test := range []struct {
    61  		name   string
    62  		html   string
    63  		expect HTMLElements
    64  	}{
    65  		{"basic", `<body class="b a"></body>`, f("body", "a b", "")},
    66  		{"duplicates", `<div class="b a b"></div><div class="b a b"></div>x'`, f("div", "a b", "")},
    67  		{"single quote", `<body class='b a'></body>`, f("body", "a b", "")},
    68  		{"no quote", `<body class=b id=myelement></body>`, f("body", "b", "myelement")},
    69  		{"short", `<i>`, f("i", "", "")},
    70  		{"invalid", `< body class="b a"></body><div></div>`, f("div", "", "")},
    71  		// https://github.com/gohugoio/hugo/issues/7318
    72  		{"thead", `<table class="cl1">
    73      <thead class="cl2"><tr class="cl3"><td class="cl4"></td></tr></thead>
    74      <tbody class="cl5"><tr class="cl6"><td class="cl7"></td></tr></tbody>
    75  </table>`, f("table tbody td thead tr", "cl1 cl2 cl3 cl4 cl5 cl6 cl7", "")},
    76  		{"thead uppercase", `<TABLE class="CL1">
    77      <THEAD class="CL2"><TR class="CL3"><TD class="CL4"></TD></TR></THEAD>
    78      <TBODY class="CL5"><TR class="CL6"><TD class="CL7"></TD></TR></TBODY>
    79  </TABLE>`, f("table tbody td thead tr", "CL1 CL2 CL3 CL4 CL5 CL6 CL7", "")},
    80  		// https://github.com/gohugoio/hugo/issues/7161
    81  		{"minified a href", `<a class="b a" href=/></a>`, f("a", "a b", "")},
    82  		{"AlpineJS bind 1", `<body>
    83      <div x-bind:class="{
    84          'class1': data.open,
    85          'class2 class3': data.foo == 'bar'
    86           }">
    87      </div>
    88  </body>`, f("body div", "class1 class2 class3", "")},
    89  		{"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>`,
    90  			f("div", "bg-black bg-gray-300 inline-block mb-2 mr-1 px-2 py-2 rounded", ""),
    91  		},
    92  		{"AlpineJS bind 3", `<div x-bind:class="{ 'text-gray-800':  !checked, 'text-white': checked }"></div>`, f("div", "text-gray-800 text-white", "")},
    93  		{"AlpineJS bind 4", `<div x-bind:class="{ 'text-gray-800':  !checked, 
    94  					 'text-white': checked }"></div>`, f("div", "text-gray-800 text-white", "")},
    95  		{"AlpineJS bind 5", `<a x-bind:class="{
    96                  'text-a': a && b,
    97                  'text-b': !a && b || c,
    98                  'pl-3': a === 1,
    99                   pl-2: b == 3,
   100                  'text-gray-600': (a > 1)
   101                  }" 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", "")},
   102  		{"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", "")},
   103  		{"Vue bind", `<div v-bind:class="{ active: isActive }"></div>`, f("div", "active", "")},
   104  		// Issue #7746
   105  		{"Apostrophe inside attribute value", `<a class="missingclass" title="Plus d'information">my text</a><div></div>`, f("a div", "missingclass", "")},
   106  		// Issue #7567
   107  		{"Script tags content should be skipped", `<script><span>foo</span><span>bar</span></script><div class="foo"></div>`, f("div script", "foo", "")},
   108  		{"Style tags content should be skipped", `<style>p{color: red;font-size: 20px;}</style><div class="foo"></div>`, f("div style", "foo", "")},
   109  		{"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", "")},
   110  		{"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", "")},
   111  		{"DOCTYPE should beskipped", `<!DOCTYPE html>`, f("", "", "")},
   112  		{"Comments should be skipped", `<!-- example comment -->`, f("", "", "")},
   113  		{"Comments with elements before and after", `<div></div><!-- example comment --><span><span>`, f("div span", "", "")},
   114  		// Issue #8530
   115  		{"Comment with single quote", `<!-- Hero Area Image d'accueil --><i class="foo">`, f("i", "foo", "")},
   116  		{"Uppercase tags", `<DIV></DIV>`, f("div", "", "")},
   117  		{"Predefined tags with distinct casing", `<script>if (a < b) { nothing(); }</SCRIPT><div></div>`, f("div script", "", "")},
   118  		// Issue #8417
   119  		{"Tabs inline", `<hr	id="a" class="foo"><div class="bar">d</div>`, f("div hr", "bar foo", "a")},
   120  		{"Tabs on multiple rows", `<form
   121  			id="a"
   122  			action="www.example.com"
   123  			method="post"
   124  ></form>
   125  <div id="b" class="foo">d</div>`, f("div form", "foo", "a b")},
   126  		{"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", "神真美好")},
   127  	} {
   128  
   129  		for _, variant := range []struct {
   130  			minify bool
   131  		}{
   132  			{minify: false},
   133  			{minify: true},
   134  		} {
   135  
   136  			c.Run(fmt.Sprintf("%s--minify-%t", test.name, variant.minify), func(c *qt.C) {
   137  				w := newHTMLElementsCollectorWriter(newHTMLElementsCollector())
   138  				if variant.minify {
   139  					if skipMinifyTest[test.name] {
   140  						c.Skip("skip minify test")
   141  					}
   142  					v := config.New()
   143  					m, _ := minifiers.New(media.DefaultTypes, output.DefaultFormats, v)
   144  					m.Minify(media.HTMLType, w, strings.NewReader(test.html))
   145  
   146  				} else {
   147  					var buff bytes.Buffer
   148  					buff.WriteString(test.html)
   149  					io.Copy(w, &buff)
   150  				}
   151  				got := w.collector.getHTMLElements()
   152  				c.Assert(got, qt.DeepEquals, test.expect)
   153  			})
   154  		}
   155  	}
   156  
   157  }
   158  
   159  func BenchmarkElementsCollectorWriter(b *testing.B) {
   160  	const benchHTML = `
   161  <!DOCTYPE html>
   162  <html>
   163  <head>
   164  <title>title</title>
   165  <style>
   166  	a {color: red;}
   167  	.c {color: blue;}
   168  </style>
   169  </head>
   170  <body id="i1" class="a b c d">
   171  <a class="c d e"></a>
   172  <hr>
   173  <a class="c d e"></a>
   174  <a class="c d e"></a>
   175  <hr>
   176  <a id="i2" class="c d e f"></a>
   177  <a id="i3" class="c d e"></a>
   178  <a class="c d e"></a>
   179  <p>To force<br> line breaks<br> in a text,<br> use the br<br> element.</p>
   180  <hr>
   181  <a class="c d e"></a>
   182  <a class="c d e"></a>
   183  <a class="c d e"></a>
   184  <a class="c d e"></a>
   185  <table>
   186    <thead class="ch">
   187    <tr>
   188      <th>Month</th>
   189      <th>Savings</th>
   190    </tr>
   191    </thead>
   192    <tbody class="cb">
   193    <tr>
   194      <td>January</td>
   195      <td>$100</td>
   196    </tr>
   197    <tr>
   198      <td>February</td>
   199      <td>$200</td>
   200    </tr>
   201    </tbody>
   202    <tfoot class="cf">
   203    <tr>
   204      <td></td>
   205      <td>$300</td>
   206    </tr>
   207    </tfoot>
   208  </table>
   209  </body>
   210  </html>
   211  `
   212  	for i := 0; i < b.N; i++ {
   213  		w := newHTMLElementsCollectorWriter(newHTMLElementsCollector())
   214  		fmt.Fprint(w, benchHTML)
   215  
   216  	}
   217  }