github.com/olliephillips/hugo@v0.42.2/hugolib/shortcodeparser_test.go (about) 1 // Copyright 2015 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 hugolib 15 16 import ( 17 "testing" 18 ) 19 20 type shortCodeLexerTest struct { 21 name string 22 input string 23 items []item 24 } 25 26 var ( 27 tstEOF = item{tEOF, 0, ""} 28 tstLeftNoMD = item{tLeftDelimScNoMarkup, 0, "{{<"} 29 tstRightNoMD = item{tRightDelimScNoMarkup, 0, ">}}"} 30 tstLeftMD = item{tLeftDelimScWithMarkup, 0, "{{%"} 31 tstRightMD = item{tRightDelimScWithMarkup, 0, "%}}"} 32 tstSCClose = item{tScClose, 0, "/"} 33 tstSC1 = item{tScName, 0, "sc1"} 34 tstSC2 = item{tScName, 0, "sc2"} 35 tstSC3 = item{tScName, 0, "sc3"} 36 tstParam1 = item{tScParam, 0, "param1"} 37 tstParam2 = item{tScParam, 0, "param2"} 38 tstVal = item{tScParamVal, 0, "Hello World"} 39 ) 40 41 var shortCodeLexerTests = []shortCodeLexerTest{ 42 {"empty", "", []item{tstEOF}}, 43 {"spaces", " \t\n", []item{{tText, 0, " \t\n"}, tstEOF}}, 44 {"text", `to be or not`, []item{{tText, 0, "to be or not"}, tstEOF}}, 45 {"no markup", `{{< sc1 >}}`, []item{tstLeftNoMD, tstSC1, tstRightNoMD, tstEOF}}, 46 {"with EOL", "{{< sc1 \n >}}", []item{tstLeftNoMD, tstSC1, tstRightNoMD, tstEOF}}, 47 48 {"simple with markup", `{{% sc1 %}}`, []item{tstLeftMD, tstSC1, tstRightMD, tstEOF}}, 49 {"with spaces", `{{< sc1 >}}`, []item{tstLeftNoMD, tstSC1, tstRightNoMD, tstEOF}}, 50 {"mismatched rightDelim", `{{< sc1 %}}`, []item{tstLeftNoMD, tstSC1, 51 {tError, 0, "unrecognized character in shortcode action: U+0025 '%'. Note: Parameters with non-alphanumeric args must be quoted"}}}, 52 {"inner, markup", `{{% sc1 %}} inner {{% /sc1 %}}`, []item{ 53 tstLeftMD, 54 tstSC1, 55 tstRightMD, 56 {tText, 0, " inner "}, 57 tstLeftMD, 58 tstSCClose, 59 tstSC1, 60 tstRightMD, 61 tstEOF, 62 }}, 63 {"close, but no open", `{{< /sc1 >}}`, []item{ 64 tstLeftNoMD, {tError, 0, "got closing shortcode, but none is open"}}}, 65 {"close wrong", `{{< sc1 >}}{{< /another >}}`, []item{ 66 tstLeftNoMD, tstSC1, tstRightNoMD, tstLeftNoMD, tstSCClose, 67 {tError, 0, "closing tag for shortcode 'another' does not match start tag"}}}, 68 {"close, but no open, more", `{{< sc1 >}}{{< /sc1 >}}{{< /another >}}`, []item{ 69 tstLeftNoMD, tstSC1, tstRightNoMD, tstLeftNoMD, tstSCClose, tstSC1, tstRightNoMD, tstLeftNoMD, tstSCClose, 70 {tError, 0, "closing tag for shortcode 'another' does not match start tag"}}}, 71 {"close with extra keyword", `{{< sc1 >}}{{< /sc1 keyword>}}`, []item{ 72 tstLeftNoMD, tstSC1, tstRightNoMD, tstLeftNoMD, tstSCClose, tstSC1, 73 {tError, 0, "unclosed shortcode"}}}, 74 {"Youtube id", `{{< sc1 -ziL-Q_456igdO-4 >}}`, []item{ 75 tstLeftNoMD, tstSC1, {tScParam, 0, "-ziL-Q_456igdO-4"}, tstRightNoMD, tstEOF}}, 76 {"non-alphanumerics param quoted", `{{< sc1 "-ziL-.%QigdO-4" >}}`, []item{ 77 tstLeftNoMD, tstSC1, {tScParam, 0, "-ziL-.%QigdO-4"}, tstRightNoMD, tstEOF}}, 78 79 {"two params", `{{< sc1 param1 param2 >}}`, []item{ 80 tstLeftNoMD, tstSC1, tstParam1, tstParam2, tstRightNoMD, tstEOF}}, 81 // issue #934 82 {"self-closing", `{{< sc1 />}}`, []item{ 83 tstLeftNoMD, tstSC1, tstSCClose, tstRightNoMD, tstEOF}}, 84 // Issue 2498 85 {"multiple self-closing", `{{< sc1 />}}{{< sc1 />}}`, []item{ 86 tstLeftNoMD, tstSC1, tstSCClose, tstRightNoMD, 87 tstLeftNoMD, tstSC1, tstSCClose, tstRightNoMD, tstEOF}}, 88 {"self-closing with param", `{{< sc1 param1 />}}`, []item{ 89 tstLeftNoMD, tstSC1, tstParam1, tstSCClose, tstRightNoMD, tstEOF}}, 90 {"multiple self-closing with param", `{{< sc1 param1 />}}{{< sc1 param1 />}}`, []item{ 91 tstLeftNoMD, tstSC1, tstParam1, tstSCClose, tstRightNoMD, 92 tstLeftNoMD, tstSC1, tstParam1, tstSCClose, tstRightNoMD, tstEOF}}, 93 {"multiple different self-closing with param", `{{< sc1 param1 />}}{{< sc2 param1 />}}`, []item{ 94 tstLeftNoMD, tstSC1, tstParam1, tstSCClose, tstRightNoMD, 95 tstLeftNoMD, tstSC2, tstParam1, tstSCClose, tstRightNoMD, tstEOF}}, 96 {"nested simple", `{{< sc1 >}}{{< sc2 >}}{{< /sc1 >}}`, []item{ 97 tstLeftNoMD, tstSC1, tstRightNoMD, 98 tstLeftNoMD, tstSC2, tstRightNoMD, 99 tstLeftNoMD, tstSCClose, tstSC1, tstRightNoMD, tstEOF}}, 100 {"nested complex", `{{< sc1 >}}ab{{% sc2 param1 %}}cd{{< sc3 >}}ef{{< /sc3 >}}gh{{% /sc2 %}}ij{{< /sc1 >}}kl`, []item{ 101 tstLeftNoMD, tstSC1, tstRightNoMD, 102 {tText, 0, "ab"}, 103 tstLeftMD, tstSC2, tstParam1, tstRightMD, 104 {tText, 0, "cd"}, 105 tstLeftNoMD, tstSC3, tstRightNoMD, 106 {tText, 0, "ef"}, 107 tstLeftNoMD, tstSCClose, tstSC3, tstRightNoMD, 108 {tText, 0, "gh"}, 109 tstLeftMD, tstSCClose, tstSC2, tstRightMD, 110 {tText, 0, "ij"}, 111 tstLeftNoMD, tstSCClose, tstSC1, tstRightNoMD, 112 {tText, 0, "kl"}, tstEOF, 113 }}, 114 115 {"two quoted params", `{{< sc1 "param nr. 1" "param nr. 2" >}}`, []item{ 116 tstLeftNoMD, tstSC1, {tScParam, 0, "param nr. 1"}, {tScParam, 0, "param nr. 2"}, tstRightNoMD, tstEOF}}, 117 {"two named params", `{{< sc1 param1="Hello World" param2="p2Val">}}`, []item{ 118 tstLeftNoMD, tstSC1, tstParam1, tstVal, tstParam2, {tScParamVal, 0, "p2Val"}, tstRightNoMD, tstEOF}}, 119 {"escaped quotes", `{{< sc1 param1=\"Hello World\" >}}`, []item{ 120 tstLeftNoMD, tstSC1, tstParam1, tstVal, tstRightNoMD, tstEOF}}, 121 {"escaped quotes, positional param", `{{< sc1 \"param1\" >}}`, []item{ 122 tstLeftNoMD, tstSC1, tstParam1, tstRightNoMD, tstEOF}}, 123 {"escaped quotes inside escaped quotes", `{{< sc1 param1=\"Hello \"escaped\" World\" >}}`, []item{ 124 tstLeftNoMD, tstSC1, tstParam1, 125 {tScParamVal, 0, `Hello `}, {tError, 0, `got positional parameter 'escaped'. Cannot mix named and positional parameters`}}}, 126 {"escaped quotes inside nonescaped quotes", 127 `{{< sc1 param1="Hello \"escaped\" World" >}}`, []item{ 128 tstLeftNoMD, tstSC1, tstParam1, {tScParamVal, 0, `Hello "escaped" World`}, tstRightNoMD, tstEOF}}, 129 {"escaped quotes inside nonescaped quotes in positional param", 130 `{{< sc1 "Hello \"escaped\" World" >}}`, []item{ 131 tstLeftNoMD, tstSC1, {tScParam, 0, `Hello "escaped" World`}, tstRightNoMD, tstEOF}}, 132 {"unterminated quote", `{{< sc1 param2="Hello World>}}`, []item{ 133 tstLeftNoMD, tstSC1, tstParam2, {tError, 0, "unterminated quoted string in shortcode parameter-argument: 'Hello World>}}'"}}}, 134 {"one named param, one not", `{{< sc1 param1="Hello World" p2 >}}`, []item{ 135 tstLeftNoMD, tstSC1, tstParam1, tstVal, 136 {tError, 0, "got positional parameter 'p2'. Cannot mix named and positional parameters"}}}, 137 {"one named param, one quoted positional param", `{{< sc1 param1="Hello World" "And Universe" >}}`, []item{ 138 tstLeftNoMD, tstSC1, tstParam1, tstVal, 139 {tError, 0, "got quoted positional parameter. Cannot mix named and positional parameters"}}}, 140 {"one quoted positional param, one named param", `{{< sc1 "param1" param2="And Universe" >}}`, []item{ 141 tstLeftNoMD, tstSC1, tstParam1, 142 {tError, 0, "got named parameter 'param2'. Cannot mix named and positional parameters"}}}, 143 {"ono positional param, one not", `{{< sc1 param1 param2="Hello World">}}`, []item{ 144 tstLeftNoMD, tstSC1, tstParam1, 145 {tError, 0, "got named parameter 'param2'. Cannot mix named and positional parameters"}}}, 146 {"commented out", `{{</* sc1 */>}}`, []item{ 147 {tText, 0, "{{<"}, {tText, 0, " sc1 "}, {tText, 0, ">}}"}, tstEOF}}, 148 {"commented out, with asterisk inside", `{{</* sc1 "**/*.pdf" */>}}`, []item{ 149 {tText, 0, "{{<"}, {tText, 0, " sc1 \"**/*.pdf\" "}, {tText, 0, ">}}"}, tstEOF}}, 150 {"commented out, missing close", `{{</* sc1 >}}`, []item{ 151 {tError, 0, "comment must be closed"}}}, 152 {"commented out, misplaced close", `{{</* sc1 >}}*/`, []item{ 153 {tError, 0, "comment must be closed"}}}, 154 } 155 156 func TestShortcodeLexer(t *testing.T) { 157 t.Parallel() 158 for i, test := range shortCodeLexerTests { 159 items := collect(&test) 160 if !equal(items, test.items) { 161 t.Errorf("[%d] %s: got\n\t%v\nexpected\n\t%v", i, test.name, items, test.items) 162 } 163 } 164 } 165 166 func BenchmarkShortcodeLexer(b *testing.B) { 167 b.ResetTimer() 168 for i := 0; i < b.N; i++ { 169 for _, test := range shortCodeLexerTests { 170 items := collect(&test) 171 if !equal(items, test.items) { 172 b.Errorf("%s: got\n\t%v\nexpected\n\t%v", test.name, items, test.items) 173 } 174 } 175 } 176 } 177 178 func collect(t *shortCodeLexerTest) (items []item) { 179 l := newShortcodeLexer(t.name, t.input, 0) 180 for { 181 item := l.nextItem() 182 items = append(items, item) 183 if item.typ == tEOF || item.typ == tError { 184 break 185 } 186 } 187 return 188 } 189 190 // no positional checking, for now ... 191 func equal(i1, i2 []item) bool { 192 if len(i1) != len(i2) { 193 return false 194 } 195 for k := range i1 { 196 if i1[k].typ != i2[k].typ { 197 return false 198 } 199 if i1[k].val != i2[k].val { 200 return false 201 } 202 } 203 return true 204 }