Make Page an interface

The main motivation of this commit is to add a `page.Page` interface to replace the very file-oriented `hugolib.Page` struct.
This is all a preparation step for issue  #5074, "pages from other data sources".

But this also fixes a set of annoying limitations, especially related to custom output formats, and shortcodes.

Most notable changes:

* The inner content of shortcodes using the `{{%` as the outer-most delimiter will now be sent to the content renderer, e.g. Blackfriday.
  This means that any markdown will partake in the global ToC and footnote context etc.
* The Custom Output formats are now "fully virtualized". This removes many of the current limitations.
* The taxonomy list type now has a reference to the `Page` object.
  This improves the taxonomy template `.Title` situation and make common template constructs much simpler.

See #5074
Fixes #5763
Fixes #5758
Fixes #5090
Fixes #5204
Fixes #4695
Fixes #5607
Fixes #5707
Fixes #5719
Fixes #3113
Fixes #5706
Fixes #5767
Fixes #5723
Fixes #5769
Fixes #5770
Fixes #5771
Fixes #5759
Fixes #5776
Fixes #5777
Fixes #5778
This commit is contained in:
Bjørn Erik Pedersen 2019-01-02 12:33:26 +01:00
parent 44f5c1c14c
commit 597e418cb0
No known key found for this signature in database
GPG key ID: 330E6E2BD4859D8F
206 changed files with 14442 additions and 9679 deletions

2
benchbep.sh Executable file
View file

@ -0,0 +1,2 @@
gobench -package=./hugolib -bench="BenchmarkSiteBuilding/TOML,num_langs=3,num_pages=5000,tags_per_page=5,shortcodes,render" -count=3 > 1.bench
benchcmp -best 0.bench 1.bench

529
codegen/methods.go Normal file
View file

@ -0,0 +1,529 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// Some functions in this file (see comments) is based on the Go source code,
// copyright The Go Authors and governed by a BSD-style license.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package codegen contains helpers for code generation.
package codegen
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path"
"path/filepath"
"reflect"
"regexp"
"sort"
"strings"
"sync"
)
// Make room for insertions
const weightWidth = 1000
// NewInspector creates a new Inspector given a source root.
func NewInspector(root string) *Inspector {
return &Inspector{ProjectRootDir: root}
}
// Inspector provides methods to help code generation. It uses a combination
// of reflection and source code AST to do the heavy lifting.
type Inspector struct {
ProjectRootDir string
init sync.Once
// Determines method order. Go's reflect sorts lexicographically, so
// we must parse the source to preserve this order.
methodWeight map[string]map[string]int
}
// MethodsFromTypes create a method set from the include slice, excluding any
// method in exclude.
func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.Type) Methods {
c.parseSource()
var methods Methods
var excludes = make(map[string]bool)
if len(exclude) > 0 {
for _, m := range c.MethodsFromTypes(exclude, nil) {
excludes[m.Name] = true
}
}
// There may be overlapping interfaces in types. Do a simple check for now.
seen := make(map[string]bool)
nameAndPackage := func(t reflect.Type) (string, string) {
var name, pkg string
isPointer := t.Kind() == reflect.Ptr
if isPointer {
t = t.Elem()
}
pkgPrefix := ""
if pkgPath := t.PkgPath(); pkgPath != "" {
pkgPath = strings.TrimSuffix(pkgPath, "/")
_, shortPath := path.Split(pkgPath)
pkgPrefix = shortPath + "."
pkg = pkgPath
}
name = t.Name()
if name == "" {
// interface{}
name = t.String()
}
if isPointer {
pkgPrefix = "*" + pkgPrefix
}
name = pkgPrefix + name
return name, pkg
}
for _, t := range include {
for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i)
if excludes[m.Name] || seen[m.Name] {
continue
}
seen[m.Name] = true
if m.PkgPath != "" {
// Not exported
continue
}
numIn := m.Type.NumIn()
ownerName, _ := nameAndPackage(t)
method := Method{Owner: t, OwnerName: ownerName, Name: m.Name}
for i := 0; i < numIn; i++ {
in := m.Type.In(i)
name, pkg := nameAndPackage(in)
if pkg != "" {
method.Imports = append(method.Imports, pkg)
}
method.In = append(method.In, name)
}
numOut := m.Type.NumOut()
if numOut > 0 {
for i := 0; i < numOut; i++ {
out := m.Type.Out(i)
name, pkg := nameAndPackage(out)
if pkg != "" {
method.Imports = append(method.Imports, pkg)
}
method.Out = append(method.Out, name)
}
}
methods = append(methods, method)
}
}
sort.SliceStable(methods, func(i, j int) bool {
mi, mj := methods[i], methods[j]
wi := c.methodWeight[mi.OwnerName][mi.Name]
wj := c.methodWeight[mj.OwnerName][mj.Name]
if wi == wj {
return mi.Name < mj.Name
}
return wi < wj
})
return methods
}
func (c *Inspector) parseSource() {
c.init.Do(func() {
if !strings.Contains(c.ProjectRootDir, "hugo") {
panic("dir must be set to the Hugo root")
}
c.methodWeight = make(map[string]map[string]int)
dirExcludes := regexp.MustCompile("docs|examples")
fileExcludes := regexp.MustCompile("autogen")
var filenames []string
filepath.Walk(c.ProjectRootDir, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
if dirExcludes.MatchString(info.Name()) {
return filepath.SkipDir
}
}
if !strings.HasSuffix(path, ".go") || fileExcludes.MatchString(path) {
return nil
}
filenames = append(filenames, path)
return nil
})
for _, filename := range filenames {
pkg := c.packageFromPath(filename)
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
if err != nil {
panic(err)
}
ast.Inspect(node, func(n ast.Node) bool {
switch t := n.(type) {
case *ast.TypeSpec:
if t.Name.IsExported() {
switch it := t.Type.(type) {
case *ast.InterfaceType:
iface := pkg + "." + t.Name.Name
methodNames := collectMethodsRecursive(pkg, it.Methods.List)
weights := make(map[string]int)
weight := weightWidth
for _, name := range methodNames {
weights[name] = weight
weight += weightWidth
}
c.methodWeight[iface] = weights
}
}
}
return true
})
}
// Complement
for _, v1 := range c.methodWeight {
for k2, w := range v1 {
if v, found := c.methodWeight[k2]; found {
for k3, v3 := range v {
v1[k3] = (v3 / weightWidth) + w
}
}
}
}
})
}
func (c *Inspector) packageFromPath(p string) string {
p = filepath.ToSlash(p)
base := path.Base(p)
if !strings.Contains(base, ".") {
return base
}
return path.Base(strings.TrimSuffix(p, base))
}
// Method holds enough information about it to recreate it.
type Method struct {
// The interface we extracted this method from.
Owner reflect.Type
// String version of the above, on the form PACKAGE.NAME, e.g.
// page.Page
OwnerName string
// Method name.
Name string
// Imports needed to satisfy the method signature.
Imports []string
// Argument types, including any package prefix, e.g. string, int, interface{},
// net.Url
In []string
// Return types.
Out []string
}
// Declaration creates a method declaration (without any body) for the given receiver.
func (m Method) Declaration(receiver string) string {
return fmt.Sprintf("func (%s %s) %s%s %s", receiverShort(receiver), receiver, m.Name, m.inStr(), m.outStr())
}
// Delegate creates a delegate call string.
func (m Method) Delegate(receiver, delegate string) string {
ret := ""
if len(m.Out) > 0 {
ret = "return "
}
return fmt.Sprintf("%s%s.%s.%s%s", ret, receiverShort(receiver), delegate, m.Name, m.inOutStr())
}
func (m Method) String() string {
return m.Name + m.inStr() + " " + m.outStr() + "\n"
}
func (m Method) inOutStr() string {
if len(m.In) == 0 {
return "()"
}
args := make([]string, len(m.In))
for i := 0; i < len(args); i++ {
args[i] = fmt.Sprintf("arg%d", i)
}
return "(" + strings.Join(args, ", ") + ")"
}
func (m Method) inStr() string {
if len(m.In) == 0 {
return "()"
}
args := make([]string, len(m.In))
for i := 0; i < len(args); i++ {
args[i] = fmt.Sprintf("arg%d %s", i, m.In[i])
}
return "(" + strings.Join(args, ", ") + ")"
}
func (m Method) outStr() string {
if len(m.Out) == 0 {
return ""
}
if len(m.Out) == 1 {
return m.Out[0]
}
return "(" + strings.Join(m.Out, ", ") + ")"
}
// Methods represents a list of methods for one or more interfaces.
// The order matches the defined order in their source file(s).
type Methods []Method
// Imports returns a sorted list of package imports needed to satisfy the
// signatures of all methods.
func (m Methods) Imports() []string {
var pkgImports []string
for _, method := range m {
pkgImports = append(pkgImports, method.Imports...)
}
if len(pkgImports) > 0 {
pkgImports = uniqueNonEmptyStrings(pkgImports)
sort.Strings(pkgImports)
}
return pkgImports
}
// ToMarshalJSON creates a MarshalJSON method for these methods. Any method name
// matchin any of the regexps in excludes will be ignored.
func (m Methods) ToMarshalJSON(receiver, pkgPath string, excludes ...string) (string, []string) {
var sb strings.Builder
r := receiverShort(receiver)
what := firstToUpper(trimAsterisk(receiver))
pgkName := path.Base(pkgPath)
fmt.Fprintf(&sb, "func Marshal%sToJSON(%s %s) ([]byte, error) {\n", what, r, receiver)
var methods Methods
var excludeRes = make([]*regexp.Regexp, len(excludes))
for i, exclude := range excludes {
excludeRes[i] = regexp.MustCompile(exclude)
}
for _, method := range m {
// Exclude methods with arguments and incompatible return values
if len(method.In) > 0 || len(method.Out) == 0 || len(method.Out) > 2 {
continue
}
if len(method.Out) == 2 {
if method.Out[1] != "error" {
continue
}
}
for _, re := range excludeRes {
if re.MatchString(method.Name) {
continue
}
}
methods = append(methods, method)
}
for _, method := range methods {
varn := varName(method.Name)
if len(method.Out) == 1 {
fmt.Fprintf(&sb, "\t%s := %s.%s()\n", varn, r, method.Name)
} else {
fmt.Fprintf(&sb, "\t%s, err := %s.%s()\n", varn, r, method.Name)
fmt.Fprint(&sb, "\tif err != nil {\n\t\treturn nil, err\n\t}\n")
}
}
fmt.Fprint(&sb, "\n\ts := struct {\n")
for _, method := range methods {
fmt.Fprintf(&sb, "\t\t%s %s\n", method.Name, typeName(method.Out[0], pgkName))
}
fmt.Fprint(&sb, "\n\t}{\n")
for _, method := range methods {
varn := varName(method.Name)
fmt.Fprintf(&sb, "\t\t%s: %s,\n", method.Name, varn)
}
fmt.Fprint(&sb, "\n\t}\n\n")
fmt.Fprint(&sb, "\treturn json.Marshal(&s)\n}")
pkgImports := append(methods.Imports(), "encoding/json")
if pkgPath != "" {
// Exclude self
for i, pkgImp := range pkgImports {
if pkgImp == pkgPath {
pkgImports = append(pkgImports[:i], pkgImports[i+1:]...)
}
}
}
return sb.String(), pkgImports
}
func collectMethodsRecursive(pkg string, f []*ast.Field) []string {
var methodNames []string
for _, m := range f {
if m.Names != nil {
methodNames = append(methodNames, m.Names[0].Name)
continue
}
if ident, ok := m.Type.(*ast.Ident); ok && ident.Obj != nil {
// Embedded interface
methodNames = append(
methodNames,
collectMethodsRecursive(
pkg,
ident.Obj.Decl.(*ast.TypeSpec).Type.(*ast.InterfaceType).Methods.List)...)
} else {
// Embedded, but in a different file/package. Return the
// package.Name and deal with that later.
name := packageName(m.Type)
if !strings.Contains(name, ".") {
// Assume current package
name = pkg + "." + name
}
methodNames = append(methodNames, name)
}
}
return methodNames
}
func firstToLower(name string) string {
return strings.ToLower(name[:1]) + name[1:]
}
func firstToUpper(name string) string {
return strings.ToUpper(name[:1]) + name[1:]
}
func packageName(e ast.Expr) string {
switch tp := e.(type) {
case *ast.Ident:
return tp.Name
case *ast.SelectorExpr:
return fmt.Sprintf("%s.%s", packageName(tp.X), packageName(tp.Sel))
}
return ""
}
func receiverShort(receiver string) string {
return strings.ToLower(trimAsterisk(receiver))[:1]
}
func trimAsterisk(name string) string {
return strings.TrimPrefix(name, "*")
}
func typeName(name, pkg string) string {
return strings.TrimPrefix(name, pkg+".")
}
func uniqueNonEmptyStrings(s []string) []string {
var unique []string
set := map[string]interface{}{}
for _, val := range s {
if val == "" {
continue
}
if _, ok := set[val]; !ok {
unique = append(unique, val)
set[val] = val
}
}
return unique
}
func varName(name string) string {
name = firstToLower(name)
// Adjust some reserved keywords, see https://golang.org/ref/spec#Keywords
switch name {
case "type":
name = "typ"
case "package":
name = "pkg"
// Not reserved, but syntax highlighters has it as a keyword.
case "len":
name = "length"
}
return name
}

View file

@ -1,4 +1,4 @@
// Copyright 2017-present The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -11,13 +11,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
package codegen
import (
"github.com/gohugoio/hugo/resources/resource"
)
var (
_ resource.Resource = (*Page)(nil)
_ resource.Resource = (*PageOutput)(nil)
)
type IEmbed interface {
MethodEmbed3(s string) string
MethodEmbed1() string
MethodEmbed2()
}

100
codegen/methods_test.go Normal file
View file

@ -0,0 +1,100 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package codegen
import (
"fmt"
"net"
"os"
"reflect"
"testing"
"github.com/gohugoio/hugo/common/herrors"
"github.com/stretchr/testify/require"
)
func TestMethods(t *testing.T) {
var (
zeroIE = reflect.TypeOf((*IEmbed)(nil)).Elem()
zeroIEOnly = reflect.TypeOf((*IEOnly)(nil)).Elem()
zeroI = reflect.TypeOf((*I)(nil)).Elem()
)
dir, _ := os.Getwd()
c := NewInspector(dir)
t.Run("MethodsFromTypes", func(t *testing.T) {
assert := require.New(t)
methods := c.MethodsFromTypes([]reflect.Type{zeroI}, nil)
methodsStr := fmt.Sprint(methods)
assert.Contains(methodsStr, "Method1(arg0 herrors.ErrorContext)")
assert.Contains(methodsStr, "Method7() interface {}")
assert.Contains(methodsStr, "Method0() string\n Method4() string")
assert.Contains(methodsStr, "MethodEmbed3(arg0 string) string\n MethodEmbed1() string")
assert.Contains(methods.Imports(), "github.com/gohugoio/hugo/common/herrors")
})
t.Run("EmbedOnly", func(t *testing.T) {
assert := require.New(t)
methods := c.MethodsFromTypes([]reflect.Type{zeroIEOnly}, nil)
methodsStr := fmt.Sprint(methods)
assert.Contains(methodsStr, "MethodEmbed3(arg0 string) string")
})
t.Run("ToMarshalJSON", func(t *testing.T) {
assert := require.New(t)
m, pkg := c.MethodsFromTypes(
[]reflect.Type{zeroI},
[]reflect.Type{zeroIE}).ToMarshalJSON("*page", "page")
assert.Contains(m, "method6 := p.Method6()")
assert.Contains(m, "Method0: method0,")
assert.Contains(m, "return json.Marshal(&s)")
assert.Contains(pkg, "github.com/gohugoio/hugo/common/herrors")
assert.Contains(pkg, "encoding/json")
fmt.Println(pkg)
})
}
type I interface {
IEmbed
Method0() string
Method4() string
Method1(myerr herrors.ErrorContext)
Method3(myint int, mystring string)
Method5() (string, error)
Method6() *net.IP
Method7() interface{}
Method8() herrors.ErrorContext
method2()
method9() os.FileInfo
}
type IEOnly interface {
IEmbed
}

View file

@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -357,6 +357,13 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
c.changeDetector = changeDetector
}
if c.Cfg.GetBool("logPathWarnings") {
fs.Destination = hugofs.NewCreateCountingFs(fs.Destination)
}
// To debug hard-to-find path issues.
//fs.Destination = hugofs.NewStacktracerFs(fs.Destination, `fr/fr`)
err = c.initFs(fs)
if err != nil {
return

View file

@ -1,4 +1,4 @@
// Copyright 2017 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -23,7 +23,6 @@ import (
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
"github.com/spf13/cobra"
"github.com/spf13/nitro"
)
type commandsBuilder struct {
@ -197,6 +196,12 @@ type hugoBuilderCommon struct {
gc bool
// Profile flags (for debugging of performance problems)
cpuprofile string
memprofile string
mutexprofile string
traceprofile string
// TODO(bep) var vs string
logging bool
verbose bool
@ -255,13 +260,22 @@ func (cc *hugoBuilderCommon) handleFlags(cmd *cobra.Command) {
cmd.Flags().Bool("enableGitInfo", false, "add Git revision, date and author info to the pages")
cmd.Flags().BoolVar(&cc.gc, "gc", false, "enable to run some cleanup tasks (remove unused cache files) after the build")
cmd.Flags().BoolVar(&nitro.AnalysisOn, "stepAnalysis", false, "display memory and timing of different steps of the program")
cmd.Flags().Bool("templateMetrics", false, "display metrics about template executions")
cmd.Flags().Bool("templateMetricsHints", false, "calculate some improvement hints when combined with --templateMetrics")
cmd.Flags().BoolP("forceSyncStatic", "", false, "copy all files when static is changed.")
cmd.Flags().BoolP("noTimes", "", false, "don't sync modification time of files")
cmd.Flags().BoolP("noChmod", "", false, "don't sync permission mode of files")
cmd.Flags().BoolP("i18n-warnings", "", false, "print missing translations")
cmd.Flags().BoolP("path-warnings", "", false, "print warnings on duplicate target paths etc.")
cmd.Flags().StringVarP(&cc.cpuprofile, "profile-cpu", "", "", "write cpu profile to `file`")
cmd.Flags().StringVarP(&cc.memprofile, "profile-mem", "", "", "write memory profile to `file`")
cmd.Flags().StringVarP(&cc.mutexprofile, "profile-mutex", "", "", "write Mutex profile to `file`")
cmd.Flags().StringVarP(&cc.traceprofile, "trace", "", "", "write trace to `file` (not useful in general)")
// Hide these for now.
cmd.Flags().MarkHidden("profile-cpu")
cmd.Flags().MarkHidden("profile-mem")
cmd.Flags().MarkHidden("profile-mutex")
cmd.Flags().StringSlice("disableKinds", []string{}, "disable different kind of pages (home, RSS etc.)")

View file

@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -20,6 +20,8 @@ import (
"path/filepath"
"testing"
"github.com/gohugoio/hugo/common/types"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@ -41,7 +43,7 @@ func TestExecute(t *testing.T) {
assert.NoError(resp.Err)
result := resp.Result
assert.True(len(result.Sites) == 1)
assert.True(len(result.Sites[0].RegularPages) == 1)
assert.True(len(result.Sites[0].RegularPages()) == 1)
}
func TestCommandsPersistentFlags(t *testing.T) {
@ -75,6 +77,7 @@ func TestCommandsPersistentFlags(t *testing.T) {
"--port=1366",
"--renderToDisk",
"--source=mysource",
"--path-warnings",
}, func(commands []cmder) {
var sc *serverCmd
for _, command := range commands {
@ -112,6 +115,9 @@ func TestCommandsPersistentFlags(t *testing.T) {
assert.True(cfg.GetBool("gc"))
// The flag is named path-warnings
assert.True(cfg.GetBool("logPathWarnings"))
// The flag is named i18n-warnings
assert.True(cfg.GetBool("logI18nWarnings"))
@ -183,8 +189,8 @@ func TestCommandsExecute(t *testing.T) {
}
for _, test := range tests {
hugoCmd := newCommandsBuilder().addAll().build().getCommand()
b := newCommandsBuilder().addAll().build()
hugoCmd := b.getCommand()
test.flags = append(test.flags, "--quiet")
hugoCmd.SetArgs(append(test.commands, test.flags...))
@ -200,6 +206,13 @@ func TestCommandsExecute(t *testing.T) {
assert.NoError(err, fmt.Sprintf("%v", test.commands))
}
// Assert that we have not left any development debug artifacts in
// the code.
if b.c != nil {
_, ok := b.c.destinationFs.(types.DevMarker)
assert.False(ok)
}
}
}

View file

@ -1,4 +1,4 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -20,6 +20,8 @@ import (
"strings"
"time"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/helpers"
@ -124,8 +126,8 @@ func (cc *convertCmd) convertContents(format metadecoders.Format) error {
site := h.Sites[0]
site.Log.FEEDBACK.Println("processing", len(site.AllPages), "content files")
for _, p := range site.AllPages {
site.Log.FEEDBACK.Println("processing", len(site.AllPages()), "content files")
for _, p := range site.AllPages() {
if err := cc.convertAndSavePage(p, site, format); err != nil {
return err
}
@ -133,24 +135,24 @@ func (cc *convertCmd) convertContents(format metadecoders.Format) error {
return nil
}
func (cc *convertCmd) convertAndSavePage(p *hugolib.Page, site *hugolib.Site, targetFormat metadecoders.Format) error {
func (cc *convertCmd) convertAndSavePage(p page.Page, site *hugolib.Site, targetFormat metadecoders.Format) error {
// The resources are not in .Site.AllPages.
for _, r := range p.Resources.ByType("page") {
if err := cc.convertAndSavePage(r.(*hugolib.Page), site, targetFormat); err != nil {
for _, r := range p.Resources().ByType("page") {
if err := cc.convertAndSavePage(r.(page.Page), site, targetFormat); err != nil {
return err
}
}
if p.Filename() == "" {
if p.File() == nil {
// No content file.
return nil
}
errMsg := fmt.Errorf("Error processing file %q", p.Path())
site.Log.INFO.Println("Attempting to convert", p.LogicalName())
site.Log.INFO.Println("Attempting to convert", p.File().Filename())
f, _ := p.File.(src.ReadableFile)
f, _ := p.File().(src.ReadableFile)
file, err := f.Open()
if err != nil {
site.Log.ERROR.Println(errMsg)
@ -186,7 +188,7 @@ func (cc *convertCmd) convertAndSavePage(p *hugolib.Page, site *hugolib.Site, ta
newContent.Write(pf.content)
newFilename := p.Filename()
newFilename := p.File().Filename()
if cc.outputDir != "" {
contentDir := strings.TrimSuffix(newFilename, p.Path())

View file

@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -18,11 +18,16 @@ package commands
import (
"fmt"
"io/ioutil"
"os/signal"
"runtime/pprof"
"runtime/trace"
"sort"
"sync/atomic"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/common/hugo"
"github.com/pkg/errors"
@ -214,6 +219,7 @@ func initializeFlags(cmd *cobra.Command, cfg config.Provider) {
"themesDir",
"verbose",
"verboseLog",
"duplicateTargetPaths",
}
// Will set a value even if it is the default.
@ -235,6 +241,7 @@ func initializeFlags(cmd *cobra.Command, cfg config.Provider) {
// Set some "config aliases"
setValueFromFlag(cmd.Flags(), "destination", cfg, "publishDir", false)
setValueFromFlag(cmd.Flags(), "i18n-warnings", cfg, "logI18nWarnings", false)
setValueFromFlag(cmd.Flags(), "path-warnings", cfg, "logPathWarnings", false)
}
@ -290,6 +297,7 @@ func (c *commandeer) fullBuild() error {
}
copyStaticFunc := func() error {
cnt, err := c.copyStatic()
if err != nil {
if !os.IsNotExist(err) {
@ -326,7 +334,7 @@ func (c *commandeer) fullBuild() error {
}
for _, s := range c.hugo.Sites {
s.ProcessingStats.Static = langCount[s.Language.Lang]
s.ProcessingStats.Static = langCount[s.Language().Lang]
}
if c.h.gc {
@ -344,9 +352,125 @@ func (c *commandeer) fullBuild() error {
}
func (c *commandeer) initCPUProfile() (func(), error) {
if c.h.cpuprofile == "" {
return nil, nil
}
f, err := os.Create(c.h.cpuprofile)
if err != nil {
return nil, errors.Wrap(err, "failed to create CPU profile")
}
if err := pprof.StartCPUProfile(f); err != nil {
return nil, errors.Wrap(err, "failed to start CPU profile")
}
return func() {
pprof.StopCPUProfile()
f.Close()
}, nil
}
func (c *commandeer) initMemProfile() {
if c.h.memprofile == "" {
return
}
f, err := os.Create(c.h.memprofile)
if err != nil {
c.logger.ERROR.Println("could not create memory profile: ", err)
}
defer f.Close()
runtime.GC() // get up-to-date statistics
if err := pprof.WriteHeapProfile(f); err != nil {
c.logger.ERROR.Println("could not write memory profile: ", err)
}
}
func (c *commandeer) initTraceProfile() (func(), error) {
if c.h.traceprofile == "" {
return nil, nil
}
f, err := os.Create(c.h.traceprofile)
if err != nil {
return nil, errors.Wrap(err, "failed to create trace file")
}
if err := trace.Start(f); err != nil {
return nil, errors.Wrap(err, "failed to start trace")
}
return func() {
trace.Stop()
f.Close()
}, nil
}
func (c *commandeer) initMutexProfile() (func(), error) {
if c.h.mutexprofile == "" {
return nil, nil
}
f, err := os.Create(c.h.mutexprofile)
if err != nil {
return nil, err
}
runtime.SetMutexProfileFraction(1)
return func() {
pprof.Lookup("mutex").WriteTo(f, 0)
f.Close()
}, nil
}
func (c *commandeer) initProfiling() (func(), error) {
stopCPUProf, err := c.initCPUProfile()
if err != nil {
return nil, err
}
defer c.initMemProfile()
stopMutexProf, err := c.initMutexProfile()
if err != nil {
return nil, err
}
stopTraceProf, err := c.initTraceProfile()
if err != nil {
return nil, err
}
return func() {
if stopCPUProf != nil {
stopCPUProf()
}
if stopMutexProf != nil {
stopMutexProf()
}
if stopTraceProf != nil {
stopTraceProf()
}
}, nil
}
func (c *commandeer) build() error {
defer c.timeTrack(time.Now(), "Total")
stopProfiling, err := c.initProfiling()
if err != nil {
return err
}
defer func() {
if stopProfiling != nil {
stopProfiling()
}
}()
if err := c.fullBuild(); err != nil {
return err
}
@ -356,6 +480,13 @@ func (c *commandeer) build() error {
fmt.Println()
c.hugo.PrintProcessingStats(os.Stdout)
fmt.Println()
if createCounter, ok := c.destinationFs.(hugofs.DuplicatesReporter); ok {
dupes := createCounter.ReportDuplicates()
if dupes != "" {
c.logger.WARN.Println("Duplicate target paths:", dupes)
}
}
}
if c.h.buildWatch {
@ -369,7 +500,7 @@ func (c *commandeer) build() error {
checkErr(c.Logger, err)
defer watcher.Close()
var sigs = make(chan os.Signal)
var sigs = make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
@ -381,6 +512,17 @@ func (c *commandeer) build() error {
func (c *commandeer) serverBuild() error {
defer c.timeTrack(time.Now(), "Total")
stopProfiling, err := c.initProfiling()
if err != nil {
return err
}
defer func() {
if stopProfiling != nil {
stopProfiling()
}
}()
if err := c.fullBuild(); err != nil {
return err
}
@ -474,11 +616,9 @@ func (c *commandeer) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint6
}
c.logger.INFO.Println("syncing static files to", publishDir)
var err error
// because we are using a baseFs (to get the union right).
// set sync src to root
err = syncer.Sync(publishDir, helpers.FilePathSeparator)
err := syncer.Sync(publishDir, helpers.FilePathSeparator)
if err != nil {
return 0, err
}
@ -619,13 +759,6 @@ func (c *commandeer) getDirList() ([]string, error) {
return a, nil
}
func (c *commandeer) resetAndBuildSites() (err error) {
if !c.h.quiet {
c.logger.FEEDBACK.Println("Started building sites ...")
}
return c.hugo.Build(hugolib.BuildCfg{ResetState: true})
}
func (c *commandeer) buildSites() (err error) {
return c.hugo.Build(hugolib.BuildCfg{})
}
@ -973,7 +1106,7 @@ func (c *commandeer) handleEvents(watcher *watcher.Batcher,
navigate := c.Cfg.GetBool("navigateToChanged")
// We have fetched the same page above, but it may have
// changed.
var p *hugolib.Page
var p page.Page
if navigate {
if onePageName != "" {
@ -982,7 +1115,7 @@ func (c *commandeer) handleEvents(watcher *watcher.Batcher,
}
if p != nil {
livereload.NavigateToPathForPort(p.RelPermalink(), p.Site.ServerPort())
livereload.NavigateToPathForPort(p.RelPermalink(), p.Site().ServerPort())
} else {
livereload.ForceRefresh()
}
@ -1044,9 +1177,11 @@ func (c *commandeer) isThemeVsHugoVersionMismatch(fs afero.Fs) (dir string, mism
}
b, err := afero.ReadFile(fs, path)
if err != nil {
continue
}
tomlMeta, err := metadecoders.Default.UnmarshalToMap(b, metadecoders.TOML)
if err != nil {
continue
}

View file

@ -1,4 +1,4 @@
// Copyright 2016 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -340,7 +340,7 @@ func copyDir(source string, dest string) error {
if err != nil {
return err
}
entries, err := ioutil.ReadDir(source)
entries, _ := ioutil.ReadDir(source)
for _, entry := range entries {
sfp := filepath.Join(source, entry.Name())
dfp := filepath.Join(dest, entry.Name())
@ -373,6 +373,10 @@ func (i *importCmd) copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyllPos
return err
}
entries, err := ioutil.ReadDir(jekyllRoot)
if err != nil {
return err
}
for _, entry := range entries {
sfp := filepath.Join(jekyllRoot, entry.Name())
dfp := filepath.Join(dest, entry.Name())
@ -464,7 +468,7 @@ func convertJekyllPost(s *hugolib.Site, path, relPath, targetDir string, draft b
fs := hugofs.Os
if err := helpers.WriteToDisk(targetFile, strings.NewReader(content), fs); err != nil {
return fmt.Errorf("Failed to save file %q:", filename)
return fmt.Errorf("failed to save file %q: %s", filename, err)
}
return nil

View file

@ -1,4 +1,4 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -20,6 +20,7 @@ import (
"time"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/resources/resource"
"github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
)
@ -70,7 +71,7 @@ List requires a subcommand, e.g. ` + "`hugo list drafts`.",
for _, p := range sites.Pages() {
if p.IsDraft() {
jww.FEEDBACK.Println(filepath.Join(p.File.Dir(), p.File.LogicalName()))
jww.FEEDBACK.Println(filepath.Join(p.File().Dir(), p.File().LogicalName()))
}
}
@ -108,8 +109,8 @@ posted in the future.`,
defer writer.Flush()
for _, p := range sites.Pages() {
if p.IsFuture() {
err := writer.Write([]string{filepath.Join(p.File.Dir(), p.File.LogicalName()), p.PublishDate.Format(time.RFC3339)})
if resource.IsFuture(p) {
err := writer.Write([]string{filepath.Join(p.File().Dir(), p.File().LogicalName()), p.PublishDate().Format(time.RFC3339)})
if err != nil {
return newSystemError("Error writing future posts to stdout", err)
}
@ -149,11 +150,12 @@ expired.`,
defer writer.Flush()
for _, p := range sites.Pages() {
if p.IsExpired() {
err := writer.Write([]string{filepath.Join(p.File.Dir(), p.File.LogicalName()), p.ExpiryDate.Format(time.RFC3339)})
if resource.IsExpired(p) {
err := writer.Write([]string{filepath.Join(p.File().Dir(), p.File().LogicalName()), p.ExpiryDate().Format(time.RFC3339)})
if err != nil {
return newSystemError("Error writing expired posts to stdout", err)
}
}
}

View file

@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -62,7 +62,7 @@ func TestDoNewSite_noerror_base_exists_but_empty(t *testing.T) {
_, fs := newTestCfg()
n := newNewSiteCmd()
require.NoError(t, fs.Source.MkdirAll(basepath, 777))
require.NoError(t, fs.Source.MkdirAll(basepath, 0777))
require.NoError(t, n.doNewSite(fs, basepath, false))
}
@ -72,7 +72,7 @@ func TestDoNewSite_error_base_exists(t *testing.T) {
_, fs := newTestCfg()
n := newNewSiteCmd()
require.NoError(t, fs.Source.MkdirAll(basepath, 777))
require.NoError(t, fs.Source.MkdirAll(basepath, 0777))
_, err := fs.Source.Create(filepath.Join(basepath, "foo"))
require.NoError(t, err)
// Since the directory already exists and isn't empty, expect an error
@ -85,7 +85,7 @@ func TestDoNewSite_force_empty_dir(t *testing.T) {
_, fs := newTestCfg()
n := newNewSiteCmd()
require.NoError(t, fs.Source.MkdirAll(basepath, 777))
require.NoError(t, fs.Source.MkdirAll(basepath, 0777))
require.NoError(t, n.doNewSite(fs, basepath, true))
@ -99,7 +99,7 @@ func TestDoNewSite_error_force_dir_inside_exists(t *testing.T) {
contentPath := filepath.Join(basepath, "content")
require.NoError(t, fs.Source.MkdirAll(contentPath, 777))
require.NoError(t, fs.Source.MkdirAll(contentPath, 0777))
require.Error(t, n.doNewSite(fs, basepath, true))
}
@ -109,7 +109,7 @@ func TestDoNewSite_error_force_config_inside_exists(t *testing.T) {
n := newNewSiteCmd()
configPath := filepath.Join(basepath, "config.toml")
require.NoError(t, fs.Source.MkdirAll(basepath, 777))
require.NoError(t, fs.Source.MkdirAll(basepath, 0777))
_, err := fs.Source.Create(configPath)
require.NoError(t, err)

View file

@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -358,7 +358,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, erro
if err := f.c.partialReRender(p); err != nil {
f.c.handleBuildErr(err, fmt.Sprintf("Failed to render %q", p))
if f.c.showErrorInBrowser {
http.Redirect(w, r, p, 301)
http.Redirect(w, r, p, http.StatusMovedPermanently)
return
}
}
@ -386,7 +386,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, erro
return mu, u.String(), endpoint, nil
}
var logErrorRe = regexp.MustCompile("(?s)ERROR \\d{4}/\\d{2}/\\d{2} \\d{2}:\\d{2}:\\d{2} ")
var logErrorRe = regexp.MustCompile(`(?s)ERROR \d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} `)
func removeErrorPrefixFromLog(content string) string {
return logErrorRe.ReplaceAllLiteralString(content, "")
@ -403,7 +403,7 @@ func (c *commandeer) serve(s *serverCmd) error {
if isMultiHost {
for _, s := range c.hugo.Sites {
baseURLs = append(baseURLs, s.BaseURL.String())
roots = append(roots, s.Language.Lang)
roots = append(roots, s.Language().Lang)
}
} else {
s := c.hugo.Sites[0]
@ -430,7 +430,7 @@ func (c *commandeer) serve(s *serverCmd) error {
livereload.Initialize()
}
var sigs = make(chan os.Signal)
var sigs = make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
for i := range baseURLs {

View file

@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -92,9 +92,7 @@ func appendToInterfaceSlice(tov reflect.Value, from ...interface{}) ([]interface
tos = append(tos, tov.Index(i).Interface())
}
for _, v := range from {
tos = append(tos, v)
}
tos = append(tos, from...)
return tos, nil
}

View file

@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -75,11 +75,11 @@ func (p *tstSlicerIn2) Slice(in interface{}) (interface{}, error) {
}
func (p *tstSlicerIn1) Name() string {
return p.Name()
return p.name
}
func (p *tstSlicerIn2) Name() string {
return p.Name()
return p.name
}
func (p *tstSlicer) Slice(in interface{}) (interface{}, error) {

View file

@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -32,6 +32,7 @@ type ReadSeekCloser interface {
}
// ReadSeekerNoOpCloser implements ReadSeekCloser by doing nothing in Close.
// TODO(bep) rename this and simila to ReadSeekerNopCloser, naming used in stdlib, which kind of makes sense.
type ReadSeekerNoOpCloser struct {
ReadSeeker
}

View file

@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -28,6 +28,24 @@ type Scratch struct {
mu sync.RWMutex
}
// Scratcher provides a scratching service.
type Scratcher interface {
Scratch() *Scratch
}
type scratcher struct {
s *Scratch
}
func (s scratcher) Scratch() *Scratch {
return s.s
}
// NewScratcher creates a new Scratcher.
func NewScratcher() Scratcher {
return scratcher{s: NewScratch()}
}
// Add will, for single values, add (using the + operator) the addend to the existing addend (if found).
// Supports numeric values and strings.
//

View file

@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -16,6 +16,7 @@ package types
import (
"fmt"
"reflect"
"github.com/spf13/cast"
)
@ -56,3 +57,24 @@ func NewKeyValuesStrings(key string, values ...string) KeyValues {
type Zeroer interface {
IsZero() bool
}
// IsNil reports whether v is nil.
func IsNil(v interface{}) bool {
if v == nil {
return true
}
value := reflect.ValueOf(v)
switch value.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
return value.IsNil()
}
return false
}
// DevMarker is a marker interface for types that should only be used during
// development.
type DevMarker interface {
DevOnly()
}

View file

@ -1,4 +1,4 @@
// Copyright 2017-present The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -40,3 +40,15 @@ func GetStringSlicePreserveString(cfg Provider, key string) []string {
}
return cast.ToStringSlice(sd)
}
// SetBaseTestDefaults provides some common config defaults used in tests.
func SetBaseTestDefaults(cfg Provider) {
cfg.Set("resourceDir", "resources")
cfg.Set("contentDir", "content")
cfg.Set("dataDir", "data")
cfg.Set("i18nDir", "i18n")
cfg.Set("layoutDir", "layouts")
cfg.Set("assetDir", "assets")
cfg.Set("archetypeDir", "archetypes")
cfg.Set("publishDir", "public")
}

View file

@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -23,6 +23,7 @@ const (
disqusShortnameKey = "disqusshortname"
googleAnalyticsKey = "googleanalytics"
rssLimitKey = "rssLimit"
)
// Config is a privacy configuration for all the relevant services in Hugo.
@ -31,6 +32,7 @@ type Config struct {
GoogleAnalytics GoogleAnalytics
Instagram Instagram
Twitter Twitter
RSS RSS
}
// Disqus holds the functional configuration settings related to the Disqus template.
@ -61,6 +63,12 @@ type Twitter struct {
DisableInlineCSS bool
}
// RSS holds the functional configuration settings related to the RSS feeds.
type RSS struct {
// Limit the number of pages.
Limit int
}
// DecodeConfig creates a services Config from a given Hugo configuration.
func DecodeConfig(cfg config.Provider) (c Config, err error) {
m := cfg.GetStringMap(servicesConfigKey)
@ -76,5 +84,9 @@ func DecodeConfig(cfg config.Provider) (c Config, err error) {
c.Disqus.Shortname = cfg.GetString(disqusShortnameKey)
}
if c.RSS.Limit == 0 {
c.RSS.Limit = cfg.GetInt(rssLimitKey)
}
return
}

View file

@ -1,4 +1,4 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
package config
import (
"github.com/spf13/cast"
@ -25,21 +25,20 @@ type Sitemap struct {
Filename string
}
func parseSitemap(input map[string]interface{}) Sitemap {
sitemap := Sitemap{Priority: -1, Filename: "sitemap.xml"}
func DecodeSitemap(prototype Sitemap, input map[string]interface{}) Sitemap {
for key, value := range input {
switch key {
case "changefreq":
sitemap.ChangeFreq = cast.ToString(value)
prototype.ChangeFreq = cast.ToString(value)
case "priority":
sitemap.Priority = cast.ToFloat64(value)
prototype.Priority = cast.ToFloat64(value)
case "filename":
sitemap.Filename = cast.ToString(value)
prototype.Filename = cast.ToString(value)
default:
jww.WARN.Printf("Unknown Sitemap field: %s\n", key)
}
}
return sitemap
return prototype
}

View file

@ -1,4 +1,4 @@
// Copyright 2016 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -50,7 +50,7 @@ func NewContent(
if isDir {
langFs := hugofs.NewLanguageFs(s.Language.Lang, sites.LanguageSet(), archetypeFs)
langFs := hugofs.NewLanguageFs(s.Language().Lang, sites.LanguageSet(), archetypeFs)
cm, err := mapArcheTypeDir(ps, langFs, archetypeFilename)
if err != nil {
@ -113,7 +113,7 @@ func NewContent(
func targetSite(sites *hugolib.HugoSites, fi *hugofs.LanguageFileInfo) *hugolib.Site {
for _, s := range sites.Sites {
if fi.Lang() == s.Language.Lang {
if fi.Lang() == s.Language().Lang {
return s
}
}
@ -245,7 +245,7 @@ func resolveContentPath(sites *hugolib.HugoSites, fs afero.Fs, targetPath string
// Try the filename: my-post.en.md
for _, ss := range sites.Sites {
if strings.Contains(targetPath, "."+ss.Language.Lang+".") {
if strings.Contains(targetPath, "."+ss.Language().Lang+".") {
s = ss
break
}

7
deps/deps.go vendored
View file

@ -7,13 +7,14 @@ import (
"github.com/pkg/errors"
"github.com/gohugoio/hugo/cache/filecache"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/metrics"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/resources"
@ -67,7 +68,7 @@ type Deps struct {
Language *langs.Language
// The site building.
Site hugo.Site
Site page.Site
// All the output formats available for the current site.
OutputFormatsConfig output.Formats
@ -325,7 +326,7 @@ type DepsCfg struct {
Language *langs.Language
// The Site in use
Site hugo.Site
Site page.Site
// The configuration to use.
Cfg config.Provider

View file

@ -79,8 +79,7 @@ See [`.Scratch`](/functions/scratch/) for page-scoped, writable variables.
: the page's *kind*. Possible return values are `page`, `home`, `section`, `taxonomy`, or `taxonomyTerm`. Note that there are also `RSS`, `sitemap`, `robotsTXT`, and `404` kinds, but these are only available during the rendering of each of these respective page's kind and therefore *not* available in any of the `Pages` collections.
.Language
: a language object that points to the language's definition in the site
`config`.
: a language object that points to the language's definition in the site `config`. `.Language.Lang` gives you the language code.
.Lastmod
: the date the content was last modified. `.Lastmod` pulls from the `lastmod` field in a content's front matter.
@ -93,10 +92,7 @@ See also `.ExpiryDate`, `.Date`, `.PublishDate`, and [`.GitInfo`][gitinfo].
.LinkTitle
: access when creating links to the content. If set, Hugo will use the `linktitle` from the front matter before `title`.
.Next (deprecated)
: In older Hugo versions this pointer went the wrong direction. Please use `.PrevPage` instead.
.NextPage
.Next
: Pointer to the next [regular page](/variables/site/#site-pages) (sorted by Hugo's [default sort](/templates/lists#default-weight-date-linktitle-filepath)). Example: `{{if .NextPage}}{{.NextPage.Permalink}}{{end}}`.
.NextInSection
@ -119,9 +115,6 @@ See also `.ExpiryDate`, `.Date`, `.PublishDate`, and [`.GitInfo`][gitinfo].
: the Page content stripped of HTML as a `[]string` using Go's [`strings.Fields`](https://golang.org/pkg/strings/#Fields) to split `.Plain` into a slice.
.Prev (deprecated)
: In older Hugo versions this pointer went the wrong direction. Please use `.NextPage` instead.
.PrevPage
: Pointer to the previous [regular page](/variables/site/#site-pages) (sorted by Hugo's [default sort](/templates/lists#default-weight-date-linktitle-filepath)). Example: `{{if .PrevPage}}{{.PrevPage.Permalink}}{{end}}`.
.PrevInSection
@ -130,8 +123,8 @@ See also `.ExpiryDate`, `.Date`, `.PublishDate`, and [`.GitInfo`][gitinfo].
.PublishDate
: the date on which the content was or will be published; `.Publishdate` pulls from the `publishdate` field in a content's front matter. See also `.ExpiryDate`, `.Date`, and `.Lastmod`.
.RSSLink
: link to the taxonomies' RSS link.
.RSSLink (deprecated)
: link to the page's RSS feed. This is deprecated. You should instead do something like this: `{{ with .OutputFormats.Get "RSS" }}{{ . RelPermalink }}{{ end }}`.
.RawContent
: raw markdown content without the front matter. Useful with [remarkjs.com](

1
go.mod
View file

@ -44,7 +44,6 @@ require (
github.com/spf13/cobra v0.0.3
github.com/spf13/fsync v0.0.0-20170320142552-12a01e648f05
github.com/spf13/jwalterweatherman v1.1.0
github.com/spf13/nitro v0.0.0-20131003134307-24d7ef30a12d
github.com/spf13/pflag v1.0.3
github.com/spf13/viper v1.3.2
github.com/stretchr/testify v1.3.0

2
go.sum
View file

@ -126,8 +126,6 @@ github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/nitro v0.0.0-20131003134307-24d7ef30a12d h1:ihvj2nmx8eqWjlgNgdW6h0DyGJuq5GiwHadJkG0wXtQ=
github.com/spf13/nitro v0.0.0-20131003134307-24d7ef30a12d/go.mod h1:jU8A+8xL+6n1OX4XaZtCj4B3mIa64tULUsD6YegdpFo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M=

View file

@ -1,4 +1,4 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -57,7 +57,7 @@ type ContentSpec struct {
Highlight func(code, lang, optsStr string) (string, error)
defatultPygmentsOpts map[string]string
cfg config.Provider
Cfg config.Provider
}
// NewContentSpec returns a ContentSpec initialized
@ -73,7 +73,7 @@ func NewContentSpec(cfg config.Provider) (*ContentSpec, error) {
BuildExpired: cfg.GetBool("buildExpired"),
BuildDrafts: cfg.GetBool("buildDrafts"),
cfg: cfg,
Cfg: cfg,
}
// Highlighting setup
@ -382,7 +382,7 @@ func (c *ContentSpec) getMmarkHTMLRenderer(defaultFlags int, ctx *RenderingConte
return &HugoMmarkHTMLRenderer{
cs: c,
Renderer: mmark.HtmlRendererWithParameters(htmlFlags, "", "", renderParameters),
Cfg: c.cfg,
Cfg: c.Cfg,
}
}

View file

@ -1,4 +1,4 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -24,7 +24,7 @@ import (
// Renders a codeblock using Blackfriday
func (c ContentSpec) render(input string) string {
ctx := &RenderingContext{Cfg: c.cfg, Config: c.BlackFriday}
ctx := &RenderingContext{Cfg: c.Cfg, Config: c.BlackFriday}
render := c.getHTMLRenderer(0, ctx)
buf := &bytes.Buffer{}
@ -34,7 +34,7 @@ func (c ContentSpec) render(input string) string {
// Renders a codeblock using Mmark
func (c ContentSpec) renderWithMmark(input string) string {
ctx := &RenderingContext{Cfg: c.cfg, Config: c.BlackFriday}
ctx := &RenderingContext{Cfg: c.Cfg, Config: c.BlackFriday}
render := c.getMmarkHTMLRenderer(0, ctx)
buf := &bytes.Buffer{}

View file

@ -1,4 +1,4 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -181,7 +181,7 @@ func TestTruncateWordsByRune(t *testing.T) {
func TestGetHTMLRendererFlags(t *testing.T) {
c := newTestContentSpec()
ctx := &RenderingContext{Cfg: c.cfg, Config: c.BlackFriday}
ctx := &RenderingContext{Cfg: c.Cfg, Config: c.BlackFriday}
renderer := c.getHTMLRenderer(blackfriday.HTML_USE_XHTML, ctx)
flags := renderer.GetFlags()
if flags&blackfriday.HTML_USE_XHTML != blackfriday.HTML_USE_XHTML {
@ -210,7 +210,7 @@ func TestGetHTMLRendererAllFlags(t *testing.T) {
{blackfriday.HTML_SMARTYPANTS_LATEX_DASHES},
}
defaultFlags := blackfriday.HTML_USE_XHTML
ctx := &RenderingContext{Cfg: c.cfg, Config: c.BlackFriday}
ctx := &RenderingContext{Cfg: c.Cfg, Config: c.BlackFriday}
ctx.Config.AngledQuotes = true
ctx.Config.Fractions = true
ctx.Config.HrefTargetBlank = true
@ -235,7 +235,7 @@ func TestGetHTMLRendererAllFlags(t *testing.T) {
func TestGetHTMLRendererAnchors(t *testing.T) {
c := newTestContentSpec()
ctx := &RenderingContext{Cfg: c.cfg, Config: c.BlackFriday}
ctx := &RenderingContext{Cfg: c.Cfg, Config: c.BlackFriday}
ctx.DocumentID = "testid"
ctx.Config.PlainIDAnchors = false
@ -259,7 +259,7 @@ func TestGetHTMLRendererAnchors(t *testing.T) {
func TestGetMmarkHTMLRenderer(t *testing.T) {
c := newTestContentSpec()
ctx := &RenderingContext{Cfg: c.cfg, Config: c.BlackFriday}
ctx := &RenderingContext{Cfg: c.Cfg, Config: c.BlackFriday}
ctx.DocumentID = "testid"
ctx.Config.PlainIDAnchors = false
actualRenderer := c.getMmarkHTMLRenderer(0, ctx)
@ -283,7 +283,7 @@ func TestGetMmarkHTMLRenderer(t *testing.T) {
func TestGetMarkdownExtensionsMasksAreRemovedFromExtensions(t *testing.T) {
c := newTestContentSpec()
ctx := &RenderingContext{Cfg: c.cfg, Config: c.BlackFriday}
ctx := &RenderingContext{Cfg: c.Cfg, Config: c.BlackFriday}
ctx.Config.Extensions = []string{"headerId"}
ctx.Config.ExtensionsMask = []string{"noIntraEmphasis"}
@ -298,7 +298,7 @@ func TestGetMarkdownExtensionsByDefaultAllExtensionsAreEnabled(t *testing.T) {
testFlag int
}
c := newTestContentSpec()
ctx := &RenderingContext{Cfg: c.cfg, Config: c.BlackFriday}
ctx := &RenderingContext{Cfg: c.Cfg, Config: c.BlackFriday}
ctx.Config.Extensions = []string{""}
ctx.Config.ExtensionsMask = []string{""}
allExtensions := []data{
@ -330,7 +330,7 @@ func TestGetMarkdownExtensionsByDefaultAllExtensionsAreEnabled(t *testing.T) {
func TestGetMarkdownExtensionsAddingFlagsThroughRenderingContext(t *testing.T) {
c := newTestContentSpec()
ctx := &RenderingContext{Cfg: c.cfg, Config: c.BlackFriday}
ctx := &RenderingContext{Cfg: c.Cfg, Config: c.BlackFriday}
ctx.Config.Extensions = []string{"definitionLists"}
ctx.Config.ExtensionsMask = []string{""}
@ -342,7 +342,7 @@ func TestGetMarkdownExtensionsAddingFlagsThroughRenderingContext(t *testing.T) {
func TestGetMarkdownRenderer(t *testing.T) {
c := newTestContentSpec()
ctx := &RenderingContext{Cfg: c.cfg, Config: c.BlackFriday}
ctx := &RenderingContext{Cfg: c.Cfg, Config: c.BlackFriday}
ctx.Content = []byte("testContent")
actualRenderedMarkdown := c.markdownRender(ctx)
expectedRenderedMarkdown := []byte("<p>testContent</p>\n")
@ -353,7 +353,7 @@ func TestGetMarkdownRenderer(t *testing.T) {
func TestGetMarkdownRendererWithTOC(t *testing.T) {
c := newTestContentSpec()
ctx := &RenderingContext{RenderTOC: true, Cfg: c.cfg, Config: c.BlackFriday}
ctx := &RenderingContext{RenderTOC: true, Cfg: c.Cfg, Config: c.BlackFriday}
ctx.Content = []byte("testContent")
actualRenderedMarkdown := c.markdownRender(ctx)
expectedRenderedMarkdown := []byte("<nav>\n</nav>\n\n<p>testContent</p>\n")
@ -368,7 +368,7 @@ func TestGetMmarkExtensions(t *testing.T) {
testFlag int
}
c := newTestContentSpec()
ctx := &RenderingContext{Cfg: c.cfg, Config: c.BlackFriday}
ctx := &RenderingContext{Cfg: c.Cfg, Config: c.BlackFriday}
ctx.Config.Extensions = []string{"tables"}
ctx.Config.ExtensionsMask = []string{""}
allExtensions := []data{
@ -397,7 +397,7 @@ func TestGetMmarkExtensions(t *testing.T) {
func TestMmarkRender(t *testing.T) {
c := newTestContentSpec()
ctx := &RenderingContext{Cfg: c.cfg, Config: c.BlackFriday}
ctx := &RenderingContext{Cfg: c.Cfg, Config: c.BlackFriday}
ctx.Content = []byte("testContent")
actualRenderedMarkdown := c.mmarkRender(ctx)
expectedRenderedMarkdown := []byte("<p>testContent</p>\n")

View file

@ -1,4 +1,4 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -92,7 +92,7 @@ func GuessType(in string) string {
return "org"
}
return "unknown"
return ""
}
// FirstUpper returns a string with the first character as upper case.
@ -325,12 +325,15 @@ func InitLoggers() {
// The idea is two remove an item in two Hugo releases to give users and theme authors
// plenty of time to fix their templates.
func Deprecated(object, item, alternative string, err bool) {
if !strings.HasSuffix(alternative, ".") {
alternative += "."
}
if err {
DistinctErrorLog.Printf("%s's %s is deprecated and will be removed in Hugo %s. %s", object, item, hugo.CurrentVersion.Next().ReleaseVersion(), alternative)
} else {
// Make sure the users see this while avoiding build breakage. This will not lead to an os.Exit(-1)
DistinctFeedbackLog.Printf("WARNING: %s's %s is deprecated and will be removed in a future release. %s", object, item, alternative)
DistinctWarnLog.Printf("%s's %s is deprecated and will be removed in a future release. %s", object, item, alternative)
}
}

View file

@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -42,7 +42,7 @@ func TestGuessType(t *testing.T) {
{"html", "html"},
{"htm", "html"},
{"org", "org"},
{"excel", "unknown"},
{"excel", ""},
} {
result := GuessType(this.in)
if result != this.expect {
@ -166,6 +166,27 @@ var containsAdditionalTestData = []struct {
{"", []byte(""), false},
}
func TestSliceToLower(t *testing.T) {
t.Parallel()
tests := []struct {
value []string
expected []string
}{
{[]string{"a", "b", "c"}, []string{"a", "b", "c"}},
{[]string{"a", "B", "c"}, []string{"a", "b", "c"}},
{[]string{"A", "B", "C"}, []string{"a", "b", "c"}},
}
for _, test := range tests {
res := SliceToLower(test.value)
for i, val := range res {
if val != test.expected[i] {
t.Errorf("Case mismatch. Expected %s, got %s", test.expected[i], res[i])
}
}
}
}
func TestReaderContains(t *testing.T) {
for i, this := range append(containsBenchTestData, containsAdditionalTestData...) {
result := ReaderContains(strings.NewReader(this.v1), this.v2)

View file

@ -1,4 +1,4 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -86,6 +86,13 @@ func (p *PathSpec) MakePath(s string) string {
return p.UnicodeSanitize(s)
}
// MakePathsSanitized applies MakePathSanitized on every item in the slice
func (p *PathSpec) MakePathsSanitized(paths []string) {
for i, path := range paths {
paths[i] = p.MakePathSanitized(path)
}
}
// MakePathSanitized creates a Unicode-sanitized string, with the spaces replaced
func (p *PathSpec) MakePathSanitized(s string) string {
if p.DisablePathToLower {

View file

@ -1,4 +1,4 @@
// Copyright 2016 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -56,7 +56,7 @@ type highlighters struct {
}
func newHiglighters(cs *ContentSpec) highlighters {
return highlighters{cs: cs, ignoreCache: cs.cfg.GetBool("ignoreCache"), cacheDir: cs.cfg.GetString("cacheDir")}
return highlighters{cs: cs, ignoreCache: cs.Cfg.GetBool("ignoreCache"), cacheDir: cs.Cfg.GetString("cacheDir")}
}
func (h highlighters) chromaHighlight(code, lang, optsStr string) (string, error) {

View file

@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -14,8 +14,13 @@
package htesting
import (
"html/template"
"time"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/navigation"
"github.com/gohugoio/hugo/resources/page"
"github.com/spf13/viper"
)
@ -28,6 +33,22 @@ func (t testSite) Hugo() hugo.Info {
return t.h
}
func (t testSite) ServerPort() int {
return 1313
}
func (testSite) LastChange() (t time.Time) {
return
}
func (t testSite) Title() string {
return "foo"
}
func (t testSite) Sites() page.Sites {
return nil
}
func (t testSite) IsServer() bool {
return false
}
@ -36,8 +57,36 @@ func (t testSite) Language() *langs.Language {
return t.l
}
func (t testSite) Pages() page.Pages {
return nil
}
func (t testSite) RegularPages() page.Pages {
return nil
}
func (t testSite) Menus() navigation.Menus {
return nil
}
func (t testSite) Taxonomies() interface{} {
return nil
}
func (t testSite) BaseURL() template.URL {
return ""
}
func (t testSite) Params() map[string]interface{} {
return nil
}
func (t testSite) Data() map[string]interface{} {
return nil
}
// NewTestHugoSite creates a new minimal test site.
func NewTestHugoSite() hugo.Site {
func NewTestHugoSite() page.Site {
return testSite{
h: hugo.NewInfo(hugo.EnvironmentProduction),
l: langs.NewLanguage("en", newTestConfig()),

View file

@ -0,0 +1,99 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugofs
import (
"fmt"
"os"
"sort"
"strings"
"sync"
"github.com/spf13/afero"
)
// Reseter is implemented by some of the stateful filesystems.
type Reseter interface {
Reset()
}
// DuplicatesReporter reports about duplicate filenames.
type DuplicatesReporter interface {
ReportDuplicates() string
}
func NewCreateCountingFs(fs afero.Fs) afero.Fs {
return &createCountingFs{Fs: fs, fileCount: make(map[string]int)}
}
// ReportDuplicates reports filenames written more than once.
func (c *createCountingFs) ReportDuplicates() string {
c.mu.Lock()
defer c.mu.Unlock()
var dupes []string
for k, v := range c.fileCount {
if v > 1 {
dupes = append(dupes, fmt.Sprintf("%s (%d)", k, v))
}
}
if len(dupes) == 0 {
return ""
}
sort.Strings(dupes)
return strings.Join(dupes, ", ")
}
// createCountingFs counts filenames of created files or files opened
// for writing.
type createCountingFs struct {
afero.Fs
mu sync.Mutex
fileCount map[string]int
}
func (c *createCountingFs) Reset() {
c.mu.Lock()
defer c.mu.Unlock()
c.fileCount = make(map[string]int)
}
func (fs *createCountingFs) onCreate(filename string) {
fs.mu.Lock()
defer fs.mu.Unlock()
fs.fileCount[filename] = fs.fileCount[filename] + 1
}
func (fs *createCountingFs) Create(name string) (afero.File, error) {
f, err := fs.Fs.Create(name)
if err == nil {
fs.onCreate(name)
}
return f, err
}
func (fs *createCountingFs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) {
f, err := fs.Fs.OpenFile(name, flag, perm)
if err == nil && isWrite(flag) {
fs.onCreate(name)
}
return f, err
}

View file

@ -1,4 +1,4 @@
// Copyright 2016 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -15,6 +15,8 @@
package hugofs
import (
"os"
"github.com/gohugoio/hugo/config"
"github.com/spf13/afero"
)
@ -80,3 +82,7 @@ func getWorkingDirFs(base afero.Fs, cfg config.Provider) *afero.BasePathFs {
return nil
}
func isWrite(flag int) bool {
return flag&os.O_RDWR != 0 || flag&os.O_WRONLY != 0
}

View file

@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -67,10 +67,6 @@ func (fs *md5HashingFs) wrapFile(f afero.File) afero.File {
return &hashingFile{File: f, h: md5.New(), hashReceiver: fs.hashReceiver}
}
func isWrite(flag int) bool {
return flag&os.O_RDWR != 0 || flag&os.O_WRONLY != 0
}
func (fs *md5HashingFs) Name() string {
return "md5HashingFs"
}

70
hugofs/stacktracer_fs.go Normal file
View file

@ -0,0 +1,70 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugofs
import (
"fmt"
"os"
"regexp"
"runtime"
"github.com/gohugoio/hugo/common/types"
"github.com/spf13/afero"
)
// Make sure we don't accidently use this in the real Hugo.
var _ types.DevMarker = (*stacktracerFs)(nil)
// NewStacktracerFs wraps the given fs printing stack traces for file creates
// matching the given regexp pattern.
func NewStacktracerFs(fs afero.Fs, pattern string) afero.Fs {
return &stacktracerFs{Fs: fs, re: regexp.MustCompile(pattern)}
}
// stacktracerFs can be used in hard-to-debug development situations where
// you get some input you don't understand where comes from.
type stacktracerFs struct {
afero.Fs
// Will print stacktrace for every file creates matching this pattern.
re *regexp.Regexp
}
func (fs *stacktracerFs) DevOnly() {
}
func (fs *stacktracerFs) onCreate(filename string) {
if fs.re.MatchString(filename) {
trace := make([]byte, 1500)
runtime.Stack(trace, true)
fmt.Printf("\n===========\n%q:\n%s\n", filename, trace)
}
}
func (fs *stacktracerFs) Create(name string) (afero.File, error) {
f, err := fs.Fs.Create(name)
if err == nil {
fs.onCreate(name)
}
return f, err
}
func (fs *stacktracerFs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) {
f, err := fs.Fs.OpenFile(name, flag, perm)
if err == nil && isWrite(flag) {
fs.onCreate(name)
}
return f, err
}

View file

@ -1,4 +1,4 @@
// Copyright 2017 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -26,6 +26,7 @@ import (
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/publisher"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/helpers"
@ -55,7 +56,12 @@ func newAliasHandler(t tpl.TemplateFinder, l *loggers.Logger, allowRoot bool) al
return aliasHandler{t, l, allowRoot}
}
func (a aliasHandler) renderAlias(isXHTML bool, permalink string, page *Page) (io.Reader, error) {
type aliasPage struct {
Permalink string
page.Page
}
func (a aliasHandler) renderAlias(isXHTML bool, permalink string, p page.Page) (io.Reader, error) {
t := "alias"
if isXHTML {
t = "alias-xhtml"
@ -75,12 +81,9 @@ func (a aliasHandler) renderAlias(isXHTML bool, permalink string, page *Page) (i
}
}
data := struct {
Permalink string
Page *Page
}{
data := aliasPage{
permalink,
page,
p,
}
buffer := new(bytes.Buffer)
@ -91,11 +94,11 @@ func (a aliasHandler) renderAlias(isXHTML bool, permalink string, page *Page) (i
return buffer, nil
}
func (s *Site) writeDestAlias(path, permalink string, outputFormat output.Format, p *Page) (err error) {
func (s *Site) writeDestAlias(path, permalink string, outputFormat output.Format, p page.Page) (err error) {
return s.publishDestAlias(false, path, permalink, outputFormat, p)
}
func (s *Site) publishDestAlias(allowRoot bool, path, permalink string, outputFormat output.Format, p *Page) (err error) {
func (s *Site) publishDestAlias(allowRoot bool, path, permalink string, outputFormat output.Format, p page.Page) (err error) {
handler := newAliasHandler(s.Tmpl, s.Log, allowRoot)
isXHTML := strings.HasSuffix(path, ".xhtml")
@ -126,19 +129,19 @@ func (s *Site) publishDestAlias(allowRoot bool, path, permalink string, outputFo
func (a aliasHandler) targetPathAlias(src string) (string, error) {
originalAlias := src
if len(src) <= 0 {
return "", fmt.Errorf("Alias \"\" is an empty string")
return "", fmt.Errorf("alias \"\" is an empty string")
}
alias := filepath.Clean(src)
components := strings.Split(alias, helpers.FilePathSeparator)
if !a.allowRoot && alias == helpers.FilePathSeparator {
return "", fmt.Errorf("Alias \"%s\" resolves to website root directory", originalAlias)
return "", fmt.Errorf("alias \"%s\" resolves to website root directory", originalAlias)
}
// Validate against directory traversal
if components[0] == ".." {
return "", fmt.Errorf("Alias \"%s\" traverses outside the website root directory", originalAlias)
return "", fmt.Errorf("alias \"%s\" traverses outside the website root directory", originalAlias)
}
// Handle Windows file and directory naming restrictions
@ -171,7 +174,7 @@ func (a aliasHandler) targetPathAlias(src string) (string, error) {
for _, m := range msgs {
a.log.ERROR.Println(m)
}
return "", fmt.Errorf("Cannot create \"%s\": Windows filename restriction", originalAlias)
return "", fmt.Errorf("cannot create \"%s\": Windows filename restriction", originalAlias)
}
for _, m := range msgs {
a.log.INFO.Println(m)

View file

@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -50,7 +50,7 @@ func TestAlias(t *testing.T) {
b.CreateSites().Build(BuildCfg{})
assert.Equal(1, len(b.H.Sites))
require.Len(t, b.H.Sites[0].RegularPages, 1)
require.Len(t, b.H.Sites[0].RegularPages(), 1)
// the real page
b.AssertFileContent("public/page/index.html", "For some moments the old man")

View file

@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -14,19 +14,13 @@
package hugolib
import (
"fmt"
"github.com/gohugoio/hugo/resources/resource"
"github.com/gohugoio/hugo/common/collections"
"github.com/gohugoio/hugo/resources/page"
)
var (
_ collections.Grouper = (*Page)(nil)
_ collections.Slicer = (*Page)(nil)
_ collections.Slicer = PageGroup{}
_ collections.Slicer = WeightedPage{}
_ resource.ResourcesConverter = Pages{}
_ collections.Grouper = (*pageState)(nil)
_ collections.Slicer = (*pageState)(nil)
)
// collections.Slicer implementations below. We keep these bridge implementations
@ -35,50 +29,8 @@ var (
// Slice is not meant to be used externally. It's a bridge function
// for the template functions. See collections.Slice.
func (p *Page) Slice(items interface{}) (interface{}, error) {
return toPages(items)
}
// Slice is not meant to be used externally. It's a bridge function
// for the template functions. See collections.Slice.
func (p PageGroup) Slice(in interface{}) (interface{}, error) {
switch items := in.(type) {
case PageGroup:
return items, nil
case []interface{}:
groups := make(PagesGroup, len(items))
for i, v := range items {
g, ok := v.(PageGroup)
if !ok {
return nil, fmt.Errorf("type %T is not a PageGroup", v)
}
groups[i] = g
}
return groups, nil
default:
return nil, fmt.Errorf("invalid slice type %T", items)
}
}
// Slice is not meant to be used externally. It's a bridge function
// for the template functions. See collections.Slice.
func (p WeightedPage) Slice(in interface{}) (interface{}, error) {
switch items := in.(type) {
case WeightedPages:
return items, nil
case []interface{}:
weighted := make(WeightedPages, len(items))
for i, v := range items {
g, ok := v.(WeightedPage)
if !ok {
return nil, fmt.Errorf("type %T is not a WeightedPage", v)
}
weighted[i] = g
}
return weighted, nil
default:
return nil, fmt.Errorf("invalid slice type %T", items)
}
func (p *pageState) Slice(items interface{}) (interface{}, error) {
return page.ToPages(items)
}
// collections.Grouper implementations below
@ -86,19 +38,10 @@ func (p WeightedPage) Slice(in interface{}) (interface{}, error) {
// Group creates a PageGroup from a key and a Pages object
// This method is not meant for external use. It got its non-typed arguments to satisfy
// a very generic interface in the tpl package.
func (p *Page) Group(key interface{}, in interface{}) (interface{}, error) {
pages, err := toPages(in)
func (p *pageState) Group(key interface{}, in interface{}) (interface{}, error) {
pages, err := page.ToPages(in)
if err != nil {
return nil, err
}
return PageGroup{Key: key, Pages: pages}, nil
}
// ToResources wraps resource.ResourcesConverter
func (pages Pages) ToResources() resource.Resources {
r := make(resource.Resources, len(pages))
for i, p := range pages {
r[i] = p
}
return r
return page.PageGroup{Key: key, Pages: pages}, nil
}

View file

@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -40,7 +40,7 @@ title: "Page"
b.CreateSites().Build(BuildCfg{})
assert.Equal(1, len(b.H.Sites))
require.Len(t, b.H.Sites[0].RegularPages, 2)
require.Len(t, b.H.Sites[0].RegularPages(), 2)
b.AssertFileContent("public/index.html", "cool: 2")
}
@ -79,12 +79,12 @@ tags_weight: %d
b.CreateSites().Build(BuildCfg{})
assert.Equal(1, len(b.H.Sites))
require.Len(t, b.H.Sites[0].RegularPages, 2)
require.Len(t, b.H.Sites[0].RegularPages(), 2)
b.AssertFileContent("public/index.html",
"pages:2:hugolib.Pages:Page(/page1.md)/Page(/page2.md)",
"pageGroups:2:hugolib.PagesGroup:Page(/page1.md)/Page(/page2.md)",
`weightedPages:2::hugolib.WeightedPages:[WeightedPage(10,"Page") WeightedPage(20,"Page")]`)
"pages:2:page.Pages:Page(/page1.md)/Page(/page2.md)",
"pageGroups:2:page.PagesGroup:Page(/page1.md)/Page(/page2.md)",
`weightedPages:2::page.WeightedPages:[WeightedPage(10,"Page") WeightedPage(20,"Page")]`)
}
func TestAppendFunc(t *testing.T) {
@ -129,11 +129,11 @@ tags_weight: %d
b.CreateSites().Build(BuildCfg{})
assert.Equal(1, len(b.H.Sites))
require.Len(t, b.H.Sites[0].RegularPages, 2)
require.Len(t, b.H.Sites[0].RegularPages(), 2)
b.AssertFileContent("public/index.html",
"pages:2:hugolib.Pages:Page(/page2.md)/Page(/page1.md)",
"appendPages:9:hugolib.Pages:home/page",
"pages:2:page.Pages:Page(/page2.md)/Page(/page1.md)",
"appendPages:9:page.Pages:home/page",
"appendStrings:[]string:[a b c d e]",
"appendStringsSlice:[]string:[a b c c d]",
"union:[]string:[a b c d e]",

View file

@ -1,4 +1,4 @@
// Copyright 2016-present The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -24,7 +24,6 @@ import (
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/hugolib/paths"
"github.com/pkg/errors"
_errors "github.com/pkg/errors"
@ -177,14 +176,6 @@ type configLoader struct {
ConfigSourceDescriptor
}
func (l configLoader) wrapFileInfoError(err error, fi os.FileInfo) error {
rfi, ok := fi.(hugofs.RealFilenameInfo)
if !ok {
return err
}
return l.wrapFileError(err, rfi.RealFilename())
}
func (l configLoader) loadConfig(configName string, v *viper.Viper) (string, error) {
baseDir := l.configFileDir()
var baseFilename string
@ -240,11 +231,6 @@ func (l configLoader) wrapFileError(err error, filename string) error {
return err
}
func (l configLoader) newRealBaseFs(path string) afero.Fs {
return hugofs.NewBasePathRealFilenameFs(afero.NewBasePathFs(l.Fs, path).(*afero.BasePathFs))
}
func (l configLoader) loadConfigFromConfigDir(v *viper.Viper) ([]string, error) {
sourceFs := l.Fs
configDir := l.AbsConfigDir
@ -274,7 +260,7 @@ func (l configLoader) loadConfigFromConfigDir(v *viper.Viper) ([]string, error)
for _, configDir := range configDirs {
err := afero.Walk(sourceFs, configDir, func(path string, fi os.FileInfo, err error) error {
if fi == nil {
if fi == nil || err != nil {
return nil
}
@ -616,8 +602,8 @@ func loadDefaultSettingsFor(v *viper.Viper) error {
v.SetDefault("removePathAccents", false)
v.SetDefault("titleCaseStyle", "AP")
v.SetDefault("taxonomies", map[string]string{"tag": "tags", "category": "categories"})
v.SetDefault("permalinks", make(PermalinkOverrides, 0))
v.SetDefault("sitemap", Sitemap{Priority: -1, Filename: "sitemap.xml"})
v.SetDefault("permalinks", make(map[string]string))
v.SetDefault("sitemap", config.Sitemap{Priority: -1, Filename: "sitemap.xml"})
v.SetDefault("pygmentsStyle", "monokai")
v.SetDefault("pygmentsUseClasses", false)
v.SetDefault("pygmentsCodeFences", false)
@ -625,7 +611,6 @@ func loadDefaultSettingsFor(v *viper.Viper) error {
v.SetDefault("pygmentsOptions", "")
v.SetDefault("disableLiveReload", false)
v.SetDefault("pluralizeListTitles", true)
v.SetDefault("preserveTaxonomyNames", false)
v.SetDefault("forceSyncStatic", false)
v.SetDefault("footnoteAnchorPrefix", "")
v.SetDefault("footnoteReturnLinkContents", "")

View file

@ -1,4 +1,4 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -349,7 +349,7 @@ func doTestDataDirImpl(t *testing.T, dd dataDir, expected interface{}, configKey
s := buildSingleSiteExpected(t, false, expectBuildError, depsCfg, BuildCfg{SkipRender: true})
if !expectBuildError && !reflect.DeepEqual(expected, s.Data) {
if !expectBuildError && !reflect.DeepEqual(expected, s.h.Data()) {
// This disabled code detects the situation described in the WARNING message below.
// The situation seems to only occur for TOML data with integer values.
// Perhaps the TOML parser returns ints in another type.
@ -366,7 +366,7 @@ func doTestDataDirImpl(t *testing.T, dd dataDir, expected interface{}, configKey
}
*/
return fmt.Sprintf("Expected data:\n%v got\n%v\n\nExpected type structure:\n%#[1]v got\n%#[2]v", expected, s.Data)
return fmt.Sprintf("Expected data:\n%v got\n%v\n\nExpected type structure:\n%#[1]v got\n%#[2]v", expected, s.h.Data())
}
return

View file

@ -1,4 +1,4 @@
// Copyright 2016 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -18,6 +18,8 @@ import (
"fmt"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/deps"
"github.com/spf13/afero"
@ -33,13 +35,13 @@ func TestDisableKindsNoneDisabled(t *testing.T) {
func TestDisableKindsSomeDisabled(t *testing.T) {
t.Parallel()
doTestDisableKinds(t, KindSection, kind404)
doTestDisableKinds(t, page.KindSection, kind404)
}
func TestDisableKindsOneDisabled(t *testing.T) {
t.Parallel()
for _, kind := range allKinds {
if kind == KindPage {
if kind == page.KindPage {
// Turning off regular page generation have some side-effects
// not handled by the assertions below (no sections), so
// skip that for now.
@ -124,64 +126,64 @@ func assertDisabledKinds(th testHelper, s *Site, disabled ...string) {
assertDisabledKind(th,
func(isDisabled bool) bool {
if isDisabled {
return len(s.RegularPages) == 0
return len(s.RegularPages()) == 0
}
return len(s.RegularPages) > 0
}, disabled, KindPage, "public/sect/p1/index.html", "Single|P1")
return len(s.RegularPages()) > 0
}, disabled, page.KindPage, "public/sect/p1/index.html", "Single|P1")
assertDisabledKind(th,
func(isDisabled bool) bool {
p := s.getPage(KindHome)
p := s.getPage(page.KindHome)
if isDisabled {
return p == nil
}
return p != nil
}, disabled, KindHome, "public/index.html", "Home")
}, disabled, page.KindHome, "public/index.html", "Home")
assertDisabledKind(th,
func(isDisabled bool) bool {
p := s.getPage(KindSection, "sect")
p := s.getPage(page.KindSection, "sect")
if isDisabled {
return p == nil
}
return p != nil
}, disabled, KindSection, "public/sect/index.html", "Sects")
}, disabled, page.KindSection, "public/sect/index.html", "Sects")
assertDisabledKind(th,
func(isDisabled bool) bool {
p := s.getPage(KindTaxonomy, "tags", "tag1")
p := s.getPage(page.KindTaxonomy, "tags", "tag1")
if isDisabled {
return p == nil
}
return p != nil
}, disabled, KindTaxonomy, "public/tags/tag1/index.html", "Tag1")
}, disabled, page.KindTaxonomy, "public/tags/tag1/index.html", "Tag1")
assertDisabledKind(th,
func(isDisabled bool) bool {
p := s.getPage(KindTaxonomyTerm, "tags")
p := s.getPage(page.KindTaxonomyTerm, "tags")
if isDisabled {
return p == nil
}
return p != nil
}, disabled, KindTaxonomyTerm, "public/tags/index.html", "Tags")
}, disabled, page.KindTaxonomyTerm, "public/tags/index.html", "Tags")
assertDisabledKind(th,
func(isDisabled bool) bool {
p := s.getPage(KindTaxonomyTerm, "categories")
p := s.getPage(page.KindTaxonomyTerm, "categories")
if isDisabled {
return p == nil
}
return p != nil
}, disabled, KindTaxonomyTerm, "public/categories/index.html", "Category Terms")
}, disabled, page.KindTaxonomyTerm, "public/categories/index.html", "Category Terms")
assertDisabledKind(th,
func(isDisabled bool) bool {
p := s.getPage(KindTaxonomy, "categories", "hugo")
p := s.getPage(page.KindTaxonomy, "categories", "hugo")
if isDisabled {
return p == nil
}
return p != nil
}, disabled, KindTaxonomy, "public/categories/hugo/index.html", "Hugo")
}, disabled, page.KindTaxonomy, "public/categories/hugo/index.html", "Hugo")
// The below have no page in any collection.
assertDisabledKind(th, func(isDisabled bool) bool { return true }, disabled, kindRSS, "public/index.xml", "<link>")
assertDisabledKind(th, func(isDisabled bool) bool { return true }, disabled, kindSitemap, "public/sitemap.xml", "sitemap")
@ -195,7 +197,7 @@ func assertDisabledKind(th testHelper, kindAssert func(bool) bool, disabled []st
if kind == kindRSS && !isDisabled {
// If the home page is also disabled, there is not RSS to look for.
if stringSliceContains(KindHome, disabled...) {
if stringSliceContains(page.KindHome, disabled...) {
isDisabled = true
}
}

View file

@ -1,4 +1,4 @@
// Copyright 2016 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -20,6 +20,8 @@ import (
"strings"
"testing"
"github.com/spf13/cast"
"path/filepath"
"github.com/gohugoio/hugo/deps"
@ -67,9 +69,11 @@ func doTestShortcodeCrossrefs(t *testing.T, relative bool) {
s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{})
require.Len(t, s.RegularPages, 1)
require.Len(t, s.RegularPages(), 1)
output := string(s.RegularPages[0].content())
content, err := s.RegularPages()[0].Content()
require.NoError(t, err)
output := cast.ToString(content)
if !strings.Contains(output, expected) {
t.Errorf("Got\n%q\nExpected\n%q", output, expected)

View file

@ -1,4 +1,4 @@
// Copyright 2016-present The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -19,6 +19,7 @@ import (
"github.com/bep/gitmap"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/resources/page"
)
type gitInfo struct {
@ -26,15 +27,12 @@ type gitInfo struct {
repo *gitmap.GitRepo
}
func (g *gitInfo) forPage(p *Page) (*gitmap.GitInfo, bool) {
if g == nil {
return nil, false
}
name := strings.TrimPrefix(filepath.ToSlash(p.Filename()), g.contentDir)
func (g *gitInfo) forPage(p page.Page) *gitmap.GitInfo {
name := strings.TrimPrefix(filepath.ToSlash(p.File().Filename()), g.contentDir)
name = strings.TrimPrefix(name, "/")
return g.repo.Files[name], true
return g.repo.Files[name]
}
func newGitInfo(cfg config.Provider) (*gitInfo, error) {

View file

@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -14,14 +14,24 @@
package hugolib
import (
"errors"
"io"
"path/filepath"
"sort"
"strings"
"sync"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/gohugoio/hugo/hugofs"
"github.com/pkg/errors"
"github.com/gohugoio/hugo/source"
"github.com/bep/gitmap"
"github.com/gohugoio/hugo/config"
"github.com/spf13/afero"
"github.com/gohugoio/hugo/publisher"
@ -30,8 +40,10 @@ import (
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/lazy"
"github.com/gohugoio/hugo/i18n"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/tpl/tplimpl"
)
@ -48,17 +60,96 @@ type HugoSites struct {
// If this is running in the dev server.
running bool
// Render output formats for all sites.
renderFormats output.Formats
*deps.Deps
gitInfo *gitInfo
// As loaded from the /data dirs
data map[string]interface{}
// Keeps track of bundle directories and symlinks to enable partial rebuilding.
ContentChanges *contentChangeMap
// If enabled, keeps a revision map for all content.
gitInfo *gitInfo
init *hugoSitesInit
*fatalErrorHandler
}
func (h *HugoSites) siteInfos() SiteInfos {
infos := make(SiteInfos, len(h.Sites))
type fatalErrorHandler struct {
mu sync.Mutex
h *HugoSites
err error
done bool
donec chan bool // will be closed when done
}
// FatalError error is used in some rare situations where it does not make sense to
// continue processing, to abort as soon as possible and log the error.
func (f *fatalErrorHandler) FatalError(err error) {
f.mu.Lock()
defer f.mu.Unlock()
if !f.done {
f.done = true
close(f.donec)
}
f.err = err
}
func (f *fatalErrorHandler) getErr() error {
f.mu.Lock()
defer f.mu.Unlock()
return f.err
}
func (f *fatalErrorHandler) Done() <-chan bool {
return f.donec
}
type hugoSitesInit struct {
// Loads the data from all of the /data folders.
data *lazy.Init
// Loads the Git info for all the pages if enabled.
gitInfo *lazy.Init
// Maps page translations.
translations *lazy.Init
}
func (h *hugoSitesInit) Reset() {
h.data.Reset()
h.gitInfo.Reset()
h.translations.Reset()
}
func (h *HugoSites) Data() map[string]interface{} {
if _, err := h.init.data.Do(); err != nil {
h.SendError(errors.Wrap(err, "failed to load data"))
return nil
}
return h.data
}
func (h *HugoSites) gitInfoForPage(p page.Page) (*gitmap.GitInfo, error) {
if _, err := h.init.gitInfo.Do(); err != nil {
return nil, err
}
if h.gitInfo == nil {
return nil, nil
}
return h.gitInfo.forPage(p), nil
}
func (h *HugoSites) siteInfos() page.Sites {
infos := make(page.Sites, len(h.Sites))
for i, site := range h.Sites {
infos[i] = &site.Info
}
@ -106,7 +197,7 @@ func (h *HugoSites) IsMultihost() bool {
func (h *HugoSites) LanguageSet() map[string]bool {
set := make(map[string]bool)
for _, s := range h.Sites {
set[s.Language.Lang] = true
set[s.language.Lang] = true
}
return set
}
@ -129,14 +220,14 @@ func (h *HugoSites) PrintProcessingStats(w io.Writer) {
func (h *HugoSites) langSite() map[string]*Site {
m := make(map[string]*Site)
for _, s := range h.Sites {
m[s.Language.Lang] = s
m[s.language.Lang] = s
}
return m
}
// GetContentPage finds a Page with content given the absolute filename.
// Returns nil if none found.
func (h *HugoSites) GetContentPage(filename string) *Page {
func (h *HugoSites) GetContentPage(filename string) page.Page {
for _, s := range h.Sites {
pos := s.rawAllPages.findPagePosByFilename(filename)
if pos == -1 {
@ -178,10 +269,40 @@ func newHugoSites(cfg deps.DepsCfg, sites ...*Site) (*HugoSites, error) {
running: cfg.Running,
multilingual: langConfig,
multihost: cfg.Cfg.GetBool("multihost"),
Sites: sites}
Sites: sites,
init: &hugoSitesInit{
data: lazy.New(),
gitInfo: lazy.New(),
translations: lazy.New(),
},
}
h.fatalErrorHandler = &fatalErrorHandler{
h: h,
donec: make(chan bool),
}
h.init.data.Add(func() (interface{}, error) {
err := h.loadData(h.PathSpec.BaseFs.Data.Fs)
return err, nil
})
h.init.translations.Add(func() (interface{}, error) {
if len(h.Sites) > 1 {
allTranslations := pagesToTranslationsMap(h.Sites)
assignTranslationsToPages(allTranslations, h.Sites)
}
return nil, nil
})
h.init.gitInfo.Add(func() (interface{}, error) {
err := h.loadGitInfo()
return nil, err
})
for _, s := range sites {
s.owner = h
s.h = h
}
if err := applyDeps(cfg, sites...); err != nil {
@ -197,14 +318,10 @@ func newHugoSites(cfg deps.DepsCfg, sites ...*Site) (*HugoSites, error) {
h.ContentChanges = contentChangeTracker
}
if err := h.initGitInfo(); err != nil {
return nil, err
}
return h, nil
}
func (h *HugoSites) initGitInfo() error {
func (h *HugoSites) loadGitInfo() error {
if h.Cfg.GetBool("enableGitInfo") {
gi, err := newGitInfo(h.Cfg)
if err != nil {
@ -247,16 +364,16 @@ func applyDeps(cfg deps.DepsCfg, sites ...*Site) error {
d.Site = &s.Info
siteConfig, err := loadSiteConfig(s.Language)
siteConfig, err := loadSiteConfig(s.language)
if err != nil {
return err
}
s.siteConfig = siteConfig
s.siteRefLinker, err = newSiteRefLinker(s.Language, s)
s.siteConfigConfig = siteConfig
s.siteRefLinker, err = newSiteRefLinker(s.language, s)
return err
}
cfg.Language = s.Language
cfg.Language = s.language
cfg.MediaTypes = s.mediaTypesConfig
cfg.OutputFormats = s.outputFormatsConfig
@ -347,12 +464,24 @@ func createSitesFromConfig(cfg deps.DepsCfg) ([]*Site, error) {
return sites, nil
}
// Reset resets the sites and template caches, making it ready for a full rebuild.
func (h *HugoSites) reset() {
// Reset resets the sites and template caches etc., making it ready for a full rebuild.
func (h *HugoSites) reset(config *BuildCfg) {
if config.ResetState {
for i, s := range h.Sites {
h.Sites[i] = s.reset()
if r, ok := s.Fs.Destination.(hugofs.Reseter); ok {
r.Reset()
}
}
}
h.fatalErrorHandler = &fatalErrorHandler{
h: h,
donec: make(chan bool),
}
h.init.Reset()
}
// resetLogs resets the log counters etc. Used to do a new build on the same sites.
func (h *HugoSites) resetLogs() {
@ -387,7 +516,7 @@ func (h *HugoSites) createSitesFromConfig(cfg config.Provider) error {
h.Sites = sites
for _, s := range sites {
s.owner = h
s.h = h
}
if err := applyDeps(depsCfg, sites...); err != nil {
@ -435,7 +564,10 @@ type BuildCfg struct {
// Note that a page does not have to have a content page / file.
// For regular builds, this will allways return true.
// TODO(bep) rename/work this.
func (cfg *BuildCfg) shouldRender(p *Page) bool {
func (cfg *BuildCfg) shouldRender(p *pageState) bool {
if !p.render {
return false
}
if p.forceRender {
p.forceRender = false
return true
@ -445,15 +577,8 @@ func (cfg *BuildCfg) shouldRender(p *Page) bool {
return true
}
if cfg.RecentlyVisited[p.RelPermalink()] {
if cfg.PartialReRender {
_ = p.initMainOutputFormat()
}
return true
}
if cfg.whatChanged != nil && p.File != nil {
return cfg.whatChanged.files[p.File.Filename()]
if cfg.whatChanged != nil && p.File() != nil {
return cfg.whatChanged.files[p.File().Filename()]
}
return false
@ -477,100 +602,85 @@ func (h *HugoSites) renderCrossSitesArtifacts() error {
return nil
}
// TODO(bep) DRY
sitemapDefault := parseSitemap(h.Cfg.GetStringMap("sitemap"))
s := h.Sites[0]
smLayouts := []string{"sitemapindex.xml", "_default/sitemapindex.xml", "_internal/_default/sitemapindex.xml"}
return s.renderAndWriteXML(&s.PathSpec.ProcessingStats.Sitemaps, "sitemapindex",
sitemapDefault.Filename, h.toSiteInfos(), smLayouts...)
}
func (h *HugoSites) assignMissingTranslations() error {
// This looks heavy, but it should be a small number of nodes by now.
allPages := h.findAllPagesByKindNotIn(KindPage)
for _, nodeType := range []string{KindHome, KindSection, KindTaxonomy, KindTaxonomyTerm} {
nodes := h.findPagesByKindIn(nodeType, allPages)
// Assign translations
for _, t1 := range nodes {
for _, t2 := range nodes {
if t1.isNewTranslation(t2) {
t1.translations = append(t1.translations, t2)
}
}
}
}
// Now we can sort the translations.
for _, p := range allPages {
if len(p.translations) > 0 {
pageBy(languagePageSort).Sort(p.translations)
}
}
return nil
s.siteCfg.sitemap.Filename, h.toSiteInfos(), smLayouts...)
}
// createMissingPages creates home page, taxonomies etc. that isnt't created as an
// effect of having a content file.
func (h *HugoSites) createMissingPages() error {
var newPages Pages
var newPages pageStatePages
for _, s := range h.Sites {
if s.isEnabled(KindHome) {
if s.isEnabled(page.KindHome) {
// home pages
home := s.findPagesByKind(KindHome)
if len(home) > 1 {
homes := s.findWorkPagesByKind(page.KindHome)
if len(homes) > 1 {
panic("Too many homes")
}
if len(home) == 0 {
n := s.newHomePage()
s.Pages = append(s.Pages, n)
newPages = append(newPages, n)
var home *pageState
if len(homes) == 0 {
home = s.newPage(page.KindHome)
s.workAllPages = append(s.workAllPages, home)
newPages = append(newPages, home)
} else {
home = homes[0]
}
s.home = home
}
// Will create content-less root sections.
newSections := s.assembleSections()
s.Pages = append(s.Pages, newSections...)
s.workAllPages = append(s.workAllPages, newSections...)
newPages = append(newPages, newSections...)
taxonomyTermEnabled := s.isEnabled(page.KindTaxonomyTerm)
taxonomyEnabled := s.isEnabled(page.KindTaxonomy)
// taxonomy list and terms pages
taxonomies := s.Language.GetStringMapString("taxonomies")
taxonomies := s.Language().GetStringMapString("taxonomies")
if len(taxonomies) > 0 {
taxonomyPages := s.findPagesByKind(KindTaxonomy)
taxonomyTermsPages := s.findPagesByKind(KindTaxonomyTerm)
taxonomyPages := s.findWorkPagesByKind(page.KindTaxonomy)
taxonomyTermsPages := s.findWorkPagesByKind(page.KindTaxonomyTerm)
// Make them navigable from WeightedPage etc.
for _, p := range taxonomyPages {
p.getTaxonomyNodeInfo().TransferValues(p)
}
for _, p := range taxonomyTermsPages {
p.getTaxonomyNodeInfo().TransferValues(p)
}
for _, plural := range taxonomies {
if s.isEnabled(KindTaxonomyTerm) {
if taxonomyTermEnabled {
foundTaxonomyTermsPage := false
for _, p := range taxonomyTermsPages {
if p.sectionsPath() == plural {
if p.SectionsPath() == plural {
foundTaxonomyTermsPage = true
break
}
}
if !foundTaxonomyTermsPage {
n := s.newTaxonomyTermsPage(plural)
s.Pages = append(s.Pages, n)
n := s.newPage(page.KindTaxonomyTerm, plural)
n.getTaxonomyNodeInfo().TransferValues(n)
s.workAllPages = append(s.workAllPages, n)
newPages = append(newPages, n)
}
}
if s.isEnabled(KindTaxonomy) {
for key := range s.Taxonomies[plural] {
foundTaxonomyPage := false
origKey := key
if taxonomyEnabled {
for termKey := range s.Taxonomies[plural] {
foundTaxonomyPage := false
if s.Info.preserveTaxonomyNames {
key = s.PathSpec.MakePathSanitized(key)
}
for _, p := range taxonomyPages {
sectionsPath := p.sectionsPath()
sectionsPath := p.SectionsPath()
if !strings.HasPrefix(sectionsPath, plural) {
continue
@ -579,20 +689,21 @@ func (h *HugoSites) createMissingPages() error {
singularKey := strings.TrimPrefix(sectionsPath, plural)
singularKey = strings.TrimPrefix(singularKey, "/")
// Some people may have /authors/MaxMustermann etc. as paths.
// p.sections contains the raw values from the file system.
// See https://github.com/gohugoio/hugo/issues/4238
singularKey = s.PathSpec.MakePathSanitized(singularKey)
if singularKey == key {
if singularKey == termKey {
foundTaxonomyPage = true
break
}
}
if !foundTaxonomyPage {
n := s.newTaxonomyPage(plural, origKey)
s.Pages = append(s.Pages, n)
info := s.taxonomyNodes.Get(plural, termKey)
if info == nil {
panic("no info found")
}
n := s.newTaxonomyPage(info.term, info.plural, info.termKey)
info.TransferValues(n)
s.workAllPages = append(s.workAllPages, n)
newPages = append(newPages, n)
}
}
@ -601,24 +712,6 @@ func (h *HugoSites) createMissingPages() error {
}
}
if len(newPages) > 0 {
// This resorting is unfortunate, but it also needs to be sorted
// when sections are created.
first := h.Sites[0]
first.AllPages = append(first.AllPages, newPages...)
first.AllPages.sort()
for _, s := range h.Sites {
s.Pages.sort()
}
for i := 1; i < len(h.Sites); i++ {
h.Sites[i].AllPages = first.AllPages
}
}
return nil
}
@ -628,61 +721,58 @@ func (h *HugoSites) removePageByFilename(filename string) {
}
}
func (h *HugoSites) setupTranslations() {
func (h *HugoSites) createPageCollections() error {
for _, s := range h.Sites {
for _, p := range s.rawAllPages {
if p.Kind == kindUnknown {
p.Kind = p.kindFromSections()
}
if !p.s.isEnabled(p.Kind) {
if !s.isEnabled(p.Kind()) {
continue
}
shouldBuild := p.shouldBuild()
s.updateBuildStats(p)
shouldBuild := s.shouldBuild(p)
s.buildStats.update(p)
if shouldBuild {
if p.headless {
if p.m.headless {
s.headlessPages = append(s.headlessPages, p)
} else {
s.Pages = append(s.Pages, p)
s.workAllPages = append(s.workAllPages, p)
}
}
}
}
allPages := make(Pages, 0)
allPages := newLazyPagesFactory(func() page.Pages {
var pages page.Pages
for _, s := range h.Sites {
pages = append(pages, s.Pages()...)
}
page.SortByDefault(pages)
return pages
})
allRegularPages := newLazyPagesFactory(func() page.Pages {
return h.findPagesByKindIn(page.KindPage, allPages.get())
})
for _, s := range h.Sites {
allPages = append(allPages, s.Pages...)
s.PageCollections.allPages = allPages
s.PageCollections.allRegularPages = allRegularPages
}
allPages.sort()
for _, s := range h.Sites {
s.AllPages = allPages
return nil
}
// Pull over the collections from the master site
for i := 1; i < len(h.Sites); i++ {
h.Sites[i].Data = h.Sites[0].Data
}
func (s *Site) preparePagesForRender(idx int) error {
if len(h.Sites) > 1 {
allTranslations := pagesToTranslationsMap(allPages)
assignTranslationsToPages(allTranslations, allPages)
}
}
func (s *Site) preparePagesForRender(start bool) error {
for _, p := range s.Pages {
if err := p.prepareForRender(start); err != nil {
for _, p := range s.workAllPages {
if err := p.initOutputFormat(idx); err != nil {
return err
}
}
for _, p := range s.headlessPages {
if err := p.prepareForRender(start); err != nil {
if err := p.initOutputFormat(idx); err != nil {
return err
}
}
@ -691,62 +781,141 @@ func (s *Site) preparePagesForRender(start bool) error {
}
// Pages returns all pages for all sites.
func (h *HugoSites) Pages() Pages {
return h.Sites[0].AllPages
func (h *HugoSites) Pages() page.Pages {
return h.Sites[0].AllPages()
}
func handleShortcodes(p *PageWithoutContent, rawContentCopy []byte) ([]byte, error) {
if p.shortcodeState != nil && p.shortcodeState.contentShortcodes.Len() > 0 {
p.s.Log.DEBUG.Printf("Replace %d shortcodes in %q", p.shortcodeState.contentShortcodes.Len(), p.BaseFileName())
err := p.shortcodeState.executeShortcodesForDelta(p)
func (h *HugoSites) loadData(fs afero.Fs) (err error) {
spec := source.NewSourceSpec(h.PathSpec, fs)
fileSystem := spec.NewFilesystem("")
h.data = make(map[string]interface{})
for _, r := range fileSystem.Files() {
if err := h.handleDataFile(r); err != nil {
return err
}
}
return
}
func (h *HugoSites) handleDataFile(r source.ReadableFile) error {
var current map[string]interface{}
f, err := r.Open()
if err != nil {
return errors.Wrapf(err, "Failed to open data file %q:", r.LogicalName())
}
defer f.Close()
return rawContentCopy, err
// Crawl in data tree to insert data
current = h.data
keyParts := strings.Split(r.Dir(), helpers.FilePathSeparator)
// The first path element is the virtual folder (typically theme name), which is
// not part of the key.
if len(keyParts) > 1 {
for _, key := range keyParts[1:] {
if key != "" {
if _, ok := current[key]; !ok {
current[key] = make(map[string]interface{})
}
current = current[key].(map[string]interface{})
}
}
}
rawContentCopy, err = replaceShortcodeTokens(rawContentCopy, shortcodePlaceholderPrefix, p.shortcodeState.renderedShortcodes)
data, err := h.readData(r)
if err != nil {
p.s.Log.FATAL.Printf("Failed to replace shortcode tokens in %s:\n%s", p.BaseFileName(), err.Error())
}
return h.errWithFileContext(err, r)
}
return rawContentCopy, nil
if data == nil {
return nil
}
func (s *Site) updateBuildStats(page *Page) {
if page.IsDraft() {
s.draftCount++
// filepath.Walk walks the files in lexical order, '/' comes before '.'
// this warning could happen if
// 1. A theme uses the same key; the main data folder wins
// 2. A sub folder uses the same key: the sub folder wins
higherPrecedentData := current[r.BaseFileName()]
switch data.(type) {
case nil:
// hear the crickets?
case map[string]interface{}:
switch higherPrecedentData.(type) {
case nil:
current[r.BaseFileName()] = data
case map[string]interface{}:
// merge maps: insert entries from data for keys that
// don't already exist in higherPrecedentData
higherPrecedentMap := higherPrecedentData.(map[string]interface{})
for key, value := range data.(map[string]interface{}) {
if _, exists := higherPrecedentMap[key]; exists {
h.Log.WARN.Printf("Data for key '%s' in path '%s' is overridden by higher precedence data already in the data tree", key, r.Path())
} else {
higherPrecedentMap[key] = value
}
}
default:
// can't merge: higherPrecedentData is not a map
h.Log.WARN.Printf("The %T data from '%s' overridden by "+
"higher precedence %T data already in the data tree", data, r.Path(), higherPrecedentData)
}
if page.IsFuture() {
s.futureCount++
case []interface{}:
if higherPrecedentData == nil {
current[r.BaseFileName()] = data
} else {
// we don't merge array data
h.Log.WARN.Printf("The %T data from '%s' overridden by "+
"higher precedence %T data already in the data tree", data, r.Path(), higherPrecedentData)
}
if page.IsExpired() {
s.expiredCount++
}
default:
h.Log.ERROR.Printf("unexpected data type %T in file %s", data, r.LogicalName())
}
func (h *HugoSites) findPagesByKindNotIn(kind string, inPages Pages) Pages {
return h.Sites[0].findPagesByKindNotIn(kind, inPages)
return nil
}
func (h *HugoSites) findPagesByKindIn(kind string, inPages Pages) Pages {
func (h *HugoSites) errWithFileContext(err error, f source.File) error {
rfi, ok := f.FileInfo().(hugofs.RealFilenameInfo)
if !ok {
return err
}
realFilename := rfi.RealFilename()
err, _ = herrors.WithFileContextForFile(
err,
realFilename,
realFilename,
h.SourceSpec.Fs.Source,
herrors.SimpleLineMatcher)
return err
}
func (h *HugoSites) readData(f source.ReadableFile) (interface{}, error) {
file, err := f.Open()
if err != nil {
return nil, errors.Wrap(err, "readData: failed to open data file")
}
defer file.Close()
content := helpers.ReaderToBytes(file)
format := metadecoders.FormatFromString(f.Extension())
return metadecoders.Default.Unmarshal(content, format)
}
func (h *HugoSites) findPagesByKindIn(kind string, inPages page.Pages) page.Pages {
return h.Sites[0].findPagesByKindIn(kind, inPages)
}
func (h *HugoSites) findAllPagesByKind(kind string) Pages {
return h.findPagesByKindIn(kind, h.Sites[0].AllPages)
}
func (h *HugoSites) findAllPagesByKindNotIn(kind string) Pages {
return h.findPagesByKindNotIn(kind, h.Sites[0].AllPages)
}
func (h *HugoSites) findPagesByShortcode(shortcode string) Pages {
var pages Pages
func (h *HugoSites) findPagesByShortcode(shortcode string) page.Pages {
var pages page.Pages
for _, s := range h.Sites {
pages = append(pages, s.findPagesByShortcode(shortcode)...)
}

View file

@ -1,4 +1,4 @@
// Copyright 2016-present The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -15,7 +15,12 @@ package hugolib
import (
"bytes"
"context"
"fmt"
"runtime/trace"
"sort"
"github.com/gohugoio/hugo/output"
"errors"
@ -26,6 +31,9 @@ import (
// Build builds all sites. If filesystem events are provided,
// this is considered to be a potential partial rebuild.
func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {
ctx, task := trace.NewTask(context.Background(), "Build")
defer task.End()
errCollector := h.StartErrorCollector()
errs := make(chan error)
@ -71,22 +79,36 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {
return err
}
} else {
if err := h.init(conf); err != nil {
if err := h.initSites(conf); err != nil {
return err
}
}
if err := h.process(conf, events...); err != nil {
var err error
f := func() {
err = h.process(conf, events...)
}
trace.WithRegion(ctx, "process", f)
if err != nil {
return err
}
if err := h.assemble(conf); err != nil {
f = func() {
err = h.assemble(conf)
}
trace.WithRegion(ctx, "assemble", f)
if err != nil {
return err
}
return nil
}
f := func() {
prepareErr = prepare()
}
trace.WithRegion(ctx, "prepare", f)
if prepareErr != nil {
h.SendError(prepareErr)
}
@ -94,7 +116,12 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {
}
if prepareErr == nil {
if err := h.render(conf); err != nil {
var err error
f := func() {
err = h.render(conf)
}
trace.WithRegion(ctx, "render", f)
if err != nil {
h.SendError(err)
}
}
@ -120,6 +147,10 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {
return err
}
if err := h.fatalErrorHandler.getErr(); err != nil {
return err
}
errorCount := h.Log.ErrorCounter.Count()
if errorCount > 0 {
return fmt.Errorf("logged %d error(s)", errorCount)
@ -132,17 +163,8 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {
// Build lifecycle methods below.
// The order listed matches the order of execution.
func (h *HugoSites) init(config *BuildCfg) error {
for _, s := range h.Sites {
if s.PageCollections == nil {
s.PageCollections = newPageCollections()
}
}
if config.ResetState {
h.reset()
}
func (h *HugoSites) initSites(config *BuildCfg) error {
h.reset(config)
if config.NewConfig != nil {
if err := h.createSitesFromConfig(config.NewConfig); err != nil {
@ -155,28 +177,22 @@ func (h *HugoSites) init(config *BuildCfg) error {
func (h *HugoSites) initRebuild(config *BuildCfg) error {
if config.NewConfig != nil {
return errors.New("Rebuild does not support 'NewConfig'.")
return errors.New("rebuild does not support 'NewConfig'")
}
if config.ResetState {
return errors.New("Rebuild does not support 'ResetState'.")
return errors.New("rebuild does not support 'ResetState'")
}
if !h.running {
return errors.New("Rebuild called when not in watch mode")
}
if config.whatChanged.source {
// This is for the non-renderable content pages (rarely used, I guess).
// We could maybe detect if this is really needed, but it should be
// pretty fast.
h.TemplateHandler().RebuildClone()
return errors.New("rebuild called when not in watch mode")
}
for _, s := range h.Sites {
s.resetBuildState()
}
h.reset(config)
h.resetLogs()
helpers.InitLoggers()
@ -203,14 +219,6 @@ func (h *HugoSites) process(config *BuildCfg, events ...fsnotify.Event) error {
}
func (h *HugoSites) assemble(config *BuildCfg) error {
if config.whatChanged.source {
for _, s := range h.Sites {
s.createTaxonomiesEntries()
}
}
// TODO(bep) we could probably wait and do this in one go later
h.setupTranslations()
if len(h.Sites) > 1 {
// The first is initialized during process; initialize the rest
@ -221,47 +229,26 @@ func (h *HugoSites) assemble(config *BuildCfg) error {
}
}
if err := h.createPageCollections(); err != nil {
return err
}
if config.whatChanged.source {
for _, s := range h.Sites {
if err := s.buildSiteMeta(); err != nil {
if err := s.assembleTaxonomies(); err != nil {
return err
}
}
}
// Create pagexs for the section pages etc. without content file.
if err := h.createMissingPages(); err != nil {
return err
}
for _, s := range h.Sites {
for _, pages := range []Pages{s.Pages, s.headlessPages} {
for _, p := range pages {
// May have been set in front matter
if len(p.outputFormats) == 0 {
p.outputFormats = s.outputFormats[p.Kind]
}
if p.headless {
// headless = 1 output format only
p.outputFormats = p.outputFormats[:1]
}
for _, r := range p.Resources.ByType(pageResourceType) {
r.(*Page).outputFormats = p.outputFormats
}
if err := p.initPaths(); err != nil {
return err
}
}
}
s.assembleMenus()
s.refreshPageCaches()
s.setupSitePages()
}
if err := h.assignMissingTranslations(); err != nil {
return err
sort.Stable(s.workAllPages)
}
return nil
@ -269,42 +256,60 @@ func (h *HugoSites) assemble(config *BuildCfg) error {
}
func (h *HugoSites) render(config *BuildCfg) error {
siteRenderContext := &siteRenderContext{cfg: config, multihost: h.multihost}
if !config.PartialReRender {
h.renderFormats = output.Formats{}
for _, s := range h.Sites {
s.initRenderFormats()
h.renderFormats = append(h.renderFormats, s.renderFormats...)
}
}
i := 0
for _, s := range h.Sites {
for i, rf := range s.renderFormats {
for siteOutIdx, renderFormat := range s.renderFormats {
siteRenderContext.outIdx = siteOutIdx
siteRenderContext.sitesOutIdx = i
i++
select {
case <-h.Done():
return nil
default:
// For the non-renderable pages, we use the content iself as
// template and we may have to re-parse and execute it for
// each output format.
h.TemplateHandler().RebuildClone()
for _, s2 := range h.Sites {
// We render site by site, but since the content is lazily rendered
// and a site can "borrow" content from other sites, every site
// needs this set.
s2.rc = &siteRenderingContext{Format: rf}
isRenderingSite := s == s2
s2.rc = &siteRenderingContext{Format: renderFormat}
if !config.PartialReRender {
if err := s2.preparePagesForRender(isRenderingSite && i == 0); err != nil {
if err := s2.preparePagesForRender(siteRenderContext.sitesOutIdx); err != nil {
return err
}
}
}
if !config.SkipRender {
if config.PartialReRender {
if err := s.renderPages(config); err != nil {
if err := s.renderPages(siteRenderContext); err != nil {
return err
}
} else {
if err := s.render(config, i); err != nil {
if err := s.render(siteRenderContext); err != nil {
return err
}
}
}
}
}
}
if !config.SkipRender {

View file

@ -7,6 +7,9 @@ import (
"runtime"
"strings"
"testing"
"time"
"github.com/fortytw2/leaktest"
"github.com/gohugoio/hugo/common/herrors"
"github.com/stretchr/testify/require"
@ -20,25 +23,24 @@ type testSiteBuildErrorAsserter struct {
func (t testSiteBuildErrorAsserter) getFileError(err error) *herrors.ErrorWithFileContext {
t.assert.NotNil(err, t.name)
ferr := herrors.UnwrapErrorWithFileContext(err)
t.assert.NotNil(ferr, fmt.Sprintf("[%s] got %T: %+v\n%s", t.name, err, err, trace()))
t.assert.NotNil(ferr, fmt.Sprintf("[%s] got %T: %+v\n%s", t.name, err, err, stackTrace()))
return ferr
}
func (t testSiteBuildErrorAsserter) assertLineNumber(lineNumber int, err error) {
fe := t.getFileError(err)
t.assert.Equal(lineNumber, fe.Position().LineNumber, fmt.Sprintf("[%s] got => %s\n%s", t.name, fe, trace()))
t.assert.Equal(lineNumber, fe.Position().LineNumber, fmt.Sprintf("[%s] got => %s\n%s", t.name, fe, stackTrace()))
}
func (t testSiteBuildErrorAsserter) assertErrorMessage(e1, e2 string) {
// The error message will contain filenames with OS slashes. Normalize before compare.
e1, e2 = filepath.ToSlash(e1), filepath.ToSlash(e2)
t.assert.Contains(e2, e1, trace())
t.assert.Contains(e2, e1, stackTrace())
}
func TestSiteBuildErrors(t *testing.T) {
t.Parallel()
assert := require.New(t)
const (
yamlcontent = "yamlcontent"
@ -88,9 +90,9 @@ func TestSiteBuildErrors(t *testing.T) {
},
assertCreateError: func(a testSiteBuildErrorAsserter, err error) {
fe := a.getFileError(err)
assert.Equal(5, fe.Position().LineNumber)
assert.Equal(1, fe.Position().ColumnNumber)
assert.Equal("go-html-template", fe.ChromaLexer)
a.assert.Equal(5, fe.Position().LineNumber)
a.assert.Equal(1, fe.Position().ColumnNumber)
a.assert.Equal("go-html-template", fe.ChromaLexer)
a.assertErrorMessage("\"layouts/_default/single.html:5:1\": parse failed: template: _default/single.html:5: unexpected \"}\" in operand", fe.Error())
},
@ -103,9 +105,9 @@ func TestSiteBuildErrors(t *testing.T) {
},
assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
fe := a.getFileError(err)
assert.Equal(5, fe.Position().LineNumber)
assert.Equal(14, fe.Position().ColumnNumber)
assert.Equal("go-html-template", fe.ChromaLexer)
a.assert.Equal(5, fe.Position().LineNumber)
a.assert.Equal(14, fe.Position().ColumnNumber)
a.assert.Equal("go-html-template", fe.ChromaLexer)
a.assertErrorMessage("\"layouts/_default/single.html:5:14\": execute of template failed", fe.Error())
},
@ -118,9 +120,9 @@ func TestSiteBuildErrors(t *testing.T) {
},
assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
fe := a.getFileError(err)
assert.Equal(5, fe.Position().LineNumber)
assert.Equal(14, fe.Position().ColumnNumber)
assert.Equal("go-html-template", fe.ChromaLexer)
a.assert.Equal(5, fe.Position().LineNumber)
a.assert.Equal(14, fe.Position().ColumnNumber)
a.assert.Equal("go-html-template", fe.ChromaLexer)
a.assertErrorMessage("\"layouts/_default/single.html:5:14\": execute of template failed", fe.Error())
},
@ -143,8 +145,8 @@ func TestSiteBuildErrors(t *testing.T) {
},
assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
fe := a.getFileError(err)
assert.Equal(7, fe.Position().LineNumber)
assert.Equal("md", fe.ChromaLexer)
a.assert.Equal(7, fe.Position().LineNumber)
a.assert.Equal("md", fe.ChromaLexer)
// Make sure that it contains both the content file and template
a.assertErrorMessage(`content/myyaml.md:7:10": failed to render shortcode "sc"`, fe.Error())
a.assertErrorMessage(`shortcodes/sc.html:4:22: executing "shortcodes/sc.html" at <.Page.Titles>: can't evaluate`, fe.Error())
@ -158,10 +160,10 @@ func TestSiteBuildErrors(t *testing.T) {
},
assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
fe := a.getFileError(err)
assert.Equal(7, fe.Position().LineNumber)
assert.Equal(14, fe.Position().ColumnNumber)
assert.Equal("md", fe.ChromaLexer)
a.assertErrorMessage("\"content/myyaml.md:7:14\": failed to extract shortcode: template for shortcode \"nono\" not found", fe.Error())
a.assert.Equal(7, fe.Position().LineNumber)
a.assert.Equal(10, fe.Position().ColumnNumber)
a.assert.Equal("md", fe.ChromaLexer)
a.assertErrorMessage(`"content/myyaml.md:7:10": failed to extract shortcode: template for shortcode "nono" not found`, fe.Error())
},
},
{
@ -182,8 +184,8 @@ func TestSiteBuildErrors(t *testing.T) {
},
assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
fe := a.getFileError(err)
assert.Equal(6, fe.Position().LineNumber)
assert.Equal("toml", fe.ErrorContext.ChromaLexer)
a.assert.Equal(6, fe.Position().LineNumber)
a.assert.Equal("toml", fe.ErrorContext.ChromaLexer)
},
},
@ -196,8 +198,8 @@ func TestSiteBuildErrors(t *testing.T) {
assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
fe := a.getFileError(err)
assert.Equal(3, fe.Position().LineNumber)
assert.Equal("json", fe.ErrorContext.ChromaLexer)
a.assert.Equal(3, fe.Position().LineNumber)
a.assert.Equal("json", fe.ErrorContext.ChromaLexer)
},
},
@ -210,21 +212,22 @@ func TestSiteBuildErrors(t *testing.T) {
},
assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
assert.Error(err)
a.assert.Error(err)
// This is fixed in latest Go source
if regexp.MustCompile("devel|12").MatchString(runtime.Version()) {
fe := a.getFileError(err)
assert.Equal(5, fe.Position().LineNumber)
assert.Equal(21, fe.Position().ColumnNumber)
a.assert.Equal(5, fe.Position().LineNumber)
a.assert.Equal(21, fe.Position().ColumnNumber)
} else {
assert.Contains(err.Error(), `execute of template failed: panic in Execute`)
a.assert.Contains(err.Error(), `execute of template failed: panic in Execute`)
}
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert := require.New(t)
errorAsserter := testSiteBuildErrorAsserter{
assert: assert,
name: test.name,
@ -313,11 +316,15 @@ Some content.
assert.NoError(buildErr)
}
}
})
}
}
// https://github.com/gohugoio/hugo/issues/5375
func TestSiteBuildTimeout(t *testing.T) {
if !isCI() {
defer leaktest.CheckTimeout(t, 10*time.Second)()
}
b := newTestSitesBuilder(t)
b.WithConfigFile("toml", `
@ -342,6 +349,6 @@ title: "A page"
}
b.CreateSites().Build(BuildCfg{})
b.CreateSites().BuildFail(BuildCfg{})
}

View file

@ -1,16 +1,16 @@
package hugolib
import (
"bytes"
"fmt"
"strings"
"testing"
"html/template"
"os"
"path/filepath"
"time"
"github.com/gohugoio/hugo/resources/page"
"github.com/fortytw2/leaktest"
"github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/helpers"
@ -66,8 +66,8 @@ func doTestMultiSitesMainLangInRoot(t *testing.T, defaultInSubDir bool) {
assert.Equal("/blog/en/foo", enSite.PathSpec.RelURL("foo", true))
doc1en := enSite.RegularPages[0]
doc1fr := frSite.RegularPages[0]
doc1en := enSite.RegularPages()[0]
doc1fr := frSite.RegularPages()[0]
enPerm := doc1en.Permalink()
enRelPerm := doc1en.RelPermalink()
@ -100,7 +100,7 @@ func doTestMultiSitesMainLangInRoot(t *testing.T, defaultInSubDir bool) {
// Check list pages
b.AssertFileContent(pathMod("public/fr/sect/index.html"), "List", "Bonjour")
b.AssertFileContent("public/en/sect/index.html", "List", "Hello")
b.AssertFileContent(pathMod("public/fr/plaques/frtag1/index.html"), "Taxonomy List", "Bonjour")
b.AssertFileContent(pathMod("public/fr/plaques/FRtag1/index.html"), "Taxonomy List", "Bonjour")
b.AssertFileContent("public/en/tags/tag1/index.html", "Taxonomy List", "Hello")
// Check sitemaps
@ -126,8 +126,8 @@ func doTestMultiSitesMainLangInRoot(t *testing.T, defaultInSubDir bool) {
pathMod(`<atom:link href="http://example.com/blog/fr/sect/index.xml"`))
b.AssertFileContent("public/en/sect/index.xml", `<atom:link href="http://example.com/blog/en/sect/index.xml"`)
b.AssertFileContent(
pathMod("public/fr/plaques/frtag1/index.xml"),
pathMod(`<atom:link href="http://example.com/blog/fr/plaques/frtag1/index.xml"`))
pathMod("public/fr/plaques/FRtag1/index.xml"),
pathMod(`<atom:link href="http://example.com/blog/fr/plaques/FRtag1/index.xml"`))
b.AssertFileContent("public/en/tags/tag1/index.xml", `<atom:link href="http://example.com/blog/en/tags/tag1/index.xml"`)
// Check paginators
@ -140,12 +140,12 @@ func doTestMultiSitesMainLangInRoot(t *testing.T, defaultInSubDir bool) {
b.AssertFileContent(pathMod("public/fr/sect/page/2/index.html"), "List Page 2", "Bonjour", pathMod("http://example.com/blog/fr/sect/"))
b.AssertFileContent("public/en/sect/page/2/index.html", "List Page 2", "Hello", "http://example.com/blog/en/sect/")
b.AssertFileContent(
pathMod("public/fr/plaques/frtag1/page/1/index.html"),
pathMod(`refresh" content="0; url=http://example.com/blog/fr/plaques/frtag1/"`))
pathMod("public/fr/plaques/FRtag1/page/1/index.html"),
pathMod(`refresh" content="0; url=http://example.com/blog/fr/plaques/FRtag1/"`))
b.AssertFileContent("public/en/tags/tag1/page/1/index.html", `refresh" content="0; url=http://example.com/blog/en/tags/tag1/"`)
b.AssertFileContent(
pathMod("public/fr/plaques/frtag1/page/2/index.html"), "List Page 2", "Bonjour",
pathMod("http://example.com/blog/fr/plaques/frtag1/"))
pathMod("public/fr/plaques/FRtag1/page/2/index.html"), "List Page 2", "Bonjour",
pathMod("http://example.com/blog/fr/plaques/FRtag1/"))
b.AssertFileContent("public/en/tags/tag1/page/2/index.html", "List Page 2", "Hello", "http://example.com/blog/en/tags/tag1/")
// nn (Nynorsk) and nb (Bokmål) have custom pagePath: side ("page" in Norwegian)
b.AssertFileContent("public/nn/side/1/index.html", `refresh" content="0; url=http://example.com/blog/nn/"`)
@ -183,12 +183,12 @@ p1 = "p1en"
assert.Len(sites, 2)
nnSite := sites[0]
nnHome := nnSite.getPage(KindHome)
nnHome := nnSite.getPage(page.KindHome)
assert.Len(nnHome.AllTranslations(), 2)
assert.Len(nnHome.Translations(), 1)
assert.True(nnHome.IsTranslated())
enHome := sites[1].getPage(KindHome)
enHome := sites[1].getPage(page.KindHome)
p1, err := enHome.Param("p1")
assert.NoError(err)
@ -199,9 +199,7 @@ p1 = "p1en"
assert.Equal("p1nn", p1)
}
//
func TestMultiSitesBuild(t *testing.T) {
t.Parallel()
for _, config := range []struct {
content string
@ -211,7 +209,11 @@ func TestMultiSitesBuild(t *testing.T) {
{multiSiteYAMLConfigTemplate, "yml"},
{multiSiteJSONConfigTemplate, "json"},
} {
t.Run(config.suffix, func(t *testing.T) {
t.Parallel()
doTestMultiSitesBuild(t, config.content, config.suffix)
})
}
}
@ -228,64 +230,51 @@ func doTestMultiSitesBuild(t *testing.T, configTemplate, configSuffix string) {
// Check site config
for _, s := range sites {
require.True(t, s.Info.defaultContentLanguageInSubdir, s.Info.Title)
require.True(t, s.Info.defaultContentLanguageInSubdir, s.Info.title)
require.NotNil(t, s.disabledKinds)
}
gp1 := b.H.GetContentPage(filepath.FromSlash("content/sect/doc1.en.md"))
require.NotNil(t, gp1)
require.Equal(t, "doc1", gp1.title)
require.Equal(t, "doc1", gp1.Title())
gp2 := b.H.GetContentPage(filepath.FromSlash("content/dummysect/notfound.md"))
require.Nil(t, gp2)
enSite := sites[0]
enSiteHome := enSite.getPage(KindHome)
enSiteHome := enSite.getPage(page.KindHome)
require.True(t, enSiteHome.IsTranslated())
require.Equal(t, "en", enSite.Language.Lang)
require.Equal(t, "en", enSite.language.Lang)
assert.Equal(5, len(enSite.RegularPages))
assert.Equal(32, len(enSite.AllPages))
assert.Equal(5, len(enSite.RegularPages()))
assert.Equal(32, len(enSite.AllPages()))
doc1en := enSite.RegularPages[0]
permalink := doc1en.Permalink()
require.Equal(t, "http://example.com/blog/en/sect/doc1-slug/", permalink, "invalid doc1.en permalink")
require.Len(t, doc1en.Translations(), 1, "doc1-en should have one translation, excluding itself")
// Check 404s
b.AssertFileContent("public/en/404.html", "404|en|404 Page not found")
b.AssertFileContent("public/fr/404.html", "404|fr|404 Page not found")
doc2 := enSite.RegularPages[1]
permalink = doc2.Permalink()
require.Equal(t, "http://example.com/blog/en/sect/doc2/", permalink, "invalid doc2 permalink")
// Check robots.txt
b.AssertFileContent("public/en/robots.txt", "robots|en|")
b.AssertFileContent("public/nn/robots.txt", "robots|nn|")
doc3 := enSite.RegularPages[2]
permalink = doc3.Permalink()
// Note that /superbob is a custom URL set in frontmatter.
// We respect that URL literally (it can be /search.json)
// and do no not do any language code prefixing.
require.Equal(t, "http://example.com/blog/superbob/", permalink, "invalid doc3 permalink")
require.Equal(t, "/superbob", doc3.URL(), "invalid url, was specified on doc3")
b.AssertFileContent("public/superbob/index.html", "doc3|Hello|en")
require.Equal(t, doc2.PrevPage, doc3, "doc3 should follow doc2, in .PrevPage")
b.AssertFileContent("public/en/sect/doc1-slug/index.html", "Permalink: http://example.com/blog/en/sect/doc1-slug/")
b.AssertFileContent("public/en/sect/doc2/index.html", "Permalink: http://example.com/blog/en/sect/doc2/")
b.AssertFileContent("public/superbob/index.html", "Permalink: http://example.com/blog/superbob/")
doc2 := enSite.RegularPages()[1]
doc3 := enSite.RegularPages()[2]
require.Equal(t, doc2.Prev(), doc3, "doc3 should follow doc2, in .PrevPage")
doc1en := enSite.RegularPages()[0]
doc1fr := doc1en.Translations()[0]
permalink = doc1fr.Permalink()
require.Equal(t, "http://example.com/blog/fr/sect/doc1/", permalink, "invalid doc1fr permalink")
b.AssertFileContent("public/fr/sect/doc1/index.html", "Permalink: http://example.com/blog/fr/sect/doc1/")
require.Equal(t, doc1en.Translations()[0], doc1fr, "doc1-en should have doc1-fr as translation")
require.Equal(t, doc1fr.Translations()[0], doc1en, "doc1-fr should have doc1-en as translation")
require.Equal(t, "fr", doc1fr.Language().Lang)
doc4 := enSite.AllPages[4]
permalink = doc4.Permalink()
require.Equal(t, "http://example.com/blog/fr/sect/doc4/", permalink, "invalid doc4 permalink")
require.Equal(t, "/blog/fr/sect/doc4/", doc4.URL())
doc4 := enSite.AllPages()[4]
require.Len(t, doc4.Translations(), 0, "found translations for doc4")
doc5 := enSite.AllPages[5]
permalink = doc5.Permalink()
require.Equal(t, "http://example.com/blog/fr/somewhere/else/doc5/", permalink, "invalid doc5 permalink")
// Taxonomies and their URLs
require.Len(t, enSite.Taxonomies, 1, "should have 1 taxonomy")
tags := enSite.Taxonomies["tags"]
@ -294,12 +283,13 @@ func doTestMultiSitesBuild(t *testing.T, configTemplate, configSuffix string) {
frSite := sites[1]
require.Equal(t, "fr", frSite.Language.Lang)
require.Len(t, frSite.RegularPages, 4, "should have 3 pages")
require.Len(t, frSite.AllPages, 32, "should have 32 total pages (including translations and nodes)")
require.Equal(t, "fr", frSite.language.Lang)
require.Len(t, frSite.RegularPages(), 4, "should have 3 pages")
require.Len(t, frSite.AllPages(), 32, "should have 32 total pages (including translations and nodes)")
for _, frenchPage := range frSite.RegularPages {
require.Equal(t, "fr", frenchPage.Lang())
for _, frenchPage := range frSite.RegularPages() {
p := frenchPage
require.Equal(t, "fr", p.Language().Lang)
}
// See https://github.com/gohugoio/hugo/issues/4285
@ -307,10 +297,10 @@ func doTestMultiSitesBuild(t *testing.T, configTemplate, configSuffix string) {
// isn't ideal in a multilingual setup. You want a way to get the current language version if available.
// Now you can do lookups with translation base name to get that behaviour.
// Let us test all the regular page variants:
getPageDoc1En := enSite.getPage(KindPage, filepath.ToSlash(doc1en.Path()))
getPageDoc1EnBase := enSite.getPage(KindPage, "sect/doc1")
getPageDoc1Fr := frSite.getPage(KindPage, filepath.ToSlash(doc1fr.Path()))
getPageDoc1FrBase := frSite.getPage(KindPage, "sect/doc1")
getPageDoc1En := enSite.getPage(page.KindPage, filepath.ToSlash(doc1en.File().Path()))
getPageDoc1EnBase := enSite.getPage(page.KindPage, "sect/doc1")
getPageDoc1Fr := frSite.getPage(page.KindPage, filepath.ToSlash(doc1fr.File().Path()))
getPageDoc1FrBase := frSite.getPage(page.KindPage, "sect/doc1")
require.Equal(t, doc1en, getPageDoc1En)
require.Equal(t, doc1fr, getPageDoc1Fr)
require.Equal(t, doc1en, getPageDoc1EnBase)
@ -328,35 +318,36 @@ func doTestMultiSitesBuild(t *testing.T, configTemplate, configSuffix string) {
b.AssertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Shortcode: Hello", "LingoDefault")
// Check node translations
homeEn := enSite.getPage(KindHome)
homeEn := enSite.getPage(page.KindHome)
require.NotNil(t, homeEn)
require.Len(t, homeEn.Translations(), 3)
require.Equal(t, "fr", homeEn.Translations()[0].Lang())
require.Equal(t, "nn", homeEn.Translations()[1].Lang())
require.Equal(t, "På nynorsk", homeEn.Translations()[1].title)
require.Equal(t, "nb", homeEn.Translations()[2].Lang())
require.Equal(t, "På bokmål", homeEn.Translations()[2].title, configSuffix)
require.Equal(t, "fr", homeEn.Translations()[0].Language().Lang)
require.Equal(t, "nn", homeEn.Translations()[1].Language().Lang)
require.Equal(t, "På nynorsk", homeEn.Translations()[1].Title())
require.Equal(t, "nb", homeEn.Translations()[2].Language().Lang)
require.Equal(t, "På bokmål", homeEn.Translations()[2].Title(), configSuffix)
require.Equal(t, "Bokmål", homeEn.Translations()[2].Language().LanguageName, configSuffix)
sectFr := frSite.getPage(KindSection, "sect")
sectFr := frSite.getPage(page.KindSection, "sect")
require.NotNil(t, sectFr)
require.Equal(t, "fr", sectFr.Lang())
require.Equal(t, "fr", sectFr.Language().Lang)
require.Len(t, sectFr.Translations(), 1)
require.Equal(t, "en", sectFr.Translations()[0].Lang())
require.Equal(t, "Sects", sectFr.Translations()[0].title)
require.Equal(t, "en", sectFr.Translations()[0].Language().Lang)
require.Equal(t, "Sects", sectFr.Translations()[0].Title())
nnSite := sites[2]
require.Equal(t, "nn", nnSite.Language.Lang)
taxNn := nnSite.getPage(KindTaxonomyTerm, "lag")
require.Equal(t, "nn", nnSite.language.Lang)
taxNn := nnSite.getPage(page.KindTaxonomyTerm, "lag")
require.NotNil(t, taxNn)
require.Len(t, taxNn.Translations(), 1)
require.Equal(t, "nb", taxNn.Translations()[0].Lang())
require.Equal(t, "nb", taxNn.Translations()[0].Language().Lang)
taxTermNn := nnSite.getPage(KindTaxonomy, "lag", "sogndal")
taxTermNn := nnSite.getPage(page.KindTaxonomy, "lag", "sogndal")
require.NotNil(t, taxTermNn)
require.Equal(t, taxTermNn, nnSite.getPage(page.KindTaxonomy, "LAG", "SOGNDAL"))
require.Len(t, taxTermNn.Translations(), 1)
require.Equal(t, "nb", taxTermNn.Translations()[0].Lang())
require.Equal(t, "nb", taxTermNn.Translations()[0].Language().Lang)
// Check sitemap(s)
b.AssertFileContent("public/sitemap.xml",
@ -371,59 +362,53 @@ func doTestMultiSitesBuild(t *testing.T, configTemplate, configSuffix string) {
require.Len(t, enTags, 2, fmt.Sprintf("Tags in en: %v", enTags))
require.Len(t, frTags, 2, fmt.Sprintf("Tags in fr: %v", frTags))
require.NotNil(t, enTags["tag1"])
require.NotNil(t, frTags["frtag1"])
b.AssertFileContent("public/fr/plaques/frtag1/index.html", "Frtag1|Bonjour|http://example.com/blog/fr/plaques/frtag1/")
b.AssertFileContent("public/en/tags/tag1/index.html", "Tag1|Hello|http://example.com/blog/en/tags/tag1/")
require.NotNil(t, frTags["FRtag1"])
b.AssertFileContent("public/fr/plaques/FRtag1/index.html", "FRtag1|Bonjour|http://example.com/blog/fr/plaques/FRtag1/")
b.AssertFileContent("public/en/tags/tag1/index.html", "tag1|Hello|http://example.com/blog/en/tags/tag1/")
// Check Blackfriday config
require.True(t, strings.Contains(string(doc1fr.content()), "&laquo;"), string(doc1fr.content()))
require.False(t, strings.Contains(string(doc1en.content()), "&laquo;"), string(doc1en.content()))
require.True(t, strings.Contains(string(doc1en.content()), "&ldquo;"), string(doc1en.content()))
// Check that the drafts etc. are not built/processed/rendered.
assertShouldNotBuild(t, b.H)
require.True(t, strings.Contains(content(doc1fr), "&laquo;"), content(doc1fr))
require.False(t, strings.Contains(content(doc1en), "&laquo;"), content(doc1en))
require.True(t, strings.Contains(content(doc1en), "&ldquo;"), content(doc1en))
// en and nn have custom site menus
require.Len(t, frSite.Menus, 0, "fr: "+configSuffix)
require.Len(t, enSite.Menus, 1, "en: "+configSuffix)
require.Len(t, nnSite.Menus, 1, "nn: "+configSuffix)
require.Len(t, frSite.Menus(), 0, "fr: "+configSuffix)
require.Len(t, enSite.Menus(), 1, "en: "+configSuffix)
require.Len(t, nnSite.Menus(), 1, "nn: "+configSuffix)
require.Equal(t, "Home", enSite.Menus["main"].ByName()[0].Name)
require.Equal(t, "Heim", nnSite.Menus["main"].ByName()[0].Name)
// Issue #1302
require.Equal(t, template.URL(""), enSite.RegularPages[0].RSSLink())
require.Equal(t, "Home", enSite.Menus()["main"].ByName()[0].Name)
require.Equal(t, "Heim", nnSite.Menus()["main"].ByName()[0].Name)
// Issue #3108
prevPage := enSite.RegularPages[0].PrevPage
prevPage := enSite.RegularPages()[0].Prev()
require.NotNil(t, prevPage)
require.Equal(t, KindPage, prevPage.Kind)
require.Equal(t, page.KindPage, prevPage.Kind())
for {
if prevPage == nil {
break
}
require.Equal(t, KindPage, prevPage.Kind)
prevPage = prevPage.PrevPage
require.Equal(t, page.KindPage, prevPage.Kind())
prevPage = prevPage.Prev()
}
// Check bundles
bundleFr := frSite.getPage(KindPage, "bundles/b1/index.md")
b.AssertFileContent("public/fr/bundles/b1/index.html", "RelPermalink: /blog/fr/bundles/b1/|")
bundleFr := frSite.getPage(page.KindPage, "bundles/b1/index.md")
require.NotNil(t, bundleFr)
require.Equal(t, "/blog/fr/bundles/b1/", bundleFr.RelPermalink())
require.Equal(t, 1, len(bundleFr.Resources))
logoFr := bundleFr.Resources.GetMatch("logo*")
require.Equal(t, 1, len(bundleFr.Resources()))
logoFr := bundleFr.Resources().GetMatch("logo*")
require.NotNil(t, logoFr)
require.Equal(t, "/blog/fr/bundles/b1/logo.png", logoFr.RelPermalink())
b.AssertFileContent("public/fr/bundles/b1/index.html", "Resources: image/png: /blog/fr/bundles/b1/logo.png")
b.AssertFileContent("public/fr/bundles/b1/logo.png", "PNG Data")
bundleEn := enSite.getPage(KindPage, "bundles/b1/index.en.md")
bundleEn := enSite.getPage(page.KindPage, "bundles/b1/index.en.md")
require.NotNil(t, bundleEn)
require.Equal(t, "/blog/en/bundles/b1/", bundleEn.RelPermalink())
require.Equal(t, 1, len(bundleEn.Resources))
logoEn := bundleEn.Resources.GetMatch("logo*")
b.AssertFileContent("public/en/bundles/b1/index.html", "RelPermalink: /blog/en/bundles/b1/|")
require.Equal(t, 1, len(bundleEn.Resources()))
logoEn := bundleEn.Resources().GetMatch("logo*")
require.NotNil(t, logoEn)
require.Equal(t, "/blog/en/bundles/b1/logo.png", logoEn.RelPermalink())
b.AssertFileContent("public/en/bundles/b1/index.html", "Resources: image/png: /blog/en/bundles/b1/logo.png")
b.AssertFileContent("public/en/bundles/b1/logo.png", "PNG Data")
}
@ -442,13 +427,13 @@ func TestMultiSitesRebuild(t *testing.T) {
sites := b.H.Sites
fs := b.Fs
b.AssertFileContent("public/en/sect/doc2/index.html", "Single: doc2|Hello|en|\n\n<h1 id=\"doc2\">doc2</h1>\n\n<p><em>some content</em>")
b.AssertFileContent("public/en/sect/doc2/index.html", "Single: doc2|Hello|en|", "\n\n<h1 id=\"doc2\">doc2</h1>\n\n<p><em>some content</em>")
enSite := sites[0]
frSite := sites[1]
assert.Len(enSite.RegularPages, 5)
assert.Len(frSite.RegularPages, 4)
assert.Len(enSite.RegularPages(), 5)
assert.Len(frSite.RegularPages(), 4)
// Verify translations
b.AssertFileContent("public/en/sect/doc1-slug/index.html", "Hello")
@ -458,6 +443,10 @@ func TestMultiSitesRebuild(t *testing.T) {
b.AssertFileContent("public/fr/sect/doc1/index.html", "Single", "Shortcode: Bonjour")
b.AssertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Shortcode: Hello")
homeEn := enSite.getPage(page.KindHome)
require.NotNil(t, homeEn)
assert.Len(homeEn.Translations(), 3)
contentFs := b.H.BaseFs.Content.Fs
for i, this := range []struct {
@ -478,15 +467,15 @@ func TestMultiSitesRebuild(t *testing.T) {
},
[]fsnotify.Event{{Name: filepath.FromSlash("content/sect/doc2.en.md"), Op: fsnotify.Remove}},
func(t *testing.T) {
assert.Len(enSite.RegularPages, 4, "1 en removed")
assert.Len(enSite.RegularPages(), 4, "1 en removed")
// Check build stats
require.Equal(t, 1, enSite.draftCount, "Draft")
require.Equal(t, 1, enSite.futureCount, "Future")
require.Equal(t, 1, enSite.expiredCount, "Expired")
require.Equal(t, 0, frSite.draftCount, "Draft")
require.Equal(t, 1, frSite.futureCount, "Future")
require.Equal(t, 1, frSite.expiredCount, "Expired")
require.Equal(t, 1, enSite.buildStats.draftCount, "Draft")
require.Equal(t, 1, enSite.buildStats.futureCount, "Future")
require.Equal(t, 1, enSite.buildStats.expiredCount, "Expired")
require.Equal(t, 0, frSite.buildStats.draftCount, "Draft")
require.Equal(t, 1, frSite.buildStats.futureCount, "Future")
require.Equal(t, 1, frSite.buildStats.expiredCount, "Expired")
},
},
{
@ -501,12 +490,12 @@ func TestMultiSitesRebuild(t *testing.T) {
{Name: filepath.FromSlash("content/new1.fr.md"), Op: fsnotify.Create},
},
func(t *testing.T) {
assert.Len(enSite.RegularPages, 6)
assert.Len(enSite.AllPages, 34)
assert.Len(frSite.RegularPages, 5)
require.Equal(t, "new_fr_1", frSite.RegularPages[3].title)
require.Equal(t, "new_en_2", enSite.RegularPages[0].title)
require.Equal(t, "new_en_1", enSite.RegularPages[1].title)
assert.Len(enSite.RegularPages(), 6)
assert.Len(enSite.AllPages(), 34)
assert.Len(frSite.RegularPages(), 5)
require.Equal(t, "new_fr_1", frSite.RegularPages()[3].Title())
require.Equal(t, "new_en_2", enSite.RegularPages()[0].Title())
require.Equal(t, "new_en_1", enSite.RegularPages()[1].Title())
rendered := readDestination(t, fs, "public/en/new1/index.html")
require.True(t, strings.Contains(rendered, "new_en_1"), rendered)
@ -521,7 +510,7 @@ func TestMultiSitesRebuild(t *testing.T) {
},
[]fsnotify.Event{{Name: filepath.FromSlash("content/sect/doc1.en.md"), Op: fsnotify.Write}},
func(t *testing.T) {
assert.Len(enSite.RegularPages, 6)
assert.Len(enSite.RegularPages(), 6)
doc1 := readDestination(t, fs, "public/en/sect/doc1-slug/index.html")
require.True(t, strings.Contains(doc1, "CHANGED"), doc1)
@ -539,8 +528,8 @@ func TestMultiSitesRebuild(t *testing.T) {
{Name: filepath.FromSlash("content/new1.en.md"), Op: fsnotify.Rename},
},
func(t *testing.T) {
assert.Len(enSite.RegularPages, 6, "Rename")
require.Equal(t, "new_en_1", enSite.RegularPages[1].title)
assert.Len(enSite.RegularPages(), 6, "Rename")
require.Equal(t, "new_en_1", enSite.RegularPages()[1].Title())
rendered := readDestination(t, fs, "public/en/new1renamed/index.html")
require.True(t, strings.Contains(rendered, "new_en_1"), rendered)
}},
@ -554,9 +543,9 @@ func TestMultiSitesRebuild(t *testing.T) {
},
[]fsnotify.Event{{Name: filepath.FromSlash("layouts/_default/single.html"), Op: fsnotify.Write}},
func(t *testing.T) {
assert.Len(enSite.RegularPages, 6)
assert.Len(enSite.AllPages, 34)
assert.Len(frSite.RegularPages, 5)
assert.Len(enSite.RegularPages(), 6)
assert.Len(enSite.AllPages(), 34)
assert.Len(frSite.RegularPages(), 5)
doc1 := readDestination(t, fs, "public/en/sect/doc1-slug/index.html")
require.True(t, strings.Contains(doc1, "Template Changed"), doc1)
},
@ -571,18 +560,18 @@ func TestMultiSitesRebuild(t *testing.T) {
},
[]fsnotify.Event{{Name: filepath.FromSlash("i18n/fr.yaml"), Op: fsnotify.Write}},
func(t *testing.T) {
assert.Len(enSite.RegularPages, 6)
assert.Len(enSite.AllPages, 34)
assert.Len(frSite.RegularPages, 5)
assert.Len(enSite.RegularPages(), 6)
assert.Len(enSite.AllPages(), 34)
assert.Len(frSite.RegularPages(), 5)
docEn := readDestination(t, fs, "public/en/sect/doc1-slug/index.html")
require.True(t, strings.Contains(docEn, "Hello"), "No Hello")
docFr := readDestination(t, fs, "public/fr/sect/doc1/index.html")
require.True(t, strings.Contains(docFr, "Salut"), "No Salut")
homeEn := enSite.getPage(KindHome)
homeEn := enSite.getPage(page.KindHome)
require.NotNil(t, homeEn)
assert.Len(homeEn.Translations(), 3)
require.Equal(t, "fr", homeEn.Translations()[0].Lang())
require.Equal(t, "fr", homeEn.Translations()[0].Language().Lang)
},
},
@ -595,9 +584,9 @@ func TestMultiSitesRebuild(t *testing.T) {
{Name: filepath.FromSlash("layouts/shortcodes/shortcode.html"), Op: fsnotify.Write},
},
func(t *testing.T) {
assert.Len(enSite.RegularPages, 6)
assert.Len(enSite.AllPages, 34)
assert.Len(frSite.RegularPages, 5)
assert.Len(enSite.RegularPages(), 6)
assert.Len(enSite.AllPages(), 34)
assert.Len(frSite.RegularPages(), 5)
b.AssertFileContent("public/fr/sect/doc1/index.html", "Single", "Modified Shortcode: Salut")
b.AssertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Modified Shortcode: Hello")
},
@ -617,23 +606,6 @@ func TestMultiSitesRebuild(t *testing.T) {
this.assertFunc(t)
}
// Check that the drafts etc. are not built/processed/rendered.
assertShouldNotBuild(t, b.H)
}
func assertShouldNotBuild(t *testing.T, sites *HugoSites) {
s := sites.Sites[0]
for _, p := range s.rawAllPages {
// No HTML when not processed
require.Equal(t, p.shouldBuild(), bytes.Contains(p.workContent, []byte("</")), p.BaseFileName()+": "+string(p.workContent))
require.Equal(t, p.shouldBuild(), p.content() != "", fmt.Sprintf("%v:%v", p.content(), p.shouldBuild()))
require.Equal(t, p.shouldBuild(), p.content() != "", p.BaseFileName())
}
}
func TestAddNewLanguage(t *testing.T) {
@ -671,31 +643,32 @@ title = "Svenska"
enSite := sites.Sites[0]
svSite := sites.Sites[1]
frSite := sites.Sites[2]
require.True(t, enSite.Language.Lang == "en", enSite.Language.Lang)
require.True(t, svSite.Language.Lang == "sv", svSite.Language.Lang)
require.True(t, frSite.Language.Lang == "fr", frSite.Language.Lang)
require.True(t, enSite.language.Lang == "en", enSite.language.Lang)
require.True(t, svSite.language.Lang == "sv", svSite.language.Lang)
require.True(t, frSite.language.Lang == "fr", frSite.language.Lang)
homeEn := enSite.getPage(KindHome)
homeEn := enSite.getPage(page.KindHome)
require.NotNil(t, homeEn)
require.Len(t, homeEn.Translations(), 4)
require.Equal(t, "sv", homeEn.Translations()[0].Lang())
require.Len(t, enSite.RegularPages, 5)
require.Len(t, frSite.RegularPages, 4)
require.Equal(t, "sv", homeEn.Translations()[0].Language().Lang)
require.Len(t, enSite.RegularPages(), 5)
require.Len(t, frSite.RegularPages(), 4)
// Veriy Swedish site
require.Len(t, svSite.RegularPages, 1)
svPage := svSite.RegularPages[0]
require.Len(t, svSite.RegularPages(), 1)
svPage := svSite.RegularPages()[0]
require.Equal(t, "Swedish Contentfile", svPage.title)
require.Equal(t, "sv", svPage.Lang())
require.Equal(t, "Swedish Contentfile", svPage.Title())
require.Equal(t, "sv", svPage.Language().Lang)
require.Len(t, svPage.Translations(), 2)
require.Len(t, svPage.AllTranslations(), 3)
require.Equal(t, "en", svPage.Translations()[0].Lang())
require.Equal(t, "en", svPage.Translations()[0].Language().Lang)
// Regular pages have no children
require.Len(t, svPage.Pages, 0)
require.Len(t, svPage.data["Pages"], 0)
require.Len(t, svPage.Pages(), 0)
require.Len(t, svPage.Data().(page.Data).Pages(), 0)
}
@ -782,12 +755,12 @@ Some text. Some more text.
content = append(content, []string{"s2/_index.md", fmt.Sprintf(contentTempl, defaultOutputs, fmt.Sprintf("S %d", 2), 2, true)}...)
b.WithSimpleConfigFile()
b.WithTemplates("layouts/_default/single.html", `Single: {{ .Content }}`)
b.WithTemplates("layouts/_default/single.html", `Single: {{ .Content }}|RelPermalink: {{ .RelPermalink }}|Permalink: {{ .Permalink }}`)
b.WithTemplates("layouts/_default/myview.html", `View: {{ len .Content }}`)
b.WithTemplates("layouts/_default/single.json", `Single JSON: {{ .Content }}`)
b.WithTemplates("layouts/_default/single.json", `Single JSON: {{ .Content }}|RelPermalink: {{ .RelPermalink }}|Permalink: {{ .Permalink }}`)
b.WithTemplates("layouts/_default/list.html", `
Page: {{ .Paginator.PageNumber }}
P: {{ path.Join .Path }}
P: {{ with .File }}{{ path.Join .Path }}{{ end }}
List: {{ len .Paginator.Pages }}|List Content: {{ len .Content }}
{{ $shuffled := where .Site.RegularPages "Params.multioutput" true | shuffle }}
{{ $first5 := $shuffled | first 5 }}
@ -810,7 +783,7 @@ END
if i%10 == 0 {
section = "s2"
}
checkContent(b, fmt.Sprintf("public/%s/page%d/index.html", section, i), 8343, contentMatchers...)
checkContent(b, fmt.Sprintf("public/%s/page%d/index.html", section, i), contentMatchers...)
}
}
@ -819,48 +792,158 @@ END
if i%10 == 0 {
section = "s2"
}
checkContent(b, fmt.Sprintf("public/%s/page%d/index.json", section, i), 8348, contentMatchers...)
checkContent(b, fmt.Sprintf("public/%s/page%d/index.json", section, i), contentMatchers...)
}
checkContent(b, "public/s1/index.html", 184, "P: s1/_index.md\nList: 10|List Content: 8335\n\n\nL1: 500 L2: 5\n\nRender 0: View: 8335\n\nRender 1: View: 8335\n\nRender 2: View: 8335\n\nRender 3: View: 8335\n\nRender 4: View: 8335\n\nEND\n")
checkContent(b, "public/s2/index.html", 184, "P: s2/_index.md\nList: 10|List Content: 8335", "Render 4: View: 8335\n\nEND")
checkContent(b, "public/index.html", 181, "P: _index.md\nList: 10|List Content: 8335", "4: View: 8335\n\nEND")
checkContent(b, "public/s1/index.html", "P: s1/_index.md\nList: 10|List Content: 8335\n\n\nL1: 500 L2: 5\n\nRender 0: View: 8335\n\nRender 1: View: 8335\n\nRender 2: View: 8335\n\nRender 3: View: 8335\n\nRender 4: View: 8335\n\nEND\n")
checkContent(b, "public/s2/index.html", "P: s2/_index.md\nList: 10|List Content: 8335", "Render 4: View: 8335\n\nEND")
checkContent(b, "public/index.html", "P: _index.md\nList: 10|List Content: 8335", "4: View: 8335\n\nEND")
// Chek paginated pages
// Check paginated pages
for i := 2; i <= 9; i++ {
checkContent(b, fmt.Sprintf("public/page/%d/index.html", i), 181, fmt.Sprintf("Page: %d", i), "Content: 8335\n\n\nL1: 500 L2: 5\n\nRender 0: View: 8335", "Render 4: View: 8335\n\nEND")
checkContent(b, fmt.Sprintf("public/page/%d/index.html", i), fmt.Sprintf("Page: %d", i), "Content: 8335\n\n\nL1: 500 L2: 5\n\nRender 0: View: 8335", "Render 4: View: 8335\n\nEND")
}
}
func checkContent(s *sitesBuilder, filename string, length int, matches ...string) {
func checkContent(s *sitesBuilder, filename string, matches ...string) {
content := readDestination(s.T, s.Fs, filename)
for _, match := range matches {
if !strings.Contains(content, match) {
s.Fatalf("No match for %q in content for %s\n%q", match, filename, content)
}
}
if len(content) != length {
s.Fatalf("got %d expected %d", len(content), length)
}
func TestTranslationsFromContentToNonContent(t *testing.T) {
b := newTestSitesBuilder(t)
b.WithConfigFile("toml", `
baseURL = "http://example.com/"
defaultContentLanguage = "en"
[languages]
[languages.en]
weight = 10
contentDir = "content/en"
[languages.nn]
weight = 20
contentDir = "content/nn"
`)
b.WithContent("en/mysection/_index.md", `
---
Title: My Section
---
`)
b.WithContent("en/_index.md", `
---
Title: My Home
---
`)
b.WithContent("en/categories/mycat/_index.md", `
---
Title: My MyCat
---
`)
b.WithContent("en/categories/_index.md", `
---
Title: My categories
---
`)
for _, lang := range []string{"en", "nn"} {
b.WithContent(lang+"/mysection/page.md", `
---
Title: My Page
categories: ["mycat"]
---
`)
}
b.Build(BuildCfg{})
for _, path := range []string{
"/",
"/mysection",
"/categories",
"/categories/mycat",
} {
t.Run(path, func(t *testing.T) {
assert := require.New(t)
s1, _ := b.H.Sites[0].getPageNew(nil, path)
s2, _ := b.H.Sites[1].getPageNew(nil, path)
assert.NotNil(s1)
assert.NotNil(s2)
assert.Equal(1, len(s1.Translations()))
assert.Equal(1, len(s2.Translations()))
assert.Equal(s2, s1.Translations()[0])
assert.Equal(s1, s2.Translations()[0])
m1 := s1.Translations().MergeByLanguage(s2.Translations())
m2 := s2.Translations().MergeByLanguage(s1.Translations())
assert.Equal(1, len(m1))
assert.Equal(1, len(m2))
})
}
}
// https://github.com/gohugoio/hugo/issues/5777
func TestTableOfContentsInShortcodes(t *testing.T) {
t.Parallel()
b := newMultiSiteTestDefaultBuilder(t)
b.WithTemplatesAdded("layouts/shortcodes/toc.html", tocShortcode)
b.WithTemplatesAdded("layouts/shortcodes/wrapper.html", "{{ .Inner }}")
b.WithContent("post/simple.en.md", tocPageSimple)
b.WithContent("post/variants1.en.md", tocPageVariants1)
b.WithContent("post/variants2.en.md", tocPageVariants2)
b.WithContent("post/withSCInHeading.en.md", tocPageWithShortcodesInHeadings)
b.CreateSites().Build(BuildCfg{})
b.AssertFileContent("public/en/post/simple/index.html", tocPageSimpleExpected)
b.AssertFileContent("public/en/post/simple/index.html",
tocPageSimpleExpected,
// Make sure it is inserted twice
`TOC1: <nav id="TableOfContents">`,
`TOC2: <nav id="TableOfContents">`,
)
b.AssertFileContentFn("public/en/post/variants1/index.html", func(s string) bool {
return strings.Count(s, "TableOfContents") == 4
})
b.AssertFileContentFn("public/en/post/variants2/index.html", func(s string) bool {
return strings.Count(s, "TableOfContents") == 6
})
b.AssertFileContent("public/en/post/withSCInHeading/index.html", tocPageWithShortcodesInHeadingsExpected)
}
var tocShortcode = `
{{ .Page.TableOfContents }}
TOC1: {{ .Page.TableOfContents }}
TOC2: {{ .Page.TableOfContents }}
`
func TestSelfReferencedContentInShortcode(t *testing.T) {
@ -901,6 +984,41 @@ Even more text.
Lorem ipsum...
`
var tocPageVariants1 = `---
title: tocTest
publishdate: "2000-01-01"
---
Variant 1:
{{% wrapper %}}
{{< toc >}}
{{% /wrapper %}}
# Heading 1
Variant 3:
{{% toc %}}
`
var tocPageVariants2 = `---
title: tocTest
publishdate: "2000-01-01"
---
Variant 1:
{{% wrapper %}}
{{< toc >}}
{{% /wrapper %}}
# Heading 1
Variant 2:
{{< wrapper >}}
{{< toc >}}
{{< /wrapper >}}
Variant 3:
{{% toc %}}
`
var tocPageSimpleExpected = `<nav id="TableOfContents">
<ul>
<li><a href="#1">Heading 1</a>
@ -958,6 +1076,7 @@ paginate = 1
disablePathToLower = true
defaultContentLanguage = "{{ .DefaultContentLanguage }}"
defaultContentLanguageInSubdir = {{ .DefaultContentLanguageInSubdir }}
enableRobotsTXT = true
[permalinks]
other = "/somewhere/else/:filename"
@ -1015,6 +1134,7 @@ disablePathToLower: true
paginate: 1
defaultContentLanguage: "{{ .DefaultContentLanguage }}"
defaultContentLanguageInSubdir: {{ .DefaultContentLanguageInSubdir }}
enableRobotsTXT: true
permalinks:
other: "/somewhere/else/:filename"
@ -1073,6 +1193,7 @@ var multiSiteJSONConfigTemplate = `
"disablePathToLower": true,
"defaultContentLanguage": "{{ .DefaultContentLanguage }}",
"defaultContentLanguageInSubdir": true,
"enableRobotsTXT": true,
"permalinks": {
"other": "/somewhere/else/:filename"
},
@ -1170,7 +1291,23 @@ func readFileFromFs(t testing.TB, fs afero.Fs, filename string) string {
b, err := afero.ReadFile(fs, filename)
if err != nil {
// Print some debug info
root := strings.Split(filename, helpers.FilePathSeparator)[0]
hadSlash := strings.HasPrefix(filename, helpers.FilePathSeparator)
start := 0
if hadSlash {
start = 1
}
end := start + 1
parts := strings.Split(filename, helpers.FilePathSeparator)
if parts[start] == "work" {
end++
}
root := filepath.Join(parts[start:end]...)
if hadSlash {
root = helpers.FilePathSeparator + root
}
helpers.PrintFs(fs, root, os.Stdout)
Fatalf(t, "Failed to read file: %s", err)
}
@ -1262,8 +1399,8 @@ NOTE: slug should be used as URL
title: doc1
weight: 1
plaques:
- frtag1
- frtag2
- FRtag1
- FRtag2
publishdate: "2000-01-04"
---
# doc1
@ -1293,7 +1430,7 @@ aliases: [/en/al/alias1,/al/alias2/]
tags:
- tag2
- tag1
url: /superbob
url: /superbob/
---
# doc3
*some content*
@ -1303,7 +1440,7 @@ NOTE: third 'en' doc, should trigger pagination on home page.
title: doc4
weight: 4
plaques:
- frtag1
- FRtag1
publishdate: "2000-01-05"
---
# doc4

View file

@ -3,6 +3,8 @@ package hugolib
import (
"testing"
"github.com/gohugoio/hugo/resources/page"
"github.com/stretchr/testify/require"
)
@ -55,7 +57,7 @@ languageName = "Nynorsk"
s1 := b.H.Sites[0]
s1h := s1.getPage(KindHome)
s1h := s1.getPage(page.KindHome)
assert.True(s1h.IsTranslated())
assert.Len(s1h.Translations(), 2)
assert.Equal("https://example.com/docs/", s1h.Permalink())
@ -66,9 +68,8 @@ languageName = "Nynorsk"
// For multihost, we never want any content in the root.
//
// check url in front matter:
pageWithURLInFrontMatter := s1.getPage(KindPage, "sect/doc3.en.md")
pageWithURLInFrontMatter := s1.getPage(page.KindPage, "sect/doc3.en.md")
assert.NotNil(pageWithURLInFrontMatter)
assert.Equal("/superbob", pageWithURLInFrontMatter.URL())
assert.Equal("/docs/superbob/", pageWithURLInFrontMatter.RelPermalink())
b.AssertFileContent("public/en/superbob/index.html", "doc3|Hello|en")
@ -78,7 +79,7 @@ languageName = "Nynorsk"
s2 := b.H.Sites[1]
s2h := s2.getPage(KindHome)
s2h := s2.getPage(page.KindHome)
assert.Equal("https://example.fr/", s2h.Permalink())
b.AssertFileContent("public/fr/index.html", "French Home Page", "String Resource: /docs/text/pipes.txt")
@ -94,22 +95,19 @@ languageName = "Nynorsk"
// Check bundles
bundleEn := s1.getPage(KindPage, "bundles/b1/index.en.md")
bundleEn := s1.getPage(page.KindPage, "bundles/b1/index.en.md")
require.NotNil(t, bundleEn)
require.Equal(t, "/docs/bundles/b1/", bundleEn.RelPermalink())
require.Equal(t, 1, len(bundleEn.Resources))
logoEn := bundleEn.Resources.GetMatch("logo*")
require.NotNil(t, logoEn)
require.Equal(t, "/docs/bundles/b1/logo.png", logoEn.RelPermalink())
b.AssertFileContent("public/en/bundles/b1/logo.png", "PNG Data")
require.Equal(t, 1, len(bundleEn.Resources()))
bundleFr := s2.getPage(KindPage, "bundles/b1/index.md")
b.AssertFileContent("public/en/bundles/b1/logo.png", "PNG Data")
b.AssertFileContent("public/en/bundles/b1/index.html", " image/png: /docs/bundles/b1/logo.png")
bundleFr := s2.getPage(page.KindPage, "bundles/b1/index.md")
require.NotNil(t, bundleFr)
require.Equal(t, "/bundles/b1/", bundleFr.RelPermalink())
require.Equal(t, 1, len(bundleFr.Resources))
logoFr := bundleFr.Resources.GetMatch("logo*")
require.NotNil(t, logoFr)
require.Equal(t, "/bundles/b1/logo.png", logoFr.RelPermalink())
require.Equal(t, 1, len(bundleFr.Resources()))
b.AssertFileContent("public/fr/bundles/b1/logo.png", "PNG Data")
b.AssertFileContent("public/fr/bundles/b1/index.html", " image/png: /bundles/b1/logo.png")
}

303
hugolib/hugo_smoke_test.go Normal file
View file

@ -0,0 +1,303 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestSmoke(t *testing.T) {
t.Parallel()
assert := require.New(t)
const configFile = `
baseURL = "https://example.com"
title = "Simple Site"
rssLimit = 3
defaultContentLanguage = "en"
enableRobotsTXT = true
[languages]
[languages.en]
weight = 1
title = "In English"
[languages.no]
weight = 2
title = "På norsk"
[params]
hugo = "Rules!"
[outputs]
home = ["HTML", "JSON", "CSV", "RSS"]
`
const pageContentAndSummaryDivider = `---
title: Page with outputs
hugo: "Rocks!"
outputs: ["HTML", "JSON"]
tags: [ "hugo" ]
aliases: [ "/a/b/c" ]
---
This is summary.
<!--more-->
This is content with some shortcodes.
Shortcode 1: {{< sc >}}.
Shortcode 2: {{< sc >}}.
`
const pageContentWithMarkdownShortcodes = `---
title: Page with markdown shortcode
hugo: "Rocks!"
outputs: ["HTML", "JSON"]
---
This is summary.
<!--more-->
This is content[^a].
# Header above
{{% markdown-shortcode %}}
# Header inside
Some **markdown**.[^b]
{{% /markdown-shortcode %}}
# Heder below
Some more content[^c].
Footnotes:
[^a]: Fn 1
[^b]: Fn 2
[^c]: Fn 3
`
var pageContentAutoSummary = strings.Replace(pageContentAndSummaryDivider, "<!--more-->", "", 1)
b := newTestSitesBuilder(t).WithConfigFile("toml", configFile)
b.WithTemplatesAdded("shortcodes/markdown-shortcode.html", `
Some **Markdown** in shortcode.
{{ .Inner }}
`)
b.WithTemplatesAdded("shortcodes/markdown-shortcode.json", `
Some **Markdown** in JSON shortcode.
{{ .Inner }}
`)
for i := 1; i <= 11; i++ {
if i%2 == 0 {
b.WithContent(fmt.Sprintf("blog/page%d.md", i), pageContentAndSummaryDivider)
b.WithContent(fmt.Sprintf("blog/page%d.no.md", i), pageContentAndSummaryDivider)
} else {
b.WithContent(fmt.Sprintf("blog/page%d.md", i), pageContentAutoSummary)
}
}
for i := 1; i <= 5; i++ {
// Root section pages
b.WithContent(fmt.Sprintf("root%d.md", i), pageContentAutoSummary)
}
// https://github.com/gohugoio/hugo/issues/4695
b.WithContent("blog/markyshort.md", pageContentWithMarkdownShortcodes)
// Add one bundle
b.WithContent("blog/mybundle/index.md", pageContentAndSummaryDivider)
b.WithContent("blog/mybundle/mydata.csv", "Bundled CSV")
const (
commonPageTemplate = `|{{ .Kind }}|{{ .Title }}|{{ .Path }}|{{ .Summary }}|{{ .Content }}|RelPermalink: {{ .RelPermalink }}|WordCount: {{ .WordCount }}|Pages: {{ .Pages }}|Data Pages: Pages({{ len .Data.Pages }})|Resources: {{ len .Resources }}|Summary: {{ .Summary }}`
commonPaginatorTemplate = `|Paginator: {{ with .Paginator }}{{ .PageNumber }}{{ else }}NIL{{ end }}`
commonListTemplateNoPaginator = `|{{ range $i, $e := (.Pages | first 1) }}|Render {{ $i }}: {{ .Kind }}|{{ .Render "li" }}|{{ end }}|Site params: {{ $.Site.Params.hugo }}|RelPermalink: {{ .RelPermalink }}`
commonListTemplate = commonPaginatorTemplate + `|{{ range $i, $e := (.Pages | first 1) }}|Render {{ $i }}: {{ .Kind }}|{{ .Render "li" }}|{{ end }}|Site params: {{ $.Site.Params.hugo }}|RelPermalink: {{ .RelPermalink }}`
commonShortcodeTemplate = `|{{ .Name }}|{{ .Ordinal }}|{{ .Page.Summary }}|{{ .Page.Content }}|WordCount: {{ .Page.WordCount }}`
prevNextTemplate = `|Prev: {{ with .Prev }}{{ .RelPermalink }}{{ end }}|Next: {{ with .Next }}{{ .RelPermalink }}{{ end }}`
prevNextInSectionTemplate = `|PrevInSection: {{ with .PrevInSection }}{{ .RelPermalink }}{{ end }}|NextInSection: {{ with .NextInSection }}{{ .RelPermalink }}{{ end }}`
paramsTemplate = `|Params: {{ .Params.hugo }}`
treeNavTemplate = `|CurrentSection: {{ .CurrentSection }}`
)
b.WithTemplates(
"_default/list.html", "HTML: List"+commonPageTemplate+commonListTemplate+"|First Site: {{ .Sites.First.Title }}",
"_default/list.json", "JSON: List"+commonPageTemplate+commonListTemplateNoPaginator,
"_default/list.csv", "CSV: List"+commonPageTemplate+commonListTemplateNoPaginator,
"_default/single.html", "HTML: Single"+commonPageTemplate+prevNextTemplate+prevNextInSectionTemplate+treeNavTemplate,
"_default/single.json", "JSON: Single"+commonPageTemplate,
// For .Render test
"_default/li.html", `HTML: LI|{{ strings.Contains .Content "HTML: Shortcode: sc" }}`+paramsTemplate,
"_default/li.json", `JSON: LI|{{ strings.Contains .Content "JSON: Shortcode: sc" }}`+paramsTemplate,
"_default/li.csv", `CSV: LI|{{ strings.Contains .Content "CSV: Shortcode: sc" }}`+paramsTemplate,
"404.html", "{{ .Kind }}|{{ .Title }}|Page not found",
"shortcodes/sc.html", "HTML: Shortcode: "+commonShortcodeTemplate,
"shortcodes/sc.json", "JSON: Shortcode: "+commonShortcodeTemplate,
"shortcodes/sc.csv", "CSV: Shortcode: "+commonShortcodeTemplate,
)
b.CreateSites().Build(BuildCfg{})
b.AssertFileContent("public/blog/page1/index.html",
"This is content with some shortcodes.",
"Page with outputs",
"Pages: Pages(0)",
"RelPermalink: /blog/page1/|",
"Shortcode 1: HTML: Shortcode: |sc|0|||WordCount: 0.",
"Shortcode 2: HTML: Shortcode: |sc|1|||WordCount: 0.",
"Prev: /blog/page10/|Next: /blog/mybundle/",
"PrevInSection: /blog/page10/|NextInSection: /blog/mybundle/",
"Summary: This is summary.",
"CurrentSection: Page(/blog)",
)
b.AssertFileContent("public/blog/page1/index.json",
"JSON: Single|page|Page with outputs|",
"SON: Shortcode: |sc|0||")
b.AssertFileContent("public/index.html",
"home|In English",
"Site params: Rules",
"Pages: Pages(18)|Data Pages: Pages(18)",
"Paginator: 1",
"First Site: In English",
"RelPermalink: /",
)
b.AssertFileContent("public/no/index.html", "home|På norsk", "RelPermalink: /no/")
// Check RSS
rssHome := b.FileContent("public/index.xml")
assert.Contains(rssHome, `<atom:link href="https://example.com/index.xml" rel="self" type="application/rss+xml" />`)
assert.Equal(3, strings.Count(rssHome, "<item>")) // rssLimit = 3
// .Render should use template/content from the current output format
// even if that output format isn't configured for that page.
b.AssertFileContent(
"public/index.json",
"Render 0: page|JSON: LI|false|Params: Rocks!",
)
b.AssertFileContent(
"public/index.html",
"Render 0: page|HTML: LI|false|Params: Rocks!|",
)
b.AssertFileContent(
"public/index.csv",
"Render 0: page|CSV: LI|false|Params: Rocks!|",
)
// Check bundled resources
b.AssertFileContent(
"public/blog/mybundle/index.html",
"Resources: 1",
)
// Check pages in root section
b.AssertFileContent(
"public/root3/index.html",
"Single|page|Page with outputs|root3.md|",
"Prev: /root4/|Next: /root2/|PrevInSection: /root4/|NextInSection: /root2/",
)
b.AssertFileContent(
"public/root3/index.json", "Shortcode 1: JSON:")
// Paginators
b.AssertFileContent("public/page/1/index.html", `rel="canonical" href="https://example.com/"`)
b.AssertFileContent("public/page/2/index.html", "HTML: List|home|In English|", "Paginator: 2")
// 404
b.AssertFileContent("public/404.html", "404|404 Page not found")
// Sitemaps
b.AssertFileContent("public/en/sitemap.xml", "<loc>https://example.com/blog/</loc>")
b.AssertFileContent("public/no/sitemap.xml", `hreflang="no"`)
b.AssertFileContent("public/sitemap.xml", "<loc>https://example.com/en/sitemap.xml</loc>", "<loc>https://example.com/no/sitemap.xml</loc>")
// robots.txt
b.AssertFileContent("public/robots.txt", `User-agent: *`)
// Aliases
b.AssertFileContent("public/a/b/c/index.html", `refresh`)
// Markdown vs shortcodes
// Check that all footnotes are grouped (even those from inside the shortcode)
b.AssertFileContentRe("public/blog/markyshort/index.html", `Footnotes:.*<ol>.*Fn 1.*Fn 2.*Fn 3.*</ol>`)
}
// https://github.com/golang/go/issues/30286
func TestDataRace(t *testing.T) {
const page = `
---
title: "The Page"
outputs: ["HTML", "JSON"]
---
The content.
`
b := newTestSitesBuilder(t).WithSimpleConfigFile()
for i := 1; i <= 50; i++ {
b.WithContent(fmt.Sprintf("blog/page%d.md", i), page)
}
b.WithContent("_index.md", `
---
title: "The Home"
outputs: ["HTML", "JSON", "CSV", "RSS"]
---
The content.
`)
commonTemplate := `{{ .Data.Pages }}`
b.WithTemplatesAdded("_default/single.html", "HTML Single: "+commonTemplate)
b.WithTemplatesAdded("_default/list.html", "HTML List: "+commonTemplate)
b.CreateSites().Build(BuildCfg{})
}

View file

@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -19,6 +19,8 @@ import (
"path/filepath"
"testing"
"github.com/gohugoio/hugo/resources/page"
"github.com/stretchr/testify/require"
)
@ -99,15 +101,19 @@ Content.
section := "sect"
var contentRoot = func(lang string) string {
contentRoot := "content/main"
switch lang {
case "nn":
contentRoot = "content/norsk"
return "content/norsk"
case "sv":
contentRoot = "content/svensk"
return "content/svensk"
default:
return "content/main"
}
return contentRoot + "/" + section
}
var contentSectionRoot = func(lang string) string {
return contentRoot(lang) + "/" + section
}
for _, lang := range []string{"en", "nn", "sv"} {
@ -124,7 +130,7 @@ Content.
}
base := fmt.Sprintf("p-%s-%d", lang, j)
slug := fmt.Sprintf("%s", base)
slug := base
langID := ""
if lang == "sv" && j%4 == 0 {
@ -139,7 +145,7 @@ Content.
slug += langID
contentRoot := contentRoot(lang)
contentRoot := contentSectionRoot(lang)
filename := filepath.Join(contentRoot, fmt.Sprintf("page%d%s.md", j, langID))
contentFiles = append(contentFiles, filename, fmt.Sprintf(pageTemplate, slug, slug, j))
@ -148,7 +154,7 @@ Content.
// Put common translations in all of them
for i, lang := range []string{"en", "nn", "sv"} {
contentRoot := contentRoot(lang)
contentRoot := contentSectionRoot(lang)
slug := fmt.Sprintf("common_%s", lang)
@ -173,7 +179,7 @@ Content.
// Add a bundle with some images
for i, lang := range []string{"en", "nn", "sv"} {
contentRoot := contentRoot(lang)
contentRoot := contentSectionRoot(lang)
slug := fmt.Sprintf("bundle_%s", lang)
filename := filepath.Join(contentRoot, "mybundle", "index.md")
contentFiles = append(contentFiles, filename, fmt.Sprintf(pageBundleTemplate, slug, 400+i))
@ -190,11 +196,20 @@ Content.
}
// Add some static files inside the content dir
// https://github.com/gohugoio/hugo/issues/5759
for _, lang := range []string{"en", "nn", "sv"} {
contentRoot := contentRoot(lang)
for i := 0; i < 2; i++ {
filename := filepath.Join(contentRoot, "mystatic", fmt.Sprintf("file%d.yaml", i))
contentFiles = append(contentFiles, filename, lang)
}
}
b := newTestSitesBuilder(t)
b.WithWorkingDir("/my/project").WithConfigFile("toml", config).WithContent(contentFiles...).CreateSites()
_ = os.Stdout
//printFs(b.H.BaseFs.ContentFs, "/", os.Stdout)
b.Build(BuildCfg{})
@ -204,11 +219,14 @@ Content.
nnSite := b.H.Sites[1]
svSite := b.H.Sites[2]
//dumpPages(nnSite.RegularPages...)
assert.Equal(12, len(nnSite.RegularPages))
assert.Equal(13, len(enSite.RegularPages))
b.AssertFileContent("/my/project/public/en/mystatic/file1.yaml", "en")
b.AssertFileContent("/my/project/public/nn/mystatic/file1.yaml", "nn")
assert.Equal(10, len(svSite.RegularPages))
//dumpPages(nnSite.RegularPages...)
assert.Equal(12, len(nnSite.RegularPages()))
assert.Equal(13, len(enSite.RegularPages()))
assert.Equal(10, len(svSite.RegularPages()))
svP2, err := svSite.getPageNew(nil, "/sect/page2.md")
assert.NoError(err)
@ -217,9 +235,9 @@ Content.
enP2, err := enSite.getPageNew(nil, "/sect/page2.md")
assert.NoError(err)
assert.Equal("en", enP2.Lang())
assert.Equal("sv", svP2.Lang())
assert.Equal("nn", nnP2.Lang())
assert.Equal("en", enP2.Language().Lang)
assert.Equal("sv", svP2.Language().Lang)
assert.Equal("nn", nnP2.Language().Lang)
content, _ := nnP2.Content()
assert.Contains(content, "SVP3-REF: https://example.org/sv/sect/p-sv-3/")
@ -241,10 +259,10 @@ Content.
assert.NoError(err)
assert.Equal("https://example.org/nn/sect/p-nn-3/", nnP3Ref)
for i, p := range enSite.RegularPages {
for i, p := range enSite.RegularPages() {
j := i + 1
msg := fmt.Sprintf("Test %d", j)
assert.Equal("en", p.Lang(), msg)
assert.Equal("en", p.Language().Lang, msg)
assert.Equal("sect", p.Section())
if j < 9 {
if j%4 == 0 {
@ -256,20 +274,20 @@ Content.
}
// Check bundles
bundleEn := enSite.RegularPages[len(enSite.RegularPages)-1]
bundleNn := nnSite.RegularPages[len(nnSite.RegularPages)-1]
bundleSv := svSite.RegularPages[len(svSite.RegularPages)-1]
bundleEn := enSite.RegularPages()[len(enSite.RegularPages())-1]
bundleNn := nnSite.RegularPages()[len(nnSite.RegularPages())-1]
bundleSv := svSite.RegularPages()[len(svSite.RegularPages())-1]
assert.Equal("/en/sect/mybundle/", bundleEn.RelPermalink())
assert.Equal("/sv/sect/mybundle/", bundleSv.RelPermalink())
assert.Equal(4, len(bundleEn.Resources))
assert.Equal(4, len(bundleNn.Resources))
assert.Equal(4, len(bundleSv.Resources))
assert.Equal(4, len(bundleEn.Resources()))
assert.Equal(4, len(bundleNn.Resources()))
assert.Equal(4, len(bundleSv.Resources()))
assert.Equal("/en/sect/mybundle/logo.png", bundleEn.Resources.GetMatch("logo*").RelPermalink())
assert.Equal("/nn/sect/mybundle/logo.png", bundleNn.Resources.GetMatch("logo*").RelPermalink())
assert.Equal("/sv/sect/mybundle/logo.png", bundleSv.Resources.GetMatch("logo*").RelPermalink())
b.AssertFileContent("/my/project/public/en/sect/mybundle/index.html", "image/png: /en/sect/mybundle/logo.png")
b.AssertFileContent("/my/project/public/nn/sect/mybundle/index.html", "image/png: /nn/sect/mybundle/logo.png")
b.AssertFileContent("/my/project/public/sv/sect/mybundle/index.html", "image/png: /sv/sect/mybundle/logo.png")
b.AssertFileContent("/my/project/public/sv/sect/mybundle/featured.png", "PNG Data for sv")
b.AssertFileContent("/my/project/public/nn/sect/mybundle/featured.png", "PNG Data for nn")
@ -278,9 +296,9 @@ Content.
b.AssertFileContent("/my/project/public/sv/sect/mybundle/logo.png", "PNG Data")
b.AssertFileContent("/my/project/public/nn/sect/mybundle/logo.png", "PNG Data")
nnSect := nnSite.getPage(KindSection, "sect")
nnSect := nnSite.getPage(page.KindSection, "sect")
assert.NotNil(nnSect)
assert.Equal(12, len(nnSect.Pages))
assert.Equal(12, len(nnSect.Pages()))
nnHome, _ := nnSite.Info.Home()
assert.Equal("/nn/", nnHome.RelPermalink())

View file

@ -1,60 +0,0 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
// An Image contains metadata for images + image sitemaps
// https://support.google.com/webmasters/answer/178636?hl=en
type Image struct {
// The URL of the image. In some cases, the image URL may not be on the
// same domain as your main site. This is fine, as long as both domains
// are verified in Webmaster Tools. If, for example, you use a
// content delivery network (CDN) to host your images, make sure that the
// hosting site is verified in Webmaster Tools OR that you submit your
// sitemap using robots.txt. In addition, make sure that your robots.txt
// file doesnt disallow the crawling of any content you want indexed.
URL string
Title string
Caption string
AltText string
// The geographic location of the image. For example,
// <image:geo_location>Limerick, Ireland</image:geo_location>.
GeoLocation string
// A URL to the license of the image.
License string
}
// A Video contains metadata for videos + video sitemaps
// https://support.google.com/webmasters/answer/80471?hl=en
type Video struct {
ThumbnailLoc string
Title string
Description string
ContentLoc string
PlayerLoc string
Duration string
ExpirationDate string
Rating string
ViewCount string
PublicationDate string
FamilyFriendly string
Restriction string
GalleryLoc string
Price string
RequiresSubscription string
Uploader string
Live string
}

View file

@ -1,4 +1,4 @@
// Copyright 2017 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -83,9 +83,9 @@ Menu Main: {{ partial "menu.html" (dict "page" . "menu" "main") }}`,
s := h.Sites[0]
require.Len(t, s.Menus, 2)
require.Len(t, s.Menus(), 2)
p1 := s.RegularPages[0].Menus()
p1 := s.RegularPages()[0].Menus()
// There is only one menu in the page, but it is "member of" 2
require.Len(t, p1, 1)

View file

@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -17,13 +17,10 @@ import (
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
func TestMinifyPublisher(t *testing.T) {
t.Parallel()
assert := require.New(t)
v := viper.New()
v.Set("minify", true)
@ -43,29 +40,24 @@ func TestMinifyPublisher(t *testing.T) {
<body id="home">
<h1>{{ .Page.Title }}</h1>
<h1>{{ .Title }}</h1>
<p>{{ .Permalink }}</p>
</body>
</html>
`
b := newTestSitesBuilder(t)
b.WithViper(v).WithContent("page.md", pageWithAlias)
b.WithTemplates("_default/list.html", htmlTemplate, "_default/single.html", htmlTemplate, "alias.html", htmlTemplate)
b.WithViper(v).WithTemplatesAdded("layouts/index.html", htmlTemplate)
b.CreateSites().Build(BuildCfg{})
assert.Equal(1, len(b.H.Sites))
require.Len(t, b.H.Sites[0].RegularPages, 1)
// Check minification
// HTML
b.AssertFileContent("public/page/index.html", "<!doctype html><html lang=en><head><meta charset=utf-8><title>HTML5 boilerplate all you really need…</title><link rel=stylesheet href=css/style.css></head><body id=home><h1>Has Alias</h1></body></html>")
// HTML alias. Note the custom template which does no redirect.
b.AssertFileContent("public/foo/bar/index.html", "<!doctype html><html lang=en><head><meta charset=utf-8><title>HTML5 boilerplate ")
b.AssertFileContent("public/index.html", "<!doctype html>")
// RSS
b.AssertFileContent("public/index.xml", "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?><rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\"><channel><title/><link>https://example.org/</link>")
// Sitemap
b.AssertFileContent("public/sitemap.xml", "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?><urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" xmlns:xhtml=\"http://www.w3.org/1999/xhtml\"><url><loc>https://example.org/</loc><priority>0</priority></url><url>")
b.AssertFileContent("public/sitemap.xml", "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?><urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" xmlns:xhtml=\"http://www.w3.org/1999/xhtml\"><url><loc>h")
}

View file

@ -1,4 +1,4 @@
// Copyright 2016-present The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -62,10 +62,10 @@ func newMultiLingualFromSites(cfg config.Provider, sites ...*Site) (*Multilingua
languages := make(langs.Languages, len(sites))
for i, s := range sites {
if s.Language == nil {
return nil, errors.New("Missing language for site")
if s.language == nil {
return nil, errors.New("missing language for site")
}
languages[i] = s.Language
languages[i] = s.language
}
defaultLang := cfg.GetString("defaultContentLanguage")
@ -78,19 +78,15 @@ func newMultiLingualFromSites(cfg config.Provider, sites ...*Site) (*Multilingua
}
func newMultiLingualForLanguage(language *langs.Language) *Multilingual {
languages := langs.Languages{language}
return &Multilingual{Languages: languages, DefaultLang: language}
}
func (ml *Multilingual) enabled() bool {
return len(ml.Languages) > 1
}
func (s *Site) multilingualEnabled() bool {
if s.owner == nil {
if s.h == nil {
return false
}
return s.owner.multilingual != nil && s.owner.multilingual.enabled()
return s.h.multilingual != nil && s.h.multilingual.enabled()
}
func toSortedLanguages(cfg config.Provider, l map[string]interface{}) (langs.Languages, error) {

View file

@ -1,99 +0,0 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"fmt"
"sync"
)
type orderedMap struct {
sync.RWMutex
keys []interface{}
m map[interface{}]interface{}
}
func newOrderedMap() *orderedMap {
return &orderedMap{m: make(map[interface{}]interface{})}
}
func newOrderedMapFromStringMapString(m map[string]string) *orderedMap {
om := newOrderedMap()
for k, v := range m {
om.Add(k, v)
}
return om
}
func (m *orderedMap) Add(k, v interface{}) {
m.Lock()
defer m.Unlock()
_, found := m.m[k]
if found {
panic(fmt.Sprintf("%v already added", v))
}
m.m[k] = v
m.keys = append(m.keys, k)
}
func (m *orderedMap) Get(k interface{}) (interface{}, bool) {
m.RLock()
defer m.RUnlock()
v, found := m.m[k]
return v, found
}
func (m *orderedMap) Contains(k interface{}) bool {
m.RLock()
defer m.RUnlock()
_, found := m.m[k]
return found
}
func (m *orderedMap) Keys() []interface{} {
m.RLock()
defer m.RUnlock()
return m.keys
}
func (m *orderedMap) Len() int {
m.RLock()
defer m.RUnlock()
return len(m.keys)
}
// Some shortcuts for known types.
func (m *orderedMap) getShortcode(k interface{}) *shortcode {
v, found := m.Get(k)
if !found {
return nil
}
return v.(*shortcode)
}
func (m *orderedMap) getShortcodeRenderer(k interface{}) func() (string, error) {
v, found := m.Get(k)
if !found {
return nil
}
return v.(func() (string, error))
}
func (m *orderedMap) getString(k interface{}) string {
v, found := m.Get(k)
if !found {
return ""
}
return v.(string)
}

View file

@ -1,69 +0,0 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"fmt"
"sync"
"testing"
"github.com/stretchr/testify/require"
)
func TestOrderedMap(t *testing.T) {
t.Parallel()
assert := require.New(t)
m := newOrderedMap()
m.Add("b", "vb")
m.Add("c", "vc")
m.Add("a", "va")
b, f1 := m.Get("b")
assert.True(f1)
assert.Equal(b, "vb")
assert.True(m.Contains("b"))
assert.False(m.Contains("e"))
assert.Equal([]interface{}{"b", "c", "a"}, m.Keys())
}
func TestOrderedMapConcurrent(t *testing.T) {
t.Parallel()
assert := require.New(t)
var wg sync.WaitGroup
m := newOrderedMap()
for i := 1; i < 20; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
key := fmt.Sprintf("key%d", id)
val := key + "val"
m.Add(key, val)
v, found := m.Get(key)
assert.True(found)
assert.Equal(v, val)
assert.True(m.Contains(key))
assert.True(m.Len() > 0)
assert.True(len(m.Keys()) > 0)
}(i)
}
wg.Wait()
}

File diff suppressed because it is too large Load diff

112
hugolib/page__common.go Normal file
View file

@ -0,0 +1,112 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"sync"
"github.com/bep/gitmap"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/compare"
"github.com/gohugoio/hugo/lazy"
"github.com/gohugoio/hugo/navigation"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/resource"
)
type pageCommon struct {
s *Site
m *pageMeta
// Laziliy initialized dependencies.
init *lazy.Init
// All of these represents the common parts of a page.Page
maps.Scratcher
navigation.PageMenusProvider
page.AuthorProvider
page.PageRenderProvider
page.AlternativeOutputFormatsProvider
page.ChildCareProvider
page.FileProvider
page.GetPageProvider
page.GitInfoProvider
page.InSectionPositioner
page.OutputFormatsProvider
page.PageMetaProvider
page.Positioner
page.RawContentProvider
page.RelatedKeywordsProvider
page.RefProvider
page.ShortcodeInfoProvider
page.SitesProvider
page.DeprecatedWarningPageMethods
page.TranslationsProvider
page.TreeProvider
resource.LanguageProvider
resource.ResourceDataProvider
resource.ResourceMetaProvider
resource.ResourceParamsProvider
resource.ResourceTypesProvider
resource.TranslationKeyProvider
compare.Eqer
// Describes how paths and URLs for this page and its descendants
// should look like.
targetPathDescriptor page.TargetPathDescriptor
layoutDescriptor output.LayoutDescriptor
layoutDescriptorInit sync.Once
// The parsed page content.
pageContent
// Set if feature enabled and this is in a Git repo.
gitInfo *gitmap.GitInfo
// Positional navigation
posNextPrev *nextPrev
posNextPrevSection *nextPrev
// Menus
pageMenus *pageMenus
// Internal use
page.InternalDependencies
// The children. Regular pages will have none.
pages page.Pages
pagesInit sync.Once
// Any bundled resources
resources resource.Resources
resourcesInit sync.Once
translations page.Pages
allTranslations page.Pages
// Calculated an cached translation mapping key
translationKey string
translationKeyInit sync.Once
// Will only be set for sections and regular pages.
parent *pageState
// Will only be set for section pages and the home page.
subSections page.Pages
// Set in fast render mode to force render a given page.
forceRender bool
}

135
hugolib/page__content.go Normal file
View file

@ -0,0 +1,135 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"fmt"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/parser/pageparser"
)
var (
internalSummaryDividerBase = "HUGOMORE42"
internalSummaryDividerBaseBytes = []byte(internalSummaryDividerBase)
internalSummaryDividerPre = []byte("\n\n" + internalSummaryDividerBase + "\n\n")
)
// The content related items on a Page.
type pageContent struct {
renderable bool
selfLayout string
truncated bool
cmap *pageContentMap
shortcodeState *shortcodeHandler
source rawPageContent
}
// returns the content to be processed by Blackfriday or similar.
func (p pageContent) contentToRender(renderedShortcodes map[string]string) []byte {
source := p.source.parsed.Input()
c := make([]byte, 0, len(source)+(len(source)/10))
for _, it := range p.cmap.items {
switch v := it.(type) {
case pageparser.Item:
c = append(c, source[v.Pos:v.Pos+len(v.Val)]...)
case pageContentReplacement:
c = append(c, v.val...)
case *shortcode:
if v.doMarkup || !p.renderable {
// Insert the rendered shortcode.
renderedShortcode, found := renderedShortcodes[v.placeholder]
if !found {
// This should never happen.
panic(fmt.Sprintf("rendered shortcode %q not found", v.placeholder))
}
c = append(c, []byte(renderedShortcode)...)
} else {
// Insert the placeholder so we can insert the content after
// markdown processing.
c = append(c, []byte(v.placeholder)...)
}
default:
panic(fmt.Sprintf("unkown item type %T", it))
}
}
return c
}
func (p pageContent) selfLayoutForOutput(f output.Format) string {
if p.selfLayout == "" {
return ""
}
return p.selfLayout + f.Name
}
type rawPageContent struct {
hasSummaryDivider bool
// The AST of the parsed page. Contains information about:
// shortcodes, front matter, summary indicators.
parsed pageparser.Result
// Returns the position in bytes after any front matter.
posMainContent int
// These are set if we're able to determine this from the source.
posSummaryEnd int
posBodyStart int
}
type pageContentReplacement struct {
val []byte
source pageparser.Item
}
type pageContentMap struct {
// If not, we can skip any pre-rendering of shortcodes.
hasMarkdownShortcode bool
// Indicates whether we must do placeholder replacements.
hasNonMarkdownShortcode bool
// *shortcode, pageContentReplacement or pageparser.Item
items []interface{}
}
func (p *pageContentMap) AddBytes(item pageparser.Item) {
p.items = append(p.items, item)
}
func (p *pageContentMap) AddReplacement(val []byte, source pageparser.Item) {
p.items = append(p.items, pageContentReplacement{val: val, source: source})
}
func (p *pageContentMap) AddShortcode(s *shortcode) {
p.items = append(p.items, s)
if s.doMarkup {
p.hasMarkdownShortcode = true
} else {
p.hasNonMarkdownShortcode = true
}
}

70
hugolib/page__data.go Normal file
View file

@ -0,0 +1,70 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"sync"
"github.com/gohugoio/hugo/resources/page"
)
type pageData struct {
*pageState
dataInit sync.Once
data page.Data
}
func (p *pageData) Data() interface{} {
p.dataInit.Do(func() {
p.data = make(page.Data)
if p.Kind() == page.KindPage {
return
}
switch p.Kind() {
case page.KindTaxonomy:
termInfo := p.getTaxonomyNodeInfo()
pluralInfo := termInfo.parent
singular := pluralInfo.singular
plural := pluralInfo.plural
term := termInfo.term
taxonomy := p.s.Taxonomies[plural].Get(termInfo.termKey)
p.data[singular] = taxonomy
p.data["Singular"] = singular
p.data["Plural"] = plural
p.data["Term"] = term
case page.KindTaxonomyTerm:
info := p.getTaxonomyNodeInfo()
plural := info.plural
singular := info.singular
p.data["Singular"] = singular
p.data["Plural"] = plural
p.data["Terms"] = p.s.Taxonomies[plural]
// keep the following just for legacy reasons
p.data["OrderedIndex"] = p.data["Terms"]
p.data["Index"] = p.data["Terms"]
}
// Assign the function to the map to make sure it is lazily initialized
p.data["pages"] = p.Pages
})
return p.data
}

74
hugolib/page__menus.go Normal file
View file

@ -0,0 +1,74 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"sync"
"github.com/gohugoio/hugo/navigation"
)
type pageMenus struct {
p *pageState
q navigation.MenyQueryProvider
pmInit sync.Once
pm navigation.PageMenus
}
func (p *pageMenus) HasMenuCurrent(menuID string, me *navigation.MenuEntry) bool {
p.p.s.init.menus.Do()
p.init()
return p.q.HasMenuCurrent(menuID, me)
}
func (p *pageMenus) IsMenuCurrent(menuID string, inme *navigation.MenuEntry) bool {
p.p.s.init.menus.Do()
p.init()
return p.q.IsMenuCurrent(menuID, inme)
}
func (p *pageMenus) Menus() navigation.PageMenus {
// There is a reverse dependency here. initMenus will, once, build the
// site menus and update any relevant page.
p.p.s.init.menus.Do()
return p.menus()
}
func (p *pageMenus) menus() navigation.PageMenus {
p.init()
return p.pm
}
func (p *pageMenus) init() {
p.pmInit.Do(func() {
p.q = navigation.NewMenuQueryProvider(
p.p.s.Info.sectionPagesMenu,
p,
p.p.s,
p.p,
)
var err error
p.pm, err = navigation.PageMenusFromPage(p.p)
if err != nil {
p.p.s.Log.ERROR.Println(p.p.wrapError(err))
}
})
}

652
hugolib/page__meta.go Normal file
View file

@ -0,0 +1,652 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"fmt"
"path"
"regexp"
"strings"
"time"
"github.com/gohugoio/hugo/related"
"github.com/gohugoio/hugo/source"
"github.com/markbates/inflect"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/page/pagemeta"
"github.com/gohugoio/hugo/resources/resource"
"github.com/spf13/cast"
)
var cjkRe = regexp.MustCompile(`\p{Han}|\p{Hangul}|\p{Hiragana}|\p{Katakana}`)
type pageMeta struct {
// kind is the discriminator that identifies the different page types
// in the different page collections. This can, as an example, be used
// to to filter regular pages, find sections etc.
// Kind will, for the pages available to the templates, be one of:
// page, home, section, taxonomy and taxonomyTerm.
// It is of string type to make it easy to reason about in
// the templates.
kind string
// This is a standalone page not part of any page collection. These
// include sitemap, robotsTXT and similar. It will have no pageOutputs, but
// a fixed pageOutput.
standalone bool
bundleType string
// Params contains configuration defined in the params section of page frontmatter.
params map[string]interface{}
title string
linkTitle string
resourcePath string
weight int
markup string
contentType string
// whether the content is in a CJK language.
isCJKLanguage bool
layout string
aliases []string
draft bool
description string
keywords []string
urlPaths pagemeta.URLPath
resource.Dates
// This is enabled if it is a leaf bundle (the "index.md" type) and it is marked as headless in front matter.
// Being headless means that
// 1. The page itself is not rendered to disk
// 2. It is not available in .Site.Pages etc.
// 3. But you can get it via .Site.GetPage
headless bool
// A key that maps to translation(s) of this page. This value is fetched
// from the page front matter.
translationKey string
// From front matter.
configuredOutputFormats output.Formats
// This is the raw front matter metadata that is going to be assigned to
// the Resources above.
resourcesMetadata []map[string]interface{}
f source.File
sections []string
// Sitemap overrides from front matter.
sitemap config.Sitemap
s *Site
renderingConfig *helpers.BlackFriday
}
func (p *pageMeta) Aliases() []string {
return p.aliases
}
func (p *pageMeta) Author() page.Author {
authors := p.Authors()
for _, author := range authors {
return author
}
return page.Author{}
}
func (p *pageMeta) Authors() page.AuthorList {
authorKeys, ok := p.params["authors"]
if !ok {
return page.AuthorList{}
}
authors := authorKeys.([]string)
if len(authors) < 1 || len(p.s.Info.Authors) < 1 {
return page.AuthorList{}
}
al := make(page.AuthorList)
for _, author := range authors {
a, ok := p.s.Info.Authors[author]
if ok {
al[author] = a
}
}
return al
}
func (p *pageMeta) BundleType() string {
return p.bundleType
}
func (p *pageMeta) Description() string {
return p.description
}
func (p *pageMeta) Lang() string {
return p.s.Lang()
}
func (p *pageMeta) Draft() bool {
return p.draft
}
func (p *pageMeta) File() source.File {
return p.f
}
func (p *pageMeta) IsHome() bool {
return p.Kind() == page.KindHome
}
func (p *pageMeta) Keywords() []string {
return p.keywords
}
func (p *pageMeta) Kind() string {
return p.kind
}
func (p *pageMeta) Layout() string {
return p.layout
}
func (p *pageMeta) LinkTitle() string {
if p.linkTitle != "" {
return p.linkTitle
}
return p.Title()
}
func (p *pageMeta) Name() string {
if p.resourcePath != "" {
return p.resourcePath
}
return p.Title()
}
func (p *pageMeta) IsNode() bool {
return !p.IsPage()
}
func (p *pageMeta) IsPage() bool {
return p.Kind() == page.KindPage
}
// Param is a convenience method to do lookups in Page's and Site's Params map,
// in that order.
//
// This method is also implemented on SiteInfo.
// TODO(bep) interface
func (p *pageMeta) Param(key interface{}) (interface{}, error) {
return resource.Param(p, p.s.Info.Params(), key)
}
func (p *pageMeta) Params() map[string]interface{} {
return p.params
}
func (p *pageMeta) Path() string {
if p.File() != nil {
return p.File().Path()
}
return p.SectionsPath()
}
// RelatedKeywords implements the related.Document interface needed for fast page searches.
func (p *pageMeta) RelatedKeywords(cfg related.IndexConfig) ([]related.Keyword, error) {
v, err := p.Param(cfg.Name)
if err != nil {
return nil, err
}
return cfg.ToKeywords(v)
}
func (p *pageMeta) IsSection() bool {
return p.Kind() == page.KindSection
}
func (p *pageMeta) Section() string {
if p.IsHome() {
return ""
}
if p.IsNode() {
if len(p.sections) == 0 {
// May be a sitemap or similar.
return ""
}
return p.sections[0]
}
if p.File() != nil {
return p.File().Section()
}
panic("invalid page state")
}
func (p *pageMeta) SectionsEntries() []string {
return p.sections
}
func (p *pageMeta) SectionsPath() string {
return path.Join(p.SectionsEntries()...)
}
func (p *pageMeta) Sitemap() config.Sitemap {
return p.sitemap
}
func (p *pageMeta) Title() string {
return p.title
}
func (p *pageMeta) Type() string {
if p.contentType != "" {
return p.contentType
}
if x := p.Section(); x != "" {
return x
}
return "page"
}
func (p *pageMeta) Weight() int {
return p.weight
}
func (pm *pageMeta) setMetadata(p *pageState, frontmatter map[string]interface{}) error {
if frontmatter == nil {
return errors.New("missing frontmatter data")
}
pm.params = make(map[string]interface{})
// Needed for case insensitive fetching of params values
maps.ToLower(frontmatter)
var mtime time.Time
if p.File().FileInfo() != nil {
mtime = p.File().FileInfo().ModTime()
}
var gitAuthorDate time.Time
if p.gitInfo != nil {
gitAuthorDate = p.gitInfo.AuthorDate
}
descriptor := &pagemeta.FrontMatterDescriptor{
Frontmatter: frontmatter,
Params: pm.params,
Dates: &pm.Dates,
PageURLs: &pm.urlPaths,
BaseFilename: p.File().ContentBaseName(),
ModTime: mtime,
GitAuthorDate: gitAuthorDate,
}
// Handle the date separately
// TODO(bep) we need to "do more" in this area so this can be split up and
// more easily tested without the Page, but the coupling is strong.
err := pm.s.frontmatterHandler.HandleDates(descriptor)
if err != nil {
p.s.Log.ERROR.Printf("Failed to handle dates for page %q: %s", p.pathOrTitle(), err)
}
var sitemapSet bool
var draft, published, isCJKLanguage *bool
for k, v := range frontmatter {
loki := strings.ToLower(k)
if loki == "published" { // Intentionally undocumented
vv, err := cast.ToBoolE(v)
if err == nil {
published = &vv
}
// published may also be a date
continue
}
if pm.s.frontmatterHandler.IsDateKey(loki) {
continue
}
switch loki {
case "title":
pm.title = cast.ToString(v)
pm.params[loki] = pm.title
case "linktitle":
pm.linkTitle = cast.ToString(v)
pm.params[loki] = pm.linkTitle
case "description":
pm.description = cast.ToString(v)
pm.params[loki] = pm.description
case "slug":
// Don't start or end with a -
pm.urlPaths.Slug = strings.Trim(cast.ToString(v), "-")
pm.params[loki] = pm.Slug()
case "url":
if url := cast.ToString(v); strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
return fmt.Errorf("only relative URLs are supported, %v provided", url)
}
pm.urlPaths.URL = cast.ToString(v)
pm.params[loki] = pm.urlPaths.URL
case "type":
pm.contentType = cast.ToString(v)
pm.params[loki] = pm.contentType
case "keywords":
pm.keywords = cast.ToStringSlice(v)
pm.params[loki] = pm.keywords
case "headless":
// For now, only the leaf bundles ("index.md") can be headless (i.e. produce no output).
// We may expand on this in the future, but that gets more complex pretty fast.
if p.File().TranslationBaseName() == "index" {
pm.headless = cast.ToBool(v)
}
pm.params[loki] = pm.headless
case "outputs":
o := cast.ToStringSlice(v)
if len(o) > 0 {
// Output formats are exlicitly set in front matter, use those.
outFormats, err := p.s.outputFormatsConfig.GetByNames(o...)
if err != nil {
p.s.Log.ERROR.Printf("Failed to resolve output formats: %s", err)
} else {
pm.configuredOutputFormats = outFormats
pm.params[loki] = outFormats
}
}
case "draft":
draft = new(bool)
*draft = cast.ToBool(v)
case "layout":
pm.layout = cast.ToString(v)
pm.params[loki] = pm.layout
case "markup":
pm.markup = cast.ToString(v)
pm.params[loki] = pm.markup
case "weight":
pm.weight = cast.ToInt(v)
pm.params[loki] = pm.weight
case "aliases":
pm.aliases = cast.ToStringSlice(v)
for _, alias := range pm.aliases {
if strings.HasPrefix(alias, "http://") || strings.HasPrefix(alias, "https://") {
return fmt.Errorf("only relative aliases are supported, %v provided", alias)
}
}
pm.params[loki] = pm.aliases
case "sitemap":
p.m.sitemap = config.DecodeSitemap(p.s.siteCfg.sitemap, cast.ToStringMap(v))
pm.params[loki] = p.m.sitemap
sitemapSet = true
case "iscjklanguage":
isCJKLanguage = new(bool)
*isCJKLanguage = cast.ToBool(v)
case "translationkey":
pm.translationKey = cast.ToString(v)
pm.params[loki] = pm.translationKey
case "resources":
var resources []map[string]interface{}
handled := true
switch vv := v.(type) {
case []map[interface{}]interface{}:
for _, vvv := range vv {
resources = append(resources, cast.ToStringMap(vvv))
}
case []map[string]interface{}:
resources = append(resources, vv...)
case []interface{}:
for _, vvv := range vv {
switch vvvv := vvv.(type) {
case map[interface{}]interface{}:
resources = append(resources, cast.ToStringMap(vvvv))
case map[string]interface{}:
resources = append(resources, vvvv)
}
}
default:
handled = false
}
if handled {
pm.params[loki] = resources
pm.resourcesMetadata = resources
break
}
fallthrough
default:
// If not one of the explicit values, store in Params
switch vv := v.(type) {
case bool:
pm.params[loki] = vv
case string:
pm.params[loki] = vv
case int64, int32, int16, int8, int:
pm.params[loki] = vv
case float64, float32:
pm.params[loki] = vv
case time.Time:
pm.params[loki] = vv
default: // handle array of strings as well
switch vvv := vv.(type) {
case []interface{}:
if len(vvv) > 0 {
switch vvv[0].(type) {
case map[interface{}]interface{}: // Proper parsing structured array from YAML based FrontMatter
pm.params[loki] = vvv
case map[string]interface{}: // Proper parsing structured array from JSON based FrontMatter
pm.params[loki] = vvv
case []interface{}:
pm.params[loki] = vvv
default:
a := make([]string, len(vvv))
for i, u := range vvv {
a[i] = cast.ToString(u)
}
pm.params[loki] = a
}
} else {
pm.params[loki] = []string{}
}
default:
pm.params[loki] = vv
}
}
}
}
if !sitemapSet {
pm.sitemap = p.s.siteCfg.sitemap
}
pm.markup = helpers.GuessType(pm.markup)
if draft != nil && published != nil {
pm.draft = *draft
p.m.s.Log.WARN.Printf("page %q has both draft and published settings in its frontmatter. Using draft.", p.File().Filename())
} else if draft != nil {
pm.draft = *draft
} else if published != nil {
pm.draft = !*published
}
pm.params["draft"] = pm.draft
if isCJKLanguage != nil {
pm.isCJKLanguage = *isCJKLanguage
} else if p.s.siteCfg.hasCJKLanguage {
if cjkRe.Match(p.source.parsed.Input()) {
pm.isCJKLanguage = true
} else {
pm.isCJKLanguage = false
}
}
pm.params["iscjklanguage"] = p.m.isCJKLanguage
return nil
}
func (p *pageMeta) applyDefaultValues() error {
if p.markup == "" {
if p.File() != nil {
// Fall back to {file extension
p.markup = helpers.GuessType(p.File().Ext())
}
if p.markup == "" {
p.markup = "unknown"
}
}
if p.title == "" {
switch p.Kind() {
case page.KindHome:
p.title = p.s.Info.title
case page.KindSection:
sectionName := helpers.FirstUpper(p.sections[0])
if p.s.Cfg.GetBool("pluralizeListTitles") {
p.title = inflect.Pluralize(sectionName)
} else {
p.title = sectionName
}
case page.KindTaxonomy:
key := p.sections[len(p.sections)-1]
p.title = strings.Replace(p.s.titleFunc(key), "-", " ", -1)
case page.KindTaxonomyTerm:
p.title = p.s.titleFunc(p.sections[0])
case kind404:
p.title = "404 Page not found"
}
}
if p.IsNode() {
p.bundleType = "branch"
} else {
source := p.File()
if fi, ok := source.(*fileInfo); ok {
switch fi.bundleTp {
case bundleBranch:
p.bundleType = "branch"
case bundleLeaf:
p.bundleType = "leaf"
}
}
}
bfParam := getParamToLower(p, "blackfriday")
if bfParam != nil {
p.renderingConfig = p.s.ContentSpec.BlackFriday
// Create a copy so we can modify it.
bf := *p.s.ContentSpec.BlackFriday
p.renderingConfig = &bf
pageParam := cast.ToStringMap(bfParam)
if err := mapstructure.Decode(pageParam, &p.renderingConfig); err != nil {
return errors.WithMessage(err, "failed to decode rendering config")
}
}
return nil
}
// The output formats this page will be rendered to.
func (m *pageMeta) outputFormats() output.Formats {
if len(m.configuredOutputFormats) > 0 {
return m.configuredOutputFormats
}
return m.s.outputFormats[m.Kind()]
}
func (p *pageMeta) Slug() string {
return p.urlPaths.Slug
}
func getParam(m resource.ResourceParamsProvider, key string, stringToLower bool) interface{} {
v := m.Params()[strings.ToLower(key)]
if v == nil {
return nil
}
switch val := v.(type) {
case bool:
return val
case string:
if stringToLower {
return strings.ToLower(val)
}
return val
case int64, int32, int16, int8, int:
return cast.ToInt(v)
case float64, float32:
return cast.ToFloat64(v)
case time.Time:
return val
case []string:
if stringToLower {
return helpers.SliceToLower(val)
}
return v
case map[string]interface{}: // JSON and TOML
return v
case map[interface{}]interface{}: // YAML
return v
}
//p.s.Log.ERROR.Printf("GetParam(\"%s\"): Unknown type %s\n", key, reflect.TypeOf(v))
return nil
}
func getParamToLower(m resource.ResourceParamsProvider, key string) interface{} {
return getParam(m, key, true)
}

291
hugolib/page__new.go Normal file
View file

@ -0,0 +1,291 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"html/template"
"strings"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/source"
"github.com/gohugoio/hugo/parser/pageparser"
"github.com/pkg/errors"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/lazy"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/resource"
)
func newPageBase(metaProvider *pageMeta) (*pageState, error) {
if metaProvider.s == nil {
panic("must provide a Site")
}
s := metaProvider.s
ps := &pageState{
pageOutput: nopPageOutput,
pageCommon: &pageCommon{
FileProvider: metaProvider,
AuthorProvider: metaProvider,
Scratcher: maps.NewScratcher(),
Positioner: page.NopPage,
InSectionPositioner: page.NopPage,
ResourceMetaProvider: metaProvider,
ResourceParamsProvider: metaProvider,
PageMetaProvider: metaProvider,
RelatedKeywordsProvider: metaProvider,
OutputFormatsProvider: page.NopPage,
ResourceTypesProvider: pageTypesProvider,
RefProvider: page.NopPage,
ShortcodeInfoProvider: page.NopPage,
LanguageProvider: s,
InternalDependencies: s,
init: lazy.New(),
m: metaProvider,
s: s},
}
siteAdapter := pageSiteAdapter{s: s, p: ps}
deprecatedWarningPage := struct {
source.FileWithoutOverlap
page.DeprecatedWarningPageMethods1
}{
FileWithoutOverlap: metaProvider.File(),
DeprecatedWarningPageMethods1: &pageDeprecatedWarning{p: ps},
}
ps.DeprecatedWarningPageMethods = page.NewDeprecatedWarningPage(deprecatedWarningPage)
ps.pageMenus = &pageMenus{p: ps}
ps.PageMenusProvider = ps.pageMenus
ps.GetPageProvider = siteAdapter
ps.GitInfoProvider = ps
ps.TranslationsProvider = ps
ps.ResourceDataProvider = &pageData{pageState: ps}
ps.RawContentProvider = ps
ps.ChildCareProvider = ps
ps.TreeProvider = pageTree{p: ps}
ps.Eqer = ps
ps.TranslationKeyProvider = ps
ps.ShortcodeInfoProvider = ps
ps.PageRenderProvider = ps
ps.AlternativeOutputFormatsProvider = ps
return ps, nil
}
func newPageFromMeta(metaProvider *pageMeta) (*pageState, error) {
ps, err := newPageBase(metaProvider)
if err != nil {
return nil, err
}
if err := metaProvider.applyDefaultValues(); err != nil {
return nil, err
}
ps.init.Add(func() (interface{}, error) {
pp, err := newPagePaths(metaProvider.s, ps, metaProvider)
if err != nil {
return nil, err
}
makeOut := func(f output.Format, render bool) *pageOutput {
return newPageOutput(nil, ps, pp, f, render)
}
if ps.m.standalone {
ps.pageOutput = makeOut(ps.m.outputFormats()[0], true)
} else {
ps.pageOutputs = make([]*pageOutput, len(ps.s.h.renderFormats))
created := make(map[string]*pageOutput)
outputFormatsForPage := ps.m.outputFormats()
for i, f := range ps.s.h.renderFormats {
po, found := created[f.Name]
if !found {
_, shouldRender := outputFormatsForPage.GetByName(f.Name)
po = makeOut(f, shouldRender)
created[f.Name] = po
}
ps.pageOutputs[i] = po
}
}
if err := ps.initCommonProviders(pp); err != nil {
return nil, err
}
return nil, nil
})
return ps, err
}
// Used by the legacy 404, sitemap and robots.txt rendering
func newPageStandalone(m *pageMeta, f output.Format) (*pageState, error) {
m.configuredOutputFormats = output.Formats{f}
m.standalone = true
p, err := newPageFromMeta(m)
if err != nil {
return nil, err
}
if err := p.initPage(); err != nil {
return nil, err
}
return p, nil
}
func newPageWithContent(f *fileInfo, s *Site, content resource.OpenReadSeekCloser) (*pageState, error) {
sections := s.sectionsFromFile(f)
kind := s.kindFromFileInfoOrSections(f, sections)
if kind == page.KindTaxonomy {
s.PathSpec.MakePathsSanitized(sections)
}
metaProvider := &pageMeta{kind: kind, sections: sections, s: s, f: f}
ps, err := newPageBase(metaProvider)
if err != nil {
return nil, err
}
gi, err := s.h.gitInfoForPage(ps)
if err != nil {
return nil, errors.Wrap(err, "failed to load Git data")
}
ps.gitInfo = gi
r, err := content()
if err != nil {
return nil, err
}
defer r.Close()
parseResult, err := pageparser.Parse(
r,
pageparser.Config{EnableEmoji: s.siteCfg.enableEmoji},
)
if err != nil {
return nil, err
}
ps.pageContent = pageContent{
source: rawPageContent{
parsed: parseResult,
posMainContent: -1,
posSummaryEnd: -1,
posBodyStart: -1,
},
}
ps.shortcodeState = newShortcodeHandler(ps, ps.s, nil)
if err := ps.mapContent(metaProvider); err != nil {
return nil, ps.wrapError(err)
}
if err := metaProvider.applyDefaultValues(); err != nil {
return nil, err
}
ps.init.Add(func() (interface{}, error) {
reuseContent := ps.renderable && !ps.shortcodeState.hasShortcodes()
// Creates what's needed for each output format.
contentPerOutput := newPageContentOutput(ps)
pp, err := newPagePaths(s, ps, metaProvider)
if err != nil {
return nil, err
}
// Prepare output formats for all sites.
ps.pageOutputs = make([]*pageOutput, len(ps.s.h.renderFormats))
created := make(map[string]*pageOutput)
outputFormatsForPage := ps.m.outputFormats()
for i, f := range ps.s.h.renderFormats {
if po, found := created[f.Name]; found {
ps.pageOutputs[i] = po
continue
}
_, render := outputFormatsForPage.GetByName(f.Name)
var contentProvider *pageContentOutput
if reuseContent && i > 0 {
contentProvider = ps.pageOutputs[0].cp
} else {
var err error
contentProvider, err = contentPerOutput(f)
if err != nil {
return nil, err
}
}
po := newPageOutput(contentProvider, ps, pp, f, render)
ps.pageOutputs[i] = po
created[f.Name] = po
}
if err := ps.initCommonProviders(pp); err != nil {
return nil, err
}
return nil, nil
})
return ps, nil
}
type pageDeprecatedWarning struct {
p *pageState
}
func (p *pageDeprecatedWarning) IsDraft() bool { return p.p.m.draft }
func (p *pageDeprecatedWarning) Hugo() hugo.Info { return p.p.s.Info.Hugo() }
func (p *pageDeprecatedWarning) LanguagePrefix() string { return p.p.s.Info.LanguagePrefix }
func (p *pageDeprecatedWarning) GetParam(key string) interface{} {
return p.p.m.params[strings.ToLower(key)]
}
func (p *pageDeprecatedWarning) RSSLink() template.URL {
f := p.p.OutputFormats().Get("RSS")
if f == nil {
return ""
}
return template.URL(f.Permalink())
}
func (p *pageDeprecatedWarning) URL() string {
if p.p.IsPage() && p.p.m.urlPaths.URL != "" {
// This is the url set in front matter
return p.p.m.urlPaths.URL
}
// Fall back to the relative permalink.
return p.p.RelPermalink()
}

107
hugolib/page__output.go Normal file
View file

@ -0,0 +1,107 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/resource"
)
func newPageOutput(
cp *pageContentOutput, // may be nil
ps *pageState,
pp pagePaths,
f output.Format,
render bool) *pageOutput {
var targetPathsProvider targetPathsHolder
var linksProvider resource.ResourceLinksProvider
ft, found := pp.targetPaths[f.Name]
if !found {
// Link to the main output format
ft = pp.targetPaths[pp.OutputFormats()[0].Format.Name]
}
targetPathsProvider = ft
linksProvider = ft
var paginatorProvider page.PaginatorProvider = page.NopPage
var pag *pagePaginator
if render && ps.IsNode() {
pag = &pagePaginator{source: ps}
paginatorProvider = pag
}
var contentProvider page.ContentProvider = page.NopPage
var tableOfContentsProvider page.TableOfContentsProvider = page.NopPage
if cp != nil {
contentProvider = cp
tableOfContentsProvider = cp
}
providers := struct {
page.ContentProvider
page.TableOfContentsProvider
page.PaginatorProvider
resource.ResourceLinksProvider
targetPather
}{
contentProvider,
tableOfContentsProvider,
paginatorProvider,
linksProvider,
targetPathsProvider,
}
po := &pageOutput{
f: f,
cp: cp,
pagePerOutputProviders: providers,
render: render,
paginator: pag,
}
return po
}
// We create a pageOutput for every output format combination, even if this
// particular page isn't configured to be rendered to that format.
type pageOutput struct {
// Set if this page isn't configured to be rendered to this format.
render bool
f output.Format
// Only set if render is set.
// Note that this will be lazily initialized, so only used if actually
// used in template(s).
paginator *pagePaginator
// This interface provides the functionality that is specific for this
// output format.
pagePerOutputProviders
// This may be nil.
cp *pageContentOutput
}
func (p *pageOutput) enablePlaceholders() {
if p.cp != nil {
p.cp.enablePlaceholders()
}
}

View file

@ -0,0 +1,83 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"sync"
"github.com/gohugoio/hugo/resources/page"
)
type pagePaginator struct {
paginatorInit sync.Once
current *page.Pager
source *pageState
}
func (p *pagePaginator) Paginate(seq interface{}, options ...interface{}) (*page.Pager, error) {
var initErr error
p.paginatorInit.Do(func() {
pagerSize, err := page.ResolvePagerSize(p.source.s.Cfg, options...)
if err != nil {
initErr = err
return
}
pd := p.source.targetPathDescriptor
pd.Type = p.source.outputFormat()
paginator, err := page.Paginate(pd, seq, pagerSize)
if err != nil {
initErr = err
return
}
p.current = paginator.Pagers()[0]
})
if initErr != nil {
return nil, initErr
}
return p.current, nil
}
func (p *pagePaginator) Paginator(options ...interface{}) (*page.Pager, error) {
var initErr error
p.paginatorInit.Do(func() {
pagerSize, err := page.ResolvePagerSize(p.source.s.Cfg, options...)
if err != nil {
initErr = err
return
}
pd := p.source.targetPathDescriptor
pd.Type = p.source.outputFormat()
paginator, err := page.Paginate(pd, p.source.Pages(), pagerSize)
if err != nil {
initErr = err
return
}
p.current = paginator.Pagers()[0]
})
if initErr != nil {
return nil, initErr
}
return p.current, nil
}

148
hugolib/page__paths.go Normal file
View file

@ -0,0 +1,148 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"net/url"
"github.com/gohugoio/hugo/resources/page"
)
func newPagePaths(
s *Site,
p page.Page,
pm *pageMeta) (pagePaths, error) {
targetPathDescriptor, err := createTargetPathDescriptor(s, p, pm)
if err != nil {
return pagePaths{}, err
}
outputFormats := pm.outputFormats()
if len(outputFormats) == 0 {
outputFormats = pm.s.outputFormats[pm.Kind()]
}
if len(outputFormats) == 0 {
return pagePaths{}, nil
}
if pm.headless {
outputFormats = outputFormats[:1]
}
pageOutputFormats := make(page.OutputFormats, len(outputFormats))
targets := make(map[string]targetPathsHolder)
for i, f := range outputFormats {
desc := targetPathDescriptor
desc.Type = f
paths := page.CreateTargetPaths(desc)
var relPermalink, permalink string
if !pm.headless {
relPermalink = paths.RelPermalink(s.PathSpec)
permalink = paths.PermalinkForOutputFormat(s.PathSpec, f)
}
pageOutputFormats[i] = page.NewOutputFormat(relPermalink, permalink, len(outputFormats) == 1, f)
// Use the main format for permalinks, usually HTML.
permalinksIndex := 0
if f.Permalinkable {
// Unless it's permalinkable
permalinksIndex = i
}
targets[f.Name] = targetPathsHolder{
paths: paths,
OutputFormat: pageOutputFormats[permalinksIndex]}
}
return pagePaths{
outputFormats: pageOutputFormats,
targetPaths: targets,
targetPathDescriptor: targetPathDescriptor,
}, nil
}
type pagePaths struct {
outputFormats page.OutputFormats
targetPaths map[string]targetPathsHolder
targetPathDescriptor page.TargetPathDescriptor
}
func (l pagePaths) OutputFormats() page.OutputFormats {
return l.outputFormats
}
func createTargetPathDescriptor(s *Site, p page.Page, pm *pageMeta) (page.TargetPathDescriptor, error) {
var (
dir string
baseName string
)
d := s.Deps
if p.File() != nil {
dir = p.File().Dir()
baseName = p.File().TranslationBaseName()
}
alwaysInSubDir := p.Kind() == kindSitemap
desc := page.TargetPathDescriptor{
PathSpec: d.PathSpec,
Kind: p.Kind(),
Sections: p.SectionsEntries(),
UglyURLs: s.Info.uglyURLs(p),
ForcePrefix: s.h.IsMultihost() || alwaysInSubDir,
Dir: dir,
URL: pm.urlPaths.URL,
}
if pm.Slug() != "" {
desc.BaseName = pm.Slug()
} else {
desc.BaseName = baseName
}
desc.PrefixFilePath = s.getLanguageTargetPathLang(alwaysInSubDir)
desc.PrefixLink = s.getLanguagePermalinkLang(alwaysInSubDir)
// Expand only page.KindPage and page.KindTaxonomy; don't expand other Kinds of Pages
// like page.KindSection or page.KindTaxonomyTerm because they are "shallower" and
// the permalink configuration values are likely to be redundant, e.g.
// naively expanding /category/:slug/ would give /category/categories/ for
// the "categories" page.KindTaxonomyTerm.
if p.Kind() == page.KindPage || p.Kind() == page.KindTaxonomy {
opath, err := d.ResourceSpec.Permalinks.Expand(p.Section(), p)
if err != nil {
return desc, err
}
if opath != "" {
opath, _ = url.QueryUnescape(opath)
desc.ExpandedPermalink = opath
}
}
return desc, nil
}

445
hugolib/page__per_output.go Normal file
View file

@ -0,0 +1,445 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"bytes"
"context"
"fmt"
"html/template"
"strings"
"sync"
"unicode/utf8"
"github.com/gohugoio/hugo/lazy"
bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/resource"
)
var (
nopTargetPath = targetPathsHolder{}
nopPagePerOutput = struct {
resource.ResourceLinksProvider
page.ContentProvider
page.PageRenderProvider
page.PaginatorProvider
page.TableOfContentsProvider
page.AlternativeOutputFormatsProvider
targetPather
}{
page.NopPage,
page.NopPage,
page.NopPage,
page.NopPage,
page.NopPage,
page.NopPage,
nopTargetPath,
}
)
func newPageContentOutput(p *pageState) func(f output.Format) (*pageContentOutput, error) {
parent := p.init
return func(f output.Format) (*pageContentOutput, error) {
cp := &pageContentOutput{
p: p,
f: f,
}
initContent := func() error {
var err error
var hasVariants bool
cp.contentPlaceholders, hasVariants, err = p.shortcodeState.renderShortcodesForPage(p, f)
if err != nil {
return err
}
if p.render && !hasVariants {
// We can reuse this for the other output formats
cp.enableReuse()
}
cp.workContent = p.contentToRender(cp.contentPlaceholders)
isHTML := cp.p.m.markup == "html"
if p.renderable {
if !isHTML {
cp.workContent = cp.renderContent(p, cp.workContent)
tmpContent, tmpTableOfContents := helpers.ExtractTOC(cp.workContent)
cp.tableOfContents = helpers.BytesToHTML(tmpTableOfContents)
cp.workContent = tmpContent
}
if cp.placeholdersEnabled {
// ToC was accessed via .Page.TableOfContents in the shortcode,
// at a time when the ToC wasn't ready.
cp.contentPlaceholders[tocShortcodePlaceholder] = string(cp.tableOfContents)
}
if p.cmap.hasNonMarkdownShortcode || cp.placeholdersEnabled {
// There are one or more replacement tokens to be replaced.
cp.workContent, err = replaceShortcodeTokens(cp.workContent, cp.contentPlaceholders)
if err != nil {
return err
}
}
if cp.p.source.hasSummaryDivider {
if isHTML {
src := p.source.parsed.Input()
// Use the summary sections as they are provided by the user.
if p.source.posSummaryEnd != -1 {
cp.summary = helpers.BytesToHTML(src[p.source.posMainContent:p.source.posSummaryEnd])
}
if cp.p.source.posBodyStart != -1 {
cp.workContent = src[cp.p.source.posBodyStart:]
}
} else {
summary, content, err := splitUserDefinedSummaryAndContent(cp.p.m.markup, cp.workContent)
if err != nil {
cp.p.s.Log.ERROR.Printf("Failed to set user defined summary for page %q: %s", cp.p.pathOrTitle(), err)
} else {
cp.workContent = content
cp.summary = helpers.BytesToHTML(summary)
}
}
}
}
cp.content = helpers.BytesToHTML(cp.workContent)
if !p.renderable {
err := cp.addSelfTemplate()
return err
}
return nil
}
// Recursive loops can only happen in content files with template code (shortcodes etc.)
// Avoid creating new goroutines if we don't have to.
needTimeout := !p.renderable || p.shortcodeState.hasShortcodes()
if needTimeout {
cp.initMain = parent.BranchdWithTimeout(p.s.siteCfg.timeout, func(ctx context.Context) (interface{}, error) {
return nil, initContent()
})
} else {
cp.initMain = parent.Branch(func() (interface{}, error) {
return nil, initContent()
})
}
cp.initPlain = cp.initMain.Branch(func() (interface{}, error) {
cp.plain = helpers.StripHTML(string(cp.content))
cp.plainWords = strings.Fields(cp.plain)
cp.setWordCounts(p.m.isCJKLanguage)
if err := cp.setAutoSummary(); err != nil {
return err, nil
}
return nil, nil
})
return cp, nil
}
}
// pageContentOutput represents the Page content for a given output format.
type pageContentOutput struct {
f output.Format
// If we can safely reuse this for other output formats.
reuse bool
reuseInit sync.Once
p *pageState
// Lazy load dependencies
initMain *lazy.Init
initPlain *lazy.Init
placeholdersEnabled bool
placeholdersEnabledInit sync.Once
// Content state
workContent []byte
// Temporary storage of placeholders mapped to their content.
// These are shortcodes etc. Some of these will need to be replaced
// after any markup is rendered, so they share a common prefix.
contentPlaceholders map[string]string
// Content sections
content template.HTML
summary template.HTML
tableOfContents template.HTML
truncated bool
plainWords []string
plain string
fuzzyWordCount int
wordCount int
readingTime int
}
func (p *pageContentOutput) Content() (interface{}, error) {
p.p.s.initInit(p.initMain, p.p)
return p.content, nil
}
func (p *pageContentOutput) FuzzyWordCount() int {
p.p.s.initInit(p.initPlain, p.p)
return p.fuzzyWordCount
}
func (p *pageContentOutput) Len() int {
p.p.s.initInit(p.initMain, p.p)
return len(p.content)
}
func (p *pageContentOutput) Plain() string {
p.p.s.initInit(p.initPlain, p.p)
return p.plain
}
func (p *pageContentOutput) PlainWords() []string {
p.p.s.initInit(p.initPlain, p.p)
return p.plainWords
}
func (p *pageContentOutput) ReadingTime() int {
p.p.s.initInit(p.initPlain, p.p)
return p.readingTime
}
func (p *pageContentOutput) Summary() template.HTML {
p.p.s.initInit(p.initMain, p.p)
if !p.p.source.hasSummaryDivider {
p.p.s.initInit(p.initPlain, p.p)
}
return p.summary
}
func (p *pageContentOutput) TableOfContents() template.HTML {
p.p.s.initInit(p.initMain, p.p)
return p.tableOfContents
}
func (p *pageContentOutput) Truncated() bool {
if p.p.truncated {
return true
}
p.p.s.initInit(p.initPlain, p.p)
return p.truncated
}
func (p *pageContentOutput) WordCount() int {
p.p.s.initInit(p.initPlain, p.p)
return p.wordCount
}
func (p *pageContentOutput) setAutoSummary() error {
if p.p.source.hasSummaryDivider {
return nil
}
var summary string
var truncated bool
if p.p.m.isCJKLanguage {
summary, truncated = p.p.s.ContentSpec.TruncateWordsByRune(p.plainWords)
} else {
summary, truncated = p.p.s.ContentSpec.TruncateWordsToWholeSentence(p.plain)
}
p.summary = template.HTML(summary)
p.truncated = truncated
return nil
}
func (cp *pageContentOutput) renderContent(p page.Page, content []byte) []byte {
return cp.p.s.ContentSpec.RenderBytes(&helpers.RenderingContext{
Content: content, RenderTOC: true, PageFmt: cp.p.m.markup,
Cfg: p.Language(),
DocumentID: p.File().UniqueID(), DocumentName: p.File().Path(),
Config: cp.p.getRenderingConfig()})
}
func (p *pageContentOutput) setWordCounts(isCJKLanguage bool) {
if isCJKLanguage {
p.wordCount = 0
for _, word := range p.plainWords {
runeCount := utf8.RuneCountInString(word)
if len(word) == runeCount {
p.wordCount++
} else {
p.wordCount += runeCount
}
}
} else {
p.wordCount = helpers.TotalWords(p.plain)
}
// TODO(bep) is set in a test. Fix that.
if p.fuzzyWordCount == 0 {
p.fuzzyWordCount = (p.wordCount + 100) / 100 * 100
}
if isCJKLanguage {
p.readingTime = (p.wordCount + 500) / 501
} else {
p.readingTime = (p.wordCount + 212) / 213
}
}
func (p *pageContentOutput) addSelfTemplate() error {
self := p.p.selfLayoutForOutput(p.f)
err := p.p.s.TemplateHandler().AddLateTemplate(self, string(p.content))
if err != nil {
return err
}
return nil
}
// A callback to signal that we have inserted a placeholder into the rendered
// content. This avoids doing extra replacement work.
func (p *pageContentOutput) enablePlaceholders() {
p.placeholdersEnabledInit.Do(func() {
p.placeholdersEnabled = true
})
}
func (p *pageContentOutput) enableReuse() {
p.reuseInit.Do(func() {
p.reuse = true
})
}
// these will be shifted out when rendering a given output format.
type pagePerOutputProviders interface {
targetPather
page.ContentProvider
page.PaginatorProvider
page.TableOfContentsProvider
resource.ResourceLinksProvider
}
type targetPather interface {
targetPaths() page.TargetPaths
}
type targetPathsHolder struct {
paths page.TargetPaths
page.OutputFormat
}
func (t targetPathsHolder) targetPaths() page.TargetPaths {
return t.paths
}
func executeToString(templ tpl.Template, data interface{}) (string, error) {
b := bp.GetBuffer()
defer bp.PutBuffer(b)
if err := templ.Execute(b, data); err != nil {
return "", err
}
return b.String(), nil
}
func splitUserDefinedSummaryAndContent(markup string, c []byte) (summary []byte, content []byte, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("summary split failed: %s", r)
}
}()
startDivider := bytes.Index(c, internalSummaryDividerBaseBytes)
if startDivider == -1 {
return
}
startTag := "p"
switch markup {
case "asciidoc":
startTag = "div"
}
// Walk back and forward to the surrounding tags.
start := bytes.LastIndex(c[:startDivider], []byte("<"+startTag))
end := bytes.Index(c[startDivider:], []byte("</"+startTag))
if start == -1 {
start = startDivider
} else {
start = startDivider - (startDivider - start)
}
if end == -1 {
end = startDivider + len(internalSummaryDividerBase)
} else {
end = startDivider + end + len(startTag) + 3
}
var addDiv bool
switch markup {
case "rst":
addDiv = true
}
withoutDivider := append(c[:start], bytes.Trim(c[end:], "\n")...)
if len(withoutDivider) > 0 {
summary = bytes.TrimSpace(withoutDivider[:start])
}
if addDiv {
// For the rst
summary = append(append([]byte(nil), summary...), []byte("</div>")...)
}
if err != nil {
return
}
content = bytes.TrimSpace(withoutDivider)
return
}

76
hugolib/page__position.go Normal file
View file

@ -0,0 +1,76 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"github.com/gohugoio/hugo/lazy"
"github.com/gohugoio/hugo/resources/page"
)
func newPagePosition(n *nextPrev) pagePosition {
return pagePosition{nextPrev: n}
}
func newPagePositionInSection(n *nextPrev) pagePositionInSection {
return pagePositionInSection{nextPrev: n}
}
type nextPrev struct {
init *lazy.Init
prevPage page.Page
nextPage page.Page
}
func (n *nextPrev) next() page.Page {
n.init.Do()
return n.nextPage
}
func (n *nextPrev) prev() page.Page {
n.init.Do()
return n.prevPage
}
type pagePosition struct {
*nextPrev
}
func (p pagePosition) Next() page.Page {
return p.next()
}
func (p pagePosition) NextPage() page.Page {
return p.Next()
}
func (p pagePosition) Prev() page.Page {
return p.prev()
}
func (p pagePosition) PrevPage() page.Page {
return p.Prev()
}
type pagePositionInSection struct {
*nextPrev
}
func (p pagePositionInSection) NextInSection() page.Page {
return p.next()
}
func (p pagePositionInSection) PrevInSection() page.Page {
return p.prev()
}

View file

@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -22,24 +22,43 @@ import (
"github.com/pkg/errors"
)
type refArgs struct {
Path string
Lang string
OutputFormat string
func newPageRef(p *pageState) pageRef {
return pageRef{p: p}
}
func (p *Page) decodeRefArgs(args map[string]interface{}) (refArgs, *Site, error) {
type pageRef struct {
p *pageState
}
func (p pageRef) Ref(argsm map[string]interface{}) (string, error) {
return p.ref(argsm, p.p)
}
func (p pageRef) RefFrom(argsm map[string]interface{}, source interface{}) (string, error) {
return p.ref(argsm, source)
}
func (p pageRef) RelRef(argsm map[string]interface{}) (string, error) {
return p.relRef(argsm, p.p)
}
func (p pageRef) RelRefFrom(argsm map[string]interface{}, source interface{}) (string, error) {
return p.relRef(argsm, source)
}
func (p pageRef) decodeRefArgs(args map[string]interface{}) (refArgs, *Site, error) {
var ra refArgs
err := mapstructure.WeakDecode(args, &ra)
if err != nil {
return ra, nil, nil
}
s := p.s
if ra.Lang != "" && ra.Lang != p.Lang() {
s := p.p.s
if ra.Lang != "" && ra.Lang != p.p.s.Language().Lang {
// Find correct site
found := false
for _, ss := range p.s.owner.Sites {
for _, ss := range p.p.s.h.Sites {
if ss.Lang() == ra.Lang {
found = true
s = ss
@ -47,7 +66,7 @@ func (p *Page) decodeRefArgs(args map[string]interface{}) (refArgs, *Site, error
}
if !found {
p.s.siteRefLinker.logNotFound(ra.Path, fmt.Sprintf("no site found with lang %q", ra.Lang), p, text.Position{})
p.p.s.siteRefLinker.logNotFound(ra.Path, fmt.Sprintf("no site found with lang %q", ra.Lang), nil, text.Position{})
return ra, nil, nil
}
}
@ -55,18 +74,14 @@ func (p *Page) decodeRefArgs(args map[string]interface{}) (refArgs, *Site, error
return ra, s, nil
}
func (p *Page) Ref(argsm map[string]interface{}) (string, error) {
return p.ref(argsm, p)
}
func (p *Page) ref(argsm map[string]interface{}, source interface{}) (string, error) {
func (p pageRef) ref(argsm map[string]interface{}, source interface{}) (string, error) {
args, s, err := p.decodeRefArgs(argsm)
if err != nil {
return "", errors.Wrap(err, "invalid arguments to Ref")
}
if s == nil {
return p.s.siteRefLinker.notFoundURL, nil
return p.p.s.siteRefLinker.notFoundURL, nil
}
if args.Path == "" {
@ -77,18 +92,14 @@ func (p *Page) ref(argsm map[string]interface{}, source interface{}) (string, er
}
func (p *Page) RelRef(argsm map[string]interface{}) (string, error) {
return p.relRef(argsm, p)
}
func (p *Page) relRef(argsm map[string]interface{}, source interface{}) (string, error) {
func (p pageRef) relRef(argsm map[string]interface{}, source interface{}) (string, error) {
args, s, err := p.decodeRefArgs(argsm)
if err != nil {
return "", errors.Wrap(err, "invalid arguments to Ref")
}
if s == nil {
return p.s.siteRefLinker.notFoundURL, nil
return p.p.s.siteRefLinker.notFoundURL, nil
}
if args.Path == "" {
@ -98,3 +109,9 @@ func (p *Page) relRef(argsm map[string]interface{}, source interface{}) (string,
return s.refLink(args.Path, source, true, args.OutputFormat)
}
type refArgs struct {
Path string
Lang string
OutputFormat string
}

113
hugolib/page__tree.go Normal file
View file

@ -0,0 +1,113 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/resources/page"
)
type pageTree struct {
p *pageState
}
func (pt pageTree) IsAncestor(other interface{}) (bool, error) {
if pt.p == nil {
return false, nil
}
pp, err := unwrapPage(other)
if err != nil || pp == nil {
return false, err
}
if pt.p.Kind() == page.KindPage && len(pt.p.SectionsEntries()) == len(pp.SectionsEntries()) {
// A regular page is never its section's ancestor.
return false, nil
}
return helpers.HasStringsPrefix(pp.SectionsEntries(), pt.p.SectionsEntries()), nil
}
func (pt pageTree) CurrentSection() page.Page {
p := pt.p
if p.IsHome() || p.IsSection() {
return p
}
return p.Parent()
}
func (pt pageTree) IsDescendant(other interface{}) (bool, error) {
if pt.p == nil {
return false, nil
}
pp, err := unwrapPage(other)
if err != nil || pp == nil {
return false, err
}
if pp.Kind() == page.KindPage && len(pt.p.SectionsEntries()) == len(pp.SectionsEntries()) {
// A regular page is never its section's descendant.
return false, nil
}
return helpers.HasStringsPrefix(pt.p.SectionsEntries(), pp.SectionsEntries()), nil
}
func (pt pageTree) FirstSection() page.Page {
p := pt.p
parent := p.Parent()
if types.IsNil(parent) || parent.IsHome() {
return p
}
for {
current := parent
parent = parent.Parent()
if types.IsNil(parent) || parent.IsHome() {
return current
}
}
}
func (pt pageTree) InSection(other interface{}) (bool, error) {
if pt.p == nil || types.IsNil(other) {
return false, nil
}
pp, err := unwrapPage(other)
if err != nil {
return false, err
}
if pp == nil {
return false, nil
}
return pp.CurrentSection().Eq(pt.p.CurrentSection()), nil
}
func (pt pageTree) Parent() page.Page {
return pt.p.parent
}
func (pt pageTree) Sections() page.Pages {
return pt.p.subSections
}

View file

@ -1,233 +0,0 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"bytes"
"io"
"github.com/gohugoio/hugo/helpers"
errors "github.com/pkg/errors"
bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/gohugoio/hugo/parser/pageparser"
)
var (
internalSummaryDividerBase = "HUGOMORE42"
internalSummaryDividerBaseBytes = []byte(internalSummaryDividerBase)
internalSummaryDividerPre = []byte("\n\n" + internalSummaryDividerBase + "\n\n")
)
// The content related items on a Page.
type pageContent struct {
renderable bool
// workContent is a copy of rawContent that may be mutated during site build.
workContent []byte
shortcodeState *shortcodeHandler
source rawPageContent
}
type rawPageContent struct {
hasSummaryDivider bool
// The AST of the parsed page. Contains information about:
// shortcodes, front matter, summary indicators.
parsed pageparser.Result
// Returns the position in bytes after any front matter.
posMainContent int
}
// TODO(bep) lazy consolidate
func (p *Page) mapContent() error {
p.shortcodeState = newShortcodeHandler(p)
s := p.shortcodeState
p.renderable = true
p.source.posMainContent = -1
result := bp.GetBuffer()
defer bp.PutBuffer(result)
iter := p.source.parsed.Iterator()
fail := func(err error, i pageparser.Item) error {
return p.parseError(err, iter.Input(), i.Pos)
}
// the parser is guaranteed to return items in proper order or fail, so …
// … it's safe to keep some "global" state
var currShortcode shortcode
var ordinal int
Loop:
for {
it := iter.Next()
switch {
case it.Type == pageparser.TypeIgnore:
case it.Type == pageparser.TypeHTMLStart:
// This is HTML without front matter. It can still have shortcodes.
p.renderable = false
result.Write(it.Val)
case it.IsFrontMatter():
f := metadecoders.FormatFromFrontMatterType(it.Type)
m, err := metadecoders.Default.UnmarshalToMap(it.Val, f)
if err != nil {
if fe, ok := err.(herrors.FileError); ok {
return herrors.ToFileErrorWithOffset(fe, iter.LineNumber()-1)
} else {
return err
}
}
if err := p.updateMetaData(m); err != nil {
return err
}
next := iter.Peek()
if !next.IsDone() {
p.source.posMainContent = next.Pos
}
if !p.shouldBuild() {
// Nothing more to do.
return nil
}
case it.Type == pageparser.TypeLeadSummaryDivider:
result.Write(internalSummaryDividerPre)
p.source.hasSummaryDivider = true
// Need to determine if the page is truncated.
f := func(item pageparser.Item) bool {
if item.IsNonWhitespace() {
p.truncated = true
// Done
return false
}
return true
}
iter.PeekWalk(f)
// Handle shortcode
case it.IsLeftShortcodeDelim():
// let extractShortcode handle left delim (will do so recursively)
iter.Backup()
currShortcode, err := s.extractShortcode(ordinal, iter, p)
if currShortcode.name != "" {
s.nameSet[currShortcode.name] = true
}
if err != nil {
return fail(errors.Wrap(err, "failed to extract shortcode"), it)
}
if currShortcode.params == nil {
currShortcode.params = make([]string, 0)
}
placeHolder := s.createShortcodePlaceholder()
result.WriteString(placeHolder)
ordinal++
s.shortcodes.Add(placeHolder, currShortcode)
case it.Type == pageparser.TypeEmoji:
if emoji := helpers.Emoji(it.ValStr()); emoji != nil {
result.Write(emoji)
} else {
result.Write(it.Val)
}
case it.IsEOF():
break Loop
case it.IsError():
err := fail(errors.WithStack(errors.New(it.ValStr())), it)
currShortcode.err = err
return err
default:
result.Write(it.Val)
}
}
resultBytes := make([]byte, result.Len())
copy(resultBytes, result.Bytes())
p.workContent = resultBytes
return nil
}
func (p *Page) parse(reader io.Reader) error {
parseResult, err := pageparser.Parse(
reader,
pageparser.Config{EnableEmoji: p.s.Cfg.GetBool("enableEmoji")},
)
if err != nil {
return err
}
p.source = rawPageContent{
parsed: parseResult,
}
p.lang = p.File.Lang()
if p.s != nil && p.s.owner != nil {
gi, enabled := p.s.owner.gitInfo.forPage(p)
if gi != nil {
p.GitInfo = gi
} else if enabled {
p.s.Log.INFO.Printf("Failed to find GitInfo for page %q", p.Path())
}
}
return nil
}
func (p *Page) parseError(err error, input []byte, offset int) error {
if herrors.UnwrapFileError(err) != nil {
// Use the most specific location.
return err
}
pos := p.posFromInput(input, offset)
return herrors.NewFileError("md", -1, pos.LineNumber, pos.ColumnNumber, err)
}
func (p *Page) posFromInput(input []byte, offset int) text.Position {
lf := []byte("\n")
input = input[:offset]
lineNumber := bytes.Count(input, lf) + 1
endOfLastLine := bytes.LastIndex(input, lf)
return text.Position{
Filename: p.pathOrTitle(),
LineNumber: lineNumber,
ColumnNumber: offset - endOfLastLine,
Offset: offset,
}
}
func (p *Page) posFromPage(offset int) text.Position {
return p.posFromInput(p.source.parsed.Input(), offset)
}

View file

@ -1,47 +0,0 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"fmt"
"github.com/gohugoio/hugo/common/herrors"
errors "github.com/pkg/errors"
)
func (p *Page) errorf(err error, format string, a ...interface{}) error {
if herrors.UnwrapErrorWithFileContext(err) != nil {
// More isn't always better.
return err
}
args := append([]interface{}{p.Lang(), p.pathOrTitle()}, a...)
format = "[%s] page %q: " + format
if err == nil {
errors.Errorf(format, args...)
return fmt.Errorf(format, args...)
}
return errors.Wrapf(err, format, args...)
}
func (p *Page) errWithFileContext(err error) error {
err, _ = herrors.WithFileContextForFile(
err,
p.Filename(),
p.Filename(),
p.s.SourceSpec.Fs.Source,
herrors.SimpleLineMatcher)
return err
}

40
hugolib/page_kinds.go Normal file
View file

@ -0,0 +1,40 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"github.com/gohugoio/hugo/resources/page"
)
var (
// This is all the kinds we can expect to find in .Site.Pages.
allKindsInPages = []string{page.KindPage, page.KindHome, page.KindSection, page.KindTaxonomy, page.KindTaxonomyTerm}
allKinds = append(allKindsInPages, []string{kindRSS, kindSitemap, kindRobotsTXT, kind404}...)
)
const (
// Temporary state.
kindUnknown = "unknown"
// The following are (currently) temporary nodes,
// i.e. nodes we create just to render in isolation.
kindRSS = "RSS"
kindSitemap = "sitemap"
kindRobotsTXT = "robotsTXT"
kind404 = "404"
pageResourceType = "page"
)

View file

@ -1,320 +0,0 @@
// Copyright 2017 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"fmt"
"html/template"
"os"
"strings"
"sync"
bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/resources/resource"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/output"
)
// PageOutput represents one of potentially many output formats of a given
// Page.
type PageOutput struct {
*Page
// Pagination
paginator *Pager
paginatorInit sync.Once
// Page output specific resources
resources resource.Resources
resourcesInit sync.Once
// Keep this to create URL/path variations, i.e. paginators.
targetPathDescriptor targetPathDescriptor
outputFormat output.Format
}
func (p *PageOutput) targetPath(addends ...string) (string, error) {
tp, err := p.createTargetPath(p.outputFormat, false, addends...)
if err != nil {
return "", err
}
return tp, nil
}
func newPageOutput(p *Page, createCopy, initContent bool, f output.Format) (*PageOutput, error) {
// TODO(bep) This is only needed for tests and we should get rid of it.
if p.targetPathDescriptorPrototype == nil {
if err := p.initPaths(); err != nil {
return nil, err
}
}
if createCopy {
p = p.copy(initContent)
}
td, err := p.createTargetPathDescriptor(f)
if err != nil {
return nil, err
}
return &PageOutput{
Page: p,
outputFormat: f,
targetPathDescriptor: td,
}, nil
}
// copy creates a copy of this PageOutput with the lazy sync.Once vars reset
// so they will be evaluated again, for word count calculations etc.
func (p *PageOutput) copyWithFormat(f output.Format, initContent bool) (*PageOutput, error) {
c, err := newPageOutput(p.Page, true, initContent, f)
if err != nil {
return nil, err
}
c.paginator = p.paginator
return c, nil
}
func (p *PageOutput) copy() (*PageOutput, error) {
return p.copyWithFormat(p.outputFormat, false)
}
func (p *PageOutput) layouts(layouts ...string) ([]string, error) {
if len(layouts) == 0 && p.selfLayout != "" {
return []string{p.selfLayout}, nil
}
layoutDescriptor := p.layoutDescriptor
if len(layouts) > 0 {
layoutDescriptor.Layout = layouts[0]
layoutDescriptor.LayoutOverride = true
}
return p.s.layoutHandler.For(
layoutDescriptor,
p.outputFormat)
}
func (p *PageOutput) Render(layout ...string) template.HTML {
l, err := p.layouts(layout...)
if err != nil {
p.s.DistinctErrorLog.Printf("in .Render: Failed to resolve layout %q for page %q", layout, p.pathOrTitle())
return ""
}
for _, layout := range l {
templ, found := p.s.Tmpl.Lookup(layout)
if !found {
// This is legacy from when we had only one output format and
// HTML templates only. Some have references to layouts without suffix.
// We default to good old HTML.
templ, found = p.s.Tmpl.Lookup(layout + ".html")
}
if templ != nil {
res, err := executeToString(templ, p)
if err != nil {
p.s.DistinctErrorLog.Printf("in .Render: Failed to execute template %q: %s", layout, err)
return template.HTML("")
}
return template.HTML(res)
}
}
return ""
}
func executeToString(templ tpl.Template, data interface{}) (string, error) {
b := bp.GetBuffer()
defer bp.PutBuffer(b)
if err := templ.Execute(b, data); err != nil {
return "", err
}
return b.String(), nil
}
func (p *Page) Render(layout ...string) template.HTML {
if p.mainPageOutput == nil {
panic(fmt.Sprintf("programming error: no mainPageOutput for %q", p.Path()))
}
return p.mainPageOutput.Render(layout...)
}
// OutputFormats holds a list of the relevant output formats for a given resource.
type OutputFormats []*OutputFormat
// OutputFormat links to a representation of a resource.
type OutputFormat struct {
// Rel constains a value that can be used to construct a rel link.
// This is value is fetched from the output format definition.
// Note that for pages with only one output format,
// this method will always return "canonical".
// As an example, the AMP output format will, by default, return "amphtml".
//
// See:
// https://www.ampproject.org/docs/guides/deploy/discovery
//
// Most other output formats will have "alternate" as value for this.
Rel string
// It may be tempting to export this, but let us hold on to that horse for a while.
f output.Format
p *Page
}
// Name returns this OutputFormat's name, i.e. HTML, AMP, JSON etc.
func (o OutputFormat) Name() string {
return o.f.Name
}
// MediaType returns this OutputFormat's MediaType (MIME type).
func (o OutputFormat) MediaType() media.Type {
return o.f.MediaType
}
// OutputFormats gives the output formats for this Page.
func (p *Page) OutputFormats() OutputFormats {
var o OutputFormats
for _, f := range p.outputFormats {
o = append(o, newOutputFormat(p, f))
}
return o
}
func newOutputFormat(p *Page, f output.Format) *OutputFormat {
rel := f.Rel
isCanonical := len(p.outputFormats) == 1
if isCanonical {
rel = "canonical"
}
return &OutputFormat{Rel: rel, f: f, p: p}
}
// AlternativeOutputFormats gives the alternative output formats for this PageOutput.
// Note that we use the term "alternative" and not "alternate" here, as it
// does not necessarily replace the other format, it is an alternative representation.
func (p *PageOutput) AlternativeOutputFormats() (OutputFormats, error) {
var o OutputFormats
for _, of := range p.OutputFormats() {
if of.f.NotAlternative || of.f.Name == p.outputFormat.Name {
continue
}
o = append(o, of)
}
return o, nil
}
// deleteResource removes the resource from this PageOutput and the Page. They will
// always be of the same length, but may contain different elements.
func (p *PageOutput) deleteResource(i int) {
p.resources = append(p.resources[:i], p.resources[i+1:]...)
p.Page.Resources = append(p.Page.Resources[:i], p.Page.Resources[i+1:]...)
}
func (p *PageOutput) Resources() resource.Resources {
p.resourcesInit.Do(func() {
// If the current out shares the same path as the main page output, we reuse
// the resource set. For the "amp" use case, we need to clone them with new
// base folder.
ff := p.outputFormats[0]
if p.outputFormat.Path == ff.Path {
p.resources = p.Page.Resources
return
}
// Clone it with new base.
resources := make(resource.Resources, len(p.Page.Resources))
for i, r := range p.Page.Resources {
if c, ok := r.(resource.Cloner); ok {
// Clone the same resource with a new target.
resources[i] = c.WithNewBase(p.outputFormat.Path)
} else {
resources[i] = r
}
}
p.resources = resources
})
return p.resources
}
func (p *PageOutput) renderResources() error {
for i, r := range p.Resources() {
src, ok := r.(resource.Source)
if !ok {
// Pages gets rendered with the owning page.
continue
}
if err := src.Publish(); err != nil {
if os.IsNotExist(err) {
// The resource has been deleted from the file system.
// This should be extremely rare, but can happen on live reload in server
// mode when the same resource is member of different page bundles.
p.deleteResource(i)
} else {
p.s.Log.ERROR.Printf("Failed to publish Resource for page %q: %s", p.pathOrTitle(), err)
}
} else {
p.s.PathSpec.ProcessingStats.Incr(&p.s.PathSpec.ProcessingStats.Files)
}
}
return nil
}
// AlternativeOutputFormats is only available on the top level rendering
// entry point, and not inside range loops on the Page collections.
// This method is just here to inform users of that restriction.
func (p *Page) AlternativeOutputFormats() (OutputFormats, error) {
return nil, fmt.Errorf("AlternativeOutputFormats only available from the top level template context for page %q", p.Path())
}
// Get gets a OutputFormat given its name, i.e. json, html etc.
// It returns nil if not found.
func (o OutputFormats) Get(name string) *OutputFormat {
for _, f := range o {
if strings.EqualFold(f.f.Name, name) {
return f
}
}
return nil
}
// Permalink returns the absolute permalink to this output format.
func (o *OutputFormat) Permalink() string {
rel := o.p.createRelativePermalinkForOutputFormat(o.f)
perm, _ := o.p.s.permalinkForOutputFormat(rel, o.f)
return perm
}
// RelPermalink returns the relative permalink to this output format.
func (o *OutputFormat) RelPermalink() string {
rel := o.p.createRelativePermalinkForOutputFormat(o.f)
return o.p.s.PathSpec.PrependBasePath(rel, false)
}

View file

@ -1,312 +0,0 @@
// Copyright 2017 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"fmt"
"path/filepath"
"net/url"
"strings"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/output"
)
// targetPathDescriptor describes how a file path for a given resource
// should look like on the file system. The same descriptor is then later used to
// create both the permalinks and the relative links, paginator URLs etc.
//
// The big motivating behind this is to have only one source of truth for URLs,
// and by that also get rid of most of the fragile string parsing/encoding etc.
//
// Page.createTargetPathDescriptor is the Page adapter.
//
type targetPathDescriptor struct {
PathSpec *helpers.PathSpec
Type output.Format
Kind string
Sections []string
// For regular content pages this is either
// 1) the Slug, if set,
// 2) the file base name (TranslationBaseName).
BaseName string
// Source directory.
Dir string
// Language prefix, set if multilingual and if page should be placed in its
// language subdir.
LangPrefix string
// Whether this is a multihost multilingual setup.
IsMultihost bool
// URL from front matter if set. Will override any Slug etc.
URL string
// Used to create paginator links.
Addends string
// The expanded permalink if defined for the section, ready to use.
ExpandedPermalink string
// Some types cannot have uglyURLs, even if globally enabled, RSS being one example.
UglyURLs bool
}
// createTargetPathDescriptor adapts a Page and the given output.Format into
// a targetPathDescriptor. This descriptor can then be used to create paths
// and URLs for this Page.
func (p *Page) createTargetPathDescriptor(t output.Format) (targetPathDescriptor, error) {
if p.targetPathDescriptorPrototype == nil {
panic(fmt.Sprintf("Must run initTargetPathDescriptor() for page %q, kind %q", p.title, p.Kind))
}
d := *p.targetPathDescriptorPrototype
d.Type = t
return d, nil
}
func (p *Page) initTargetPathDescriptor() error {
d := &targetPathDescriptor{
PathSpec: p.s.PathSpec,
Kind: p.Kind,
Sections: p.sections,
UglyURLs: p.s.Info.uglyURLs(p),
Dir: filepath.ToSlash(p.Dir()),
URL: p.frontMatterURL,
IsMultihost: p.s.owner.IsMultihost(),
}
if p.Slug != "" {
d.BaseName = p.Slug
} else {
d.BaseName = p.TranslationBaseName()
}
if p.shouldAddLanguagePrefix() {
d.LangPrefix = p.Lang()
}
// Expand only KindPage and KindTaxonomy; don't expand other Kinds of Pages
// like KindSection or KindTaxonomyTerm because they are "shallower" and
// the permalink configuration values are likely to be redundant, e.g.
// naively expanding /category/:slug/ would give /category/categories/ for
// the "categories" KindTaxonomyTerm.
if p.Kind == KindPage || p.Kind == KindTaxonomy {
if override, ok := p.Site.Permalinks[p.Section()]; ok {
opath, err := override.Expand(p)
if err != nil {
return err
}
opath, _ = url.QueryUnescape(opath)
opath = filepath.FromSlash(opath)
d.ExpandedPermalink = opath
}
}
p.targetPathDescriptorPrototype = d
return nil
}
func (p *Page) initURLs() error {
if len(p.outputFormats) == 0 {
p.outputFormats = p.s.outputFormats[p.Kind]
}
target := filepath.ToSlash(p.createRelativeTargetPath())
rel := p.s.PathSpec.URLizeFilename(target)
var err error
f := p.outputFormats[0]
p.permalink, err = p.s.permalinkForOutputFormat(rel, f)
if err != nil {
return err
}
p.relTargetPathBase = strings.TrimPrefix(strings.TrimSuffix(target, f.MediaType.FullSuffix()), "/")
if prefix := p.s.GetLanguagePrefix(); prefix != "" {
// Any language code in the path will be added later.
p.relTargetPathBase = strings.TrimPrefix(p.relTargetPathBase, prefix+"/")
}
p.relPermalink = p.s.PathSpec.PrependBasePath(rel, false)
p.layoutDescriptor = p.createLayoutDescriptor()
return nil
}
func (p *Page) initPaths() error {
if err := p.initTargetPathDescriptor(); err != nil {
return err
}
if err := p.initURLs(); err != nil {
return err
}
return nil
}
// createTargetPath creates the target filename for this Page for the given
// output.Format. Some additional URL parts can also be provided, the typical
// use case being pagination.
func (p *Page) createTargetPath(t output.Format, noLangPrefix bool, addends ...string) (string, error) {
d, err := p.createTargetPathDescriptor(t)
if err != nil {
return "", nil
}
if noLangPrefix {
d.LangPrefix = ""
}
if len(addends) > 0 {
d.Addends = filepath.Join(addends...)
}
return createTargetPath(d), nil
}
func createTargetPath(d targetPathDescriptor) string {
pagePath := helpers.FilePathSeparator
// The top level index files, i.e. the home page etc., needs
// the index base even when uglyURLs is enabled.
needsBase := true
isUgly := d.UglyURLs && !d.Type.NoUgly
if d.ExpandedPermalink == "" && d.BaseName != "" && d.BaseName == d.Type.BaseName {
isUgly = true
}
if d.Kind != KindPage && d.URL == "" && len(d.Sections) > 0 {
if d.ExpandedPermalink != "" {
pagePath = filepath.Join(pagePath, d.ExpandedPermalink)
} else {
pagePath = filepath.Join(d.Sections...)
}
needsBase = false
}
if d.Type.Path != "" {
pagePath = filepath.Join(pagePath, d.Type.Path)
}
if d.Kind != KindHome && d.URL != "" {
if d.IsMultihost && d.LangPrefix != "" && !strings.HasPrefix(d.URL, "/"+d.LangPrefix) {
pagePath = filepath.Join(d.LangPrefix, pagePath, d.URL)
} else {
pagePath = filepath.Join(pagePath, d.URL)
}
if d.Addends != "" {
pagePath = filepath.Join(pagePath, d.Addends)
}
if strings.HasSuffix(d.URL, "/") || !strings.Contains(d.URL, ".") {
pagePath = filepath.Join(pagePath, d.Type.BaseName+d.Type.MediaType.FullSuffix())
}
} else if d.Kind == KindPage {
if d.ExpandedPermalink != "" {
pagePath = filepath.Join(pagePath, d.ExpandedPermalink)
} else {
if d.Dir != "" {
pagePath = filepath.Join(pagePath, d.Dir)
}
if d.BaseName != "" {
pagePath = filepath.Join(pagePath, d.BaseName)
}
}
if d.Addends != "" {
pagePath = filepath.Join(pagePath, d.Addends)
}
if isUgly {
pagePath += d.Type.MediaType.FullSuffix()
} else {
pagePath = filepath.Join(pagePath, d.Type.BaseName+d.Type.MediaType.FullSuffix())
}
if d.LangPrefix != "" {
pagePath = filepath.Join(d.LangPrefix, pagePath)
}
} else {
if d.Addends != "" {
pagePath = filepath.Join(pagePath, d.Addends)
}
needsBase = needsBase && d.Addends == ""
// No permalink expansion etc. for node type pages (for now)
base := ""
if needsBase || !isUgly {
base = helpers.FilePathSeparator + d.Type.BaseName
}
pagePath += base + d.Type.MediaType.FullSuffix()
if d.LangPrefix != "" {
pagePath = filepath.Join(d.LangPrefix, pagePath)
}
}
pagePath = filepath.Join(helpers.FilePathSeparator, pagePath)
// Note: MakePathSanitized will lower case the path if
// disablePathToLower isn't set.
return d.PathSpec.MakePathSanitized(pagePath)
}
func (p *Page) createRelativeTargetPath() string {
if len(p.outputFormats) == 0 {
if p.Kind == kindUnknown {
panic(fmt.Sprintf("Page %q has unknown kind", p.title))
}
panic(fmt.Sprintf("Page %q missing output format(s)", p.title))
}
// Choose the main output format. In most cases, this will be HTML.
f := p.outputFormats[0]
return p.createRelativeTargetPathForOutputFormat(f)
}
func (p *Page) createRelativePermalinkForOutputFormat(f output.Format) string {
return p.s.PathSpec.URLizeFilename(p.createRelativeTargetPathForOutputFormat(f))
}
func (p *Page) createRelativeTargetPathForOutputFormat(f output.Format) string {
tp, err := p.createTargetPath(f, p.s.owner.IsMultihost())
if err != nil {
p.s.Log.ERROR.Printf("Failed to create permalink for page %q: %s", p.FullFilePath(), err)
return ""
}
// For /index.json etc. we must use the full path.
if f.MediaType.FullSuffix() == ".html" && filepath.Base(tp) == "index.html" {
tp = strings.TrimSuffix(tp, f.BaseFilename())
}
return tp
}

View file

@ -1,194 +0,0 @@
// Copyright 2017 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"path/filepath"
"strings"
"testing"
"github.com/gohugoio/hugo/media"
"fmt"
"github.com/gohugoio/hugo/output"
)
func TestPageTargetPath(t *testing.T) {
pathSpec := newTestDefaultPathSpec(t)
noExtNoDelimMediaType := media.TextType
noExtNoDelimMediaType.Suffixes = []string{}
noExtNoDelimMediaType.Delimiter = ""
// Netlify style _redirects
noExtDelimFormat := output.Format{
Name: "NER",
MediaType: noExtNoDelimMediaType,
BaseName: "_redirects",
}
for _, multiHost := range []bool{false, true} {
for _, langPrefix := range []string{"", "no"} {
for _, uglyURLs := range []bool{false, true} {
t.Run(fmt.Sprintf("multihost=%t,langPrefix=%q,uglyURLs=%t", multiHost, langPrefix, uglyURLs),
func(t *testing.T) {
tests := []struct {
name string
d targetPathDescriptor
expected string
}{
{"JSON home", targetPathDescriptor{Kind: KindHome, Type: output.JSONFormat}, "/index.json"},
{"AMP home", targetPathDescriptor{Kind: KindHome, Type: output.AMPFormat}, "/amp/index.html"},
{"HTML home", targetPathDescriptor{Kind: KindHome, BaseName: "_index", Type: output.HTMLFormat}, "/index.html"},
{"Netlify redirects", targetPathDescriptor{Kind: KindHome, BaseName: "_index", Type: noExtDelimFormat}, "/_redirects"},
{"HTML section list", targetPathDescriptor{
Kind: KindSection,
Sections: []string{"sect1"},
BaseName: "_index",
Type: output.HTMLFormat}, "/sect1/index.html"},
{"HTML taxonomy list", targetPathDescriptor{
Kind: KindTaxonomy,
Sections: []string{"tags", "hugo"},
BaseName: "_index",
Type: output.HTMLFormat}, "/tags/hugo/index.html"},
{"HTML taxonomy term", targetPathDescriptor{
Kind: KindTaxonomy,
Sections: []string{"tags"},
BaseName: "_index",
Type: output.HTMLFormat}, "/tags/index.html"},
{
"HTML page", targetPathDescriptor{
Kind: KindPage,
Dir: "/a/b",
BaseName: "mypage",
Sections: []string{"a"},
Type: output.HTMLFormat}, "/a/b/mypage/index.html"},
{
"HTML page with index as base", targetPathDescriptor{
Kind: KindPage,
Dir: "/a/b",
BaseName: "index",
Sections: []string{"a"},
Type: output.HTMLFormat}, "/a/b/index.html"},
{
"HTML page with special chars", targetPathDescriptor{
Kind: KindPage,
Dir: "/a/b",
BaseName: "My Page!",
Type: output.HTMLFormat}, "/a/b/My-Page/index.html"},
{"RSS home", targetPathDescriptor{Kind: kindRSS, Type: output.RSSFormat}, "/index.xml"},
{"RSS section list", targetPathDescriptor{
Kind: kindRSS,
Sections: []string{"sect1"},
Type: output.RSSFormat}, "/sect1/index.xml"},
{
"AMP page", targetPathDescriptor{
Kind: KindPage,
Dir: "/a/b/c",
BaseName: "myamp",
Type: output.AMPFormat}, "/amp/a/b/c/myamp/index.html"},
{
"AMP page with URL with suffix", targetPathDescriptor{
Kind: KindPage,
Dir: "/sect/",
BaseName: "mypage",
URL: "/some/other/url.xhtml",
Type: output.HTMLFormat}, "/some/other/url.xhtml"},
{
"JSON page with URL without suffix", targetPathDescriptor{
Kind: KindPage,
Dir: "/sect/",
BaseName: "mypage",
URL: "/some/other/path/",
Type: output.JSONFormat}, "/some/other/path/index.json"},
{
"JSON page with URL without suffix and no trailing slash", targetPathDescriptor{
Kind: KindPage,
Dir: "/sect/",
BaseName: "mypage",
URL: "/some/other/path",
Type: output.JSONFormat}, "/some/other/path/index.json"},
{
"HTML page with expanded permalink", targetPathDescriptor{
Kind: KindPage,
Dir: "/a/b",
BaseName: "mypage",
ExpandedPermalink: "/2017/10/my-title",
Type: output.HTMLFormat}, "/2017/10/my-title/index.html"},
{
"Paginated HTML home", targetPathDescriptor{
Kind: KindHome,
BaseName: "_index",
Type: output.HTMLFormat,
Addends: "page/3"}, "/page/3/index.html"},
{
"Paginated Taxonomy list", targetPathDescriptor{
Kind: KindTaxonomy,
BaseName: "_index",
Sections: []string{"tags", "hugo"},
Type: output.HTMLFormat,
Addends: "page/3"}, "/tags/hugo/page/3/index.html"},
{
"Regular page with addend", targetPathDescriptor{
Kind: KindPage,
Dir: "/a/b",
BaseName: "mypage",
Addends: "c/d/e",
Type: output.HTMLFormat}, "/a/b/mypage/c/d/e/index.html"},
}
for i, test := range tests {
test.d.PathSpec = pathSpec
test.d.UglyURLs = uglyURLs
test.d.LangPrefix = langPrefix
test.d.IsMultihost = multiHost
test.d.Dir = filepath.FromSlash(test.d.Dir)
isUgly := uglyURLs && !test.d.Type.NoUgly
expected := test.expected
// TODO(bep) simplify
if test.d.Kind == KindPage && test.d.BaseName == test.d.Type.BaseName {
} else if test.d.Kind == KindHome && test.d.Type.Path != "" {
} else if (!strings.HasPrefix(expected, "/index") || test.d.Addends != "") && test.d.URL == "" && isUgly {
expected = strings.Replace(expected,
"/"+test.d.Type.BaseName+"."+test.d.Type.MediaType.Suffix(),
"."+test.d.Type.MediaType.Suffix(), -1)
}
if test.d.LangPrefix != "" && !(test.d.Kind == KindPage && test.d.URL != "") {
expected = "/" + test.d.LangPrefix + expected
} else if multiHost && test.d.LangPrefix != "" && test.d.URL != "" {
expected = "/" + test.d.LangPrefix + expected
}
expected = filepath.FromSlash(expected)
pagePath := createTargetPath(test.d)
if pagePath != expected {
t.Fatalf("[%d] [%s] targetPath expected %q, got: %q", i, test.name, expected, pagePath)
}
}
})
}
}
}
}

View file

@ -1,4 +1,4 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -63,6 +63,7 @@ func TestPermalink(t *testing.T) {
}
for i, test := range tests {
t.Run(fmt.Sprintf("%s-%d", test.file, i), func(t *testing.T) {
cfg, fs := newTestCfg()
@ -74,6 +75,7 @@ func TestPermalink(t *testing.T) {
title: Page
slug: %q
url: %q
output: ["HTML"]
---
Content
`, test.slug, test.url)
@ -81,9 +83,9 @@ Content
writeSource(t, fs, filepath.Join("content", filepath.FromSlash(test.file)), pageContent)
s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true})
require.Len(t, s.RegularPages, 1)
require.Len(t, s.RegularPages(), 1)
p := s.RegularPages[0]
p := s.RegularPages()[0]
u := p.Permalink()
@ -98,5 +100,7 @@ Content
if u != expected {
t.Errorf("[%d] Expected rel url: %s, got: %s", i, expected, u)
}
})
}
}

View file

@ -1,96 +0,0 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"reflect"
"strings"
"testing"
)
var pageYamlWithTaxonomiesA = `---
tags: ['a', 'B', 'c']
categories: 'd'
---
YAML frontmatter with tags and categories taxonomy.`
var pageYamlWithTaxonomiesB = `---
tags:
- "a"
- "B"
- "c"
categories: 'd'
---
YAML frontmatter with tags and categories taxonomy.`
var pageYamlWithTaxonomiesC = `---
tags: 'E'
categories: 'd'
---
YAML frontmatter with tags and categories taxonomy.`
var pageJSONWithTaxonomies = `{
"categories": "D",
"tags": [
"a",
"b",
"c"
]
}
JSON Front Matter with tags and categories`
var pageTomlWithTaxonomies = `+++
tags = [ "a", "B", "c" ]
categories = "d"
+++
TOML Front Matter with tags and categories`
func TestParseTaxonomies(t *testing.T) {
t.Parallel()
for _, test := range []string{pageTomlWithTaxonomies,
pageJSONWithTaxonomies,
pageYamlWithTaxonomiesA,
pageYamlWithTaxonomiesB,
pageYamlWithTaxonomiesC,
} {
s := newTestSite(t)
p, _ := s.NewPage("page/with/taxonomy")
_, err := p.ReadFrom(strings.NewReader(test))
if err != nil {
t.Fatalf("Failed parsing %q: %s", test, err)
}
param := p.getParamToLower("tags")
if params, ok := param.([]string); ok {
expected := []string{"a", "b", "c"}
if !reflect.DeepEqual(params, expected) {
t.Errorf("Expected %s: got: %s", expected, params)
}
} else if params, ok := param.(string); ok {
expected := "e"
if params != expected {
t.Errorf("Expected %s: got: %s", expected, params)
}
}
param = p.getParamToLower("categories")
singleparam := param.(string)
if singleparam != "d" {
t.Fatalf("Expected: d, got: %s", singleparam)
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,183 +0,0 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"fmt"
"os"
"strings"
"sync"
"testing"
"time"
"github.com/spf13/cast"
)
const (
pageWithInvalidDate = `---
date: 2010-05-02_15:29:31+08:00
---
Page With Invalid Date (replace T with _ for RFC 3339)`
pageWithDateRFC3339 = `---
date: 2010-05-02T15:29:31+08:00
---
Page With Date RFC3339`
pageWithDateRFC3339NoT = `---
date: 2010-05-02 15:29:31+08:00
---
Page With Date RFC3339_NO_T`
pageWithRFC1123 = `---
date: Sun, 02 May 2010 15:29:31 PST
---
Page With Date RFC1123`
pageWithDateRFC1123Z = `---
date: Sun, 02 May 2010 15:29:31 +0800
---
Page With Date RFC1123Z`
pageWithDateRFC822 = `---
date: 02 May 10 15:29 PST
---
Page With Date RFC822`
pageWithDateRFC822Z = `---
date: 02 May 10 15:29 +0800
---
Page With Date RFC822Z`
pageWithDateANSIC = `---
date: Sun May 2 15:29:31 2010
---
Page With Date ANSIC`
pageWithDateUnixDate = `---
date: Sun May 2 15:29:31 PST 2010
---
Page With Date UnixDate`
pageWithDateRubyDate = `---
date: Sun May 02 15:29:31 +0800 2010
---
Page With Date RubyDate`
pageWithDateHugoYearNumeric = `---
date: 2010-05-02
---
Page With Date HugoYearNumeric`
pageWithDateHugoYear = `---
date: 02 May 2010
---
Page With Date HugoYear`
pageWithDateHugoLong = `---
date: 02 May 2010 15:29 PST
---
Page With Date HugoLong`
)
func TestDegenerateDateFrontMatter(t *testing.T) {
t.Parallel()
s := newTestSite(t)
p, _ := s.newPageFrom(strings.NewReader(pageWithInvalidDate), "page/with/invalid/date")
if p.Date != *new(time.Time) {
t.Fatalf("Date should be set to time.Time zero value. Got: %s", p.Date)
}
}
func TestParsingDateInFrontMatter(t *testing.T) {
t.Parallel()
s := newTestSite(t)
tests := []struct {
buf string
dt string
}{
{pageWithDateRFC3339, "2010-05-02T15:29:31+08:00"},
{pageWithDateRFC3339NoT, "2010-05-02T15:29:31+08:00"},
{pageWithDateRFC1123Z, "2010-05-02T15:29:31+08:00"},
{pageWithDateRFC822Z, "2010-05-02T15:29:00+08:00"},
{pageWithDateANSIC, "2010-05-02T15:29:31Z"},
{pageWithDateRubyDate, "2010-05-02T15:29:31+08:00"},
{pageWithDateHugoYearNumeric, "2010-05-02T00:00:00Z"},
{pageWithDateHugoYear, "2010-05-02T00:00:00Z"},
}
tzShortCodeTests := []struct {
buf string
dt string
}{
{pageWithRFC1123, "2010-05-02T15:29:31-08:00"},
{pageWithDateRFC822, "2010-05-02T15:29:00-08:00Z"},
{pageWithDateUnixDate, "2010-05-02T15:29:31-08:00"},
{pageWithDateHugoLong, "2010-05-02T15:21:00+08:00"},
}
if _, err := time.LoadLocation("PST"); err == nil {
tests = append(tests, tzShortCodeTests...)
} else {
fmt.Fprintf(os.Stderr, "Skipping shortname timezone tests.\n")
}
for _, test := range tests {
dt, e := time.Parse(time.RFC3339, test.dt)
if e != nil {
t.Fatalf("Unable to parse date time (RFC3339) for running the test: %s", e)
}
p, err := s.newPageFrom(strings.NewReader(test.buf), "page/with/date")
if err != nil {
t.Fatalf("Expected to be able to parse page.")
}
if !dt.Equal(p.Date) {
t.Errorf("Date does not equal frontmatter:\n%s\nExpecting: %s\n Got: %s. Diff: %s\n internal: %#v\n %#v", test.buf, dt, p.Date, dt.Sub(p.Date), dt, p.Date)
}
}
}
// Temp test https://github.com/gohugoio/hugo/issues/3059
func TestParsingDateParallel(t *testing.T) {
t.Parallel()
var wg sync.WaitGroup
for j := 0; j < 100; j++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 100; j++ {
dateStr := "2010-05-02 15:29:31 +08:00"
dt, err := time.Parse("2006-01-02 15:04:05 -07:00", dateStr)
if err != nil {
t.Fatal(err)
}
if dt.Year() != 2010 {
t.Fatal("time.Parse: Invalid date:", dt)
}
dt2 := cast.ToTime(dateStr)
if dt2.Year() != 2010 {
t.Fatal("cast.ToTime: Invalid date:", dt2.Year())
}
}
}()
}
wg.Wait()
}

50
hugolib/page_unwrap.go Normal file
View file

@ -0,0 +1,50 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"github.com/pkg/errors"
"github.com/gohugoio/hugo/resources/page"
)
// Wraps a Page.
type pageWrapper interface {
page() page.Page
}
// unwrapPage is used in equality checks and similar.
func unwrapPage(in interface{}) (page.Page, error) {
switch v := in.(type) {
case *pageState:
return v, nil
case pageWrapper:
return v.page(), nil
case page.Page:
return v, nil
case nil:
return nil, nil
default:
return nil, errors.Errorf("unwrapPage: %T not supported", in)
}
}
func mustUnwrapPage(in interface{}) page.Page {
p, err := unwrapPage(in)
if err != nil {
panic(err)
}
return p
}

View file

@ -1,4 +1,4 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -14,25 +14,24 @@
package hugolib
import (
"path/filepath"
"strings"
"testing"
"github.com/gohugoio/hugo/resources/page"
"github.com/stretchr/testify/require"
)
var simplePageYAML = `---
contenttype: ""
---
Sample Text
`
func TestUnwrapPage(t *testing.T) {
assert := require.New(t)
func TestDegenerateMissingFolderInPageFilename(t *testing.T) {
t.Parallel()
s := newTestSite(t)
p, err := s.newPageFrom(strings.NewReader(simplePageYAML), filepath.Join("foobar"))
p := &pageState{}
assert.Equal(p, mustUnwrap(newPageForShortcode(p)))
}
func mustUnwrap(v interface{}) page.Page {
p, err := unwrapPage(v)
if err != nil {
t.Fatalf("Error in NewPageFrom")
}
if p.Section() != "" {
t.Fatalf("No section should be set for a file path: foobar")
panic(err)
}
return p
}

View file

@ -1,67 +0,0 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"html/template"
)
// PageWithoutContent is sent to the shortcodes. They cannot access the content
// they're a part of. It would cause an infinite regress.
//
// Go doesn't support virtual methods, so this careful dance is currently (I think)
// the best we can do.
type PageWithoutContent struct {
*Page
}
// Content returns an empty string.
func (p *PageWithoutContent) Content() (interface{}, error) {
return "", nil
}
// Truncated always returns false.
func (p *PageWithoutContent) Truncated() bool {
return false
}
// Summary returns an empty string.
func (p *PageWithoutContent) Summary() template.HTML {
return ""
}
// WordCount always returns 0.
func (p *PageWithoutContent) WordCount() int {
return 0
}
// ReadingTime always returns 0.
func (p *PageWithoutContent) ReadingTime() int {
return 0
}
// FuzzyWordCount always returns 0.
func (p *PageWithoutContent) FuzzyWordCount() int {
return 0
}
// Plain returns an empty string.
func (p *PageWithoutContent) Plain() string {
return ""
}
// PlainWords returns an empty string slice.
func (p *PageWithoutContent) PlainWords() []string {
return []string{}
}

View file

@ -1,4 +1,4 @@
// Copyright 2017-present The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -17,6 +17,7 @@ import (
"context"
"fmt"
"math"
"path/filepath"
"runtime"
_errors "github.com/pkg/errors"
@ -38,12 +39,12 @@ type siteContentProcessor struct {
fileSinglesChan chan *fileInfo
// These assets should be just copied to destination.
fileAssetsChan chan []pathLangFile
fileAssetsChan chan pathLangFile
numWorkers int
// The output Pages
pagesChan chan *Page
pagesChan chan *pageState
// Used for partial rebuilds (aka. live reload)
// Will signal replacement of pages in the site collection.
@ -64,9 +65,9 @@ func (s *siteContentProcessor) processSingle(fi *fileInfo) {
}
}
func (s *siteContentProcessor) processAssets(assets []pathLangFile) {
func (s *siteContentProcessor) processAsset(asset pathLangFile) {
select {
case s.fileAssetsChan <- assets:
case s.fileAssetsChan <- asset:
case <-s.ctx.Done():
}
}
@ -77,7 +78,7 @@ func newSiteContentProcessor(ctx context.Context, partialBuild bool, s *Site) *s
numWorkers = n
}
numWorkers = int(math.Ceil(float64(numWorkers) / float64(len(s.owner.Sites))))
numWorkers = int(math.Ceil(float64(numWorkers) / float64(len(s.h.Sites))))
return &siteContentProcessor{
ctx: ctx,
@ -86,9 +87,9 @@ func newSiteContentProcessor(ctx context.Context, partialBuild bool, s *Site) *s
handleContent: newHandlerChain(s),
fileBundlesChan: make(chan *bundleDir, numWorkers),
fileSinglesChan: make(chan *fileInfo, numWorkers),
fileAssetsChan: make(chan []pathLangFile, numWorkers),
fileAssetsChan: make(chan pathLangFile, numWorkers),
numWorkers: numWorkers,
pagesChan: make(chan *Page, numWorkers),
pagesChan: make(chan *pageState, numWorkers),
}
}
@ -127,6 +128,7 @@ func (s *siteContentProcessor) process(ctx context.Context) error {
if !ok {
return nil
}
err := s.readAndConvertContentFile(f)
if err != nil {
return err
@ -140,22 +142,20 @@ func (s *siteContentProcessor) process(ctx context.Context) error {
g2.Go(func() error {
for {
select {
case files, ok := <-s.fileAssetsChan:
case file, ok := <-s.fileAssetsChan:
if !ok {
return nil
}
for _, file := range files {
f, err := s.site.BaseFs.Content.Fs.Open(file.Filename())
if err != nil {
return _errors.Wrap(err, "failed to open assets file")
}
err = s.site.publish(&s.site.PathSpec.ProcessingStats.Files, file.Path(), f)
filename := filepath.Join(s.site.GetTargetLanguageBasePath(), file.Path())
err = s.site.publish(&s.site.PathSpec.ProcessingStats.Files, filename, f)
f.Close()
if err != nil {
return err
}
}
case <-ctx.Done():
return ctx.Err()
}
@ -192,8 +192,6 @@ func (s *siteContentProcessor) process(ctx context.Context) error {
return err
}
s.site.rawAllPages.sort()
return nil
}

View file

@ -116,7 +116,7 @@ func newCapturer(
// these channels.
type captureResultHandler interface {
handleSingles(fis ...*fileInfo)
handleCopyFiles(fis ...pathLangFile)
handleCopyFile(fi pathLangFile)
captureBundlesHandler
}
@ -141,10 +141,10 @@ func (c *captureResultHandlerChain) handleBundles(b *bundleDirs) {
}
}
func (c *captureResultHandlerChain) handleCopyFiles(files ...pathLangFile) {
func (c *captureResultHandlerChain) handleCopyFile(file pathLangFile) {
for _, h := range c.handlers {
if hh, ok := h.(captureResultHandler); ok {
hh.handleCopyFiles(files...)
hh.handleCopyFile(file)
}
}
}
@ -444,7 +444,7 @@ func (c *capturer) handleNonBundle(
}
c.handler.handleSingles(f)
} else {
c.handler.handleCopyFiles(fi)
c.handler.handleCopyFile(fi)
}
}
}
@ -457,7 +457,7 @@ func (c *capturer) copyOrHandleSingle(fi *fileInfo) {
c.handler.handleSingles(fi)
} else {
// These do not currently need any further processing.
c.handler.handleCopyFiles(fi)
c.handler.handleCopyFile(fi)
}
}

View file

@ -64,13 +64,11 @@ func (s *storeFilenames) handleBundles(d *bundleDirs) {
s.dirKeys = append(s.dirKeys, keys...)
}
func (s *storeFilenames) handleCopyFiles(files ...pathLangFile) {
func (s *storeFilenames) handleCopyFile(file pathLangFile) {
s.Lock()
defer s.Unlock()
for _, file := range files {
s.copyNames = append(s.copyNames, filepath.ToSlash(file.Filename()))
}
}
func (s *storeFilenames) sortedStr() string {
s.Lock()
@ -226,7 +224,7 @@ type noOpFileStore int
func (noOpFileStore) handleSingles(fis ...*fileInfo) {}
func (noOpFileStore) handleBundles(b *bundleDirs) {}
func (noOpFileStore) handleCopyFiles(files ...pathLangFile) {}
func (noOpFileStore) handleCopyFile(file pathLangFile) {}
func BenchmarkPageBundlerCapture(b *testing.B) {
capturers := make([]*capturer, b.N)

View file

@ -1,4 +1,4 @@
// Copyright 2017-present The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -17,11 +17,11 @@ import (
"errors"
"fmt"
"path/filepath"
"sort"
"github.com/gohugoio/hugo/common/hugio"
"strings"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/resource"
)
@ -50,13 +50,9 @@ func init() {
func newHandlerChain(s *Site) contentHandler {
c := &contentHandlers{s: s}
contentFlow := c.parsePage(c.processFirstMatch(
// Handles all files with a content file extension. See above.
contentFlow := c.parsePage(
c.handlePageContent(),
// Every HTML file without front matter will be passed on to this handler.
c.handleHTMLContent(),
))
)
c.rootHandler = c.processFirstMatch(
contentFlow,
@ -93,12 +89,12 @@ func (c *contentHandlers) processFirstMatch(handlers ...contentHandler) func(ctx
type handlerContext struct {
// These are the pages stored in Site.
pages chan<- *Page
pages chan<- *pageState
doNotAddToSiteCollections bool
currentPage *Page
parentPage *Page
currentPage *pageState
parentPage *pageState
bundle *bundleDir
@ -110,10 +106,7 @@ type handlerContext struct {
func (c *handlerContext) ext() string {
if c.currentPage != nil {
if c.currentPage.Markup != "" {
return c.currentPage.Markup
}
return c.currentPage.Ext()
return c.currentPage.contentMarkupType()
}
if c.bundle != nil {
@ -177,7 +170,7 @@ type (
handlerResult struct {
err error
handled bool
resource resource.Resource
result interface{}
}
contentHandler func(ctx *handlerContext) handlerResult
@ -196,27 +189,27 @@ func (c *contentHandlers) parsePage(h contentHandler) contentHandler {
result := handlerResult{handled: true}
fi := ctx.file()
content := func() (hugio.ReadSeekCloser, error) {
f, err := fi.Open()
if err != nil {
return handlerResult{err: fmt.Errorf("(%s) failed to open content file: %s", fi.Filename(), err)}
return nil, fmt.Errorf("failed to open content file %q: %s", fi.Filename(), err)
}
return f, nil
}
defer f.Close()
p := c.s.newPageFromFile(fi)
_, err = p.ReadFrom(f)
ps, err := newPageWithContent(fi, c.s, content)
if err != nil {
return handlerResult{err: err}
}
if !p.shouldBuild() {
if !c.s.shouldBuild(ps) {
if !ctx.doNotAddToSiteCollections {
ctx.pages <- p
ctx.pages <- ps
}
return result
}
ctx.currentPage = p
ctx.currentPage = ps
if ctx.bundle != nil {
// Add the bundled files
@ -226,39 +219,20 @@ func (c *contentHandlers) parsePage(h contentHandler) contentHandler {
if res.err != nil {
return res
}
if res.resource != nil {
if pageResource, ok := res.resource.(*Page); ok {
pageResource.resourcePath = filepath.ToSlash(childCtx.target)
pageResource.parent = p
}
p.Resources = append(p.Resources, res.resource)
if res.result != nil {
switch resv := res.result.(type) {
case *pageState:
resv.m.resourcePath = filepath.ToSlash(childCtx.target)
resv.parent = ps
ps.addResources(resv)
case resource.Resource:
ps.addResources(resv)
default:
panic("Unknown type")
}
}
sort.SliceStable(p.Resources, func(i, j int) bool {
if p.Resources[i].ResourceType() < p.Resources[j].ResourceType() {
return true
}
p1, ok1 := p.Resources[i].(*Page)
p2, ok2 := p.Resources[j].(*Page)
if ok1 != ok2 {
return ok2
}
if ok1 {
return defaultPageSort(p1, p2)
}
return p.Resources[i].RelPermalink() < p.Resources[j].RelPermalink()
})
// Assign metadata from front matter if set
if len(p.resourcesMetadata) > 0 {
resources.AssignMetadata(p.resourcesMetadata, p.Resources...)
}
}
return h(ctx)
@ -267,39 +241,13 @@ func (c *contentHandlers) parsePage(h contentHandler) contentHandler {
func (c *contentHandlers) handlePageContent() contentHandler {
return func(ctx *handlerContext) handlerResult {
if ctx.supports("html", "htm") {
return notHandled
}
p := ctx.currentPage
p.workContent = p.renderContent(p.workContent)
tmpContent, tmpTableOfContents := helpers.ExtractTOC(p.workContent)
p.TableOfContents = helpers.BytesToHTML(tmpTableOfContents)
p.workContent = tmpContent
if !ctx.doNotAddToSiteCollections {
ctx.pages <- p
}
return handlerResult{handled: true, resource: p}
}
}
func (c *contentHandlers) handleHTMLContent() contentHandler {
return func(ctx *handlerContext) handlerResult {
if !ctx.supports("html", "htm") {
return notHandled
}
p := ctx.currentPage
if !ctx.doNotAddToSiteCollections {
ctx.pages <- p
}
return handlerResult{handled: true, resource: p}
return handlerResult{handled: true, result: p}
}
}
@ -309,16 +257,31 @@ func (c *contentHandlers) createResource() contentHandler {
return notHandled
}
// TODO(bep) consolidate with multihost logic + clean up
outputFormats := ctx.parentPage.m.outputFormats()
seen := make(map[string]bool)
var targetBasePaths []string
// Make sure bundled resources are published to all of the ouptput formats'
// sub paths.
for _, f := range outputFormats {
p := f.Path
if seen[p] {
continue
}
seen[p] = true
targetBasePaths = append(targetBasePaths, p)
}
resource, err := c.s.ResourceSpec.New(
resources.ResourceSourceDescriptor{
TargetPathBuilder: ctx.parentPage.subResourceTargetPathFactory,
TargetPaths: ctx.parentPage.getTargetPaths,
SourceFile: ctx.source,
RelTargetFilename: ctx.target,
URLBase: c.s.GetURLLanguageBasePath(),
TargetBasePaths: []string{c.s.GetTargetLanguageBasePath()},
TargetBasePaths: targetBasePaths,
})
return handlerResult{err: err, handled: true, resource: resource}
return handlerResult{err: err, handled: true, result: resource}
}
}

View file

@ -1,4 +1,4 @@
// Copyright 2017-present The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -14,12 +14,15 @@
package hugolib
import (
"github.com/gohugoio/hugo/common/loggers"
"os"
"path"
"runtime"
"strings"
"testing"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/helpers"
"io"
@ -47,7 +50,11 @@ func TestPageBundlerSiteRegular(t *testing.T) {
for _, baseURLPath := range []string{"", "/hugo"} {
for _, canonify := range []bool{false, true} {
for _, ugly := range []bool{false, true} {
t.Run(fmt.Sprintf("ugly=%t,canonify=%t,path=%s", ugly, canonify, baseURLPath),
baseURLPathId := baseURLPath
if baseURLPathId == "" {
baseURLPathId = "NONE"
}
t.Run(fmt.Sprintf("ugly=%t,canonify=%t,path=%s", ugly, canonify, baseURLPathId),
func(t *testing.T) {
baseURL := baseBaseURL + baseURLPath
relURLBase := baseURLPath
@ -73,6 +80,7 @@ func TestPageBundlerSiteRegular(t *testing.T) {
"mediaType": media.HTMLType,
"baseName": "cindex",
"path": "cpath",
"permalinkable": true,
},
})
@ -84,70 +92,92 @@ func TestPageBundlerSiteRegular(t *testing.T) {
cfg.Set("uglyURLs", ugly)
s := buildSingleSite(t, deps.DepsCfg{Logger: loggers.NewWarningLogger(), Fs: fs, Cfg: cfg}, BuildCfg{})
s := buildSingleSite(t, deps.DepsCfg{Logger: loggers.NewErrorLogger(), Fs: fs, Cfg: cfg}, BuildCfg{})
th := testHelper{s.Cfg, s.Fs, t}
assert.Len(s.RegularPages, 8)
assert.Len(s.RegularPages(), 8)
singlePage := s.getPage(KindPage, "a/1.md")
singlePage := s.getPage(page.KindPage, "a/1.md")
assert.Equal("", singlePage.BundleType())
assert.NotNil(singlePage)
assert.Equal(singlePage, s.getPage("page", "a/1"))
assert.Equal(singlePage, s.getPage("page", "1"))
assert.Contains(singlePage.content(), "TheContent")
assert.Contains(content(singlePage), "TheContent")
relFilename := func(basePath, outBase string) (string, string) {
rel := basePath
if ugly {
assert.Equal(relURLBase+"/a/1.html", singlePage.RelPermalink())
th.assertFileContent(filepath.FromSlash("/work/public/a/1.html"), "TheContent")
} else {
assert.Equal(relURLBase+"/a/1/", singlePage.RelPermalink())
th.assertFileContent(filepath.FromSlash("/work/public/a/1/index.html"), "TheContent")
rel = strings.TrimSuffix(basePath, "/") + ".html"
}
var filename string
if !ugly {
filename = path.Join(basePath, outBase)
} else {
filename = rel
}
rel = fmt.Sprintf("%s%s", relURLBase, rel)
return rel, filename
}
// Check both output formats
rel, filename := relFilename("/a/1/", "index.html")
th.assertFileContent(filepath.Join("/work/public", filename),
"TheContent",
"Single RelPermalink: "+rel,
)
rel, filename = relFilename("/cpath/a/1/", "cindex.html")
th.assertFileContent(filepath.Join("/work/public", filename),
"TheContent",
"Single RelPermalink: "+rel,
)
th.assertFileContent(filepath.FromSlash("/work/public/images/hugo-logo.png"), "content")
// This should be just copied to destination.
th.assertFileContent(filepath.FromSlash("/work/public/assets/pic1.png"), "content")
leafBundle1 := s.getPage(KindPage, "b/my-bundle/index.md")
leafBundle1 := s.getPage(page.KindPage, "b/my-bundle/index.md")
assert.NotNil(leafBundle1)
assert.Equal("leaf", leafBundle1.BundleType())
assert.Equal("b", leafBundle1.Section())
sectionB := s.getPage(KindSection, "b")
sectionB := s.getPage(page.KindSection, "b")
assert.NotNil(sectionB)
home, _ := s.Info.Home()
assert.Equal("branch", home.BundleType())
// This is a root bundle and should live in the "home section"
// See https://github.com/gohugoio/hugo/issues/4332
rootBundle := s.getPage(KindPage, "root")
rootBundle := s.getPage(page.KindPage, "root")
assert.NotNil(rootBundle)
assert.True(rootBundle.Parent().IsHome())
if ugly {
assert.Equal(relURLBase+"/root.html", rootBundle.RelPermalink())
} else {
assert.Equal(relURLBase+"/root/", rootBundle.RelPermalink())
if !ugly {
th.assertFileContent(filepath.FromSlash("/work/public/root/index.html"), "Single RelPermalink: "+relURLBase+"/root/")
th.assertFileContent(filepath.FromSlash("/work/public/cpath/root/cindex.html"), "Single RelPermalink: "+relURLBase+"/cpath/root/")
}
leafBundle2 := s.getPage(KindPage, "a/b/index.md")
leafBundle2 := s.getPage(page.KindPage, "a/b/index.md")
assert.NotNil(leafBundle2)
unicodeBundle := s.getPage(KindPage, "c/bundle/index.md")
unicodeBundle := s.getPage(page.KindPage, "c/bundle/index.md")
assert.NotNil(unicodeBundle)
pageResources := leafBundle1.Resources.ByType(pageResourceType)
pageResources := leafBundle1.Resources().ByType(pageResourceType)
assert.Len(pageResources, 2)
firstPage := pageResources[0].(*Page)
secondPage := pageResources[1].(*Page)
assert.Equal(filepath.FromSlash("/work/base/b/my-bundle/1.md"), firstPage.pathOrTitle(), secondPage.pathOrTitle())
assert.Contains(firstPage.content(), "TheContent")
assert.Equal(6, len(leafBundle1.Resources))
firstPage := pageResources[0].(page.Page)
secondPage := pageResources[1].(page.Page)
assert.Equal(filepath.FromSlash("/work/base/b/my-bundle/1.md"), firstPage.File().Filename(), secondPage.File().Filename())
assert.Contains(content(firstPage), "TheContent")
assert.Equal(6, len(leafBundle1.Resources()))
// Verify shortcode in bundled page
assert.Contains(secondPage.content(), filepath.FromSlash("MyShort in b/my-bundle/2.md"))
assert.Contains(content(secondPage), filepath.FromSlash("MyShort in b/my-bundle/2.md"))
// https://github.com/gohugoio/hugo/issues/4582
assert.Equal(leafBundle1, firstPage.Parent())
@ -157,20 +187,10 @@ func TestPageBundlerSiteRegular(t *testing.T) {
assert.Equal(secondPage, pageResources.GetMatch("2*"))
assert.Nil(pageResources.GetMatch("doesnotexist*"))
imageResources := leafBundle1.Resources.ByType("image")
imageResources := leafBundle1.Resources().ByType("image")
assert.Equal(3, len(imageResources))
image := imageResources[0]
altFormat := leafBundle1.OutputFormats().Get("CUSTOMO")
assert.NotNil(altFormat)
assert.Equal(baseURL+"/2017/pageslug/c/logo.png", image.Permalink())
th.assertFileContent(filepath.FromSlash("/work/public/2017/pageslug/c/logo.png"), "content")
th.assertFileContent(filepath.FromSlash("/work/public/cpath/2017/pageslug/c/logo.png"), "content")
// Custom media type defined in site config.
assert.Len(leafBundle1.Resources.ByType("bepsays"), 1)
assert.NotNil(leafBundle1.OutputFormats().Get("CUSTOMO"))
relPermalinker := func(s string) string {
return fmt.Sprintf(s, relURLBase)
@ -180,12 +200,33 @@ func TestPageBundlerSiteRegular(t *testing.T) {
return fmt.Sprintf(s, baseURL)
}
if permalinker == nil {
if ugly {
th.assertFileContent("/work/public/2017/pageslug.html",
relPermalinker("Single RelPermalink: %s/2017/pageslug.html"),
permalinker("Single Permalink: %s/2017/pageslug.html"),
relPermalinker("Sunset RelPermalink: %s/2017/pageslug/sunset1.jpg"),
permalinker("Sunset Permalink: %s/2017/pageslug/sunset1.jpg"))
} else {
th.assertFileContent("/work/public/2017/pageslug/index.html",
relPermalinker("Sunset RelPermalink: %s/2017/pageslug/sunset1.jpg"),
permalinker("Sunset Permalink: %s/2017/pageslug/sunset1.jpg"))
th.assertFileContent("/work/public/cpath/2017/pageslug/cindex.html",
relPermalinker("Single RelPermalink: %s/cpath/2017/pageslug/"),
relPermalinker("Short Sunset RelPermalink: %s/cpath/2017/pageslug/sunset2.jpg"),
relPermalinker("Sunset RelPermalink: %s/cpath/2017/pageslug/sunset1.jpg"),
permalinker("Sunset Permalink: %s/cpath/2017/pageslug/sunset1.jpg"),
)
}
th.assertFileContent(filepath.FromSlash("/work/public/2017/pageslug/c/logo.png"), "content")
th.assertFileContent(filepath.FromSlash("/work/public/cpath/2017/pageslug/c/logo.png"), "content")
th.assertFileNotExist("/work/public/cpath/cpath/2017/pageslug/c/logo.png")
// Custom media type defined in site config.
assert.Len(leafBundle1.Resources().ByType("bepsays"), 1)
if ugly {
assert.Equal(relURLBase+"/2017/pageslug.html", leafBundle1.RelPermalink())
assert.Equal(baseURL+"/2017/pageslug.html", leafBundle1.Permalink())
th.assertFileContent(filepath.FromSlash("/work/public/2017/pageslug.html"),
"TheContent",
relPermalinker("Sunset RelPermalink: %s/2017/pageslug/sunset1.jpg"),
@ -202,23 +243,15 @@ func TestPageBundlerSiteRegular(t *testing.T) {
)
th.assertFileContent(filepath.FromSlash("/work/public/cpath/2017/pageslug.html"), "TheContent")
assert.Equal(relURLBase+"/a/b.html", leafBundle2.RelPermalink())
// 은행
assert.Equal(relURLBase+"/c/%EC%9D%80%ED%96%89.html", unicodeBundle.RelPermalink())
th.assertFileContent(filepath.FromSlash("/work/public/c/은행.html"), "Content for 은행")
th.assertFileContent(filepath.FromSlash("/work/public/c/은행/logo-은행.png"), "은행 PNG")
} else {
assert.Equal(relURLBase+"/2017/pageslug/", leafBundle1.RelPermalink())
assert.Equal(baseURL+"/2017/pageslug/", leafBundle1.Permalink())
th.assertFileContent(filepath.FromSlash("/work/public/2017/pageslug/index.html"), "TheContent")
th.assertFileContent(filepath.FromSlash("/work/public/cpath/2017/pageslug/cindex.html"), "TheContent")
th.assertFileContent(filepath.FromSlash("/work/public/2017/pageslug/index.html"), "Single Title")
th.assertFileContent(filepath.FromSlash("/work/public/root/index.html"), "Single Title")
assert.Equal(relURLBase+"/a/b/", leafBundle2.RelPermalink())
}
})
@ -249,11 +282,11 @@ func TestPageBundlerSiteMultilingual(t *testing.T) {
s := sites.Sites[0]
assert.Equal(8, len(s.RegularPages))
assert.Equal(16, len(s.Pages))
assert.Equal(31, len(s.AllPages))
assert.Equal(8, len(s.RegularPages()))
assert.Equal(16, len(s.Pages()))
assert.Equal(31, len(s.AllPages()))
bundleWithSubPath := s.getPage(KindPage, "lb/index")
bundleWithSubPath := s.getPage(page.KindPage, "lb/index")
assert.NotNil(bundleWithSubPath)
// See https://github.com/gohugoio/hugo/issues/4312
@ -267,30 +300,30 @@ func TestPageBundlerSiteMultilingual(t *testing.T) {
// and probably also just b (aka "my-bundle")
// These may also be translated, so we also need to test that.
// "bf", "my-bf-bundle", "index.md + nn
bfBundle := s.getPage(KindPage, "bf/my-bf-bundle/index")
bfBundle := s.getPage(page.KindPage, "bf/my-bf-bundle/index")
assert.NotNil(bfBundle)
assert.Equal("en", bfBundle.Lang())
assert.Equal(bfBundle, s.getPage(KindPage, "bf/my-bf-bundle/index.md"))
assert.Equal(bfBundle, s.getPage(KindPage, "bf/my-bf-bundle"))
assert.Equal(bfBundle, s.getPage(KindPage, "my-bf-bundle"))
assert.Equal("en", bfBundle.Language().Lang)
assert.Equal(bfBundle, s.getPage(page.KindPage, "bf/my-bf-bundle/index.md"))
assert.Equal(bfBundle, s.getPage(page.KindPage, "bf/my-bf-bundle"))
assert.Equal(bfBundle, s.getPage(page.KindPage, "my-bf-bundle"))
nnSite := sites.Sites[1]
assert.Equal(7, len(nnSite.RegularPages))
assert.Equal(7, len(nnSite.RegularPages()))
bfBundleNN := nnSite.getPage(KindPage, "bf/my-bf-bundle/index")
bfBundleNN := nnSite.getPage(page.KindPage, "bf/my-bf-bundle/index")
assert.NotNil(bfBundleNN)
assert.Equal("nn", bfBundleNN.Lang())
assert.Equal(bfBundleNN, nnSite.getPage(KindPage, "bf/my-bf-bundle/index.nn.md"))
assert.Equal(bfBundleNN, nnSite.getPage(KindPage, "bf/my-bf-bundle"))
assert.Equal(bfBundleNN, nnSite.getPage(KindPage, "my-bf-bundle"))
assert.Equal("nn", bfBundleNN.Language().Lang)
assert.Equal(bfBundleNN, nnSite.getPage(page.KindPage, "bf/my-bf-bundle/index.nn.md"))
assert.Equal(bfBundleNN, nnSite.getPage(page.KindPage, "bf/my-bf-bundle"))
assert.Equal(bfBundleNN, nnSite.getPage(page.KindPage, "my-bf-bundle"))
// See https://github.com/gohugoio/hugo/issues/4295
// Every resource should have its Name prefixed with its base folder.
cBundleResources := bundleWithSubPath.Resources.Match("c/**")
cBundleResources := bundleWithSubPath.Resources().Match("c/**")
assert.Equal(4, len(cBundleResources))
bundlePage := bundleWithSubPath.Resources.GetMatch("c/page*")
bundlePage := bundleWithSubPath.Resources().GetMatch("c/page*")
assert.NotNil(bundlePage)
assert.IsType(&Page{}, bundlePage)
assert.IsType(&pageState{}, bundlePage)
})
}
@ -329,15 +362,15 @@ func TestMultilingualDisableLanguage(t *testing.T) {
s := sites.Sites[0]
assert.Equal(8, len(s.RegularPages))
assert.Equal(16, len(s.Pages))
assert.Equal(8, len(s.RegularPages()))
assert.Equal(16, len(s.Pages()))
// No nn pages
assert.Equal(16, len(s.AllPages))
assert.Equal(16, len(s.AllPages()))
for _, p := range s.rawAllPages {
assert.True(p.Lang() != "nn")
assert.True(p.Language().Lang != "nn")
}
for _, p := range s.AllPages {
assert.True(p.Lang() != "nn")
for _, p := range s.AllPages() {
assert.True(p.Language().Lang != "nn")
}
}
@ -358,11 +391,11 @@ func TestPageBundlerSiteWitSymbolicLinksInContent(t *testing.T) {
th := testHelper{s.Cfg, s.Fs, t}
assert.Equal(7, len(s.RegularPages))
a1Bundle := s.getPage(KindPage, "symbolic2/a1/index.md")
assert.Equal(7, len(s.RegularPages()))
a1Bundle := s.getPage(page.KindPage, "symbolic2/a1/index.md")
assert.NotNil(a1Bundle)
assert.Equal(2, len(a1Bundle.Resources))
assert.Equal(1, len(a1Bundle.Resources.ByType(pageResourceType)))
assert.Equal(2, len(a1Bundle.Resources()))
assert.Equal(1, len(a1Bundle.Resources().ByType(pageResourceType)))
th.assertFileContent(filepath.FromSlash(workDir+"/public/a/page/index.html"), "TheContent")
th.assertFileContent(filepath.FromSlash(workDir+"/public/symbolic1/s1/index.html"), "TheContent")
@ -416,28 +449,27 @@ HEADLESS {{< myShort >}}
s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{})
assert.Equal(1, len(s.RegularPages))
assert.Equal(1, len(s.RegularPages()))
assert.Equal(1, len(s.headlessPages))
regular := s.getPage(KindPage, "a/index")
regular := s.getPage(page.KindPage, "a/index")
assert.Equal("/a/s1/", regular.RelPermalink())
headless := s.getPage(KindPage, "b/index")
headless := s.getPage(page.KindPage, "b/index")
assert.NotNil(headless)
assert.True(headless.headless)
assert.Equal("Headless Bundle in Topless Bar", headless.Title())
assert.Equal("", headless.RelPermalink())
assert.Equal("", headless.Permalink())
assert.Contains(headless.content(), "HEADLESS SHORTCODE")
assert.Contains(content(headless), "HEADLESS SHORTCODE")
headlessResources := headless.Resources
headlessResources := headless.Resources()
assert.Equal(3, len(headlessResources))
assert.Equal(2, len(headlessResources.Match("l*")))
pageResource := headlessResources.GetMatch("p*")
assert.NotNil(pageResource)
assert.IsType(&Page{}, pageResource)
p := pageResource.(*Page)
assert.Contains(p.content(), "SHORTCODE")
assert.IsType(&pageState{}, pageResource)
p := pageResource.(page.Page)
assert.Contains(content(p), "SHORTCODE")
assert.Equal("p1.md", p.Name())
th := testHelper{s.Cfg, s.Fs, t}
@ -451,6 +483,91 @@ HEADLESS {{< myShort >}}
}
func TestMultiSiteBundles(t *testing.T) {
assert := require.New(t)
b := newTestSitesBuilder(t)
b.WithConfigFile("toml", `
baseURL = "http://example.com/"
defaultContentLanguage = "en"
[languages]
[languages.en]
weight = 10
contentDir = "content/en"
[languages.nn]
weight = 20
contentDir = "content/nn"
`)
b.WithContent("en/mybundle/index.md", `
---
headless: true
---
`)
b.WithContent("nn/mybundle/index.md", `
---
headless: true
---
`)
b.WithContent("en/mybundle/data.yaml", `data en`)
b.WithContent("en/mybundle/forms.yaml", `forms en`)
b.WithContent("nn/mybundle/data.yaml", `data nn`)
b.WithContent("en/_index.md", `
---
Title: Home
---
Home content.
`)
b.WithContent("en/section-not-bundle/_index.md", `
---
Title: Section Page
---
Section content.
`)
b.WithContent("en/section-not-bundle/single.md", `
---
Title: Section Single
Date: 2018-02-01
---
Single content.
`)
b.Build(BuildCfg{})
b.AssertFileContent("public/nn/mybundle/data.yaml", "data nn")
b.AssertFileContent("public/nn/mybundle/forms.yaml", "forms en")
b.AssertFileContent("public/mybundle/data.yaml", "data en")
b.AssertFileContent("public/mybundle/forms.yaml", "forms en")
assert.False(b.CheckExists("public/nn/nn/mybundle/data.yaml"))
assert.False(b.CheckExists("public/en/mybundle/data.yaml"))
homeEn := b.H.Sites[0].home
assert.NotNil(homeEn)
assert.Equal(2018, homeEn.Date().Year())
b.AssertFileContent("public/section-not-bundle/index.html", "Section Page", "Content: <p>Section content.</p>")
b.AssertFileContent("public/section-not-bundle/single/index.html", "Section Single", "|<p>Single content.</p>")
}
func newTestBundleSources(t *testing.T) (*hugofs.Fs, *viper.Viper) {
cfg, fs := newTestCfg()
assert := require.New(t)
@ -512,6 +629,8 @@ TheContent.
singleLayout := `
Single Title: {{ .Title }}
Single RelPermalink: {{ .RelPermalink }}
Single Permalink: {{ .Permalink }}
Content: {{ .Content }}
{{ $sunset := .Resources.GetMatch "my-sunset-1*" }}
{{ with $sunset }}
@ -532,7 +651,7 @@ Thumb RelPermalink: {{ $thumb.RelPermalink }}
`
myShort := `
MyShort in {{ .Page.Path }}:
MyShort in {{ .Page.File.Path }}:
{{ $sunset := .Page.Resources.GetMatch "my-sunset-2*" }}
{{ with $sunset }}
Short Sunset RelPermalink: {{ .RelPermalink }}
@ -599,6 +718,7 @@ Content for 은행.
assert.NoError(err)
_, err = io.Copy(out, src)
assert.NoError(err)
out.Close()
src.Seek(0, 0)
_, err = io.Copy(out2, src)

View file

@ -1,4 +1,4 @@
// Copyright 2016 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -18,43 +18,65 @@ import (
"path"
"path/filepath"
"strings"
"sync"
"github.com/pkg/errors"
"github.com/gohugoio/hugo/cache"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/resources/page"
)
// Used in the page cache to mark more than one hit for a given key.
var ambiguityFlag = &pageState{}
// PageCollections contains the page collections for a site.
type PageCollections struct {
// Includes only pages of all types, and only pages in the current language.
Pages Pages
// Includes all pages in all languages, including the current one.
// Includes pages of all types.
AllPages Pages
// A convenience cache for the traditional index types, taxonomies, home page etc.
// This is for the current language only.
indexPages Pages
// A convenience cache for the regular pages.
// This is for the current language only.
RegularPages Pages
// A convenience cache for the all the regular pages.
AllRegularPages Pages
// Includes absolute all pages (of all types), including drafts etc.
rawAllPages Pages
rawAllPages pageStatePages
// rawAllPages plus additional pages created during the build process.
workAllPages pageStatePages
// Includes headless bundles, i.e. bundles that produce no output for its content page.
headlessPages Pages
headlessPages pageStatePages
// Lazy initialized page collections
pages *lazyPagesFactory
regularPages *lazyPagesFactory
allPages *lazyPagesFactory
allRegularPages *lazyPagesFactory
// The index for .Site.GetPage etc.
pageIndex *cache.Lazy
}
// Pages returns all pages.
// This is for the current language only.
func (c *PageCollections) Pages() page.Pages {
return c.pages.get()
}
// RegularPages returns all the regular pages.
// This is for the current language only.
func (c *PageCollections) RegularPages() page.Pages {
return c.regularPages.get()
}
// AllPages returns all pages for all languages.
func (c *PageCollections) AllPages() page.Pages {
return c.allPages.get()
}
// AllPages returns all regular pages for all languages.
func (c *PageCollections) AllRegularPages() page.Pages {
return c.allRegularPages.get()
}
// Get initializes the index if not already done so, then
// looks up the given page ref, returns nil if no value found.
func (c *PageCollections) getFromCache(ref string) (*Page, error) {
func (c *PageCollections) getFromCache(ref string) (page.Page, error) {
v, found, err := c.pageIndex.Get(ref)
if err != nil {
return nil, err
@ -63,7 +85,7 @@ func (c *PageCollections) getFromCache(ref string) (*Page, error) {
return nil, nil
}
p := v.(*Page)
p := v.(page.Page)
if p != ambiguityFlag {
return p, nil
@ -71,17 +93,49 @@ func (c *PageCollections) getFromCache(ref string) (*Page, error) {
return nil, fmt.Errorf("page reference %q is ambiguous", ref)
}
var ambiguityFlag = &Page{Kind: kindUnknown, title: "ambiguity flag"}
type lazyPagesFactory struct {
pages page.Pages
func (c *PageCollections) refreshPageCaches() {
c.indexPages = c.findPagesByKindNotIn(KindPage, c.Pages)
c.RegularPages = c.findPagesByKindIn(KindPage, c.Pages)
c.AllRegularPages = c.findPagesByKindIn(KindPage, c.AllPages)
init sync.Once
factory page.PagesFactory
}
indexLoader := func() (map[string]interface{}, error) {
func (l *lazyPagesFactory) get() page.Pages {
l.init.Do(func() {
l.pages = l.factory()
})
return l.pages
}
func newLazyPagesFactory(factory page.PagesFactory) *lazyPagesFactory {
return &lazyPagesFactory{factory: factory}
}
func newPageCollections() *PageCollections {
return newPageCollectionsFromPages(nil)
}
func newPageCollectionsFromPages(pages pageStatePages) *PageCollections {
c := &PageCollections{rawAllPages: pages}
c.pages = newLazyPagesFactory(func() page.Pages {
pages := make(page.Pages, len(c.workAllPages))
for i, p := range c.workAllPages {
pages[i] = p
}
return pages
})
c.regularPages = newLazyPagesFactory(func() page.Pages {
return c.findPagesByKindInWorkPages(page.KindPage, c.workAllPages)
})
c.pageIndex = cache.NewLazy(func() (map[string]interface{}, error) {
index := make(map[string]interface{})
add := func(ref string, p *Page) {
add := func(ref string, p page.Page) {
ref = strings.ToLower(ref)
existing := index[ref]
if existing == nil {
index[ref] = p
@ -90,9 +144,10 @@ func (c *PageCollections) refreshPageCaches() {
}
}
for _, pageCollection := range []Pages{c.RegularPages, c.headlessPages} {
for _, pageCollection := range []pageStatePages{c.workAllPages, c.headlessPages} {
for _, p := range pageCollection {
sourceRef := p.absoluteSourceRef()
if p.IsPage() {
sourceRef := p.sourceRef()
if sourceRef != "" {
// index the canonical ref
@ -101,9 +156,9 @@ func (c *PageCollections) refreshPageCaches() {
}
// Ref/Relref supports this potentially ambiguous lookup.
add(p.LogicalName(), p)
add(p.File().LogicalName(), p)
translationBaseName := p.TranslationBaseName()
translationBaseName := p.File().TranslationBaseName()
dir, _ := path.Split(sourceRef)
dir = strings.TrimSuffix(dir, "/")
@ -118,43 +173,34 @@ func (c *PageCollections) refreshPageCaches() {
// We need a way to get to the current language version.
pathWithNoExtensions := path.Join(dir, translationBaseName)
add(pathWithNoExtensions, p)
}
}
for _, p := range c.indexPages {
} else {
// index the canonical, unambiguous ref for any backing file
// e.g. /section/_index.md
sourceRef := p.absoluteSourceRef()
sourceRef := p.sourceRef()
if sourceRef != "" {
add(sourceRef, p)
}
ref := path.Join(p.sections...)
ref := p.SectionsPath()
// index the canonical, unambiguous virtual ref
// e.g. /section
// (this may already have been indexed above)
add("/"+ref, p)
}
}
}
return index, nil
}
})
c.pageIndex = cache.NewLazy(indexLoader)
}
func newPageCollections() *PageCollections {
return &PageCollections{}
}
func newPageCollectionsFromPages(pages Pages) *PageCollections {
return &PageCollections{rawAllPages: pages}
return c
}
// This is an adapter func for the old API with Kind as first argument.
// This is invoked when you do .Site.GetPage. We drop the Kind and fails
// if there are more than 2 arguments, which would be ambigous.
func (c *PageCollections) getPageOldVersion(ref ...string) (*Page, error) {
func (c *PageCollections) getPageOldVersion(ref ...string) (page.Page, error) {
var refs []string
for _, r := range ref {
// A common construct in the wild is
@ -173,10 +219,10 @@ func (c *PageCollections) getPageOldVersion(ref ...string) (*Page, error) {
return nil, fmt.Errorf(`too many arguments to .Site.GetPage: %v. Use lookups on the form {{ .Site.GetPage "/posts/mypage-md" }}`, ref)
}
if len(refs) == 0 || refs[0] == KindHome {
if len(refs) == 0 || refs[0] == page.KindHome {
key = "/"
} else if len(refs) == 1 {
if len(ref) == 2 && refs[0] == KindSection {
if len(ref) == 2 && refs[0] == page.KindSection {
// This is an old style reference to the "Home Page section".
// Typically fetched via {{ .Site.GetPage "section" .Section }}
// See https://github.com/gohugoio/hugo/issues/4989
@ -197,17 +243,18 @@ func (c *PageCollections) getPageOldVersion(ref ...string) (*Page, error) {
}
// Only used in tests.
func (c *PageCollections) getPage(typ string, sections ...string) *Page {
func (c *PageCollections) getPage(typ string, sections ...string) page.Page {
refs := append([]string{typ}, path.Join(sections...))
p, _ := c.getPageOldVersion(refs...)
return p
}
// Ref is either unix-style paths (i.e. callers responsible for
// calling filepath.ToSlash as necessary) or shorthand refs.
func (c *PageCollections) getPageNew(context *Page, ref string) (*Page, error) {
// Case insensitive page lookup.
func (c *PageCollections) getPageNew(context page.Page, ref string) (page.Page, error) {
var anError error
ref = strings.ToLower(ref)
// Absolute (content root relative) reference.
if strings.HasPrefix(ref, "/") {
p, err := c.getFromCache(ref)
@ -220,7 +267,7 @@ func (c *PageCollections) getPageNew(context *Page, ref string) (*Page, error) {
} else if context != nil {
// Try the page-relative path.
ppath := path.Join("/", strings.Join(context.sections, "/"), ref)
ppath := path.Join("/", strings.ToLower(context.SectionsPath()), ref)
p, err := c.getFromCache(ppath)
if err == nil && p != nil {
return p, nil
@ -236,7 +283,8 @@ func (c *PageCollections) getPageNew(context *Page, ref string) (*Page, error) {
if err == nil && p != nil {
if context != nil {
// TODO(bep) remove this case and the message below when the storm has passed
helpers.DistinctFeedbackLog.Printf(`WARNING: make non-relative ref/relref page reference(s) in page %q absolute, e.g. {{< ref "/blog/my-post.md" >}}`, context.absoluteSourceRef())
err := wrapErr(errors.New(`make non-relative ref/relref page reference(s) in page %q absolute, e.g. {{< ref "/blog/my-post.md" >}}`), context)
helpers.DistinctWarnLog.Println(err)
}
return p, nil
}
@ -253,49 +301,56 @@ func (c *PageCollections) getPageNew(context *Page, ref string) (*Page, error) {
}
if p == nil && anError != nil {
if context != nil {
return nil, fmt.Errorf("failed to resolve path from page %q: %s", context.absoluteSourceRef(), anError)
}
return nil, fmt.Errorf("failed to resolve page: %s", anError)
return nil, wrapErr(errors.Wrap(anError, "failed to resolve ref"), context)
}
return p, nil
}
func (*PageCollections) findPagesByKindIn(kind string, inPages Pages) Pages {
var pages Pages
func (*PageCollections) findPagesByKindIn(kind string, inPages page.Pages) page.Pages {
var pages page.Pages
for _, p := range inPages {
if p.Kind == kind {
if p.Kind() == kind {
pages = append(pages, p)
}
}
return pages
}
func (*PageCollections) findFirstPageByKindIn(kind string, inPages Pages) *Page {
func (c *PageCollections) findPagesByKind(kind string) page.Pages {
return c.findPagesByKindIn(kind, c.Pages())
}
func (c *PageCollections) findWorkPagesByKind(kind string) pageStatePages {
var pages pageStatePages
for _, p := range c.workAllPages {
if p.Kind() == kind {
pages = append(pages, p)
}
}
return pages
}
func (*PageCollections) findPagesByKindInWorkPages(kind string, inPages pageStatePages) page.Pages {
var pages page.Pages
for _, p := range inPages {
if p.Kind == kind {
if p.Kind() == kind {
pages = append(pages, p)
}
}
return pages
}
func (c *PageCollections) findFirstWorkPageByKindIn(kind string) *pageState {
for _, p := range c.workAllPages {
if p.Kind() == kind {
return p
}
}
return nil
}
func (*PageCollections) findPagesByKindNotIn(kind string, inPages Pages) Pages {
var pages Pages
for _, p := range inPages {
if p.Kind != kind {
pages = append(pages, p)
}
}
return pages
}
func (c *PageCollections) findPagesByKind(kind string) Pages {
return c.findPagesByKindIn(kind, c.Pages)
}
func (c *PageCollections) addPage(page *Page) {
func (c *PageCollections) addPage(page *pageState) {
c.rawAllPages = append(c.rawAllPages, page)
}
@ -307,35 +362,31 @@ func (c *PageCollections) removePageFilename(filename string) {
}
func (c *PageCollections) removePage(page *Page) {
func (c *PageCollections) removePage(page *pageState) {
if i := c.rawAllPages.findPagePos(page); i >= 0 {
c.clearResourceCacheForPage(c.rawAllPages[i])
c.rawAllPages = append(c.rawAllPages[:i], c.rawAllPages[i+1:]...)
}
}
func (c *PageCollections) findPagesByShortcode(shortcode string) Pages {
var pages Pages
func (c *PageCollections) findPagesByShortcode(shortcode string) page.Pages {
var pages page.Pages
for _, p := range c.rawAllPages {
if p.shortcodeState != nil {
if _, ok := p.shortcodeState.nameSet[shortcode]; ok {
if p.HasShortcode(shortcode) {
pages = append(pages, p)
}
}
}
return pages
}
func (c *PageCollections) replacePage(page *Page) {
func (c *PageCollections) replacePage(page *pageState) {
// will find existing page that matches filepath and remove it
c.removePage(page)
c.addPage(page)
}
func (c *PageCollections) clearResourceCacheForPage(page *Page) {
if len(page.Resources) > 0 {
page.s.ResourceSpec.DeleteCacheByPrefix(page.relTargetPathBase)
func (c *PageCollections) clearResourceCacheForPage(page *pageState) {
if len(page.resources) > 0 {
page.s.ResourceSpec.DeleteCacheByPrefix(page.targetPaths().SubResourceBaseTarget)
}
}

View file

@ -1,4 +1,4 @@
// Copyright 2017 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -21,6 +21,8 @@ import (
"testing"
"time"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/deps"
"github.com/stretchr/testify/require"
)
@ -98,12 +100,12 @@ func BenchmarkGetPageRegular(b *testing.B) {
type testCase struct {
kind string
context *Page
context page.Page
path []string
expectedTitle string
}
func (t *testCase) check(p *Page, err error, errorMsg string, assert *require.Assertions) {
func (t *testCase) check(p page.Page, err error, errorMsg string, assert *require.Assertions) {
switch t.kind {
case "Ambiguous":
assert.Error(err)
@ -114,8 +116,8 @@ func (t *testCase) check(p *Page, err error, errorMsg string, assert *require.As
default:
assert.NoError(err, errorMsg)
assert.NotNil(p, errorMsg)
assert.Equal(t.kind, p.Kind, errorMsg)
assert.Equal(t.expectedTitle, p.title, errorMsg)
assert.Equal(t.kind, p.Kind(), errorMsg)
assert.Equal(t.expectedTitle, p.Title(), errorMsg)
}
}
@ -159,62 +161,62 @@ func TestGetPage(t *testing.T) {
tests := []testCase{
// legacy content root relative paths
{KindHome, nil, []string{}, "home page"},
{KindPage, nil, []string{"about.md"}, "about page"},
{KindSection, nil, []string{"sect3"}, "section 3"},
{KindPage, nil, []string{"sect3/page1.md"}, "Title3_1"},
{KindPage, nil, []string{"sect4/page2.md"}, "Title4_2"},
{KindSection, nil, []string{"sect3/sect7"}, "another sect7"},
{KindPage, nil, []string{"sect3/subsect/deep.md"}, "deep page"},
{KindPage, nil, []string{filepath.FromSlash("sect5/page3.md")}, "Title5_3"}, //test OS-specific path
{page.KindHome, nil, []string{}, "home page"},
{page.KindPage, nil, []string{"about.md"}, "about page"},
{page.KindSection, nil, []string{"sect3"}, "section 3"},
{page.KindPage, nil, []string{"sect3/page1.md"}, "Title3_1"},
{page.KindPage, nil, []string{"sect4/page2.md"}, "Title4_2"},
{page.KindSection, nil, []string{"sect3/sect7"}, "another sect7"},
{page.KindPage, nil, []string{"sect3/subsect/deep.md"}, "deep page"},
{page.KindPage, nil, []string{filepath.FromSlash("sect5/page3.md")}, "Title5_3"}, //test OS-specific path
// shorthand refs (potentially ambiguous)
{KindPage, nil, []string{"unique.md"}, "UniqueBase"},
{page.KindPage, nil, []string{"unique.md"}, "UniqueBase"},
{"Ambiguous", nil, []string{"page1.md"}, ""},
// ISSUE: This is an ambiguous ref, but because we have to support the legacy
// content root relative paths without a leading slash, the lookup
// returns /sect7. This undermines ambiguity detection, but we have no choice.
//{"Ambiguous", nil, []string{"sect7"}, ""},
{KindSection, nil, []string{"sect7"}, "Sect7s"},
{page.KindSection, nil, []string{"sect7"}, "Sect7s"},
// absolute paths
{KindHome, nil, []string{"/"}, "home page"},
{KindPage, nil, []string{"/about.md"}, "about page"},
{KindSection, nil, []string{"/sect3"}, "section 3"},
{KindPage, nil, []string{"/sect3/page1.md"}, "Title3_1"},
{KindPage, nil, []string{"/sect4/page2.md"}, "Title4_2"},
{KindSection, nil, []string{"/sect3/sect7"}, "another sect7"},
{KindPage, nil, []string{"/sect3/subsect/deep.md"}, "deep page"},
{KindPage, nil, []string{filepath.FromSlash("/sect5/page3.md")}, "Title5_3"}, //test OS-specific path
{KindPage, nil, []string{"/sect3/unique.md"}, "UniqueBase"}, //next test depends on this page existing
{page.KindHome, nil, []string{"/"}, "home page"},
{page.KindPage, nil, []string{"/about.md"}, "about page"},
{page.KindSection, nil, []string{"/sect3"}, "section 3"},
{page.KindPage, nil, []string{"/sect3/page1.md"}, "Title3_1"},
{page.KindPage, nil, []string{"/sect4/page2.md"}, "Title4_2"},
{page.KindSection, nil, []string{"/sect3/sect7"}, "another sect7"},
{page.KindPage, nil, []string{"/sect3/subsect/deep.md"}, "deep page"},
{page.KindPage, nil, []string{filepath.FromSlash("/sect5/page3.md")}, "Title5_3"}, //test OS-specific path
{page.KindPage, nil, []string{"/sect3/unique.md"}, "UniqueBase"}, //next test depends on this page existing
// {"NoPage", nil, []string{"/unique.md"}, ""}, // ISSUE #4969: this is resolving to /sect3/unique.md
{"NoPage", nil, []string{"/missing-page.md"}, ""},
{"NoPage", nil, []string{"/missing-section"}, ""},
// relative paths
{KindHome, sec3, []string{".."}, "home page"},
{KindHome, sec3, []string{"../"}, "home page"},
{KindPage, sec3, []string{"../about.md"}, "about page"},
{KindSection, sec3, []string{"."}, "section 3"},
{KindSection, sec3, []string{"./"}, "section 3"},
{KindPage, sec3, []string{"page1.md"}, "Title3_1"},
{KindPage, sec3, []string{"./page1.md"}, "Title3_1"},
{KindPage, sec3, []string{"../sect4/page2.md"}, "Title4_2"},
{KindSection, sec3, []string{"sect7"}, "another sect7"},
{KindSection, sec3, []string{"./sect7"}, "another sect7"},
{KindPage, sec3, []string{"./subsect/deep.md"}, "deep page"},
{KindPage, sec3, []string{"./subsect/../../sect7/page9.md"}, "Title7_9"},
{KindPage, sec3, []string{filepath.FromSlash("../sect5/page3.md")}, "Title5_3"}, //test OS-specific path
{KindPage, sec3, []string{"./unique.md"}, "UniqueBase"},
{page.KindHome, sec3, []string{".."}, "home page"},
{page.KindHome, sec3, []string{"../"}, "home page"},
{page.KindPage, sec3, []string{"../about.md"}, "about page"},
{page.KindSection, sec3, []string{"."}, "section 3"},
{page.KindSection, sec3, []string{"./"}, "section 3"},
{page.KindPage, sec3, []string{"page1.md"}, "Title3_1"},
{page.KindPage, sec3, []string{"./page1.md"}, "Title3_1"},
{page.KindPage, sec3, []string{"../sect4/page2.md"}, "Title4_2"},
{page.KindSection, sec3, []string{"sect7"}, "another sect7"},
{page.KindSection, sec3, []string{"./sect7"}, "another sect7"},
{page.KindPage, sec3, []string{"./subsect/deep.md"}, "deep page"},
{page.KindPage, sec3, []string{"./subsect/../../sect7/page9.md"}, "Title7_9"},
{page.KindPage, sec3, []string{filepath.FromSlash("../sect5/page3.md")}, "Title5_3"}, //test OS-specific path
{page.KindPage, sec3, []string{"./unique.md"}, "UniqueBase"},
{"NoPage", sec3, []string{"./sect2"}, ""},
//{"NoPage", sec3, []string{"sect2"}, ""}, // ISSUE: /sect3 page relative query is resolving to /sect2
// absolute paths ignore context
{KindHome, sec3, []string{"/"}, "home page"},
{KindPage, sec3, []string{"/about.md"}, "about page"},
{KindPage, sec3, []string{"/sect4/page2.md"}, "Title4_2"},
{KindPage, sec3, []string{"/sect3/subsect/deep.md"}, "deep page"}, //next test depends on this page existing
{page.KindHome, sec3, []string{"/"}, "home page"},
{page.KindPage, sec3, []string{"/about.md"}, "about page"},
{page.KindPage, sec3, []string{"/sect4/page2.md"}, "Title4_2"},
{page.KindPage, sec3, []string{"/sect3/subsect/deep.md"}, "deep page"}, //next test depends on this page existing
{"NoPage", sec3, []string{"/subsect/deep.md"}, ""},
}

View file

@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -21,6 +21,8 @@ import (
"github.com/stretchr/testify/require"
)
// TODO(bep) move and rewrite in resource/page.
func TestMergeLanguages(t *testing.T) {
t.Parallel()
assert := require.New(t)
@ -36,12 +38,12 @@ func TestMergeLanguages(t *testing.T) {
frSite := h.Sites[1]
nnSite := h.Sites[2]
assert.Equal(31, len(enSite.RegularPages))
assert.Equal(6, len(frSite.RegularPages))
assert.Equal(12, len(nnSite.RegularPages))
assert.Equal(31, len(enSite.RegularPages()))
assert.Equal(6, len(frSite.RegularPages()))
assert.Equal(12, len(nnSite.RegularPages()))
for i := 0; i < 2; i++ {
mergedNN := nnSite.RegularPages.MergeByLanguage(enSite.RegularPages)
mergedNN := nnSite.RegularPages().MergeByLanguage(enSite.RegularPages())
assert.Equal(31, len(mergedNN))
for i := 1; i <= 31; i++ {
expectedLang := "en"
@ -49,11 +51,11 @@ func TestMergeLanguages(t *testing.T) {
expectedLang = "nn"
}
p := mergedNN[i-1]
assert.Equal(expectedLang, p.Lang(), fmt.Sprintf("Test %d", i))
assert.Equal(expectedLang, p.Language().Lang, fmt.Sprintf("Test %d", i))
}
}
mergedFR := frSite.RegularPages.MergeByLanguage(enSite.RegularPages)
mergedFR := frSite.RegularPages().MergeByLanguage(enSite.RegularPages())
assert.Equal(31, len(mergedFR))
for i := 1; i <= 31; i++ {
expectedLang := "en"
@ -61,28 +63,28 @@ func TestMergeLanguages(t *testing.T) {
expectedLang = "fr"
}
p := mergedFR[i-1]
assert.Equal(expectedLang, p.Lang(), fmt.Sprintf("Test %d", i))
assert.Equal(expectedLang, p.Language().Lang, fmt.Sprintf("Test %d", i))
}
firstNN := nnSite.RegularPages[0]
firstNN := nnSite.RegularPages()[0]
assert.Equal(4, len(firstNN.Sites()))
assert.Equal("en", firstNN.Sites().First().Language().Lang)
nnBundle := nnSite.getPage("page", "bundle")
enBundle := enSite.getPage("page", "bundle")
assert.Equal(6, len(enBundle.Resources))
assert.Equal(2, len(nnBundle.Resources))
assert.Equal(6, len(enBundle.Resources()))
assert.Equal(2, len(nnBundle.Resources()))
var ri interface{} = nnBundle.Resources
var ri interface{} = nnBundle.Resources()
// This looks less ugly in the templates ...
mergedNNResources := ri.(resource.ResourcesLanguageMerger).MergeByLanguage(enBundle.Resources)
mergedNNResources := ri.(resource.ResourcesLanguageMerger).MergeByLanguage(enBundle.Resources())
assert.Equal(6, len(mergedNNResources))
unchanged, err := nnSite.RegularPages.MergeByLanguageInterface(nil)
unchanged, err := nnSite.RegularPages().MergeByLanguageInterface(nil)
assert.NoError(err)
assert.Equal(nnSite.RegularPages, unchanged)
assert.Equal(nnSite.RegularPages(), unchanged)
}
@ -93,7 +95,7 @@ func TestMergeLanguagesTemplate(t *testing.T) {
b.WithTemplates("home.html", `
{{ $pages := .Site.RegularPages }}
{{ .Scratch.Set "pages" $pages }}
{{ if eq .Lang "nn" }}:
{{ if eq .Language.Lang "nn" }}:
{{ $enSite := index .Sites 0 }}
{{ $frSite := index .Sites 1 }}
{{ $nnBundle := .Site.GetPage "page" "bundle" }}
@ -103,8 +105,8 @@ func TestMergeLanguagesTemplate(t *testing.T) {
{{ end }}
{{ $pages := .Scratch.Get "pages" }}
{{ $pages2 := .Scratch.Get "pages2" }}
Pages1: {{ range $i, $p := $pages }}{{ add $i 1 }}: {{ .Path }} {{ .Lang }} | {{ end }}
Pages2: {{ range $i, $p := $pages2 }}{{ add $i 1 }}: {{ .Title }} {{ .Lang }} | {{ end }}
Pages1: {{ range $i, $p := $pages }}{{ add $i 1 }}: {{ .File.Path }} {{ .Language.Lang }} | {{ end }}
Pages2: {{ range $i, $p := $pages2 }}{{ add $i 1 }}: {{ .Title }} {{ .Language.Lang }} | {{ end }}
`,
"shortcodes/shortcode.html", "MyShort",
@ -178,7 +180,7 @@ func BenchmarkMergeByLanguage(b *testing.B) {
nnSite := h.Sites[2]
for i := 0; i < b.N; i++ {
merged := nnSite.RegularPages.MergeByLanguage(enSite.RegularPages)
merged := nnSite.RegularPages().MergeByLanguage(enSite.RegularPages())
if len(merged) != count {
b.Fatal("Count mismatch")
}

View file

@ -1,75 +0,0 @@
// Copyright 2017-present The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"fmt"
"path/filepath"
"testing"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/deps"
"github.com/stretchr/testify/require"
)
func TestRelated(t *testing.T) {
assert := require.New(t)
t.Parallel()
var (
cfg, fs = newTestCfg()
//th = testHelper{cfg, fs, t}
)
pageTmpl := `---
title: Page %d
keywords: [%s]
date: %s
---
Content
`
writeSource(t, fs, filepath.Join("content", "page1.md"), fmt.Sprintf(pageTmpl, 1, "hugo, says", "2017-01-03"))
writeSource(t, fs, filepath.Join("content", "page2.md"), fmt.Sprintf(pageTmpl, 2, "hugo, rocks", "2017-01-02"))
writeSource(t, fs, filepath.Join("content", "page3.md"), fmt.Sprintf(pageTmpl, 3, "bep, says", "2017-01-01"))
s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true})
assert.Len(s.RegularPages, 3)
result, err := s.RegularPages.RelatedTo(types.NewKeyValuesStrings("keywords", "hugo", "rocks"))
assert.NoError(err)
assert.Len(result, 2)
assert.Equal("Page 2", result[0].title)
assert.Equal("Page 1", result[1].title)
result, err = s.RegularPages.Related(s.RegularPages[0])
assert.Len(result, 2)
assert.Equal("Page 2", result[0].title)
assert.Equal("Page 3", result[1].title)
result, err = s.RegularPages.RelatedIndices(s.RegularPages[0], "keywords")
assert.Len(result, 2)
assert.Equal("Page 2", result[0].title)
assert.Equal("Page 3", result[1].title)
result, err = s.RegularPages.RelatedTo(types.NewKeyValuesStrings("keywords", "bep", "rocks"))
assert.NoError(err)
assert.Len(result, 2)
assert.Equal("Page 2", result[0].title)
assert.Equal("Page 3", result[1].title)
}

View file

@ -1,579 +0,0 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"fmt"
"html/template"
"path/filepath"
"strings"
"testing"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/output"
"github.com/stretchr/testify/require"
)
func TestSplitPages(t *testing.T) {
t.Parallel()
s := newTestSite(t)
pages := createTestPages(s, 21)
chunks := splitPages(pages, 5)
require.Equal(t, 5, len(chunks))
for i := 0; i < 4; i++ {
require.Equal(t, 5, chunks[i].Len())
}
lastChunk := chunks[4]
require.Equal(t, 1, lastChunk.Len())
}
func TestSplitPageGroups(t *testing.T) {
t.Parallel()
s := newTestSite(t)
pages := createTestPages(s, 21)
groups, _ := pages.GroupBy("Weight", "desc")
chunks := splitPageGroups(groups, 5)
require.Equal(t, 5, len(chunks))
firstChunk := chunks[0]
// alternate weight 5 and 10
if groups, ok := firstChunk.(PagesGroup); ok {
require.Equal(t, 5, groups.Len())
for _, pg := range groups {
// first group 10 in weight
require.Equal(t, 10, pg.Key)
for _, p := range pg.Pages {
require.True(t, p.fuzzyWordCount%2 == 0) // magic test
}
}
} else {
t.Fatal("Excepted PageGroup")
}
lastChunk := chunks[4]
if groups, ok := lastChunk.(PagesGroup); ok {
require.Equal(t, 1, groups.Len())
for _, pg := range groups {
// last should have 5 in weight
require.Equal(t, 5, pg.Key)
for _, p := range pg.Pages {
require.True(t, p.fuzzyWordCount%2 != 0) // magic test
}
}
} else {
t.Fatal("Excepted PageGroup")
}
}
func TestPager(t *testing.T) {
t.Parallel()
s := newTestSite(t)
pages := createTestPages(s, 21)
groups, _ := pages.GroupBy("Weight", "desc")
urlFactory := func(page int) string {
return fmt.Sprintf("page/%d/", page)
}
_, err := newPaginatorFromPages(pages, -1, urlFactory)
require.NotNil(t, err)
_, err = newPaginatorFromPageGroups(groups, -1, urlFactory)
require.NotNil(t, err)
pag, err := newPaginatorFromPages(pages, 5, urlFactory)
require.Nil(t, err)
doTestPages(t, pag)
first := pag.Pagers()[0].First()
require.Equal(t, "Pager 1", first.String())
require.NotEmpty(t, first.Pages())
require.Empty(t, first.PageGroups())
pag, err = newPaginatorFromPageGroups(groups, 5, urlFactory)
require.Nil(t, err)
doTestPages(t, pag)
first = pag.Pagers()[0].First()
require.NotEmpty(t, first.PageGroups())
require.Empty(t, first.Pages())
}
func doTestPages(t *testing.T, paginator *paginator) {
paginatorPages := paginator.Pagers()
require.Equal(t, 5, len(paginatorPages))
require.Equal(t, 21, paginator.TotalNumberOfElements())
require.Equal(t, 5, paginator.PageSize())
require.Equal(t, 5, paginator.TotalPages())
first := paginatorPages[0]
require.Equal(t, template.HTML("page/1/"), first.URL())
require.Equal(t, first, first.First())
require.True(t, first.HasNext())
require.Equal(t, paginatorPages[1], first.Next())
require.False(t, first.HasPrev())
require.Nil(t, first.Prev())
require.Equal(t, 5, first.NumberOfElements())
require.Equal(t, 1, first.PageNumber())
third := paginatorPages[2]
require.True(t, third.HasNext())
require.True(t, third.HasPrev())
require.Equal(t, paginatorPages[1], third.Prev())
last := paginatorPages[4]
require.Equal(t, template.HTML("page/5/"), last.URL())
require.Equal(t, last, last.Last())
require.False(t, last.HasNext())
require.Nil(t, last.Next())
require.True(t, last.HasPrev())
require.Equal(t, 1, last.NumberOfElements())
require.Equal(t, 5, last.PageNumber())
}
func TestPagerNoPages(t *testing.T) {
t.Parallel()
s := newTestSite(t)
pages := createTestPages(s, 0)
groups, _ := pages.GroupBy("Weight", "desc")
urlFactory := func(page int) string {
return fmt.Sprintf("page/%d/", page)
}
paginator, _ := newPaginatorFromPages(pages, 5, urlFactory)
doTestPagerNoPages(t, paginator)
first := paginator.Pagers()[0].First()
require.Empty(t, first.PageGroups())
require.Empty(t, first.Pages())
paginator, _ = newPaginatorFromPageGroups(groups, 5, urlFactory)
doTestPagerNoPages(t, paginator)
first = paginator.Pagers()[0].First()
require.Empty(t, first.PageGroups())
require.Empty(t, first.Pages())
}
func doTestPagerNoPages(t *testing.T, paginator *paginator) {
paginatorPages := paginator.Pagers()
require.Equal(t, 1, len(paginatorPages))
require.Equal(t, 0, paginator.TotalNumberOfElements())
require.Equal(t, 5, paginator.PageSize())
require.Equal(t, 0, paginator.TotalPages())
// pageOne should be nothing but the first
pageOne := paginatorPages[0]
require.NotNil(t, pageOne.First())
require.False(t, pageOne.HasNext())
require.False(t, pageOne.HasPrev())
require.Nil(t, pageOne.Next())
require.Equal(t, 1, len(pageOne.Pagers()))
require.Equal(t, 0, pageOne.Pages().Len())
require.Equal(t, 0, pageOne.NumberOfElements())
require.Equal(t, 0, pageOne.TotalNumberOfElements())
require.Equal(t, 0, pageOne.TotalPages())
require.Equal(t, 1, pageOne.PageNumber())
require.Equal(t, 5, pageOne.PageSize())
}
func TestPaginationURLFactory(t *testing.T) {
t.Parallel()
cfg, fs := newTestCfg()
cfg.Set("paginatePath", "zoo")
for _, uglyURLs := range []bool{false, true} {
for _, canonifyURLs := range []bool{false, true} {
t.Run(fmt.Sprintf("uglyURLs=%t,canonifyURLs=%t", uglyURLs, canonifyURLs), func(t *testing.T) {
tests := []struct {
name string
d targetPathDescriptor
baseURL string
page int
expected string
}{
{"HTML home page 32",
targetPathDescriptor{Kind: KindHome, Type: output.HTMLFormat}, "http://example.com/", 32, "/zoo/32/"},
{"JSON home page 42",
targetPathDescriptor{Kind: KindHome, Type: output.JSONFormat}, "http://example.com/", 42, "/zoo/42/"},
// Issue #1252
{"BaseURL with sub path",
targetPathDescriptor{Kind: KindHome, Type: output.HTMLFormat}, "http://example.com/sub/", 999, "/sub/zoo/999/"},
}
for _, test := range tests {
d := test.d
cfg.Set("baseURL", test.baseURL)
cfg.Set("canonifyURLs", canonifyURLs)
cfg.Set("uglyURLs", uglyURLs)
d.UglyURLs = uglyURLs
expected := test.expected
if canonifyURLs {
expected = strings.Replace(expected, "/sub", "", 1)
}
if uglyURLs {
expected = expected[:len(expected)-1] + "." + test.d.Type.MediaType.Suffix()
}
pathSpec := newTestPathSpec(fs, cfg)
d.PathSpec = pathSpec
factory := newPaginationURLFactory(d)
got := factory(test.page)
require.Equal(t, expected, got)
}
})
}
}
}
func TestPaginator(t *testing.T) {
t.Parallel()
for _, useViper := range []bool{false, true} {
doTestPaginator(t, useViper)
}
}
func doTestPaginator(t *testing.T, useViper bool) {
cfg, fs := newTestCfg()
pagerSize := 5
if useViper {
cfg.Set("paginate", pagerSize)
} else {
cfg.Set("paginate", -1)
}
s, err := NewSiteForCfg(deps.DepsCfg{Cfg: cfg, Fs: fs})
require.NoError(t, err)
pages := createTestPages(s, 12)
n1, _ := newPageOutput(s.newHomePage(), false, false, output.HTMLFormat)
n2, _ := newPageOutput(s.newHomePage(), false, false, output.HTMLFormat)
n1.data["Pages"] = pages
var paginator1 *Pager
if useViper {
paginator1, err = n1.Paginator()
} else {
paginator1, err = n1.Paginator(pagerSize)
}
require.Nil(t, err)
require.NotNil(t, paginator1)
require.Equal(t, 3, paginator1.TotalPages())
require.Equal(t, 12, paginator1.TotalNumberOfElements())
n2.paginator = paginator1.Next()
paginator2, err := n2.Paginator()
require.Nil(t, err)
require.Equal(t, paginator2, paginator1.Next())
n1.data["Pages"] = createTestPages(s, 1)
samePaginator, _ := n1.Paginator()
require.Equal(t, paginator1, samePaginator)
pp, _ := s.NewPage("test")
p, _ := newPageOutput(pp, false, false, output.HTMLFormat)
_, err = p.Paginator()
require.NotNil(t, err)
}
func TestPaginatorWithNegativePaginate(t *testing.T) {
t.Parallel()
s := newTestSite(t, "paginate", -1)
n1, _ := newPageOutput(s.newHomePage(), false, false, output.HTMLFormat)
_, err := n1.Paginator()
require.Error(t, err)
}
func TestPaginate(t *testing.T) {
t.Parallel()
for _, useViper := range []bool{false, true} {
doTestPaginate(t, useViper)
}
}
func TestPaginatorURL(t *testing.T) {
t.Parallel()
cfg, fs := newTestCfg()
cfg.Set("paginate", 2)
cfg.Set("paginatePath", "testing")
for i := 0; i < 10; i++ {
// Issue #2177, do not double encode URLs
writeSource(t, fs, filepath.Join("content", "阅读", fmt.Sprintf("page%d.md", (i+1))),
fmt.Sprintf(`---
title: Page%d
---
Conten%d
`, (i+1), i+1))
}
writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), "<html><body>{{.Content}}</body></html>")
writeSource(t, fs, filepath.Join("layouts", "_default", "list.html"),
`
<html><body>
Count: {{ .Paginator.TotalNumberOfElements }}
Pages: {{ .Paginator.TotalPages }}
{{ range .Paginator.Pagers -}}
{{ .PageNumber }}: {{ .URL }}
{{ end }}
</body></html>`)
s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{})
th := testHelper{s.Cfg, s.Fs, t}
th.assertFileContent(filepath.Join("public", "阅读", "testing", "2", "index.html"), "2: /%E9%98%85%E8%AF%BB/testing/2/")
}
func doTestPaginate(t *testing.T, useViper bool) {
pagerSize := 5
var (
s *Site
err error
)
if useViper {
s = newTestSite(t, "paginate", pagerSize)
} else {
s = newTestSite(t, "paginate", -1)
}
pages := createTestPages(s, 6)
n1, _ := newPageOutput(s.newHomePage(), false, false, output.HTMLFormat)
n2, _ := newPageOutput(s.newHomePage(), false, false, output.HTMLFormat)
var paginator1, paginator2 *Pager
if useViper {
paginator1, err = n1.Paginate(pages)
} else {
paginator1, err = n1.Paginate(pages, pagerSize)
}
require.Nil(t, err)
require.NotNil(t, paginator1)
require.Equal(t, 2, paginator1.TotalPages())
require.Equal(t, 6, paginator1.TotalNumberOfElements())
n2.paginator = paginator1.Next()
if useViper {
paginator2, err = n2.Paginate(pages)
} else {
paginator2, err = n2.Paginate(pages, pagerSize)
}
require.Nil(t, err)
require.Equal(t, paginator2, paginator1.Next())
pp, err := s.NewPage("test")
p, _ := newPageOutput(pp, false, false, output.HTMLFormat)
_, err = p.Paginate(pages)
require.NotNil(t, err)
}
func TestInvalidOptions(t *testing.T) {
t.Parallel()
s := newTestSite(t)
n1, _ := newPageOutput(s.newHomePage(), false, false, output.HTMLFormat)
_, err := n1.Paginate(createTestPages(s, 1), 1, 2)
require.NotNil(t, err)
_, err = n1.Paginator(1, 2)
require.NotNil(t, err)
_, err = n1.Paginator(-1)
require.NotNil(t, err)
}
func TestPaginateWithNegativePaginate(t *testing.T) {
t.Parallel()
cfg, fs := newTestCfg()
cfg.Set("paginate", -1)
s, err := NewSiteForCfg(deps.DepsCfg{Cfg: cfg, Fs: fs})
require.NoError(t, err)
n, _ := newPageOutput(s.newHomePage(), false, false, output.HTMLFormat)
_, err = n.Paginate(createTestPages(s, 2))
require.NotNil(t, err)
}
func TestPaginatePages(t *testing.T) {
t.Parallel()
s := newTestSite(t)
groups, _ := createTestPages(s, 31).GroupBy("Weight", "desc")
pd := targetPathDescriptor{Kind: KindHome, Type: output.HTMLFormat, PathSpec: s.PathSpec, Addends: "t"}
for i, seq := range []interface{}{createTestPages(s, 11), groups, WeightedPages{}, PageGroup{}, &Pages{}} {
v, err := paginatePages(pd, seq, 11)
require.NotNil(t, v, "Val %d", i)
require.Nil(t, err, "Err %d", i)
}
_, err := paginatePages(pd, Site{}, 11)
require.NotNil(t, err)
}
// Issue #993
func TestPaginatorFollowedByPaginateShouldFail(t *testing.T) {
t.Parallel()
s := newTestSite(t, "paginate", 10)
n1, _ := newPageOutput(s.newHomePage(), false, false, output.HTMLFormat)
n2, _ := newPageOutput(s.newHomePage(), false, false, output.HTMLFormat)
_, err := n1.Paginator()
require.Nil(t, err)
_, err = n1.Paginate(createTestPages(s, 2))
require.NotNil(t, err)
_, err = n2.Paginate(createTestPages(s, 2))
require.Nil(t, err)
}
func TestPaginateFollowedByDifferentPaginateShouldFail(t *testing.T) {
t.Parallel()
s := newTestSite(t, "paginate", 10)
n1, _ := newPageOutput(s.newHomePage(), false, false, output.HTMLFormat)
n2, _ := newPageOutput(s.newHomePage(), false, false, output.HTMLFormat)
p1 := createTestPages(s, 2)
p2 := createTestPages(s, 10)
_, err := n1.Paginate(p1)
require.Nil(t, err)
_, err = n1.Paginate(p1)
require.Nil(t, err)
_, err = n1.Paginate(p2)
require.NotNil(t, err)
_, err = n2.Paginate(p2)
require.Nil(t, err)
}
func TestProbablyEqualPageLists(t *testing.T) {
t.Parallel()
s := newTestSite(t)
fivePages := createTestPages(s, 5)
zeroPages := createTestPages(s, 0)
zeroPagesByWeight, _ := createTestPages(s, 0).GroupBy("Weight", "asc")
fivePagesByWeight, _ := createTestPages(s, 5).GroupBy("Weight", "asc")
ninePagesByWeight, _ := createTestPages(s, 9).GroupBy("Weight", "asc")
for i, this := range []struct {
v1 interface{}
v2 interface{}
expect bool
}{
{nil, nil, true},
{"a", "b", true},
{"a", fivePages, false},
{fivePages, "a", false},
{fivePages, createTestPages(s, 2), false},
{fivePages, fivePages, true},
{zeroPages, zeroPages, true},
{fivePagesByWeight, fivePagesByWeight, true},
{zeroPagesByWeight, fivePagesByWeight, false},
{zeroPagesByWeight, zeroPagesByWeight, true},
{fivePagesByWeight, fivePages, false},
{fivePagesByWeight, ninePagesByWeight, false},
} {
result := probablyEqualPageLists(this.v1, this.v2)
if result != this.expect {
t.Errorf("[%d] got %t but expected %t", i, result, this.expect)
}
}
}
func TestPage(t *testing.T) {
t.Parallel()
urlFactory := func(page int) string {
return fmt.Sprintf("page/%d/", page)
}
s := newTestSite(t)
fivePages := createTestPages(s, 7)
fivePagesFuzzyWordCount, _ := createTestPages(s, 7).GroupBy("FuzzyWordCount", "asc")
p1, _ := newPaginatorFromPages(fivePages, 2, urlFactory)
p2, _ := newPaginatorFromPageGroups(fivePagesFuzzyWordCount, 2, urlFactory)
f1 := p1.pagers[0].First()
f2 := p2.pagers[0].First()
page11, _ := f1.page(1)
page1Nil, _ := f1.page(3)
page21, _ := f2.page(1)
page2Nil, _ := f2.page(3)
require.Equal(t, 3, page11.fuzzyWordCount)
require.Nil(t, page1Nil)
require.Equal(t, 3, page21.fuzzyWordCount)
require.Nil(t, page2Nil)
}
func createTestPages(s *Site, num int) Pages {
pages := make(Pages, num)
for i := 0; i < num; i++ {
p := s.newPage(filepath.FromSlash(fmt.Sprintf("/x/y/z/p%d.md", i)))
w := 5
if i%2 == 0 {
w = 10
}
p.fuzzyWordCount = i + 2
p.Weight = w
pages[i] = p
}
return pages
}

View file

@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -75,7 +75,7 @@ func (c *themesCollector) add(name, configFilename string) (ThemeConfig, error)
var err error
cfg, err = config.FromFile(c.fs, configFilename)
if err != nil {
return tc, nil
return tc, err
}
}

View file

@ -1,4 +1,4 @@
// Copyright 2017-present The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -14,8 +14,7 @@
package hugolib
var (
_ Permalinker = (*Page)(nil)
_ Permalinker = (*OutputFormat)(nil)
_ Permalinker = (*pageState)(nil)
)
// Permalinker provides permalinks of both the relative and absolute kind.

View file

@ -1,213 +0,0 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"errors"
"fmt"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/gohugoio/hugo/helpers"
)
// pathPattern represents a string which builds up a URL from attributes
type pathPattern string
// pageToPermaAttribute is the type of a function which, given a page and a tag
// can return a string to go in that position in the page (or an error)
type pageToPermaAttribute func(*Page, string) (string, error)
// PermalinkOverrides maps a section name to a PathPattern
type PermalinkOverrides map[string]pathPattern
// knownPermalinkAttributes maps :tags in a permalink specification to a
// function which, given a page and the tag, returns the resulting string
// to be used to replace that tag.
var knownPermalinkAttributes map[string]pageToPermaAttribute
var attributeRegexp = regexp.MustCompile(`:\w+`)
// validate determines if a PathPattern is well-formed
func (pp pathPattern) validate() bool {
fragments := strings.Split(string(pp[1:]), "/")
var bail = false
for i := range fragments {
if bail {
return false
}
if len(fragments[i]) == 0 {
bail = true
continue
}
matches := attributeRegexp.FindAllStringSubmatch(fragments[i], -1)
if matches == nil {
continue
}
for _, match := range matches {
k := strings.ToLower(match[0][1:])
if _, ok := knownPermalinkAttributes[k]; !ok {
return false
}
}
}
return true
}
type permalinkExpandError struct {
pattern pathPattern
section string
err error
}
func (pee *permalinkExpandError) Error() string {
return fmt.Sprintf("error expanding %q section %q: %s", string(pee.pattern), pee.section, pee.err)
}
var (
errPermalinkIllFormed = errors.New("permalink ill-formed")
errPermalinkAttributeUnknown = errors.New("permalink attribute not recognised")
)
// Expand on a PathPattern takes a Page and returns the fully expanded Permalink
// or an error explaining the failure.
func (pp pathPattern) Expand(p *Page) (string, error) {
if !pp.validate() {
return "", &permalinkExpandError{pattern: pp, section: "<all>", err: errPermalinkIllFormed}
}
sections := strings.Split(string(pp), "/")
for i, field := range sections {
if len(field) == 0 {
continue
}
matches := attributeRegexp.FindAllStringSubmatch(field, -1)
if matches == nil {
continue
}
newField := field
for _, match := range matches {
attr := match[0][1:]
callback, ok := knownPermalinkAttributes[attr]
if !ok {
return "", &permalinkExpandError{pattern: pp, section: strconv.Itoa(i), err: errPermalinkAttributeUnknown}
}
newAttr, err := callback(p, attr)
if err != nil {
return "", &permalinkExpandError{pattern: pp, section: strconv.Itoa(i), err: err}
}
newField = strings.Replace(newField, match[0], newAttr, 1)
}
sections[i] = newField
}
return strings.Join(sections, "/"), nil
}
func pageToPermalinkDate(p *Page, dateField string) (string, error) {
// a Page contains a Node which provides a field Date, time.Time
switch dateField {
case "year":
return strconv.Itoa(p.Date.Year()), nil
case "month":
return fmt.Sprintf("%02d", int(p.Date.Month())), nil
case "monthname":
return p.Date.Month().String(), nil
case "day":
return fmt.Sprintf("%02d", p.Date.Day()), nil
case "weekday":
return strconv.Itoa(int(p.Date.Weekday())), nil
case "weekdayname":
return p.Date.Weekday().String(), nil
case "yearday":
return strconv.Itoa(p.Date.YearDay()), nil
}
//TODO: support classic strftime escapes too
// (and pass those through despite not being in the map)
panic("coding error: should not be here")
}
// pageToPermalinkTitle returns the URL-safe form of the title
func pageToPermalinkTitle(p *Page, _ string) (string, error) {
// Page contains Node which has Title
// (also contains URLPath which has Slug, sometimes)
return p.s.PathSpec.URLize(p.title), nil
}
// pageToPermalinkFilename returns the URL-safe form of the filename
func pageToPermalinkFilename(p *Page, _ string) (string, error) {
name := p.File.TranslationBaseName()
if name == "index" {
// Page bundles; the directory name will hopefully have a better name.
dir := strings.TrimSuffix(p.File.Dir(), helpers.FilePathSeparator)
_, name = filepath.Split(dir)
}
return p.s.PathSpec.URLize(name), nil
}
// if the page has a slug, return the slug, else return the title
func pageToPermalinkSlugElseTitle(p *Page, a string) (string, error) {
if p.Slug != "" {
// Don't start or end with a -
// TODO(bep) this doesn't look good... Set the Slug once.
if strings.HasPrefix(p.Slug, "-") {
p.Slug = p.Slug[1:len(p.Slug)]
}
if strings.HasSuffix(p.Slug, "-") {
p.Slug = p.Slug[0 : len(p.Slug)-1]
}
return p.s.PathSpec.URLize(p.Slug), nil
}
return pageToPermalinkTitle(p, a)
}
func pageToPermalinkSection(p *Page, _ string) (string, error) {
return p.Section(), nil
}
func pageToPermalinkSections(p *Page, _ string) (string, error) {
return path.Join(p.CurrentSection().sections...), nil
}
func init() {
knownPermalinkAttributes = map[string]pageToPermaAttribute{
"year": pageToPermalinkDate,
"month": pageToPermalinkDate,
"monthname": pageToPermalinkDate,
"day": pageToPermalinkDate,
"weekday": pageToPermalinkDate,
"weekdayname": pageToPermalinkDate,
"yearday": pageToPermalinkDate,
"section": pageToPermalinkSection,
"sections": pageToPermalinkSections,
"title": pageToPermalinkTitle,
"slug": pageToPermalinkSlugElseTitle,
"filename": pageToPermalinkFilename,
}
}

Some files were not shown because too many files have changed in this diff Show more