From baa29f6534fcd324dbade7dd6c32c90547e3fa4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Sun, 19 Mar 2017 21:09:31 +0100 Subject: output: Rework the base template logic Extract the logic to a testable function and add support for custom output types. Fixes #2995 --- output/layout_base.go | 175 ++++++++++++++++++++++++++++++++++++++++++++ output/layout_base_test.go | 159 ++++++++++++++++++++++++++++++++++++++++ output/outputFormat.go | 6 +- output/outputFormat_test.go | 6 +- 4 files changed, 340 insertions(+), 6 deletions(-) create mode 100644 output/layout_base.go create mode 100644 output/layout_base_test.go (limited to 'output') diff --git a/output/layout_base.go b/output/layout_base.go new file mode 100644 index 000000000..929ee07a2 --- /dev/null +++ b/output/layout_base.go @@ -0,0 +1,175 @@ +// 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 output + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/spf13/hugo/helpers" +) + +const baseFileBase = "baseof" + +var ( + aceTemplateInnerMarkers = [][]byte{[]byte("= content")} + goTemplateInnerMarkers = [][]byte{[]byte("{{define"), []byte("{{ define")} +) + +type TemplateNames struct { + Name string + OverlayFilename string + MasterFilename string +} + +// TODO(bep) output this is refactoring in progress. +type TemplateLookupDescriptor struct { + // The full path to the site or theme root. + WorkingDir string + + // Main project layout dir, defaults to "layouts" + LayoutDir string + + // The path to the template relative the the base. + // I.e. shortcodes/youtube.html + RelPath string + + // The template name prefix to look for, i.e. "theme". + Prefix string + + // The theme name if active. + Theme string + + FileExists func(filename string) (bool, error) + ContainsAny func(filename string, subslices [][]byte) (bool, error) +} + +func CreateTemplateID(d TemplateLookupDescriptor) (TemplateNames, error) { + + var id TemplateNames + + name := filepath.FromSlash(d.RelPath) + + if d.Prefix != "" { + name = strings.Trim(d.Prefix, "/") + "/" + name + } + + baseLayoutDir := filepath.Join(d.WorkingDir, d.LayoutDir) + fullPath := filepath.Join(baseLayoutDir, d.RelPath) + + // The filename will have a suffix with an optional type indicator. + // Examples: + // index.html + // index.amp.html + // index.json + filename := filepath.Base(d.RelPath) + + var ext, outFormat string + + parts := strings.Split(filename, ".") + if len(parts) > 2 { + outFormat = parts[1] + ext = parts[2] + } else if len(parts) > 1 { + ext = parts[1] + } + + filenameNoSuffix := parts[0] + + id.OverlayFilename = fullPath + id.Name = name + + // Ace and Go templates may have both a base and inner template. + pathDir := filepath.Dir(fullPath) + + if ext == "amber" || strings.HasSuffix(pathDir, "partials") || strings.HasSuffix(pathDir, "shortcodes") { + // No base template support + return id, nil + } + + innerMarkers := goTemplateInnerMarkers + + var baseFilename string + + if outFormat != "" { + baseFilename = fmt.Sprintf("%s.%s.%s", baseFileBase, outFormat, ext) + } else { + baseFilename = fmt.Sprintf("%s.%s", baseFileBase, ext) + } + + if ext == "ace" { + innerMarkers = aceTemplateInnerMarkers + } + + // This may be a view that shouldn't have base template + // Have to look inside it to make sure + needsBase, err := d.ContainsAny(fullPath, innerMarkers) + if err != nil { + return id, err + } + + if needsBase { + currBaseFilename := fmt.Sprintf("%s-%s", filenameNoSuffix, baseFilename) + + templateDir := filepath.Dir(fullPath) + themeDir := filepath.Join(d.WorkingDir, d.Theme) + + baseTemplatedDir := strings.TrimPrefix(templateDir, baseLayoutDir) + baseTemplatedDir = strings.TrimPrefix(baseTemplatedDir, helpers.FilePathSeparator) + + // Look for base template in the follwing order: + // 1. /-baseof.(optional)., e.g. list-baseof.(optional).. + // 2. /baseof.(optional). + // 3. _default/-baseof.(optional)., e.g. list-baseof.(optional).. + // 4. _default/baseof.(optional). + // For each of the steps above, it will first look in the project, then, if theme is set, + // in the theme's layouts folder. + // Also note that the may be both the project's layout folder and the theme's. + pairsToCheck := [][]string{ + []string{baseTemplatedDir, currBaseFilename}, + []string{baseTemplatedDir, baseFilename}, + []string{"_default", currBaseFilename}, + []string{"_default", baseFilename}, + } + + Loop: + for _, pair := range pairsToCheck { + pathsToCheck := basePathsToCheck(pair, baseLayoutDir, themeDir) + + for _, pathToCheck := range pathsToCheck { + if ok, err := d.FileExists(pathToCheck); err == nil && ok { + id.MasterFilename = pathToCheck + break Loop + } + } + } + } + + return id, nil + +} + +func basePathsToCheck(path []string, layoutDir, themeDir string) []string { + // Always look in the project. + pathsToCheck := []string{filepath.Join((append([]string{layoutDir}, path...))...)} + + // May have a theme + if themeDir != "" { + pathsToCheck = append(pathsToCheck, filepath.Join((append([]string{themeDir, "layouts"}, path...))...)) + } + + return pathsToCheck + +} diff --git a/output/layout_base_test.go b/output/layout_base_test.go new file mode 100644 index 000000000..60d9b8c62 --- /dev/null +++ b/output/layout_base_test.go @@ -0,0 +1,159 @@ +// 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 output + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLayoutBase(t *testing.T) { + + var ( + workingDir = "/sites/mysite/" + layoutBase1 = "layouts" + layoutPath1 = "_default/single.html" + layoutPathAmp = "_default/single.amp.html" + layoutPathJSON = "_default/single.json" + ) + + for _, this := range []struct { + name string + d TemplateLookupDescriptor + needsBase bool + basePathMatchStrings string + expect TemplateNames + }{ + {"No base", TemplateLookupDescriptor{WorkingDir: workingDir, LayoutDir: layoutBase1, RelPath: layoutPath1}, false, "", + TemplateNames{ + Name: "_default/single.html", + OverlayFilename: "/sites/mysite/layouts/_default/single.html", + }}, + {"Base", TemplateLookupDescriptor{WorkingDir: workingDir, LayoutDir: layoutBase1, RelPath: layoutPath1}, true, "", + TemplateNames{ + Name: "_default/single.html", + OverlayFilename: "/sites/mysite/layouts/_default/single.html", + MasterFilename: "/sites/mysite/layouts/_default/single-baseof.html", + }}, + {"Base in theme", TemplateLookupDescriptor{WorkingDir: workingDir, LayoutDir: layoutBase1, RelPath: layoutPath1, Theme: "mytheme"}, true, + "mytheme/layouts/_default/baseof.html", + TemplateNames{ + Name: "_default/single.html", + OverlayFilename: "/sites/mysite/layouts/_default/single.html", + MasterFilename: "/sites/mysite/mytheme/layouts/_default/baseof.html", + }}, + {"Template in theme, base in theme", TemplateLookupDescriptor{WorkingDir: filepath.Join(workingDir, "mytheme"), LayoutDir: layoutBase1, RelPath: layoutPath1, Theme: "mytheme"}, true, + "mytheme/layouts/_default/baseof.html", + TemplateNames{ + Name: "_default/single.html", + OverlayFilename: "/sites/mysite/mytheme/layouts/_default/single.html", + MasterFilename: "/sites/mysite/mytheme/layouts/_default/baseof.html", + }}, + {"Template in theme, base in site", TemplateLookupDescriptor{WorkingDir: filepath.Join(workingDir, "mytheme"), LayoutDir: layoutBase1, RelPath: layoutPath1, Theme: "mytheme"}, true, + "mytheme/layouts/_default/baseof.html", + TemplateNames{ + Name: "_default/single.html", + OverlayFilename: "/sites/mysite/mytheme/layouts/_default/single.html", + MasterFilename: "/sites/mysite/mytheme/layouts/_default/baseof.html", + }}, + {"Template in site, base in theme", TemplateLookupDescriptor{WorkingDir: workingDir, LayoutDir: layoutBase1, RelPath: layoutPath1, Theme: "mytheme"}, true, + "/sites/mysite/mytheme/layouts/_default/baseof.html", + TemplateNames{ + Name: "_default/single.html", + OverlayFilename: "/sites/mysite/layouts/_default/single.html", + MasterFilename: "/sites/mysite/mytheme/layouts/_default/baseof.html", + }}, + {"With prefix, base in theme", TemplateLookupDescriptor{WorkingDir: workingDir, LayoutDir: layoutBase1, RelPath: layoutPath1, + Theme: "mytheme", Prefix: "someprefix"}, true, + "mytheme/layouts/_default/baseof.html", + TemplateNames{ + Name: "someprefix/_default/single.html", + OverlayFilename: "/sites/mysite/layouts/_default/single.html", + MasterFilename: "/sites/mysite/mytheme/layouts/_default/baseof.html", + }}, + {"Partial", TemplateLookupDescriptor{WorkingDir: workingDir, LayoutDir: layoutBase1, RelPath: "partials/menu.html"}, true, + "mytheme/layouts/_default/baseof.html", + TemplateNames{ + Name: "partials/menu.html", + OverlayFilename: "/sites/mysite/layouts/partials/menu.html", + }}, + {"AMP, no base", TemplateLookupDescriptor{WorkingDir: workingDir, LayoutDir: layoutBase1, RelPath: layoutPathAmp}, false, "", + TemplateNames{ + Name: "_default/single.amp.html", + OverlayFilename: "/sites/mysite/layouts/_default/single.amp.html", + }}, + {"JSON, no base", TemplateLookupDescriptor{WorkingDir: workingDir, LayoutDir: layoutBase1, RelPath: layoutPathJSON}, false, "", + TemplateNames{ + Name: "_default/single.json", + OverlayFilename: "/sites/mysite/layouts/_default/single.json", + }}, + {"AMP with base", TemplateLookupDescriptor{WorkingDir: workingDir, LayoutDir: layoutBase1, RelPath: layoutPathAmp}, true, "single-baseof.html|single-baseof.amp.html", + TemplateNames{ + Name: "_default/single.amp.html", + OverlayFilename: "/sites/mysite/layouts/_default/single.amp.html", + MasterFilename: "/sites/mysite/layouts/_default/single-baseof.amp.html", + }}, + {"AMP with no match in base", TemplateLookupDescriptor{WorkingDir: workingDir, LayoutDir: layoutBase1, RelPath: layoutPathAmp}, true, "single-baseof.html", + TemplateNames{ + Name: "_default/single.amp.html", + OverlayFilename: "/sites/mysite/layouts/_default/single.amp.html", + // There is a single-baseof.html, but that makes no sense. + MasterFilename: "", + }}, + + {"JSON with base", TemplateLookupDescriptor{WorkingDir: workingDir, LayoutDir: layoutBase1, RelPath: layoutPathJSON}, true, "single-baseof.json", + TemplateNames{ + Name: "_default/single.json", + OverlayFilename: "/sites/mysite/layouts/_default/single.json", + MasterFilename: "/sites/mysite/layouts/_default/single-baseof.json", + }}, + } { + t.Run(this.name, func(t *testing.T) { + + fileExists := func(filename string) (bool, error) { + stringsToMatch := strings.Split(this.basePathMatchStrings, "|") + for _, s := range stringsToMatch { + if strings.Contains(filename, s) { + return true, nil + } + + } + return false, nil + } + + needsBase := func(filename string, subslices [][]byte) (bool, error) { + return this.needsBase, nil + } + + this.d.WorkingDir = filepath.FromSlash(this.d.WorkingDir) + this.d.LayoutDir = filepath.FromSlash(this.d.LayoutDir) + this.d.RelPath = filepath.FromSlash(this.d.RelPath) + this.d.ContainsAny = needsBase + this.d.FileExists = fileExists + + this.expect.MasterFilename = filepath.FromSlash(this.expect.MasterFilename) + this.expect.OverlayFilename = filepath.FromSlash(this.expect.OverlayFilename) + + id, err := CreateTemplateID(this.d) + + require.NoError(t, err) + require.Equal(t, this.expect, id, this.name) + + }) + } + +} diff --git a/output/outputFormat.go b/output/outputFormat.go index 8c99aa139..cc04bcbe4 100644 --- a/output/outputFormat.go +++ b/output/outputFormat.go @@ -92,7 +92,7 @@ type Format struct { NoUgly bool } -func GetType(key string) (Format, bool) { +func GetFormat(key string) (Format, bool) { found, ok := builtInTypes[key] if !ok { found, ok = builtInTypes[strings.ToLower(key)] @@ -101,11 +101,11 @@ func GetType(key string) (Format, bool) { } // TODO(bep) outputs rewamp on global config? -func GetTypes(keys ...string) (Formats, error) { +func GetFormats(keys ...string) (Formats, error) { var types []Format for _, key := range keys { - tpe, ok := GetType(key) + tpe, ok := GetFormat(key) if !ok { return types, fmt.Errorf("OutputFormat with key %q not found", key) } diff --git a/output/outputFormat_test.go b/output/outputFormat_test.go index 3eb56d8d3..21375bf56 100644 --- a/output/outputFormat_test.go +++ b/output/outputFormat_test.go @@ -34,10 +34,10 @@ func TestDefaultTypes(t *testing.T) { } func TestGetType(t *testing.T) { - tp, _ := GetType("html") + tp, _ := GetFormat("html") require.Equal(t, HTMLType, tp) - tp, _ = GetType("HTML") + tp, _ = GetFormat("HTML") require.Equal(t, HTMLType, tp) - _, found := GetType("FOO") + _, found := GetFormat("FOO") require.False(t, found) } -- cgit v1.2.3