2020-03-03 06:25:03 -05:00
// Copyright 2020 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package publisher
import (
2021-05-13 07:10:32 -04:00
"bytes"
2020-03-03 06:25:03 -05:00
"fmt"
2021-05-13 07:10:32 -04:00
"io"
"math/rand"
2020-03-03 06:25:03 -05:00
"strings"
"testing"
2021-05-13 07:10:32 -04:00
"time"
2020-03-03 06:25:03 -05:00
2021-06-09 04:58:18 -04:00
"github.com/gohugoio/hugo/config"
2021-04-07 02:33:31 -04:00
"github.com/gohugoio/hugo/media"
2021-04-12 17:42:51 -04:00
"github.com/gohugoio/hugo/minifiers"
2021-04-07 02:33:31 -04:00
"github.com/gohugoio/hugo/output"
2020-03-03 06:25:03 -05:00
qt "github.com/frankban/quicktest"
)
func TestClassCollector ( t * testing . T ) {
c := qt . New ( ( t ) )
2021-05-13 07:10:32 -04:00
rnd := rand . New ( rand . NewSource ( time . Now ( ) . Unix ( ) ) )
2020-03-03 06:25:03 -05:00
f := func ( tags , classes , ids string ) HTMLElements {
var tagss , classess , idss [ ] string
if tags != "" {
tagss = strings . Split ( tags , " " )
}
if classes != "" {
classess = strings . Split ( classes , " " )
}
if ids != "" {
idss = strings . Split ( ids , " " )
}
return HTMLElements {
Tags : tagss ,
Classes : classess ,
IDs : idss ,
}
}
2021-04-07 02:33:31 -04:00
skipMinifyTest := map [ string ] bool {
"Script tags content should be skipped" : true , // https://github.com/tdewolff/minify/issues/396
}
2020-03-03 06:25:03 -05:00
for _ , test := range [ ] struct {
name string
html string
expect HTMLElements
} {
{ "basic" , ` <body class="b a"></body> ` , f ( "body" , "a b" , "" ) } ,
2021-05-13 07:10:32 -04:00
{ "duplicates" , ` <div class="b a b"></div><div class="b a b"></div>x' ` , f ( "div" , "a b" , "" ) } ,
2020-03-03 06:25:03 -05:00
{ "single quote" , ` <body class='b a'></body> ` , f ( "body" , "a b" , "" ) } ,
{ "no quote" , ` <body class=b id=myelement></body> ` , f ( "body" , "b" , "myelement" ) } ,
2021-05-13 07:10:32 -04:00
{ "short" , ` <i> ` , f ( "i" , "" , "" ) } ,
{ "invalid" , ` < body class="b a"></body><div></div> ` , f ( "div" , "" , "" ) } ,
2021-04-12 17:42:51 -04:00
// https://github.com/gohugoio/hugo/issues/7318
{ "thead" , ` < table class = "cl1" >
2020-05-25 15:05:59 -04:00
< thead class = "cl2" > < tr class = "cl3" > < td class = "cl4" > < / td > < / tr > < / thead >
< tbody class = "cl5" > < tr class = "cl6" > < td class = "cl7" > < / td > < / tr > < / tbody >
< / table > ` , f ( "table tbody td thead tr" , "cl1 cl2 cl3 cl4 cl5 cl6 cl7" , "" ) } ,
2021-05-13 07:10:32 -04:00
{ "thead uppercase" , ` < TABLE class = "CL1" >
< THEAD class = "CL2" > < TR class = "CL3" > < TD class = "CL4" > < / TD > < / TR > < / THEAD >
< TBODY class = "CL5" > < TR class = "CL6" > < TD class = "CL7" > < / TD > < / TR > < / TBODY >
< / TABLE > ` , f ( "table tbody td thead tr" , "CL1 CL2 CL3 CL4 CL5 CL6 CL7" , "" ) } ,
2020-04-21 06:57:45 -04:00
// https://github.com/gohugoio/hugo/issues/7161
{ "minified a href" , ` <a class="b a" href=/></a> ` , f ( "a" , "a b" , "" ) } ,
2020-03-03 06:25:03 -05:00
{ "AlpineJS bind 1" , ` < body >
2021-04-12 17:42:51 -04:00
< div x - bind : class = " {
2020-03-03 06:25:03 -05:00
' class1 ' : data . open ,
' class2 class3 ' : data . foo == ' bar '
} " >
2021-04-12 17:42:51 -04:00
< / div >
< / body > ` , f ( "body div" , "class1 class2 class3" , "" ) } ,
{ "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> ` ,
2020-12-02 07:23:25 -05:00
f ( "div" , "bg-black bg-gray-300 inline-block mb-2 mr-1 px-2 py-2 rounded" , "" ) ,
} ,
2021-04-12 17:42:51 -04:00
{ "AlpineJS bind 3" , ` <div x-bind:class=" { 'text-gray-800': !checked, 'text-white': checked }"></div> ` , f ( "div" , "text-gray-800 text-white" , "" ) } ,
{ "AlpineJS bind 4" , ` < div x - bind : class = " { ' text - gray - 800 ' : ! checked ,
2020-03-03 06:25:03 -05:00
' text - white ' : checked } "></div>`, f(" div ", " text - gray - 800 text - white ", " " ) } ,
2021-04-12 17:42:51 -04:00
{ "AlpineJS bind 5" , ` < a x - bind : class = " {
2020-04-27 11:49:51 -04:00
' text - a ' : a && b ,
' text - b ' : ! a && b || c ,
' pl - 3 ' : a == = 1 ,
pl - 2 : b == 3 ,
' text - gray - 600 ' : ( a > 1 )
} " 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 ", " " ) } ,
2021-04-12 17:42:51 -04:00
{ "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" , "" ) } ,
2020-03-03 06:25:03 -05:00
{ "Vue bind" , ` <div v-bind:class=" { active: isActive }"></div> ` , f ( "div" , "active" , "" ) } ,
2021-04-06 12:19:25 -04:00
// Issue #7746
2020-09-28 16:17:36 -04:00
{ "Apostrophe inside attribute value" , ` <a class="missingclass" title="Plus d'information">my text</a><div></div> ` , f ( "a div" , "missingclass" , "" ) } ,
2021-04-06 12:19:25 -04:00
// Issue #7567
{ "Script tags content should be skipped" , ` <script><span>foo</span><span>bar</span></script><div class="foo"></div> ` , f ( "div script" , "foo" , "" ) } ,
2021-04-12 17:42:51 -04:00
{ "Style tags content should be skipped" , ` <style>p { color: red;font-size: 20px;}</style><div class="foo"></div> ` , f ( "div style" , "foo" , "" ) } ,
2021-04-06 12:19:25 -04:00
{ "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" , "" ) } ,
2021-04-12 17:42:51 -04:00
{ "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" , "" ) } ,
{ "DOCTYPE should beskipped" , ` <!DOCTYPE html> ` , f ( "" , "" , "" ) } ,
{ "Comments should be skipped" , ` <!-- example comment --> ` , f ( "" , "" , "" ) } ,
2021-05-13 07:10:32 -04:00
{ "Comments with elements before and after" , ` <div></div><!-- example comment --><span><span> ` , f ( "div span" , "" , "" ) } ,
// Issue #8530
{ "Comment with single quote" , ` <!-- Hero Area Image d'accueil --><i class="foo"> ` , f ( "i" , "foo" , "" ) } ,
{ "Uppercase tags" , ` <DIV></DIV> ` , f ( "div" , "" , "" ) } ,
{ "Predefined tags with distinct casing" , ` <script>if (a < b) { nothing(); }</SCRIPT><div></div> ` , f ( "div script" , "" , "" ) } ,
2021-04-12 17:42:51 -04:00
// Issue #8417
{ "Tabs inline" , ` <hr id="a" class="foo"><div class="bar">d</div> ` , f ( "div hr" , "bar foo" , "a" ) } ,
{ "Tabs on multiple rows" , ` < form
id = "a"
action = "www.example.com"
method = "post"
> < / form >
< div id = "b" class = "foo" > d < / div > ` , f ( "div form" , "foo" , "a b" ) } ,
2021-05-13 07:10:32 -04:00
{ "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" , "神真美好" ) } ,
2020-03-03 06:25:03 -05:00
} {
2021-04-07 02:33:31 -04:00
2021-05-13 07:10:32 -04:00
for _ , variant := range [ ] struct {
minify bool
} {
{ minify : false } ,
{ minify : true } ,
} {
c . Run ( fmt . Sprintf ( "%s--minify-%t" , test . name , variant . minify ) , func ( c * qt . C ) {
2021-04-07 02:33:31 -04:00
w := newHTMLElementsCollectorWriter ( newHTMLElementsCollector ( ) )
2021-05-13 07:10:32 -04:00
if variant . minify {
2021-04-07 02:33:31 -04:00
if skipMinifyTest [ test . name ] {
c . Skip ( "skip minify test" )
}
2022-03-21 04:35:15 -04:00
v := config . NewWithTestDefaults ( )
2021-04-07 02:33:31 -04:00
m , _ := minifiers . New ( media . DefaultTypes , output . DefaultFormats , v )
m . Minify ( media . HTMLType , w , strings . NewReader ( test . html ) )
2021-05-13 07:10:32 -04:00
2021-04-07 02:33:31 -04:00
} else {
2021-05-13 07:10:32 -04:00
var buff bytes . Buffer
buff . WriteString ( test . html )
io . Copy ( w , & buff )
2021-04-07 02:33:31 -04:00
}
got := w . collector . getHTMLElements ( )
c . Assert ( got , qt . DeepEquals , test . expect )
} )
}
2020-03-03 06:25:03 -05:00
}
2021-05-13 07:10:32 -04:00
2020-03-03 06:25:03 -05:00
}
2020-11-27 02:46:58 -05:00
2023-02-05 09:14:30 -05:00
func TestEndsWithTag ( t * testing . T ) {
c := qt . New ( ( t ) )
for _ , test := range [ ] struct {
name string
s string
tagName string
expect bool
} {
{ "empty" , "" , "div" , false } ,
{ "no match" , "foo" , "div" , false } ,
{ "no close" , "foo<div>" , "div" , false } ,
{ "no close 2" , "foo/div>" , "div" , false } ,
{ "no close 2" , "foo//div>" , "div" , false } ,
{ "no tag" , "foo</>" , "div" , false } ,
{ "match" , "foo</div>" , "div" , true } ,
{ "match space" , "foo< / div>" , "div" , true } ,
{ "match space 2" , "foo< / div \n>" , "div" , true } ,
{ "match case" , "foo</DIV>" , "div" , true } ,
} {
c . Run ( test . name , func ( c * qt . C ) {
got := isClosedByTag ( [ ] byte ( test . s ) , [ ] byte ( test . tagName ) )
c . Assert ( got , qt . Equals , test . expect )
} )
}
}
2021-04-20 10:50:03 -04:00
func BenchmarkElementsCollectorWriter ( b * testing . B ) {
2020-11-27 02:46:58 -05:00
const benchHTML = `
2021-04-12 17:42:51 -04:00
< ! DOCTYPE html >
< html >
< head >
< title > title < / title >
< style >
a { color : red ; }
. c { color : blue ; }
< / style >
< / head >
< body id = "i1" class = "a b c d" >
< a class = "c d e" > < / a >
< hr >
< a class = "c d e" > < / a >
< a class = "c d e" > < / a >
< hr >
< a id = "i2" class = "c d e f" > < / a >
< a id = "i3" class = "c d e" > < / a >
< a class = "c d e" > < / a >
< p > To force < br > line breaks < br > in a text , < br > use the br < br > element . < / p >
< hr >
< a class = "c d e" > < / a >
< a class = "c d e" > < / a >
< a class = "c d e" > < / a >
< a class = "c d e" > < / a >
< table >
< thead class = "ch" >
< tr >
< th > Month < / th >
< th > Savings < / th >
< / tr >
< / thead >
< tbody class = "cb" >
< tr >
< td > January < / td >
< td > $ 100 < / td >
< / tr >
< tr >
< td > February < / td >
< td > $ 200 < / td >
< / tr >
< / tbody >
< tfoot class = "cf" >
< tr >
< td > < / td >
< td > $ 300 < / td >
< / tr >
< / tfoot >
< / table >
< / body >
< / html >
`
for i := 0 ; i < b . N ; i ++ {
w := newHTMLElementsCollectorWriter ( newHTMLElementsCollector ( ) )
fmt . Fprint ( w , benchHTML )
2020-11-27 02:46:58 -05:00
}
}
2023-02-05 10:39:31 -05:00
func BenchmarkElementsCollectorWriterPre ( b * testing . B ) {
const benchHTML = `
< pre class = "preclass" >
< span > foo < / span > < span > bar < / span >
< ! -- many more span elements -- >
< span class = "foo" > foo < / span >
< span class = "bar" > bar < / span >
< span class = "baz" > baz < / span >
< span class = "qux" > qux < / span >
< span class = "quux" > quux < / span >
< span class = "quuz" > quuz < / span >
< span class = "corge" > corge < / span >
< / pre >
< div class = "foo" > < / div >
`
w := newHTMLElementsCollectorWriter ( newHTMLElementsCollector ( ) )
for i := 0 ; i < b . N ; i ++ {
fmt . Fprint ( w , benchHTML )
}
}