github.com/SuCicada/su-hugo@v1.0.0/parser/pageparser/pageparser_shortcode_test.go (about)

     1  // Copyright 2018 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 pageparser
    15  
    16  import (
    17  	"testing"
    18  
    19  	qt "github.com/frankban/quicktest"
    20  )
    21  
    22  var (
    23  	tstEOF            = nti(tEOF, "")
    24  	tstLeftNoMD       = nti(tLeftDelimScNoMarkup, "{{<")
    25  	tstRightNoMD      = nti(tRightDelimScNoMarkup, ">}}")
    26  	tstLeftMD         = nti(tLeftDelimScWithMarkup, "{{%")
    27  	tstRightMD        = nti(tRightDelimScWithMarkup, "%}}")
    28  	tstSCClose        = nti(tScClose, "/")
    29  	tstSC1            = nti(tScName, "sc1")
    30  	tstSC1Inline      = nti(tScNameInline, "sc1.inline")
    31  	tstSC2Inline      = nti(tScNameInline, "sc2.inline")
    32  	tstSC2            = nti(tScName, "sc2")
    33  	tstSC3            = nti(tScName, "sc3")
    34  	tstSCSlash        = nti(tScName, "sc/sub")
    35  	tstParam1         = nti(tScParam, "param1")
    36  	tstParam2         = nti(tScParam, "param2")
    37  	tstParamBoolTrue  = nti(tScParam, "true")
    38  	tstParamBoolFalse = nti(tScParam, "false")
    39  	tstParamInt       = nti(tScParam, "32")
    40  	tstParamFloat     = nti(tScParam, "3.14")
    41  	tstVal            = nti(tScParamVal, "Hello World")
    42  	tstText           = nti(tText, "Hello World")
    43  )
    44  
    45  var shortCodeLexerTests = []lexerTest{
    46  	{"empty", "", []typeText{tstEOF}},
    47  	{"spaces", " \t\n", []typeText{nti(tText, " \t\n"), tstEOF}},
    48  	{"text", `to be or not`, []typeText{nti(tText, "to be or not"), tstEOF}},
    49  	{"no markup", `{{< sc1 >}}`, []typeText{tstLeftNoMD, tstSC1, tstRightNoMD, tstEOF}},
    50  	{"with EOL", "{{< sc1 \n >}}", []typeText{tstLeftNoMD, tstSC1, tstRightNoMD, tstEOF}},
    51  
    52  	{"forward slash inside name", `{{< sc/sub >}}`, []typeText{tstLeftNoMD, tstSCSlash, tstRightNoMD, tstEOF}},
    53  
    54  	{"simple with markup", `{{% sc1 %}}`, []typeText{tstLeftMD, tstSC1, tstRightMD, tstEOF}},
    55  	{"with spaces", `{{<     sc1     >}}`, []typeText{tstLeftNoMD, tstSC1, tstRightNoMD, tstEOF}},
    56  	{"indented on new line", "Hello\n    {{% sc1 %}}", []typeText{nti(tText, "Hello\n"), nti(tIndentation, "    "), tstLeftMD, tstSC1, tstRightMD, tstEOF}},
    57  	{"indented on new line tab", "Hello\n\t{{% sc1 %}}", []typeText{nti(tText, "Hello\n"), nti(tIndentation, "\t"), tstLeftMD, tstSC1, tstRightMD, tstEOF}},
    58  	{"indented on first line", "    {{% sc1 %}}", []typeText{nti(tIndentation, "    "), tstLeftMD, tstSC1, tstRightMD, tstEOF}},
    59  	{"mismatched rightDelim", `{{< sc1 %}}`, []typeText{
    60  		tstLeftNoMD, tstSC1,
    61  		nti(tError, "unrecognized character in shortcode action: U+0025 '%'. Note: Parameters with non-alphanumeric args must be quoted"),
    62  	}},
    63  	{"inner, markup", `{{% sc1 %}} inner {{% /sc1 %}}`, []typeText{
    64  		tstLeftMD,
    65  		tstSC1,
    66  		tstRightMD,
    67  		nti(tText, " inner "),
    68  		tstLeftMD,
    69  		tstSCClose,
    70  		tstSC1,
    71  		tstRightMD,
    72  		tstEOF,
    73  	}},
    74  	{"close, but no open", `{{< /sc1 >}}`, []typeText{
    75  		tstLeftNoMD, nti(tError, "got closing shortcode, but none is open"),
    76  	}},
    77  	{"close wrong", `{{< sc1 >}}{{< /another >}}`, []typeText{
    78  		tstLeftNoMD, tstSC1, tstRightNoMD, tstLeftNoMD, tstSCClose,
    79  		nti(tError, "closing tag for shortcode 'another' does not match start tag"),
    80  	}},
    81  	{"close, but no open, more", `{{< sc1 >}}{{< /sc1 >}}{{< /another >}}`, []typeText{
    82  		tstLeftNoMD, tstSC1, tstRightNoMD, tstLeftNoMD, tstSCClose, tstSC1, tstRightNoMD, tstLeftNoMD, tstSCClose,
    83  		nti(tError, "closing tag for shortcode 'another' does not match start tag"),
    84  	}},
    85  	{"close with extra keyword", `{{< sc1 >}}{{< /sc1 keyword>}}`, []typeText{
    86  		tstLeftNoMD, tstSC1, tstRightNoMD, tstLeftNoMD, tstSCClose, tstSC1,
    87  		nti(tError, "unclosed shortcode"),
    88  	}},
    89  	{"float param, positional", `{{< sc1 3.14 >}}`, []typeText{
    90  		tstLeftNoMD, tstSC1, nti(tScParam, "3.14"), tstRightNoMD, tstEOF,
    91  	}},
    92  	{"float param, named", `{{< sc1 param1=3.14 >}}`, []typeText{
    93  		tstLeftNoMD, tstSC1, tstParam1, nti(tScParamVal, "3.14"), tstRightNoMD, tstEOF,
    94  	}},
    95  	{"named param, raw string", `{{< sc1 param1=` + "`" + "Hello World" + "`" + " >}}", []typeText{
    96  		tstLeftNoMD, tstSC1, tstParam1, nti(tScParamVal, "Hello World"), tstRightNoMD, tstEOF,
    97  	}},
    98  	{"float param, named, space before", `{{< sc1 param1= 3.14 >}}`, []typeText{
    99  		tstLeftNoMD, tstSC1, tstParam1, nti(tScParamVal, "3.14"), tstRightNoMD, tstEOF,
   100  	}},
   101  	{"Youtube id", `{{< sc1 -ziL-Q_456igdO-4 >}}`, []typeText{
   102  		tstLeftNoMD, tstSC1, nti(tScParam, "-ziL-Q_456igdO-4"), tstRightNoMD, tstEOF,
   103  	}},
   104  	{"non-alphanumerics param quoted", `{{< sc1 "-ziL-.%QigdO-4" >}}`, []typeText{
   105  		tstLeftNoMD, tstSC1, nti(tScParam, "-ziL-.%QigdO-4"), tstRightNoMD, tstEOF,
   106  	}},
   107  	{"raw string", `{{< sc1` + "`" + "Hello World" + "`" + ` >}}`, []typeText{
   108  		tstLeftNoMD, tstSC1, nti(tScParam, "Hello World"), tstRightNoMD, tstEOF,
   109  	}},
   110  	{"raw string with newline", `{{< sc1` + "`" + `Hello 
   111  	World` + "`" + ` >}}`, []typeText{
   112  		tstLeftNoMD, tstSC1, nti(tScParam, `Hello 
   113  	World`), tstRightNoMD, tstEOF,
   114  	}},
   115  	{"raw string with escape character", `{{< sc1` + "`" + `Hello \b World` + "`" + ` >}}`, []typeText{
   116  		tstLeftNoMD, tstSC1, nti(tScParam, `Hello \b World`), tstRightNoMD, tstEOF,
   117  	}},
   118  	{"two params", `{{< sc1 param1   param2 >}}`, []typeText{
   119  		tstLeftNoMD, tstSC1, tstParam1, tstParam2, tstRightNoMD, tstEOF,
   120  	}},
   121  	// issue #934
   122  	{"self-closing", `{{< sc1 />}}`, []typeText{
   123  		tstLeftNoMD, tstSC1, tstSCClose, tstRightNoMD, tstEOF,
   124  	}},
   125  	// Issue 2498
   126  	{"multiple self-closing", `{{< sc1 />}}{{< sc1 />}}`, []typeText{
   127  		tstLeftNoMD, tstSC1, tstSCClose, tstRightNoMD,
   128  		tstLeftNoMD, tstSC1, tstSCClose, tstRightNoMD, tstEOF,
   129  	}},
   130  	{"self-closing with param", `{{< sc1 param1 />}}`, []typeText{
   131  		tstLeftNoMD, tstSC1, tstParam1, tstSCClose, tstRightNoMD, tstEOF,
   132  	}},
   133  	{"multiple self-closing with param", `{{< sc1 param1 />}}{{< sc1 param1 />}}`, []typeText{
   134  		tstLeftNoMD, tstSC1, tstParam1, tstSCClose, tstRightNoMD,
   135  		tstLeftNoMD, tstSC1, tstParam1, tstSCClose, tstRightNoMD, tstEOF,
   136  	}},
   137  	{"multiple different self-closing with param", `{{< sc1 param1 />}}{{< sc2 param1 />}}`, []typeText{
   138  		tstLeftNoMD, tstSC1, tstParam1, tstSCClose, tstRightNoMD,
   139  		tstLeftNoMD, tstSC2, tstParam1, tstSCClose, tstRightNoMD, tstEOF,
   140  	}},
   141  	{"nested simple", `{{< sc1 >}}{{< sc2 >}}{{< /sc1 >}}`, []typeText{
   142  		tstLeftNoMD, tstSC1, tstRightNoMD,
   143  		tstLeftNoMD, tstSC2, tstRightNoMD,
   144  		tstLeftNoMD, tstSCClose, tstSC1, tstRightNoMD, tstEOF,
   145  	}},
   146  	{"nested complex", `{{< sc1 >}}ab{{% sc2 param1 %}}cd{{< sc3 >}}ef{{< /sc3 >}}gh{{% /sc2 %}}ij{{< /sc1 >}}kl`, []typeText{
   147  		tstLeftNoMD, tstSC1, tstRightNoMD,
   148  		nti(tText, "ab"),
   149  		tstLeftMD, tstSC2, tstParam1, tstRightMD,
   150  		nti(tText, "cd"),
   151  		tstLeftNoMD, tstSC3, tstRightNoMD,
   152  		nti(tText, "ef"),
   153  		tstLeftNoMD, tstSCClose, tstSC3, tstRightNoMD,
   154  		nti(tText, "gh"),
   155  		tstLeftMD, tstSCClose, tstSC2, tstRightMD,
   156  		nti(tText, "ij"),
   157  		tstLeftNoMD, tstSCClose, tstSC1, tstRightNoMD,
   158  		nti(tText, "kl"), tstEOF,
   159  	}},
   160  
   161  	{"two quoted params", `{{< sc1 "param nr. 1" "param nr. 2" >}}`, []typeText{
   162  		tstLeftNoMD, tstSC1, nti(tScParam, "param nr. 1"), nti(tScParam, "param nr. 2"), tstRightNoMD, tstEOF,
   163  	}},
   164  	{"two named params", `{{< sc1 param1="Hello World" param2="p2Val">}}`, []typeText{
   165  		tstLeftNoMD, tstSC1, tstParam1, tstVal, tstParam2, nti(tScParamVal, "p2Val"), tstRightNoMD, tstEOF,
   166  	}},
   167  	{"escaped quotes", `{{< sc1 param1=\"Hello World\"  >}}`, []typeText{
   168  		tstLeftNoMD, tstSC1, tstParam1, tstVal, tstRightNoMD, tstEOF,
   169  	}},
   170  	{"escaped quotes, positional param", `{{< sc1 \"param1\"  >}}`, []typeText{
   171  		tstLeftNoMD, tstSC1, tstParam1, tstRightNoMD, tstEOF,
   172  	}},
   173  	{"escaped quotes inside escaped quotes", `{{< sc1 param1=\"Hello \"escaped\" World\"  >}}`, []typeText{
   174  		tstLeftNoMD, tstSC1, tstParam1,
   175  		nti(tScParamVal, `Hello `), nti(tError, `got positional parameter 'escaped'. Cannot mix named and positional parameters`),
   176  	}},
   177  	{
   178  		"escaped quotes inside nonescaped quotes",
   179  		`{{< sc1 param1="Hello \"escaped\" World"  >}}`,
   180  		[]typeText{
   181  			tstLeftNoMD, tstSC1, tstParam1, nti(tScParamVal, `Hello "escaped" World`), tstRightNoMD, tstEOF,
   182  		},
   183  	},
   184  	{
   185  		"escaped quotes inside nonescaped quotes in positional param",
   186  		`{{< sc1 "Hello \"escaped\" World"  >}}`,
   187  		[]typeText{
   188  			tstLeftNoMD, tstSC1, nti(tScParam, `Hello "escaped" World`), tstRightNoMD, tstEOF,
   189  		},
   190  	},
   191  	{"escaped raw string, named param", `{{< sc1 param1=` + `\` + "`" + "Hello World" + `\` + "`" + ` >}}`, []typeText{
   192  		tstLeftNoMD, tstSC1, tstParam1, nti(tError, "unrecognized escape character"),
   193  	}},
   194  	{"escaped raw string, positional param", `{{< sc1 param1 ` + `\` + "`" + "Hello World" + `\` + "`" + ` >}}`, []typeText{
   195  		tstLeftNoMD, tstSC1, tstParam1, nti(tError, "unrecognized escape character"),
   196  	}},
   197  	{"two raw string params", `{{< sc1` + "`" + "Hello World" + "`" + "`" + "Second Param" + "`" + ` >}}`, []typeText{
   198  		tstLeftNoMD, tstSC1, nti(tScParam, "Hello World"), nti(tScParam, "Second Param"), tstRightNoMD, tstEOF,
   199  	}},
   200  	{"unterminated quote", `{{< sc1 param2="Hello World>}}`, []typeText{
   201  		tstLeftNoMD, tstSC1, tstParam2, nti(tError, "unterminated quoted string in shortcode parameter-argument: 'Hello World>}}'"),
   202  	}},
   203  	{"unterminated raw string", `{{< sc1` + "`" + "Hello World" + ` >}}`, []typeText{
   204  		tstLeftNoMD, tstSC1, nti(tError, "unterminated raw string in shortcode parameter-argument: 'Hello World >}}'"),
   205  	}},
   206  	{"unterminated raw string in second argument", `{{< sc1` + "`" + "Hello World" + "`" + "`" + "Second Param" + ` >}}`, []typeText{
   207  		tstLeftNoMD, tstSC1, nti(tScParam, "Hello World"), nti(tError, "unterminated raw string in shortcode parameter-argument: 'Second Param >}}'"),
   208  	}},
   209  	{"one named param, one not", `{{< sc1 param1="Hello World" p2 >}}`, []typeText{
   210  		tstLeftNoMD, tstSC1, tstParam1, tstVal,
   211  		nti(tError, "got positional parameter 'p2'. Cannot mix named and positional parameters"),
   212  	}},
   213  	{"one named param, one quoted positional param, both raw strings", `{{< sc1 param1=` + "`" + "Hello World" + "`" + "`" + "Second Param" + "`" + ` >}}`, []typeText{
   214  		tstLeftNoMD, tstSC1, tstParam1, tstVal,
   215  		nti(tError, "got quoted positional parameter. Cannot mix named and positional parameters"),
   216  	}},
   217  	{"one named param, one quoted positional param", `{{< sc1 param1="Hello World" "And Universe" >}}`, []typeText{
   218  		tstLeftNoMD, tstSC1, tstParam1, tstVal,
   219  		nti(tError, "got quoted positional parameter. Cannot mix named and positional parameters"),
   220  	}},
   221  	{"one quoted positional param, one named param", `{{< sc1 "param1" param2="And Universe" >}}`, []typeText{
   222  		tstLeftNoMD, tstSC1, tstParam1,
   223  		nti(tError, "got named parameter 'param2'. Cannot mix named and positional parameters"),
   224  	}},
   225  	{"ono positional param, one not", `{{< sc1 param1 param2="Hello World">}}`, []typeText{
   226  		tstLeftNoMD, tstSC1, tstParam1,
   227  		nti(tError, "got named parameter 'param2'. Cannot mix named and positional parameters"),
   228  	}},
   229  	{"commented out", `{{</* sc1 */>}}`, []typeText{
   230  		nti(tText, "{{<"), nti(tText, " sc1 "), nti(tText, ">}}"), tstEOF,
   231  	}},
   232  	{"commented out, with asterisk inside", `{{</* sc1 "**/*.pdf" */>}}`, []typeText{
   233  		nti(tText, "{{<"), nti(tText, " sc1 \"**/*.pdf\" "), nti(tText, ">}}"), tstEOF,
   234  	}},
   235  	{"commented out, missing close", `{{</* sc1 >}}`, []typeText{
   236  		nti(tError, "comment must be closed"),
   237  	}},
   238  	{"commented out, misplaced close", `{{</* sc1 >}}*/`, []typeText{
   239  		nti(tError, "comment must be closed"),
   240  	}},
   241  	// Inline shortcodes
   242  	{"basic inline", `{{< sc1.inline >}}Hello World{{< /sc1.inline >}}`, []typeText{tstLeftNoMD, tstSC1Inline, tstRightNoMD, tstText, tstLeftNoMD, tstSCClose, tstSC1Inline, tstRightNoMD, tstEOF}},
   243  	{"basic inline with space", `{{< sc1.inline >}}Hello World{{< / sc1.inline >}}`, []typeText{tstLeftNoMD, tstSC1Inline, tstRightNoMD, tstText, tstLeftNoMD, tstSCClose, tstSC1Inline, tstRightNoMD, tstEOF}},
   244  	{"inline self closing", `{{< sc1.inline >}}Hello World{{< /sc1.inline >}}Hello World{{< sc1.inline />}}`, []typeText{tstLeftNoMD, tstSC1Inline, tstRightNoMD, tstText, tstLeftNoMD, tstSCClose, tstSC1Inline, tstRightNoMD, tstText, tstLeftNoMD, tstSC1Inline, tstSCClose, tstRightNoMD, tstEOF}},
   245  	{"inline self closing, then a new inline", `{{< sc1.inline >}}Hello World{{< /sc1.inline >}}Hello World{{< sc1.inline />}}{{< sc2.inline >}}Hello World{{< /sc2.inline >}}`, []typeText{
   246  		tstLeftNoMD, tstSC1Inline, tstRightNoMD, tstText, tstLeftNoMD, tstSCClose, tstSC1Inline, tstRightNoMD, tstText, tstLeftNoMD, tstSC1Inline, tstSCClose, tstRightNoMD,
   247  		tstLeftNoMD, tstSC2Inline, tstRightNoMD, tstText, tstLeftNoMD, tstSCClose, tstSC2Inline, tstRightNoMD, tstEOF,
   248  	}},
   249  	{"inline with template syntax", `{{< sc1.inline >}}{{ .Get 0 }}{{ .Get 1 }}{{< /sc1.inline >}}`, []typeText{tstLeftNoMD, tstSC1Inline, tstRightNoMD, nti(tText, "{{ .Get 0 }}"), nti(tText, "{{ .Get 1 }}"), tstLeftNoMD, tstSCClose, tstSC1Inline, tstRightNoMD, tstEOF}},
   250  	{"inline with nested shortcode (not supported)", `{{< sc1.inline >}}Hello World{{< sc1 >}}{{< /sc1.inline >}}`, []typeText{tstLeftNoMD, tstSC1Inline, tstRightNoMD, tstText, nti(tError, "inline shortcodes do not support nesting")}},
   251  	{"inline case mismatch", `{{< sc1.Inline >}}Hello World{{< /sc1.Inline >}}`, []typeText{tstLeftNoMD, nti(tError, "period in shortcode name only allowed for inline identifiers")}},
   252  }
   253  
   254  func TestShortcodeLexer(t *testing.T) {
   255  	t.Parallel()
   256  	c := qt.New(t)
   257  	for i, test := range shortCodeLexerTests {
   258  		t.Run(test.name, func(t *testing.T) {
   259  			items := collect([]byte(test.input), true, lexMainSection)
   260  			if !equal(test.input, items, test.items) {
   261  				got := itemsToString(items, []byte(test.input))
   262  				expected := testItemsToString(test.items)
   263  				c.Assert(got, qt.Equals, expected, qt.Commentf("Test %d: %s", i, test.name))
   264  			}
   265  		})
   266  	}
   267  }
   268  
   269  func BenchmarkShortcodeLexer(b *testing.B) {
   270  	testInputs := make([][]byte, len(shortCodeLexerTests))
   271  	for i, input := range shortCodeLexerTests {
   272  		testInputs[i] = []byte(input.input)
   273  	}
   274  	var cfg Config
   275  	b.ResetTimer()
   276  	for i := 0; i < b.N; i++ {
   277  		for _, input := range testInputs {
   278  			items := collectWithConfig(input, true, lexMainSection, cfg)
   279  			if len(items) == 0 {
   280  			}
   281  
   282  		}
   283  	}
   284  }