1// Copyright 2019 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
14package hugolib
15
16import (
17	"fmt"
18	"html/template"
19	"os"
20	"path/filepath"
21	"strings"
22	"testing"
23	"time"
24
25	"github.com/gohugoio/hugo/htesting"
26
27	"github.com/gohugoio/hugo/markup/asciidocext"
28
29	"github.com/gohugoio/hugo/config"
30
31	"github.com/gohugoio/hugo/common/loggers"
32
33	"github.com/gohugoio/hugo/hugofs"
34
35	"github.com/gohugoio/hugo/resources/page"
36	"github.com/gohugoio/hugo/resources/resource"
37	"github.com/spf13/afero"
38
39	qt "github.com/frankban/quicktest"
40	"github.com/gohugoio/hugo/deps"
41	"github.com/gohugoio/hugo/helpers"
42)
43
44const (
45	homePage   = "---\ntitle: Home\n---\nHome Page Content\n"
46	simplePage = "---\ntitle: Simple\n---\nSimple Page\n"
47
48	simplePageRFC3339Date = "---\ntitle: RFC3339 Date\ndate: \"2013-05-17T16:59:30Z\"\n---\nrfc3339 content"
49
50	simplePageWithoutSummaryDelimiter = `---
51title: SimpleWithoutSummaryDelimiter
52---
53[Lorem ipsum](https://lipsum.com/) dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
54
55Additional text.
56
57Further text.
58`
59
60	simplePageWithSummaryDelimiter = `---
61title: Simple
62---
63Summary Next Line
64
65<!--more-->
66Some more text
67`
68
69	simplePageWithSummaryParameter = `---
70title: SimpleWithSummaryParameter
71summary: "Page with summary parameter and [a link](http://www.example.com/)"
72---
73
74Some text.
75
76Some more text.
77`
78
79	simplePageWithSummaryDelimiterAndMarkdownThatCrossesBorder = `---
80title: Simple
81---
82The [best static site generator][hugo].[^1]
83<!--more-->
84[hugo]: http://gohugo.io/
85[^1]: Many people say so.
86`
87	simplePageWithShortcodeInSummary = `---
88title: Simple
89---
90Summary Next Line. {{<figure src="/not/real" >}}.
91More text here.
92
93Some more text
94`
95
96	simplePageWithSummaryDelimiterSameLine = `---
97title: Simple
98---
99Summary Same Line<!--more-->
100
101Some more text
102`
103
104	simplePageWithAllCJKRunes = `---
105title: Simple
106---
107
108
109€ € € € €
110你好
111도형이
112カテゴリー
113
114
115`
116
117	simplePageWithMainEnglishWithCJKRunes = `---
118title: Simple
119---
120
121
122In Chinese, 好 means good.  In Chinese, 好 means good.
123In Chinese, 好 means good.  In Chinese, 好 means good.
124In Chinese, 好 means good.  In Chinese, 好 means good.
125In Chinese, 好 means good.  In Chinese, 好 means good.
126In Chinese, 好 means good.  In Chinese, 好 means good.
127In Chinese, 好 means good.  In Chinese, 好 means good.
128In Chinese, 好 means good.  In Chinese, 好 means good.
129More then 70 words.
130
131
132`
133	simplePageWithMainEnglishWithCJKRunesSummary = "In Chinese, 好 means good. In Chinese, 好 means good. " +
134		"In Chinese, 好 means good. In Chinese, 好 means good. " +
135		"In Chinese, 好 means good. In Chinese, 好 means good. " +
136		"In Chinese, 好 means good. In Chinese, 好 means good. " +
137		"In Chinese, 好 means good. In Chinese, 好 means good. " +
138		"In Chinese, 好 means good. In Chinese, 好 means good. " +
139		"In Chinese, 好 means good. In Chinese, 好 means good."
140
141	simplePageWithIsCJKLanguageFalse = `---
142title: Simple
143isCJKLanguage: false
144---
145
146In Chinese, 好的啊 means good.  In Chinese, 好的呀 means good.
147In Chinese, 好的啊 means good.  In Chinese, 好的呀 means good.
148In Chinese, 好的啊 means good.  In Chinese, 好的呀 means good.
149In Chinese, 好的啊 means good.  In Chinese, 好的呀 means good.
150In Chinese, 好的啊 means good.  In Chinese, 好的呀 means good.
151In Chinese, 好的啊 means good.  In Chinese, 好的呀 means good.
152In Chinese, 好的啊 means good.  In Chinese, 好的呀呀 means good enough.
153More then 70 words.
154
155
156`
157	simplePageWithIsCJKLanguageFalseSummary = "In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. " +
158		"In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. " +
159		"In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. " +
160		"In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. " +
161		"In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. " +
162		"In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. " +
163		"In Chinese, 好的啊 means good. In Chinese, 好的呀呀 means good enough."
164
165	simplePageWithLongContent = `---
166title: Simple
167---
168
169Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor
170incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
171nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
172Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
173fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
174culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit
175amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore
176et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
177ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
178in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
179pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
180officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet,
181consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et
182dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
183laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
184reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
185Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia
186deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur
187adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
188aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi
189ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
190voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
191occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim
192id est laborum. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed
193do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
194veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
195consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
196cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
197proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem
198ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor
199incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
200nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
201Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
202fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
203culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit
204amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore
205et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
206ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
207in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
208pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
209officia deserunt mollit anim id est laborum.`
210
211	pageWithToC = `---
212title: TOC
213---
214For some moments the old man did not reply. He stood with bowed head, buried in deep thought. But at last he spoke.
215
216## AA
217
218I have no idea, of course, how long it took me to reach the limit of the plain,
219but at last I entered the foothills, following a pretty little canyon upward
220toward the mountains. Beside me frolicked a laughing brooklet, hurrying upon
221its noisy way down to the silent sea. In its quieter pools I discovered many
222small fish, of four-or five-pound weight I should imagine. In appearance,
223except as to size and color, they were not unlike the whale of our own seas. As
224I watched them playing about I discovered, not only that they suckled their
225young, but that at intervals they rose to the surface to breathe as well as to
226feed upon certain grasses and a strange, scarlet lichen which grew upon the
227rocks just above the water line.
228
229### AAA
230
231I remember I felt an extraordinary persuasion that I was being played with,
232that presently, when I was upon the very verge of safety, this mysterious
233death--as swift as the passage of light--would leap after me from the pit about
234the cylinder and strike me down. ## BB
235
236### BBB
237
238"You're a great Granser," he cried delightedly, "always making believe them little marks mean something."
239`
240
241	simplePageWithAdditionalExtension = `+++
242[blackfriday]
243  extensions = ["hardLineBreak"]
244+++
245first line.
246second line.
247
248fourth line.
249`
250
251	simplePageWithURL = `---
252title: Simple
253url: simple/url/
254---
255Simple Page With URL`
256
257	simplePageWithSlug = `---
258title: Simple
259slug: simple-slug
260---
261Simple Page With Slug`
262
263	simplePageWithDate = `---
264title: Simple
265date: '2013-10-15T06:16:13'
266---
267Simple Page With Date`
268
269	UTF8Page = `---
270title: ラーメン
271---
272UTF8 Page`
273
274	UTF8PageWithURL = `---
275title: ラーメン
276url: ラーメン/url/
277---
278UTF8 Page With URL`
279
280	UTF8PageWithSlug = `---
281title: ラーメン
282slug: ラーメン-slug
283---
284UTF8 Page With Slug`
285
286	UTF8PageWithDate = `---
287title: ラーメン
288date: '2013-10-15T06:16:13'
289---
290UTF8 Page With Date`
291)
292
293func checkPageTitle(t *testing.T, page page.Page, title string) {
294	if page.Title() != title {
295		t.Fatalf("Page title is: %s.  Expected %s", page.Title(), title)
296	}
297}
298
299func checkPageContent(t *testing.T, page page.Page, expected string, msg ...interface{}) {
300	t.Helper()
301	a := normalizeContent(expected)
302	b := normalizeContent(content(page))
303	if a != b {
304		t.Fatalf("Page content is:\n%q\nExpected:\n%q (%q)", b, a, msg)
305	}
306}
307
308func normalizeContent(c string) string {
309	norm := c
310	norm = strings.Replace(norm, "\n", " ", -1)
311	norm = strings.Replace(norm, "    ", " ", -1)
312	norm = strings.Replace(norm, "   ", " ", -1)
313	norm = strings.Replace(norm, "  ", " ", -1)
314	norm = strings.Replace(norm, "p> ", "p>", -1)
315	norm = strings.Replace(norm, ">  <", "> <", -1)
316	return strings.TrimSpace(norm)
317}
318
319func checkPageTOC(t *testing.T, page page.Page, toc string) {
320	t.Helper()
321	if page.TableOfContents() != template.HTML(toc) {
322		t.Fatalf("Page TableOfContents is:\n%q.\nExpected %q", page.TableOfContents(), toc)
323	}
324}
325
326func checkPageSummary(t *testing.T, page page.Page, summary string, msg ...interface{}) {
327	a := normalizeContent(string(page.Summary()))
328	b := normalizeContent(summary)
329	if a != b {
330		t.Fatalf("Page summary is:\n%q.\nExpected\n%q (%q)", a, b, msg)
331	}
332}
333
334func checkPageType(t *testing.T, page page.Page, pageType string) {
335	if page.Type() != pageType {
336		t.Fatalf("Page type is: %s.  Expected: %s", page.Type(), pageType)
337	}
338}
339
340func checkPageDate(t *testing.T, page page.Page, time time.Time) {
341	if page.Date() != time {
342		t.Fatalf("Page date is: %s.  Expected: %s", page.Date(), time)
343	}
344}
345
346func normalizeExpected(ext, str string) string {
347	str = normalizeContent(str)
348	switch ext {
349	default:
350		return str
351	case "html":
352		return strings.Trim(helpers.StripHTML(str), " ")
353	case "ad":
354		paragraphs := strings.Split(str, "</p>")
355		expected := ""
356		for _, para := range paragraphs {
357			if para == "" {
358				continue
359			}
360			expected += fmt.Sprintf("<div class=\"paragraph\">\n%s</p></div>\n", para)
361		}
362
363		return expected
364	case "rst":
365		return fmt.Sprintf("<div class=\"document\">\n\n\n%s</div>", str)
366	}
367}
368
369func testAllMarkdownEnginesForPages(t *testing.T,
370	assertFunc func(t *testing.T, ext string, pages page.Pages), settings map[string]interface{}, pageSources ...string) {
371
372	engines := []struct {
373		ext           string
374		shouldExecute func() bool
375	}{
376		{"md", func() bool { return true }},
377		{"mmark", func() bool { return true }},
378		{"ad", func() bool { return asciidocext.Supports() }},
379		{"rst", func() bool { return true }},
380	}
381
382	for _, e := range engines {
383		if !e.shouldExecute() {
384			continue
385		}
386
387		t.Run(e.ext, func(t *testing.T) {
388
389			cfg, fs := newTestCfg(func(cfg config.Provider) error {
390				for k, v := range settings {
391					cfg.Set(k, v)
392				}
393				return nil
394			})
395
396			contentDir := "content"
397
398			if s := cfg.GetString("contentDir"); s != "" {
399				contentDir = s
400			}
401
402			cfg.Set("security", map[string]interface{}{
403				"exec": map[string]interface{}{
404					"allow": []string{"^python$", "^rst2html.*", "^asciidoctor$"},
405				},
406			})
407
408			var fileSourcePairs []string
409
410			for i, source := range pageSources {
411				fileSourcePairs = append(fileSourcePairs, fmt.Sprintf("p%d.%s", i, e.ext), source)
412			}
413
414			for i := 0; i < len(fileSourcePairs); i += 2 {
415				writeSource(t, fs, filepath.Join(contentDir, fileSourcePairs[i]), fileSourcePairs[i+1])
416			}
417
418			// Add a content page for the home page
419			homePath := fmt.Sprintf("_index.%s", e.ext)
420			writeSource(t, fs, filepath.Join(contentDir, homePath), homePage)
421
422			b := newTestSitesBuilderFromDepsCfg(t, deps.DepsCfg{Fs: fs, Cfg: cfg}).WithNothingAdded()
423			b.Build(BuildCfg{})
424
425			s := b.H.Sites[0]
426
427			b.Assert(len(s.RegularPages()), qt.Equals, len(pageSources))
428
429			assertFunc(t, e.ext, s.RegularPages())
430
431			home, err := s.Info.Home()
432			b.Assert(err, qt.IsNil)
433			b.Assert(home, qt.Not(qt.IsNil))
434			b.Assert(home.File().Path(), qt.Equals, homePath)
435			b.Assert(content(home), qt.Contains, "Home Page Content")
436
437		})
438
439	}
440}
441
442// Issue #1076
443func TestPageWithDelimiterForMarkdownThatCrossesBorder(t *testing.T) {
444	t.Parallel()
445	cfg, fs := newTestCfg()
446
447	c := qt.New(t)
448
449	writeSource(t, fs, filepath.Join("content", "simple.md"), simplePageWithSummaryDelimiterAndMarkdownThatCrossesBorder)
450
451	s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true})
452
453	c.Assert(len(s.RegularPages()), qt.Equals, 1)
454
455	p := s.RegularPages()[0]
456
457	if p.Summary() != template.HTML(
458		"<p>The <a href=\"http://gohugo.io/\">best static site generator</a>.<sup id=\"fnref:1\"><a href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\">1</a></sup></p>") {
459		t.Fatalf("Got summary:\n%q", p.Summary())
460	}
461
462	cnt := content(p)
463	if cnt != "<p>The <a href=\"http://gohugo.io/\">best static site generator</a>.<sup id=\"fnref:1\"><a href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\">1</a></sup></p>\n<section class=\"footnotes\" role=\"doc-endnotes\">\n<hr>\n<ol>\n<li id=\"fn:1\" role=\"doc-endnote\">\n<p>Many people say so.&#160;<a href=\"#fnref:1\" class=\"footnote-backref\" role=\"doc-backlink\">&#x21a9;&#xfe0e;</a></p>\n</li>\n</ol>\n</section>" {
464		t.Fatalf("Got content:\n%q", cnt)
465	}
466}
467
468func TestPageDatesAllKinds(t *testing.T) {
469	t.Parallel()
470
471	pageContent := `
472---
473title: Page
474date: 2017-01-15
475tags: ["hugo"]
476categories: ["cool stuff"]
477---
478`
479
480	b := newTestSitesBuilder(t)
481	b.WithSimpleConfigFile().WithContent("page.md", pageContent)
482	b.WithContent("blog/page.md", pageContent)
483
484	b.CreateSites().Build(BuildCfg{})
485
486	b.Assert(len(b.H.Sites), qt.Equals, 1)
487	s := b.H.Sites[0]
488
489	checkDate := func(t time.Time, msg string) {
490		b.Assert(t.Year(), qt.Equals, 2017, qt.Commentf(msg))
491	}
492
493	checkDated := func(d resource.Dated, msg string) {
494		checkDate(d.Date(), "date: "+msg)
495		checkDate(d.Lastmod(), "lastmod: "+msg)
496	}
497	for _, p := range s.Pages() {
498		checkDated(p, p.Kind())
499	}
500	checkDate(s.Info.LastChange(), "site")
501}
502
503func TestPageDatesSections(t *testing.T) {
504	t.Parallel()
505
506	b := newTestSitesBuilder(t)
507	b.WithSimpleConfigFile().WithContent("no-index/page.md", `
508---
509title: Page
510date: 2017-01-15
511---
512`, "with-index-no-date/_index.md", `---
513title: No Date
514---
515
516`,
517		// https://github.com/gohugoio/hugo/issues/5854
518		"with-index-date/_index.md", `---
519title: Date
520date: 2018-01-15
521---
522
523`, "with-index-date/p1.md", `---
524title: Date
525date: 2018-01-15
526---
527
528`, "with-index-date/p1.md", `---
529title: Date
530date: 2018-01-15
531---
532
533`)
534
535	for i := 1; i <= 20; i++ {
536		b.WithContent(fmt.Sprintf("main-section/p%d.md", i), `---
537title: Date
538date: 2012-01-12
539---
540
541`)
542	}
543
544	b.CreateSites().Build(BuildCfg{})
545
546	b.Assert(len(b.H.Sites), qt.Equals, 1)
547	s := b.H.Sites[0]
548
549	checkDate := func(p page.Page, year int) {
550		b.Assert(p.Date().Year(), qt.Equals, year)
551		b.Assert(p.Lastmod().Year(), qt.Equals, year)
552	}
553
554	checkDate(s.getPage("/"), 2018)
555	checkDate(s.getPage("/no-index"), 2017)
556	b.Assert(s.getPage("/with-index-no-date").Date().IsZero(), qt.Equals, true)
557	checkDate(s.getPage("/with-index-date"), 2018)
558
559	b.Assert(s.Site.LastChange().Year(), qt.Equals, 2018)
560}
561
562func TestCreateNewPage(t *testing.T) {
563	t.Parallel()
564	c := qt.New(t)
565	assertFunc := func(t *testing.T, ext string, pages page.Pages) {
566		p := pages[0]
567
568		// issue #2290: Path is relative to the content dir and will continue to be so.
569		c.Assert(p.File().Path(), qt.Equals, fmt.Sprintf("p0.%s", ext))
570		c.Assert(p.IsHome(), qt.Equals, false)
571		checkPageTitle(t, p, "Simple")
572		checkPageContent(t, p, normalizeExpected(ext, "<p>Simple Page</p>\n"))
573		checkPageSummary(t, p, "Simple Page")
574		checkPageType(t, p, "page")
575	}
576
577	settings := map[string]interface{}{
578		"contentDir": "mycontent",
579	}
580
581	testAllMarkdownEnginesForPages(t, assertFunc, settings, simplePage)
582}
583
584func TestPageSummary(t *testing.T) {
585	t.Parallel()
586	assertFunc := func(t *testing.T, ext string, pages page.Pages) {
587		p := pages[0]
588		checkPageTitle(t, p, "SimpleWithoutSummaryDelimiter")
589		// Source is not Asciidoctor- or RST-compatible so don't test them
590		if ext != "ad" && ext != "rst" {
591			checkPageContent(t, p, normalizeExpected(ext, "<p><a href=\"https://lipsum.com/\">Lorem ipsum</a> dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>\n\n<p>Additional text.</p>\n\n<p>Further text.</p>\n"), ext)
592			checkPageSummary(t, p, normalizeExpected(ext, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Additional text."), ext)
593		}
594		checkPageType(t, p, "page")
595	}
596
597	testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithoutSummaryDelimiter)
598}
599
600func TestPageWithDelimiter(t *testing.T) {
601	t.Parallel()
602	assertFunc := func(t *testing.T, ext string, pages page.Pages) {
603		p := pages[0]
604		checkPageTitle(t, p, "Simple")
605		checkPageContent(t, p, normalizeExpected(ext, "<p>Summary Next Line</p>\n\n<p>Some more text</p>\n"), ext)
606		checkPageSummary(t, p, normalizeExpected(ext, "<p>Summary Next Line</p>"), ext)
607		checkPageType(t, p, "page")
608	}
609
610	testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithSummaryDelimiter)
611}
612
613func TestPageWithSummaryParameter(t *testing.T) {
614	t.Parallel()
615	assertFunc := func(t *testing.T, ext string, pages page.Pages) {
616		p := pages[0]
617		checkPageTitle(t, p, "SimpleWithSummaryParameter")
618		checkPageContent(t, p, normalizeExpected(ext, "<p>Some text.</p>\n\n<p>Some more text.</p>\n"), ext)
619		// Summary is not Asciidoctor- or RST-compatible so don't test them
620		if ext != "ad" && ext != "rst" {
621			checkPageSummary(t, p, normalizeExpected(ext, "Page with summary parameter and <a href=\"http://www.example.com/\">a link</a>"), ext)
622		}
623		checkPageType(t, p, "page")
624	}
625
626	testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithSummaryParameter)
627}
628
629// Issue #3854
630// Also see https://github.com/gohugoio/hugo/issues/3977
631func TestPageWithDateFields(t *testing.T) {
632	c := qt.New(t)
633	pageWithDate := `---
634title: P%d
635weight: %d
636%s: 2017-10-13
637---
638Simple Page With Some Date`
639
640	hasDate := func(p page.Page) bool {
641		return p.Date().Year() == 2017
642	}
643
644	datePage := func(field string, weight int) string {
645		return fmt.Sprintf(pageWithDate, weight, weight, field)
646	}
647
648	t.Parallel()
649	assertFunc := func(t *testing.T, ext string, pages page.Pages) {
650		c.Assert(len(pages) > 0, qt.Equals, true)
651		for _, p := range pages {
652			c.Assert(hasDate(p), qt.Equals, true)
653		}
654	}
655
656	fields := []string{"date", "publishdate", "pubdate", "published"}
657	pageContents := make([]string, len(fields))
658	for i, field := range fields {
659		pageContents[i] = datePage(field, i+1)
660	}
661
662	testAllMarkdownEnginesForPages(t, assertFunc, nil, pageContents...)
663}
664
665// Issue #2601
666func TestPageRawContent(t *testing.T) {
667	t.Parallel()
668	cfg, fs := newTestCfg()
669	c := qt.New(t)
670
671	writeSource(t, fs, filepath.Join("content", "raw.md"), `---
672title: Raw
673---
674**Raw**`)
675
676	writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), `{{ .RawContent }}`)
677
678	s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true})
679
680	c.Assert(len(s.RegularPages()), qt.Equals, 1)
681	p := s.RegularPages()[0]
682
683	c.Assert("**Raw**", qt.Equals, p.RawContent())
684}
685
686func TestPageWithShortCodeInSummary(t *testing.T) {
687	t.Parallel()
688	assertFunc := func(t *testing.T, ext string, pages page.Pages) {
689		p := pages[0]
690		checkPageTitle(t, p, "Simple")
691		checkPageContent(t, p, normalizeExpected(ext, "<p>Summary Next Line. <figure><img src=\"/not/real\"/> </figure> . More text here.</p><p>Some more text</p>"))
692		checkPageSummary(t, p, "Summary Next Line.  . More text here. Some more text")
693		checkPageType(t, p, "page")
694	}
695
696	testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithShortcodeInSummary)
697}
698
699func TestPageWithAdditionalExtension(t *testing.T) {
700	t.Parallel()
701	cfg, fs := newTestCfg()
702	cfg.Set("markup", map[string]interface{}{
703		"defaultMarkdownHandler": "blackfriday", // TODO(bep)
704	})
705
706	c := qt.New(t)
707
708	writeSource(t, fs, filepath.Join("content", "simple.md"), simplePageWithAdditionalExtension)
709
710	s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true})
711
712	c.Assert(len(s.RegularPages()), qt.Equals, 1)
713
714	p := s.RegularPages()[0]
715
716	checkPageContent(t, p, "<p>first line.<br />\nsecond line.</p>\n\n<p>fourth line.</p>\n")
717}
718
719func TestTableOfContents(t *testing.T) {
720	cfg, fs := newTestCfg()
721	c := qt.New(t)
722
723	writeSource(t, fs, filepath.Join("content", "tocpage.md"), pageWithToC)
724
725	s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true})
726
727	c.Assert(len(s.RegularPages()), qt.Equals, 1)
728
729	p := s.RegularPages()[0]
730
731	checkPageContent(t, p, "<p>For some moments the old man did not reply. He stood with bowed head, buried in deep thought. But at last he spoke.</p><h2 id=\"aa\">AA</h2> <p>I have no idea, of course, how long it took me to reach the limit of the plain, but at last I entered the foothills, following a pretty little canyon upward toward the mountains. Beside me frolicked a laughing brooklet, hurrying upon its noisy way down to the silent sea. In its quieter pools I discovered many small fish, of four-or five-pound weight I should imagine. In appearance, except as to size and color, they were not unlike the whale of our own seas. As I watched them playing about I discovered, not only that they suckled their young, but that at intervals they rose to the surface to breathe as well as to feed upon certain grasses and a strange, scarlet lichen which grew upon the rocks just above the water line.</p><h3 id=\"aaa\">AAA</h3> <p>I remember I felt an extraordinary persuasion that I was being played with, that presently, when I was upon the very verge of safety, this mysterious death&ndash;as swift as the passage of light&ndash;would leap after me from the pit about the cylinder and strike me down. ## BB</p><h3 id=\"bbb\">BBB</h3> <p>&ldquo;You&rsquo;re a great Granser,&rdquo; he cried delightedly, &ldquo;always making believe them little marks mean something.&rdquo;</p>")
732	checkPageTOC(t, p, "<nav id=\"TableOfContents\">\n  <ul>\n    <li><a href=\"#aa\">AA</a>\n      <ul>\n        <li><a href=\"#aaa\">AAA</a></li>\n        <li><a href=\"#bbb\">BBB</a></li>\n      </ul>\n    </li>\n  </ul>\n</nav>")
733}
734
735func TestPageWithMoreTag(t *testing.T) {
736	t.Parallel()
737	assertFunc := func(t *testing.T, ext string, pages page.Pages) {
738		p := pages[0]
739		checkPageTitle(t, p, "Simple")
740		checkPageContent(t, p, normalizeExpected(ext, "<p>Summary Same Line</p>\n\n<p>Some more text</p>\n"))
741		checkPageSummary(t, p, normalizeExpected(ext, "<p>Summary Same Line</p>"))
742		checkPageType(t, p, "page")
743	}
744
745	testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithSummaryDelimiterSameLine)
746}
747
748// #2973
749func TestSummaryWithHTMLTagsOnNextLine(t *testing.T) {
750	assertFunc := func(t *testing.T, ext string, pages page.Pages) {
751		c := qt.New(t)
752		p := pages[0]
753		s := string(p.Summary())
754		c.Assert(s, qt.Contains, "Happy new year everyone!")
755		c.Assert(s, qt.Not(qt.Contains), "User interface")
756	}
757
758	testAllMarkdownEnginesForPages(t, assertFunc, nil, `---
759title: Simple
760---
761Happy new year everyone!
762
763Here is the last report for commits in the year 2016. It covers hrev50718-hrev50829.
764
765<!--more-->
766
767<h3>User interface</h3>
768
769`)
770}
771
772func TestPageWithDate(t *testing.T) {
773	t.Parallel()
774	cfg, fs := newTestCfg()
775	c := qt.New(t)
776
777	writeSource(t, fs, filepath.Join("content", "simple.md"), simplePageRFC3339Date)
778
779	s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true})
780
781	c.Assert(len(s.RegularPages()), qt.Equals, 1)
782
783	p := s.RegularPages()[0]
784	d, _ := time.Parse(time.RFC3339, "2013-05-17T16:59:30Z")
785
786	checkPageDate(t, p, d)
787}
788
789func TestPageWithLastmodFromGitInfo(t *testing.T) {
790	if htesting.IsCI() {
791		// TODO(bep) figure out why this fails on GitHub actions.
792		t.Skip("Skip GitInfo test on CI")
793	}
794	c := qt.New(t)
795
796	// We need to use the OS fs for this.
797	cfg := config.New()
798	fs := hugofs.NewFrom(hugofs.Os, cfg)
799	fs.Destination = &afero.MemMapFs{}
800
801	wd, err := os.Getwd()
802	c.Assert(err, qt.IsNil)
803
804	cfg.Set("frontmatter", map[string]interface{}{
805		"lastmod": []string{":git", "lastmod"},
806	})
807	cfg.Set("defaultContentLanguage", "en")
808
809	langConfig := map[string]interface{}{
810		"en": map[string]interface{}{
811			"weight":       1,
812			"languageName": "English",
813			"contentDir":   "content",
814		},
815		"nn": map[string]interface{}{
816			"weight":       2,
817			"languageName": "Nynorsk",
818			"contentDir":   "content_nn",
819		},
820	}
821
822	cfg.Set("languages", langConfig)
823	cfg.Set("enableGitInfo", true)
824
825	cfg.Set("workingDir", filepath.Join(wd, "testsite"))
826
827	b := newTestSitesBuilderFromDepsCfg(t, deps.DepsCfg{Fs: fs, Cfg: cfg}).WithNothingAdded()
828
829	b.Build(BuildCfg{SkipRender: true})
830	h := b.H
831
832	c.Assert(len(h.Sites), qt.Equals, 2)
833
834	enSite := h.Sites[0]
835	c.Assert(len(enSite.RegularPages()), qt.Equals, 1)
836
837	// 2018-03-11 is the Git author date for testsite/content/first-post.md
838	c.Assert(enSite.RegularPages()[0].Lastmod().Format("2006-01-02"), qt.Equals, "2018-03-11")
839
840	nnSite := h.Sites[1]
841	c.Assert(len(nnSite.RegularPages()), qt.Equals, 1)
842
843	// 2018-08-11 is the Git author date for testsite/content_nn/first-post.md
844	c.Assert(nnSite.RegularPages()[0].Lastmod().Format("2006-01-02"), qt.Equals, "2018-08-11")
845}
846
847func TestPageWithFrontMatterConfig(t *testing.T) {
848	for _, dateHandler := range []string{":filename", ":fileModTime"} {
849		dateHandler := dateHandler
850		t.Run(fmt.Sprintf("dateHandler=%q", dateHandler), func(t *testing.T) {
851			t.Parallel()
852			c := qt.New(t)
853			cfg, fs := newTestCfg()
854
855			pageTemplate := `
856---
857title: Page
858weight: %d
859lastMod: 2018-02-28
860%s
861---
862Content
863`
864
865			cfg.Set("frontmatter", map[string]interface{}{
866				"date": []string{dateHandler, "date"},
867			})
868
869			c1 := filepath.Join("content", "section", "2012-02-21-noslug.md")
870			c2 := filepath.Join("content", "section", "2012-02-22-slug.md")
871
872			writeSource(t, fs, c1, fmt.Sprintf(pageTemplate, 1, ""))
873			writeSource(t, fs, c2, fmt.Sprintf(pageTemplate, 2, "slug: aslug"))
874
875			c1fi, err := fs.Source.Stat(c1)
876			c.Assert(err, qt.IsNil)
877			c2fi, err := fs.Source.Stat(c2)
878			c.Assert(err, qt.IsNil)
879
880			b := newTestSitesBuilderFromDepsCfg(t, deps.DepsCfg{Fs: fs, Cfg: cfg}).WithNothingAdded()
881			b.Build(BuildCfg{SkipRender: true})
882
883			s := b.H.Sites[0]
884			c.Assert(len(s.RegularPages()), qt.Equals, 2)
885
886			noSlug := s.RegularPages()[0]
887			slug := s.RegularPages()[1]
888
889			c.Assert(noSlug.Lastmod().Day(), qt.Equals, 28)
890
891			switch strings.ToLower(dateHandler) {
892			case ":filename":
893				c.Assert(noSlug.Date().IsZero(), qt.Equals, false)
894				c.Assert(slug.Date().IsZero(), qt.Equals, false)
895				c.Assert(noSlug.Date().Year(), qt.Equals, 2012)
896				c.Assert(slug.Date().Year(), qt.Equals, 2012)
897				c.Assert(noSlug.Slug(), qt.Equals, "noslug")
898				c.Assert(slug.Slug(), qt.Equals, "aslug")
899			case ":filemodtime":
900				c.Assert(noSlug.Date().Year(), qt.Equals, c1fi.ModTime().Year())
901				c.Assert(slug.Date().Year(), qt.Equals, c2fi.ModTime().Year())
902				fallthrough
903			default:
904				c.Assert(noSlug.Slug(), qt.Equals, "")
905				c.Assert(slug.Slug(), qt.Equals, "aslug")
906
907			}
908		})
909	}
910}
911
912func TestWordCountWithAllCJKRunesWithoutHasCJKLanguage(t *testing.T) {
913	t.Parallel()
914	assertFunc := func(t *testing.T, ext string, pages page.Pages) {
915		p := pages[0]
916		if p.WordCount() != 8 {
917			t.Fatalf("[%s] incorrect word count. expected %v, got %v", ext, 8, p.WordCount())
918		}
919	}
920
921	testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithAllCJKRunes)
922}
923
924func TestWordCountWithAllCJKRunesHasCJKLanguage(t *testing.T) {
925	t.Parallel()
926	settings := map[string]interface{}{"hasCJKLanguage": true}
927
928	assertFunc := func(t *testing.T, ext string, pages page.Pages) {
929		p := pages[0]
930		if p.WordCount() != 15 {
931			t.Fatalf("[%s] incorrect word count, expected %v, got %v", ext, 15, p.WordCount())
932		}
933	}
934	testAllMarkdownEnginesForPages(t, assertFunc, settings, simplePageWithAllCJKRunes)
935}
936
937func TestWordCountWithMainEnglishWithCJKRunes(t *testing.T) {
938	t.Parallel()
939	settings := map[string]interface{}{"hasCJKLanguage": true}
940
941	assertFunc := func(t *testing.T, ext string, pages page.Pages) {
942		p := pages[0]
943		if p.WordCount() != 74 {
944			t.Fatalf("[%s] incorrect word count, expected %v, got %v", ext, 74, p.WordCount())
945		}
946
947		if p.Summary() != simplePageWithMainEnglishWithCJKRunesSummary {
948			t.Fatalf("[%s] incorrect Summary for content '%s'. expected %v, got %v", ext, p.Plain(),
949				simplePageWithMainEnglishWithCJKRunesSummary, p.Summary())
950		}
951	}
952
953	testAllMarkdownEnginesForPages(t, assertFunc, settings, simplePageWithMainEnglishWithCJKRunes)
954}
955
956func TestWordCountWithIsCJKLanguageFalse(t *testing.T) {
957	t.Parallel()
958	settings := map[string]interface{}{
959		"hasCJKLanguage": true,
960	}
961
962	assertFunc := func(t *testing.T, ext string, pages page.Pages) {
963		p := pages[0]
964		if p.WordCount() != 75 {
965			t.Fatalf("[%s] incorrect word count for content '%s'. expected %v, got %v", ext, p.Plain(), 74, p.WordCount())
966		}
967
968		if p.Summary() != simplePageWithIsCJKLanguageFalseSummary {
969			t.Fatalf("[%s] incorrect Summary for content '%s'. expected %v, got %v", ext, p.Plain(),
970				simplePageWithIsCJKLanguageFalseSummary, p.Summary())
971		}
972	}
973
974	testAllMarkdownEnginesForPages(t, assertFunc, settings, simplePageWithIsCJKLanguageFalse)
975}
976
977func TestWordCount(t *testing.T) {
978	t.Parallel()
979	assertFunc := func(t *testing.T, ext string, pages page.Pages) {
980		p := pages[0]
981		if p.WordCount() != 483 {
982			t.Fatalf("[%s] incorrect word count. expected %v, got %v", ext, 483, p.WordCount())
983		}
984
985		if p.FuzzyWordCount() != 500 {
986			t.Fatalf("[%s] incorrect word count. expected %v, got %v", ext, 500, p.FuzzyWordCount())
987		}
988
989		if p.ReadingTime() != 3 {
990			t.Fatalf("[%s] incorrect min read. expected %v, got %v", ext, 3, p.ReadingTime())
991		}
992	}
993
994	testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithLongContent)
995}
996
997func TestPagePaths(t *testing.T) {
998	t.Parallel()
999	c := qt.New(t)
1000
1001	siteParmalinksSetting := map[string]string{
1002		"post": ":year/:month/:day/:title/",
1003	}
1004
1005	tests := []struct {
1006		content      string
1007		path         string
1008		hasPermalink bool
1009		expected     string
1010	}{
1011		{simplePage, "post/x.md", false, "post/x.html"},
1012		{simplePageWithURL, "post/x.md", false, "simple/url/index.html"},
1013		{simplePageWithSlug, "post/x.md", false, "post/simple-slug.html"},
1014		{simplePageWithDate, "post/x.md", true, "2013/10/15/simple/index.html"},
1015		{UTF8Page, "post/x.md", false, "post/x.html"},
1016		{UTF8PageWithURL, "post/x.md", false, "ラーメン/url/index.html"},
1017		{UTF8PageWithSlug, "post/x.md", false, "post/ラーメン-slug.html"},
1018		{UTF8PageWithDate, "post/x.md", true, "2013/10/15/ラーメン/index.html"},
1019	}
1020
1021	for _, test := range tests {
1022		cfg, fs := newTestCfg()
1023
1024		if test.hasPermalink {
1025			cfg.Set("permalinks", siteParmalinksSetting)
1026		}
1027
1028		writeSource(t, fs, filepath.Join("content", filepath.FromSlash(test.path)), test.content)
1029
1030		s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true})
1031		c.Assert(len(s.RegularPages()), qt.Equals, 1)
1032
1033	}
1034}
1035
1036func TestTranslationKey(t *testing.T) {
1037	t.Parallel()
1038	c := qt.New(t)
1039	cfg, fs := newTestCfg()
1040
1041	writeSource(t, fs, filepath.Join("content", filepath.FromSlash("sect/simple.no.md")), "---\ntitle: \"A1\"\ntranslationKey: \"k1\"\n---\nContent\n")
1042	writeSource(t, fs, filepath.Join("content", filepath.FromSlash("sect/simple.en.md")), "---\ntitle: \"A2\"\n---\nContent\n")
1043
1044	s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true})
1045
1046	c.Assert(len(s.RegularPages()), qt.Equals, 2)
1047
1048	home, _ := s.Info.Home()
1049	c.Assert(home, qt.Not(qt.IsNil))
1050	c.Assert(home.TranslationKey(), qt.Equals, "home")
1051	c.Assert(s.RegularPages()[0].TranslationKey(), qt.Equals, "page/k1")
1052	p2 := s.RegularPages()[1]
1053
1054	c.Assert(p2.TranslationKey(), qt.Equals, "page/sect/simple")
1055}
1056
1057func TestChompBOM(t *testing.T) {
1058	t.Parallel()
1059	c := qt.New(t)
1060	const utf8BOM = "\xef\xbb\xbf"
1061
1062	cfg, fs := newTestCfg()
1063
1064	writeSource(t, fs, filepath.Join("content", "simple.md"), utf8BOM+simplePage)
1065
1066	s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true})
1067
1068	c.Assert(len(s.RegularPages()), qt.Equals, 1)
1069
1070	p := s.RegularPages()[0]
1071
1072	checkPageTitle(t, p, "Simple")
1073}
1074
1075func TestPageWithEmoji(t *testing.T) {
1076	for _, enableEmoji := range []bool{true, false} {
1077		v := config.New()
1078		v.Set("enableEmoji", enableEmoji)
1079
1080		b := newTestSitesBuilder(t).WithViper(v)
1081
1082		b.WithContent("page-emoji.md", `---
1083title: "Hugo Smile"
1084---
1085This is a :smile:.
1086<!--more-->
1087
1088Another :smile: This is :not: :an: :emoji:.
1089
1090O :christmas_tree:
1091
1092Write me an :e-mail: or :email:?
1093
1094Too many colons: :: ::: :::: :?: :!: :.:
1095
1096If you dislike this video, you can hit that :-1: button :stuck_out_tongue_winking_eye:,
1097but if you like it, hit :+1: and get subscribed!
1098`)
1099
1100		b.CreateSites().Build(BuildCfg{})
1101
1102		if enableEmoji {
1103			b.AssertFileContent("public/page-emoji/index.html",
1104				"This is a ��",
1105				"Another ��",
1106				"This is :not: :an: :emoji:.",
1107				"O ��",
1108				"Write me an �� or ✉️?",
1109				"Too many colons: :: ::: :::: :?: :!: :.:",
1110				"you can hit that �� button ��,",
1111				"hit �� and get subscribed!",
1112			)
1113		} else {
1114			b.AssertFileContent("public/page-emoji/index.html",
1115				"This is a :smile:",
1116				"Another :smile:",
1117				"This is :not: :an: :emoji:.",
1118				"O :christmas_tree:",
1119				"Write me an :e-mail: or :email:?",
1120				"Too many colons: :: ::: :::: :?: :!: :.:",
1121				"you can hit that :-1: button :stuck_out_tongue_winking_eye:,",
1122				"hit :+1: and get subscribed!",
1123			)
1124		}
1125
1126	}
1127}
1128
1129func TestPageHTMLContent(t *testing.T) {
1130	b := newTestSitesBuilder(t)
1131	b.WithSimpleConfigFile()
1132
1133	frontmatter := `---
1134title: "HTML Content"
1135---
1136`
1137	b.WithContent("regular.html", frontmatter+`<h1>Hugo</h1>`)
1138	b.WithContent("noblackfridayforyou.html", frontmatter+`**Hugo!**`)
1139	b.WithContent("manualsummary.html", frontmatter+`
1140<p>This is summary</p>
1141<!--more-->
1142<p>This is the main content.</p>`)
1143
1144	b.Build(BuildCfg{})
1145
1146	b.AssertFileContent(
1147		"public/regular/index.html",
1148		"Single: HTML Content|Hello|en|RelPermalink: /regular/|",
1149		"Summary: Hugo|Truncated: false")
1150
1151	b.AssertFileContent(
1152		"public/noblackfridayforyou/index.html",
1153		"Permalink: http://example.com/noblackfridayforyou/|**Hugo!**|",
1154	)
1155
1156	// https://github.com/gohugoio/hugo/issues/5723
1157	b.AssertFileContent(
1158		"public/manualsummary/index.html",
1159		"Single: HTML Content|Hello|en|RelPermalink: /manualsummary/|",
1160		"Summary: \n<p>This is summary</p>\n|Truncated: true",
1161		"|<p>This is the main content.</p>|",
1162	)
1163}
1164
1165// https://github.com/gohugoio/hugo/issues/5381
1166func TestPageManualSummary(t *testing.T) {
1167	b := newTestSitesBuilder(t)
1168	b.WithSimpleConfigFile()
1169
1170	b.WithContent("page-md-shortcode.md", `---
1171title: "Hugo"
1172---
1173This is a {{< sc >}}.
1174<!--more-->
1175Content.
1176`)
1177
1178	// https://github.com/gohugoio/hugo/issues/5464
1179	b.WithContent("page-md-only-shortcode.md", `---
1180title: "Hugo"
1181---
1182{{< sc >}}
1183<!--more-->
1184{{< sc >}}
1185`)
1186
1187	b.WithContent("page-md-shortcode-same-line.md", `---
1188title: "Hugo"
1189---
1190This is a {{< sc >}}<!--more-->Same line.
1191`)
1192
1193	b.WithContent("page-md-shortcode-same-line-after.md", `---
1194title: "Hugo"
1195---
1196Summary<!--more-->{{< sc >}}
1197`)
1198
1199	b.WithContent("page-org-shortcode.org", `#+TITLE: T1
1200#+AUTHOR: A1
1201#+DESCRIPTION: D1
1202This is a {{< sc >}}.
1203# more
1204Content.
1205`)
1206
1207	b.WithContent("page-org-variant1.org", `#+TITLE: T1
1208Summary.
1209
1210# more
1211
1212Content.
1213`)
1214
1215	b.WithTemplatesAdded("layouts/shortcodes/sc.html", "a shortcode")
1216	b.WithTemplatesAdded("layouts/_default/single.html", `
1217SUMMARY:{{ .Summary }}:END
1218--------------------------
1219CONTENT:{{ .Content }}
1220`)
1221
1222	b.CreateSites().Build(BuildCfg{})
1223
1224	b.AssertFileContent("public/page-md-shortcode/index.html",
1225		"SUMMARY:<p>This is a a shortcode.</p>:END",
1226		"CONTENT:<p>This is a a shortcode.</p>\n\n<p>Content.</p>\n",
1227	)
1228
1229	b.AssertFileContent("public/page-md-shortcode-same-line/index.html",
1230		"SUMMARY:<p>This is a a shortcode</p>:END",
1231		"CONTENT:<p>This is a a shortcode</p>\n\n<p>Same line.</p>\n",
1232	)
1233
1234	b.AssertFileContent("public/page-md-shortcode-same-line-after/index.html",
1235		"SUMMARY:<p>Summary</p>:END",
1236		"CONTENT:<p>Summary</p>\n\na shortcode",
1237	)
1238
1239	b.AssertFileContent("public/page-org-shortcode/index.html",
1240		"SUMMARY:<p>\nThis is a a shortcode.\n</p>:END",
1241		"CONTENT:<p>\nThis is a a shortcode.\n</p>\n<p>\nContent.\t\n</p>\n",
1242	)
1243	b.AssertFileContent("public/page-org-variant1/index.html",
1244		"SUMMARY:<p>\nSummary.\n</p>:END",
1245		"CONTENT:<p>\nSummary.\n</p>\n<p>\nContent.\t\n</p>\n",
1246	)
1247
1248	b.AssertFileContent("public/page-md-only-shortcode/index.html",
1249		"SUMMARY:a shortcode:END",
1250		"CONTENT:a shortcode\n\na shortcode\n",
1251	)
1252}
1253
1254// https://github.com/gohugoio/hugo/issues/5478
1255func TestPageWithCommentedOutFrontMatter(t *testing.T) {
1256	b := newTestSitesBuilder(t)
1257	b.WithSimpleConfigFile()
1258
1259	b.WithContent("page.md", `<!--
1260+++
1261title = "hello"
1262+++
1263-->
1264This is the content.
1265`)
1266
1267	b.WithTemplatesAdded("layouts/_default/single.html", `
1268Title: {{ .Title }}
1269Content:{{ .Content }}
1270`)
1271
1272	b.CreateSites().Build(BuildCfg{})
1273
1274	b.AssertFileContent("public/page/index.html",
1275		"Title: hello",
1276		"Content:<p>This is the content.</p>",
1277	)
1278}
1279
1280// https://github.com/gohugoio/hugo/issues/5781
1281func TestPageWithZeroFile(t *testing.T) {
1282	newTestSitesBuilder(t).WithLogger(loggers.NewWarningLogger()).WithSimpleConfigFile().
1283		WithTemplatesAdded("index.html", "{{ .File.Filename }}{{ with .File }}{{ .Dir }}{{ end }}").Build(BuildCfg{})
1284}
1285
1286func TestHomePageWithNoTitle(t *testing.T) {
1287	b := newTestSitesBuilder(t).WithConfigFile("toml", `
1288title = "Site Title"
1289`)
1290	b.WithTemplatesAdded("index.html", "Title|{{ with .Title }}{{ . }}{{ end }}|")
1291	b.WithContent("_index.md", `---
1292description: "No title for you!"
1293---
1294
1295Content.
1296`)
1297
1298	b.Build(BuildCfg{})
1299	b.AssertFileContent("public/index.html", "Title||")
1300}
1301
1302func TestShouldBuild(t *testing.T) {
1303	t.Parallel()
1304	past := time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
1305	future := time.Date(2037, 11, 17, 20, 34, 58, 651387237, time.UTC)
1306	zero := time.Time{}
1307
1308	publishSettings := []struct {
1309		buildFuture  bool
1310		buildExpired bool
1311		buildDrafts  bool
1312		draft        bool
1313		publishDate  time.Time
1314		expiryDate   time.Time
1315		out          bool
1316	}{
1317		// publishDate and expiryDate
1318		{false, false, false, false, zero, zero, true},
1319		{false, false, false, false, zero, future, true},
1320		{false, false, false, false, past, zero, true},
1321		{false, false, false, false, past, future, true},
1322		{false, false, false, false, past, past, false},
1323		{false, false, false, false, future, future, false},
1324		{false, false, false, false, future, past, false},
1325
1326		// buildFuture and buildExpired
1327		{false, true, false, false, past, past, true},
1328		{true, true, false, false, past, past, true},
1329		{true, false, false, false, past, past, false},
1330		{true, false, false, false, future, future, true},
1331		{true, true, false, false, future, future, true},
1332		{false, true, false, false, future, past, false},
1333
1334		// buildDrafts and draft
1335		{true, true, false, true, past, future, false},
1336		{true, true, true, true, past, future, true},
1337		{true, true, true, true, past, future, true},
1338	}
1339
1340	for _, ps := range publishSettings {
1341		s := shouldBuild(ps.buildFuture, ps.buildExpired, ps.buildDrafts, ps.draft,
1342			ps.publishDate, ps.expiryDate)
1343		if s != ps.out {
1344			t.Errorf("AssertShouldBuild unexpected output with params: %+v", ps)
1345		}
1346	}
1347}
1348
1349// "dot" in path: #1885 and #2110
1350// disablePathToLower regression: #3374
1351func TestPathIssues(t *testing.T) {
1352	for _, disablePathToLower := range []bool{false, true} {
1353		for _, uglyURLs := range []bool{false, true} {
1354			disablePathToLower := disablePathToLower
1355			uglyURLs := uglyURLs
1356			t.Run(fmt.Sprintf("disablePathToLower=%t,uglyURLs=%t", disablePathToLower, uglyURLs), func(t *testing.T) {
1357				t.Parallel()
1358				cfg, fs := newTestCfg()
1359				th := newTestHelper(cfg, fs, t)
1360				c := qt.New(t)
1361
1362				cfg.Set("permalinks", map[string]string{
1363					"post": ":section/:title",
1364				})
1365
1366				cfg.Set("uglyURLs", uglyURLs)
1367				cfg.Set("disablePathToLower", disablePathToLower)
1368				cfg.Set("paginate", 1)
1369
1370				writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), "<html><body>{{.Content}}</body></html>")
1371				writeSource(t, fs, filepath.Join("layouts", "_default", "list.html"),
1372					"<html><body>P{{.Paginator.PageNumber}}|URL: {{.Paginator.URL}}|{{ if .Paginator.HasNext }}Next: {{.Paginator.Next.URL }}{{ end }}</body></html>")
1373
1374				for i := 0; i < 3; i++ {
1375					writeSource(t, fs, filepath.Join("content", "post", fmt.Sprintf("doc%d.md", i)),
1376						fmt.Sprintf(`---
1377title: "test%d.dot"
1378tags:
1379- ".net"
1380---
1381# doc1
1382*some content*`, i))
1383				}
1384
1385				writeSource(t, fs, filepath.Join("content", "Blog", "Blog1.md"),
1386					fmt.Sprintf(`---
1387title: "testBlog"
1388tags:
1389- "Blog"
1390---
1391# doc1
1392*some blog content*`))
1393
1394				s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{})
1395
1396				c.Assert(len(s.RegularPages()), qt.Equals, 4)
1397
1398				pathFunc := func(s string) string {
1399					if uglyURLs {
1400						return strings.Replace(s, "/index.html", ".html", 1)
1401					}
1402					return s
1403				}
1404
1405				blog := "blog"
1406
1407				if disablePathToLower {
1408					blog = "Blog"
1409				}
1410
1411				th.assertFileContent(pathFunc("public/"+blog+"/"+blog+"1/index.html"), "some blog content")
1412
1413				th.assertFileContent(pathFunc("public/post/test0.dot/index.html"), "some content")
1414
1415				if uglyURLs {
1416					th.assertFileContent("public/post/page/1.html", `canonical" href="/post.html"/`)
1417					th.assertFileContent("public/post.html", `<body>P1|URL: /post.html|Next: /post/page/2.html</body>`)
1418					th.assertFileContent("public/post/page/2.html", `<body>P2|URL: /post/page/2.html|Next: /post/page/3.html</body>`)
1419				} else {
1420					th.assertFileContent("public/post/page/1/index.html", `canonical" href="/post/"/`)
1421					th.assertFileContent("public/post/index.html", `<body>P1|URL: /post/|Next: /post/page/2/</body>`)
1422					th.assertFileContent("public/post/page/2/index.html", `<body>P2|URL: /post/page/2/|Next: /post/page/3/</body>`)
1423					th.assertFileContent("public/tags/.net/index.html", `<body>P1|URL: /tags/.net/|Next: /tags/.net/page/2/</body>`)
1424
1425				}
1426
1427				p := s.RegularPages()[0]
1428				if uglyURLs {
1429					c.Assert(p.RelPermalink(), qt.Equals, "/post/test0.dot.html")
1430				} else {
1431					c.Assert(p.RelPermalink(), qt.Equals, "/post/test0.dot/")
1432				}
1433			})
1434		}
1435	}
1436}
1437
1438// https://github.com/gohugoio/hugo/issues/4675
1439func TestWordCountAndSimilarVsSummary(t *testing.T) {
1440	t.Parallel()
1441	c := qt.New(t)
1442
1443	single := []string{"_default/single.html", `
1444WordCount: {{ .WordCount }}
1445FuzzyWordCount: {{ .FuzzyWordCount }}
1446ReadingTime: {{ .ReadingTime }}
1447Len Plain: {{ len .Plain }}
1448Len PlainWords: {{ len .PlainWords }}
1449Truncated: {{ .Truncated }}
1450Len Summary: {{ len .Summary }}
1451Len Content: {{ len .Content }}
1452
1453SUMMARY:{{ .Summary }}:{{ len .Summary }}:END
1454`}
1455
1456	b := newTestSitesBuilder(t)
1457	b.WithSimpleConfigFile().WithTemplatesAdded(single...).WithContent("p1.md", fmt.Sprintf(`---
1458title: p1
1459---
1460
1461%s
1462
1463`, strings.Repeat("word ", 510)),
1464
1465		"p2.md", fmt.Sprintf(`---
1466title: p2
1467---
1468This is a summary.
1469
1470<!--more-->
1471
1472%s
1473
1474`, strings.Repeat("word ", 310)),
1475		"p3.md", fmt.Sprintf(`---
1476title: p3
1477isCJKLanguage: true
1478---
1479Summary: In Chinese, 好 means good.
1480
1481<!--more-->
1482
1483%s
1484
1485`, strings.Repeat("好", 200)),
1486		"p4.md", fmt.Sprintf(`---
1487title: p4
1488isCJKLanguage: false
1489---
1490Summary: In Chinese, 好 means good.
1491
1492<!--more-->
1493
1494%s
1495
1496`, strings.Repeat("好", 200)),
1497
1498		"p5.md", fmt.Sprintf(`---
1499title: p4
1500isCJKLanguage: true
1501---
1502Summary: In Chinese, 好 means good.
1503
1504%s
1505
1506`, strings.Repeat("好", 200)),
1507		"p6.md", fmt.Sprintf(`---
1508title: p4
1509isCJKLanguage: false
1510---
1511Summary: In Chinese, 好 means good.
1512
1513%s
1514
1515`, strings.Repeat("好", 200)),
1516	)
1517
1518	b.CreateSites().Build(BuildCfg{})
1519
1520	c.Assert(len(b.H.Sites), qt.Equals, 1)
1521	c.Assert(len(b.H.Sites[0].RegularPages()), qt.Equals, 6)
1522
1523	b.AssertFileContent("public/p1/index.html", "WordCount: 510\nFuzzyWordCount: 600\nReadingTime: 3\nLen Plain: 2550\nLen PlainWords: 510\nTruncated: false\nLen Summary: 2549\nLen Content: 2557")
1524
1525	b.AssertFileContent("public/p2/index.html", "WordCount: 314\nFuzzyWordCount: 400\nReadingTime: 2\nLen Plain: 1569\nLen PlainWords: 314\nTruncated: true\nLen Summary: 25\nLen Content: 1582")
1526
1527	b.AssertFileContent("public/p3/index.html", "WordCount: 206\nFuzzyWordCount: 300\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: true\nLen Summary: 43\nLen Content: 651")
1528	b.AssertFileContent("public/p4/index.html", "WordCount: 7\nFuzzyWordCount: 100\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: true\nLen Summary: 43\nLen Content: 651")
1529	b.AssertFileContent("public/p5/index.html", "WordCount: 206\nFuzzyWordCount: 300\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: true\nLen Summary: 229\nLen Content: 652")
1530	b.AssertFileContent("public/p6/index.html", "WordCount: 7\nFuzzyWordCount: 100\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: false\nLen Summary: 637\nLen Content: 652")
1531}
1532
1533func TestScratchSite(t *testing.T) {
1534	t.Parallel()
1535
1536	b := newTestSitesBuilder(t)
1537	b.WithSimpleConfigFile().WithTemplatesAdded("index.html", `
1538{{ .Scratch.Set "b" "bv" }}
1539B: {{ .Scratch.Get "b" }}
1540`,
1541		"shortcodes/scratch.html", `
1542{{ .Scratch.Set "c" "cv" }}
1543C: {{ .Scratch.Get "c" }}
1544`,
1545	)
1546
1547	b.WithContentAdded("scratchme.md", `
1548---
1549title: Scratch Me!
1550---
1551
1552{{< scratch >}}
1553`)
1554	b.Build(BuildCfg{})
1555
1556	b.AssertFileContent("public/index.html", "B: bv")
1557	b.AssertFileContent("public/scratchme/index.html", "C: cv")
1558}
1559
1560func TestPageParam(t *testing.T) {
1561	t.Parallel()
1562
1563	b := newTestSitesBuilder(t).WithConfigFile("toml", `
1564
1565baseURL = "https://example.org"
1566
1567[params]
1568[params.author]
1569  name = "Kurt Vonnegut"
1570
1571`)
1572	b.WithTemplatesAdded("index.html", `
1573
1574{{ $withParam := .Site.GetPage "withparam" }}
1575{{ $noParam := .Site.GetPage "noparam" }}
1576{{ $withStringParam := .Site.GetPage "withstringparam" }}
1577
1578Author page: {{ $withParam.Param "author.name" }}
1579Author name page string: {{ $withStringParam.Param "author.name" }}|
1580Author page string: {{ $withStringParam.Param "author" }}|
1581Author site config:  {{ $noParam.Param "author.name" }}
1582
1583`,
1584	)
1585
1586	b.WithContent("withparam.md", `
1587+++
1588title = "With Param!"
1589[author]
1590  name = "Ernest Miller Hemingway"
1591
1592+++
1593
1594`,
1595
1596		"noparam.md", `
1597---
1598title: "No Param!"
1599---
1600`, "withstringparam.md", `
1601+++
1602title = "With string Param!"
1603author = "Jo Nesbø"
1604
1605+++
1606
1607`)
1608	b.Build(BuildCfg{})
1609
1610	b.AssertFileContent("public/index.html",
1611		"Author page: Ernest Miller Hemingway",
1612		"Author name page string: Kurt Vonnegut|",
1613		"Author page string: Jo Nesbø|",
1614		"Author site config:  Kurt Vonnegut")
1615}
1616
1617func TestGoldmark(t *testing.T) {
1618	t.Parallel()
1619
1620	b := newTestSitesBuilder(t).WithConfigFile("toml", `
1621baseURL = "https://example.org"
1622
1623[markup]
1624defaultMarkdownHandler="goldmark"
1625[markup.goldmark]
1626[markup.goldmark.renderer]
1627unsafe = false
1628[markup.highlight]
1629noClasses=false
1630
1631
1632`)
1633	b.WithTemplatesAdded("_default/single.html", `
1634Title: {{ .Title }}
1635ToC: {{ .TableOfContents }}
1636Content: {{ .Content }}
1637
1638`, "shortcodes/t.html", `T-SHORT`, "shortcodes/s.html", `## Code
1639{{ .Inner }}
1640`)
1641
1642	content := `
1643+++
1644title = "A Page!"
1645+++
1646
1647## Shortcode {{% t %}} in header
1648
1649## Code Fense in Shortcode
1650
1651{{% s %}}
1652$$$bash {hl_lines=[1]}
1653SHORT
1654$$$
1655{{% /s %}}
1656
1657## Code Fence
1658
1659$$$bash {hl_lines=[1]}
1660MARKDOWN
1661$$$
1662
1663Link with URL as text
1664
1665[https://google.com](https://google.com)
1666
1667
1668`
1669	content = strings.ReplaceAll(content, "$$$", "```")
1670
1671	b.WithContent("page.md", content)
1672
1673	b.Build(BuildCfg{})
1674
1675	b.AssertFileContent("public/page/index.html",
1676		`<nav id="TableOfContents">
1677<li><a href="#shortcode-t-short-in-header">Shortcode T-SHORT in header</a></li>
1678<code class="language-bash" data-lang="bash"><span class="hl">SHORT
1679<code class="language-bash" data-lang="bash"><span class="hl">MARKDOWN
1680<p><a href="https://google.com">https://google.com</a></p>
1681`)
1682}
1683
1684func TestBlackfridayDefault(t *testing.T) {
1685	t.Parallel()
1686
1687	b := newTestSitesBuilder(t).WithConfigFile("toml", `
1688baseURL = "https://example.org"
1689
1690[markup]
1691defaultMarkdownHandler="blackfriday"
1692[markup.highlight]
1693noClasses=false
1694[markup.goldmark]
1695[markup.goldmark.renderer]
1696unsafe=true
1697
1698
1699`)
1700	// Use the new attribute syntax to make sure it's not Goldmark.
1701	b.WithTemplatesAdded("_default/single.html", `
1702Title: {{ .Title }}
1703Content: {{ .Content }}
1704
1705`, "shortcodes/s.html", `## Code
1706{{ .Inner }}
1707`)
1708
1709	content := `
1710+++
1711title = "A Page!"
1712+++
1713
1714
1715## Code Fense in Shortcode
1716
1717{{% s %}}
1718S:
1719{{% s %}}
1720$$$bash {hl_lines=[1]}
1721SHORT
1722$$$
1723{{% /s %}}
1724{{% /s %}}
1725
1726## Code Fence
1727
1728$$$bash {hl_lines=[1]}
1729MARKDOWN
1730$$$
1731
1732`
1733	content = strings.ReplaceAll(content, "$$$", "```")
1734
1735	for i, ext := range []string{"md", "html"} {
1736		b.WithContent(fmt.Sprintf("page%d.%s", i+1, ext), content)
1737	}
1738
1739	b.Build(BuildCfg{})
1740
1741	// Blackfriday does not support this extended attribute syntax.
1742	b.AssertFileContent("public/page1/index.html",
1743		`<pre tabindex="0"><code class="language-bash {hl_lines=[1]}" data-lang="bash {hl_lines=[1]}">SHORT</code></pre>`,
1744		`<pre tabindex="0"><code class="language-bash {hl_lines=[1]}" data-lang="bash {hl_lines=[1]}">MARKDOWN`,
1745	)
1746
1747	b.AssertFileContent("public/page2/index.html",
1748		`<pre tabindex="0"><code class="language-bash {hl_lines=[1]}" data-lang="bash {hl_lines=[1]}">SHORT`,
1749	)
1750}
1751
1752func TestPageCaseIssues(t *testing.T) {
1753	t.Parallel()
1754
1755	b := newTestSitesBuilder(t)
1756	b.WithConfigFile("toml", `defaultContentLanguage = "no"
1757[languages]
1758[languages.NO]
1759title = "Norsk"
1760`)
1761	b.WithContent("a/B/C/Page1.md", "---\ntitle: Page1\n---")
1762	b.WithTemplates("index.html", `
1763{{ $p1 := site.GetPage "a/B/C/Page1" }}
1764Lang: {{ .Lang }}
1765Page1: {{ $p1.Path }}
1766`)
1767
1768	b.Build(BuildCfg{})
1769
1770	b.AssertFileContent("public/index.html", "Lang: no", filepath.FromSlash("Page1: a/B/C/Page1.md"))
1771}
1772