github.com/olliephillips/hugo@v0.42.2/parser/parse_frontmatter_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 parser 15 16 // TODO Support Mac Encoding (\r) 17 18 import ( 19 "bufio" 20 "bytes" 21 "io" 22 "os" 23 "path/filepath" 24 "strings" 25 "testing" 26 ) 27 28 const ( 29 contentNoFrontmatter = "a page with no front matter" 30 contentWithFrontmatter = "---\ntitle: front matter\n---\nContent with front matter" 31 contentHTMLNoDoctype = "<html>\n\t<body>\n\t</body>\n</html>" 32 contentHTMLWithDoctype = "<!doctype html><html><body></body></html>" 33 contentHTMLWithFrontmatter = "---\ntitle: front matter\n---\n<!doctype><html><body></body></html>" 34 contentHTML = " <html><body></body></html>" 35 contentLinefeedAndHTML = "\n<html><body></body></html>" 36 contentIncompleteEndFrontmatterDelim = "---\ntitle: incomplete end fm delim\n--\nincomplete frontmatter delim" 37 contentMissingEndFrontmatterDelim = "---\ntitle: incomplete end fm delim\nincomplete frontmatter delim" 38 contentSlugWorking = "---\ntitle: slug doc 2\nslug: slug-doc-2\n\n---\nslug doc 2 content" 39 contentSlugWorkingVariation = "---\ntitle: slug doc 3\nslug: slug-doc 3\n---\nslug doc 3 content" 40 contentSlugBug = "---\ntitle: slug doc 2\nslug: slug-doc-2\n---\nslug doc 2 content" 41 contentSlugWithJSONFrontMatter = "{\n \"categories\": \"d\",\n \"tags\": [\n \"a\", \n \"b\", \n \"c\"\n ]\n}\nJSON Front Matter with tags and categories" 42 contentWithJSONLooseFrontmatter = "{\n \"categories\": \"d\"\n \"tags\": [\n \"a\" \n \"b\" \n \"c\"\n ]\n}\nJSON Front Matter with tags and categories" 43 contentSlugWithJSONFrontMatterFirstLineOnly = "{\"categories\":\"d\",\"tags\":[\"a\",\"b\",\"c\"]}\nJSON Front Matter with tags and categories" 44 contentSlugWithJSONFrontMatterFirstLine = "{\"categories\":\"d\",\n \"tags\":[\"a\",\"b\",\"c\"]}\nJSON Front Matter with tags and categories" 45 ) 46 47 var lineEndings = []string{"\n", "\r\n"} 48 var delimiters = []string{"---", "+++"} 49 50 func pageMust(p Page, err error) *page { 51 if err != nil { 52 panic(err) 53 } 54 return p.(*page) 55 } 56 57 func TestDegenerateCreatePageFrom(t *testing.T) { 58 tests := []struct { 59 content string 60 }{ 61 {contentMissingEndFrontmatterDelim}, 62 {contentIncompleteEndFrontmatterDelim}, 63 } 64 65 for _, test := range tests { 66 for _, ending := range lineEndings { 67 test.content = strings.Replace(test.content, "\n", ending, -1) 68 _, err := ReadFrom(strings.NewReader(test.content)) 69 if err == nil { 70 t.Errorf("Content should return an err:\n%q\n", test.content) 71 } 72 } 73 } 74 } 75 76 func checkPageRender(t *testing.T, p *page, expected bool) { 77 if p.render != expected { 78 t.Errorf("page.render should be %t, got: %t", expected, p.render) 79 } 80 } 81 82 func checkPageFrontMatterIsNil(t *testing.T, p *page, content string, expected bool) { 83 if bool(p.frontmatter == nil) != expected { 84 t.Logf("\n%q\n", content) 85 t.Errorf("page.frontmatter == nil? %t, got %t", expected, p.frontmatter == nil) 86 } 87 } 88 89 func checkPageFrontMatterContent(t *testing.T, p *page, frontMatter string) { 90 if p.frontmatter == nil { 91 return 92 } 93 if !bytes.Equal(p.frontmatter, []byte(frontMatter)) { 94 t.Errorf("frontmatter mismatch\nexp: %q\ngot: %q", frontMatter, p.frontmatter) 95 } 96 } 97 98 func checkPageContent(t *testing.T, p *page, expected string) { 99 if !bytes.Equal(p.content, []byte(expected)) { 100 t.Errorf("content mismatch\nexp: %q\ngot: %q", expected, p.content) 101 } 102 } 103 104 func TestStandaloneCreatePageFrom(t *testing.T) { 105 tests := []struct { 106 content string 107 expectedMustRender bool 108 frontMatterIsNil bool 109 frontMatter string 110 bodycontent string 111 }{ 112 113 {contentNoFrontmatter, true, true, "", "a page with no front matter"}, 114 {contentWithFrontmatter, true, false, "---\ntitle: front matter\n---\n", "Content with front matter"}, 115 {contentHTMLNoDoctype, false, true, "", "<html>\n\t<body>\n\t</body>\n</html>"}, 116 {contentHTMLWithDoctype, false, true, "", "<!doctype html><html><body></body></html>"}, 117 {contentHTMLWithFrontmatter, true, false, "---\ntitle: front matter\n---\n", "<!doctype><html><body></body></html>"}, 118 {contentHTML, false, true, "", "<html><body></body></html>"}, 119 {contentLinefeedAndHTML, false, true, "", "<html><body></body></html>"}, 120 {contentSlugWithJSONFrontMatter, true, false, "{\n \"categories\": \"d\",\n \"tags\": [\n \"a\", \n \"b\", \n \"c\"\n ]\n}", "JSON Front Matter with tags and categories"}, 121 {contentWithJSONLooseFrontmatter, true, false, "{\n \"categories\": \"d\"\n \"tags\": [\n \"a\" \n \"b\" \n \"c\"\n ]\n}", "JSON Front Matter with tags and categories"}, 122 {contentSlugWithJSONFrontMatterFirstLineOnly, true, false, "{\"categories\":\"d\",\"tags\":[\"a\",\"b\",\"c\"]}", "JSON Front Matter with tags and categories"}, 123 {contentSlugWithJSONFrontMatterFirstLine, true, false, "{\"categories\":\"d\",\n \"tags\":[\"a\",\"b\",\"c\"]}", "JSON Front Matter with tags and categories"}, 124 {contentSlugWorking, true, false, "---\ntitle: slug doc 2\nslug: slug-doc-2\n\n---\n", "slug doc 2 content"}, 125 {contentSlugWorkingVariation, true, false, "---\ntitle: slug doc 3\nslug: slug-doc 3\n---\n", "slug doc 3 content"}, 126 {contentSlugBug, true, false, "---\ntitle: slug doc 2\nslug: slug-doc-2\n---\n", "slug doc 2 content"}, 127 } 128 129 for _, test := range tests { 130 for _, ending := range lineEndings { 131 test.content = strings.Replace(test.content, "\n", ending, -1) 132 test.frontMatter = strings.Replace(test.frontMatter, "\n", ending, -1) 133 test.bodycontent = strings.Replace(test.bodycontent, "\n", ending, -1) 134 135 p := pageMust(ReadFrom(strings.NewReader(test.content))) 136 137 checkPageRender(t, p, test.expectedMustRender) 138 checkPageFrontMatterIsNil(t, p, test.content, test.frontMatterIsNil) 139 checkPageFrontMatterContent(t, p, test.frontMatter) 140 checkPageContent(t, p, test.bodycontent) 141 } 142 } 143 } 144 145 func BenchmarkLongFormRender(b *testing.B) { 146 147 tests := []struct { 148 filename string 149 buf []byte 150 }{ 151 {filename: "long_text_test.md"}, 152 } 153 for i, test := range tests { 154 path := filepath.FromSlash(test.filename) 155 f, err := os.Open(path) 156 if err != nil { 157 b.Fatalf("Unable to open %s: %s", path, err) 158 } 159 defer f.Close() 160 membuf := new(bytes.Buffer) 161 if _, err := io.Copy(membuf, f); err != nil { 162 b.Fatalf("Unable to read %s: %s", path, err) 163 } 164 tests[i].buf = membuf.Bytes() 165 } 166 167 b.ResetTimer() 168 169 for i := 0; i <= b.N; i++ { 170 for _, test := range tests { 171 ReadFrom(bytes.NewReader(test.buf)) 172 } 173 } 174 } 175 176 func TestPageShouldRender(t *testing.T) { 177 tests := []struct { 178 content []byte 179 expected bool 180 }{ 181 {[]byte{}, false}, 182 {[]byte{'<'}, false}, 183 {[]byte{'-'}, true}, 184 {[]byte("--"), true}, 185 {[]byte("---"), true}, 186 {[]byte("---\n"), true}, 187 {[]byte{'a'}, true}, 188 } 189 190 for _, test := range tests { 191 for _, ending := range lineEndings { 192 test.content = bytes.Replace(test.content, []byte("\n"), []byte(ending), -1) 193 if render := shouldRender(test.content); render != test.expected { 194 195 t.Errorf("Expected %s to shouldRender = %t, got: %t", test.content, test.expected, render) 196 } 197 } 198 } 199 } 200 201 func TestPageHasFrontMatter(t *testing.T) { 202 tests := []struct { 203 content []byte 204 expected bool 205 }{ 206 {[]byte{'-'}, false}, 207 {[]byte("--"), false}, 208 {[]byte("---"), false}, 209 {[]byte("---\n"), true}, 210 {[]byte("---\n"), true}, 211 {[]byte("--- \n"), true}, 212 {[]byte("--- \n"), true}, 213 {[]byte{'a'}, false}, 214 {[]byte{'{'}, true}, 215 {[]byte("{\n "), true}, 216 {[]byte{'}'}, false}, 217 } 218 for _, test := range tests { 219 for _, ending := range lineEndings { 220 test.content = bytes.Replace(test.content, []byte("\n"), []byte(ending), -1) 221 if isFrontMatterDelim := isFrontMatterDelim(test.content); isFrontMatterDelim != test.expected { 222 t.Errorf("Expected %q isFrontMatterDelim = %t, got: %t", test.content, test.expected, isFrontMatterDelim) 223 } 224 } 225 } 226 } 227 228 func TestExtractFrontMatter(t *testing.T) { 229 230 tests := []struct { 231 frontmatter string 232 extracted []byte 233 errIsNil bool 234 }{ 235 {"", nil, false}, 236 {"-", nil, false}, 237 {"---\n", nil, false}, 238 {"---\nfoobar", nil, false}, 239 {"---\nfoobar\nbarfoo\nfizbaz\n", nil, false}, 240 {"---\nblar\n-\n", nil, false}, 241 {"---\nralb\n---\n", []byte("---\nralb\n---\n"), true}, 242 {"---\neof\n---", []byte("---\neof\n---"), true}, 243 {"--- \neof\n---", []byte("---\neof\n---"), true}, 244 {"---\nminc\n---\ncontent", []byte("---\nminc\n---\n"), true}, 245 {"---\nminc\n--- \ncontent", []byte("---\nminc\n---\n"), true}, 246 {"--- \nminc\n--- \ncontent", []byte("---\nminc\n---\n"), true}, 247 {"---\ncnim\n---\ncontent\n", []byte("---\ncnim\n---\n"), true}, 248 {"---\ntitle: slug doc 2\nslug: slug-doc-2\n---\ncontent\n", []byte("---\ntitle: slug doc 2\nslug: slug-doc-2\n---\n"), true}, 249 {"---\npermalink: '/blog/title---subtitle.html'\n---\ncontent\n", []byte("---\npermalink: '/blog/title---subtitle.html'\n---\n"), true}, 250 } 251 252 for _, test := range tests { 253 for _, ending := range lineEndings { 254 test.frontmatter = strings.Replace(test.frontmatter, "\n", ending, -1) 255 test.extracted = bytes.Replace(test.extracted, []byte("\n"), []byte(ending), -1) 256 for _, delim := range delimiters { 257 test.frontmatter = strings.Replace(test.frontmatter, "---", delim, -1) 258 test.extracted = bytes.Replace(test.extracted, []byte("---"), []byte(delim), -1) 259 line, err := peekLine(bufio.NewReader(strings.NewReader(test.frontmatter))) 260 if err != nil { 261 continue 262 } 263 l, r := determineDelims(line) 264 fm, err := extractFrontMatterDelims(bufio.NewReader(strings.NewReader(test.frontmatter)), l, r) 265 if (err == nil) != test.errIsNil { 266 t.Logf("\n%q\n", string(test.frontmatter)) 267 t.Errorf("Expected err == nil => %t, got: %t. err: %s", test.errIsNil, err == nil, err) 268 continue 269 } 270 if !bytes.Equal(fm, test.extracted) { 271 t.Errorf("Frontmatter did not match:\nexp: %q\ngot: %q", string(test.extracted), fm) 272 } 273 } 274 } 275 } 276 } 277 278 func TestExtractFrontMatterDelim(t *testing.T) { 279 var ( 280 noErrExpected = true 281 errExpected = false 282 ) 283 tests := []struct { 284 frontmatter string 285 extracted string 286 errIsNil bool 287 }{ 288 {"", "", errExpected}, 289 {"{", "", errExpected}, 290 {"{}", "{}", noErrExpected}, 291 {"{} ", "{}", noErrExpected}, 292 {"{ } ", "{ }", noErrExpected}, 293 {"{ { }", "", errExpected}, 294 {"{ { } }", "{ { } }", noErrExpected}, 295 {"{ { } { } }", "{ { } { } }", noErrExpected}, 296 {"{\n{\n}\n}\n", "{\n{\n}\n}", noErrExpected}, 297 {"{\n \"categories\": \"d\",\n \"tags\": [\n \"a\", \n \"b\", \n \"c\"\n ]\n}\nJSON Front Matter with tags and categories", "{\n \"categories\": \"d\",\n \"tags\": [\n \"a\", \n \"b\", \n \"c\"\n ]\n}", noErrExpected}, 298 {"{\n \"categories\": \"d\"\n \"tags\": [\n \"a\" \n \"b\" \n \"c\"\n ]\n}\nJSON Front Matter with tags and categories", "{\n \"categories\": \"d\"\n \"tags\": [\n \"a\" \n \"b\" \n \"c\"\n ]\n}", noErrExpected}, 299 // Issue #3511 300 {`{ "title": "{" }`, `{ "title": "{" }`, noErrExpected}, 301 {`{ "title": "{}" }`, `{ "title": "{}" }`, noErrExpected}, 302 // Issue #3661 303 {`{ "title": "\"" }`, `{ "title": "\"" }`, noErrExpected}, 304 {`{ "title": "\"{", "other": "\"{}" }`, `{ "title": "\"{", "other": "\"{}" }`, noErrExpected}, 305 {`{ "title": "\"Foo\"" }`, `{ "title": "\"Foo\"" }`, noErrExpected}, 306 {`{ "title": "\"Foo\"\"" }`, `{ "title": "\"Foo\"\"" }`, noErrExpected}, 307 {`{ "url": "http:\/\/example.com\/play\/url?id=1" }`, `{ "url": "http:\/\/example.com\/play\/url?id=1" }`, noErrExpected}, 308 {`{ "test": "\"New\r\nString\"" }`, `{ "test": "\"New\r\nString\"" }`, noErrExpected}, 309 {`{ "test": "RTS\/RPG" }`, `{ "test": "RTS\/RPG" }`, noErrExpected}, 310 } 311 312 for i, test := range tests { 313 fm, err := extractFrontMatterDelims(bufio.NewReader(strings.NewReader(test.frontmatter)), []byte("{"), []byte("}")) 314 if (err == nil) != test.errIsNil { 315 t.Logf("\n%q\n", string(test.frontmatter)) 316 t.Errorf("[%d] Expected err == nil => %t, got: %t. err: %s", i, test.errIsNil, err == nil, err) 317 continue 318 } 319 if !bytes.Equal(fm, []byte(test.extracted)) { 320 t.Logf("\n%q\n", string(test.frontmatter)) 321 t.Errorf("[%d] Frontmatter did not match:\nexp: %q\ngot: %q", i, string(test.extracted), fm) 322 } 323 } 324 }