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