ref: e52e2a70e5e0a2d15fc9befbcd7290761c98589e
parent: ea165bf9e71c7ca9ddb9f14ddbdbcd506ce554bb
author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
date: Fri Mar 3 05:47:43 EST 2017
hugolib, target: Rework/move the target package This relates to #3123. The interfaces and types in `target` made sense at some point, but now this package is too restricted to a hardcoded set of media types. The overall current logic: * Create a file path based on some `Translator` with some hardcoded logic handling uglyURLs, hardcoded html suffix etc. * In in some cases (alias), a template is applied to create the alias file. * Then the content is written to destination. One could argue that it is the last bullet that is the actual core responsibility. This commit fixes that by moving the `hugolib`-related logic where it belong, and simplify the code, i.e. remove the abstractions. This code will most certainly evolve once we start on #3123, but now it is at least possible to understand where to start. Fixes #3123
--- /dev/null
+++ b/hugolib/alias.go
@@ -1,0 +1,69 @@
+// 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 (
+ "bytes"
+ "html/template"
+ "io"
+)
+
+const (
+ alias = "<!DOCTYPE html><html><head><title>{{ .Permalink }}</title><link rel=\"canonical\" href=\"{{ .Permalink }}\"/><meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" /><meta http-equiv=\"refresh\" content=\"0; url={{ .Permalink }}\" /></head></html>"+ aliasXHtml = "<!DOCTYPE html><html xmlns=\"http://www.w3.org/1999/xhtml\"><head><title>{{ .Permalink }}</title><link rel=\"canonical\" href=\"{{ .Permalink }}\"/><meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" /><meta http-equiv=\"refresh\" content=\"0; url={{ .Permalink }}\" /></head></html>"+)
+
+var defaultAliasTemplates *template.Template
+
+func init() {+ defaultAliasTemplates = template.New("")+ template.Must(defaultAliasTemplates.New("alias").Parse(alias))+ template.Must(defaultAliasTemplates.New("alias-xhtml").Parse(aliasXHtml))+}
+
+type aliasHandler struct {+ Templates *template.Template
+}
+
+func newAliasHandler(t *template.Template) aliasHandler {+ return aliasHandler{t}+}
+
+func (a aliasHandler) renderAlias(isXHTML bool, permalink string, page *Page) (io.Reader, error) {+ t := "alias"
+ if isXHTML {+ t = "alias-xhtml"
+ }
+
+ template := defaultAliasTemplates
+ if a.Templates != nil {+ template = a.Templates
+ t = "alias.html"
+ }
+
+ data := struct {+ Permalink string
+ Page *Page
+ }{+ permalink,
+ page,
+ }
+
+ buffer := new(bytes.Buffer)
+ err := template.ExecuteTemplate(buffer, t, data)
+ if err != nil {+ return nil, err
+ }
+ return buffer, nil
+}
--- a/hugolib/handler_file.go
+++ b/hugolib/handler_file.go
@@ -39,7 +39,7 @@
func (h defaultHandler) Extensions() []string { return []string{"*"} } func (h defaultHandler) FileConvert(f *source.File, s *Site) HandledResult {- s.writeDestFile(f.Path(), f.Contents)
+ s.w.writeDestFile(f.Path(), f.Contents)
return HandledResult{file: f}}
@@ -48,6 +48,6 @@
func (h cssHandler) Extensions() []string { return []string{"css"} } func (h cssHandler) FileConvert(f *source.File, s *Site) HandledResult {x := cssmin.Minify(f.Bytes())
- s.writeDestFile(f.Path(), bytes.NewReader(x))
+ s.w.writeDestFile(f.Path(), bytes.NewReader(x))
return HandledResult{file: f}}
--- a/hugolib/hugo_sites_build.go
+++ b/hugolib/hugo_sites_build.go
@@ -194,6 +194,8 @@
func (h *HugoSites) render(config *BuildCfg) error { if !config.SkipRender { for _, s := range h.Sites {+ s.initSiteWriter()
+
if err := s.render(); err != nil {return err
}
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -39,7 +39,6 @@
"github.com/spf13/hugo/helpers"
"github.com/spf13/hugo/parser"
"github.com/spf13/hugo/source"
- "github.com/spf13/hugo/target"
"github.com/spf13/hugo/tpl"
"github.com/spf13/hugo/transform"
"github.com/spf13/nitro"
@@ -92,19 +91,21 @@
// is set.
taxonomiesOrigKey map[string]string
- Source source.Input
- Sections Taxonomy
- Info SiteInfo
- Menus Menus
- timer *nitro.B
- targets targetList
- targetListInit sync.Once
- draftCount int
- futureCount int
- expiredCount int
- Data map[string]interface{}- Language *helpers.Language
+ Source source.Input
+ Sections Taxonomy
+ Info SiteInfo
+ Menus Menus
+ timer *nitro.B
+ // This is not a pointer by design.
+ w siteWriter
+
+ draftCount int
+ futureCount int
+ expiredCount int
+ Data map[string]interface{}+ Language *helpers.Language
+
disabledKinds map[string]bool
// Logger etc.
@@ -139,6 +140,7 @@
s := &Site{PageCollections: c, Language: cfg.Language, disabledKinds: disabledKinds} s.Info = newSiteInfo(siteBuilderCfg{s: s, pageCollections: c, language: s.Language})+
return s, nil
}
@@ -210,14 +212,6 @@
return s, nil
}
-type targetList struct {- page target.Output
- pageUgly target.Output
- file target.Output
- alias target.AliasPublisher
- languageAlias target.AliasPublisher
-}
-
type SiteInfo struct {// atomic requires 64-bit alignment for struct field access
// According to the docs, " The first word in a global variable or in an
@@ -1180,6 +1174,8 @@
numWorkers := getGoMaxProcs() * 4
wg := &sync.WaitGroup{}+ s.initSiteWriter()
+
for i := 0; i < numWorkers; i++ {wg.Add(2)
go fileConverter(s, fileConvChan, results, wg)
@@ -1757,7 +1753,7 @@
transformer := transform.NewChain(transform.AbsURLInXML)
transformer.Apply(outBuffer, renderBuffer, path)
- return s.writeDestFile(dest, outBuffer)
+ return s.w.writeDestFile(dest, outBuffer)
}
@@ -1774,14 +1770,13 @@
outBuffer := bp.GetBuffer()
defer bp.PutBuffer(outBuffer)
- var pageTarget target.Output
+ // Note: this is not a pointer, as we may mutate the state below.
+ w := s.w
if p, ok := d.(*Page); ok && p.IsPage() && path.Ext(p.URLPath.URL) != "" {// user has explicitly set a URL with extension for this page
// make sure it sticks even if "ugly URLs" are turned off.
- pageTarget = s.pageUglyTarget()
- } else {- pageTarget = s.pageTarget()
+ w.uglyURLs = true
}
transformLinks := transform.NewEmptyTransforms()
@@ -1804,7 +1799,7 @@
var path []byte
if s.Info.relativeURLs {- translated, err := pageTarget.(target.OptionalTranslator).TranslateRelative(dest)
+ translated, err := w.baseTargetPathPage(dest)
if err != nil {return err
}
@@ -1844,7 +1839,7 @@
}
- if err = s.writeDestPage(dest, pageTarget, outBuffer); err != nil {+ if err = w.writeDestPage(dest, outBuffer); err != nil {return err
}
@@ -1893,95 +1888,37 @@
}
-func (s *Site) pageTarget() target.Output {- s.initTargetList()
- return s.targets.page
+func (s *Site) langDir() string {+ if s.Language.Lang != s.Info.multilingual.DefaultLang.Lang || s.Info.defaultContentLanguageInSubdir {+ return s.Language.Lang
+ }
+ return ""
}
-func (s *Site) pageUglyTarget() target.Output {- s.initTargetList()
- return s.targets.pageUgly
-}
-
-func (s *Site) fileTarget() target.Output {- s.initTargetList()
- return s.targets.file
-}
-
-func (s *Site) aliasTarget() target.AliasPublisher {- s.initTargetList()
- return s.targets.alias
-}
-
-func (s *Site) languageAliasTarget() target.AliasPublisher {- s.initTargetList()
- return s.targets.languageAlias
-}
-
-func (s *Site) initTargetList() {+func (s *Site) initSiteWriter() { if s.Fs == nil { panic("Must have Fs")}
- s.targetListInit.Do(func() {- langDir := ""
- if s.Language.Lang != s.Info.multilingual.DefaultLang.Lang || s.Info.defaultContentLanguageInSubdir {- langDir = s.Language.Lang
- }
- if s.targets.page == nil {- s.targets.page = &target.PagePub{- Fs: s.Fs,
- PublishDir: s.absPublishDir(),
- UglyURLs: s.Cfg.GetBool("uglyURLs"),- LangDir: langDir,
- }
- }
- if s.targets.pageUgly == nil {- s.targets.pageUgly = &target.PagePub{- Fs: s.Fs,
- PublishDir: s.absPublishDir(),
- UglyURLs: true,
- LangDir: langDir,
- }
- }
- if s.targets.file == nil {- s.targets.file = &target.Filesystem{- Fs: s.Fs,
- PublishDir: s.absPublishDir(),
- }
- }
- if s.targets.alias == nil {- s.targets.alias = &target.HTMLRedirectAlias{- Fs: s.Fs,
- PublishDir: s.absPublishDir(),
- Templates: s.Tmpl.Lookup("alias.html"),- }
- }
- if s.targets.languageAlias == nil {- s.targets.languageAlias = &target.HTMLRedirectAlias{- Fs: s.Fs,
- PublishDir: s.absPublishDir(),
- AllowRoot: true,
- }
- }
- })
+ s.w = siteWriter{+ langDir: s.langDir(),
+ publishDir: s.absPublishDir(),
+ uglyURLs: s.Cfg.GetBool("uglyURLs"),+ relativeURLs: s.Info.relativeURLs,
+ fs: s.Fs,
+ log: s.Log,
+ }
}
-func (s *Site) writeDestFile(path string, reader io.Reader) (err error) {- s.Log.DEBUG.Println("creating file:", path)- return s.fileTarget().Publish(path, reader)
+func (s *Site) writeDestAlias(path, permalink string, p *Page) (err error) {+ return s.publishDestAlias(false, path, permalink, p)
}
-func (s *Site) writeDestPage(path string, publisher target.Publisher, reader io.Reader) (err error) {- s.Log.DEBUG.Println("creating page:", path)- return publisher.Publish(path, reader)
-}
+func (s *Site) publishDestAlias(allowRoot bool, path, permalink string, p *Page) (err error) {+ w := s.w
+ w.allowRoot = allowRoot
-// AliasPublisher
-func (s *Site) writeDestAlias(path, permalink string, p *Page) (err error) {- return s.publishDestAlias(s.aliasTarget(), path, permalink, p)
-}
+ isXHTML := strings.HasSuffix(path, ".xhtml")
-func (s *Site) publishDestAlias(aliasPublisher target.AliasPublisher, path, permalink string, p *Page) (err error) { if s.Info.relativeURLs {// convert `permalink` into URI relative to location of `path`
baseURL := helpers.SanitizeURLKeepTrailingSlash(s.Cfg.GetString("baseURL"))@@ -1995,7 +1932,20 @@
permalink = filepath.ToSlash(permalink)
}
s.Log.DEBUG.Println("creating alias:", path, "redirecting to", permalink)- return aliasPublisher.Publish(path, permalink, p)
+
+ targetPath, err := w.targetPathAlias(path)
+ if err != nil {+ return err
+ }
+
+ handler := newAliasHandler(s.Tmpl.Lookup("alias.html"))+ aliasContent, err := handler.renderAlias(isXHTML, permalink, p)
+ if err != nil {+ return err
+ }
+
+ return w.publish(targetPath, aliasContent)
+
}
func (s *Site) draftStats() string {--- a/hugolib/site_render.go
+++ b/hugolib/site_render.go
@@ -257,7 +257,7 @@
err := s.renderForLayouts("robots", n, outBuffer, s.appendThemeTemplates(rLayouts)...) if err == nil {- err = s.writeDestFile("robots.txt", outBuffer)+ err = s.w.writeDestFile("robots.txt", outBuffer)}
return err
@@ -284,13 +284,13 @@
if s.Info.defaultContentLanguageInSubdir {mainLangURL := s.PathSpec.AbsURL(mainLang.Lang, false)
s.Log.DEBUG.Printf("Write redirect to main language %s: %s", mainLang, mainLangURL)- if err := s.publishDestAlias(s.languageAliasTarget(), "/", mainLangURL, nil); err != nil {+ if err := s.publishDestAlias(true, "/", mainLangURL, nil); err != nil {return err
}
} else { mainLangURL := s.PathSpec.AbsURL("", false) s.Log.DEBUG.Printf("Write redirect to main language %s: %s", mainLang, mainLangURL)- if err := s.publishDestAlias(s.languageAliasTarget(), mainLang.Lang, mainLangURL, nil); err != nil {+ if err := s.publishDestAlias(true, mainLang.Lang, mainLangURL, nil); err != nil {return err
}
}
--- /dev/null
+++ b/hugolib/site_writer.go
@@ -1,0 +1,193 @@
+// 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"
+ "io"
+ "path/filepath"
+ "runtime"
+ "strings"
+
+ "github.com/spf13/hugo/helpers"
+ "github.com/spf13/hugo/hugofs"
+ jww "github.com/spf13/jwalterweatherman"
+)
+
+// We may find some abstractions/interface(s) here once we star with
+// "Multiple Output Types".
+type siteWriter struct {+ langDir string
+ publishDir string
+ relativeURLs bool
+ uglyURLs bool
+ allowRoot bool // For aliases
+
+ fs *hugofs.Fs
+
+ log *jww.Notepad
+}
+
+func (w siteWriter) targetPathPage(src string) (string, error) {+ dir, err := w.baseTargetPathPage(src)
+ if err != nil {+ return "", err
+ }
+ if w.publishDir != "" {+ dir = filepath.Join(w.publishDir, dir)
+ }
+ return dir, nil
+}
+
+func (w siteWriter) baseTargetPathPage(src string) (string, error) {+ if src == helpers.FilePathSeparator {+ return "index.html", nil
+ }
+
+ dir, file := filepath.Split(src)
+ isRoot := dir == ""
+ ext := extension(filepath.Ext(file))
+ name := filename(file)
+
+ if w.langDir != "" && dir == helpers.FilePathSeparator && name == w.langDir {+ return filepath.Join(dir, name, "index"+ext), nil
+ }
+
+ if w.uglyURLs || file == "index.html" || (isRoot && file == "404.html") {+ return filepath.Join(dir, name+ext), nil
+ }
+
+ dir = filepath.Join(dir, name, "index"+ext)
+
+ return dir, nil
+
+}
+
+func (w siteWriter) targetPathFile(src string) (string, error) {+ return filepath.Join(w.publishDir, filepath.FromSlash(src)), nil
+}
+
+func (w siteWriter) targetPathAlias(src string) (string, error) {+ originalAlias := src
+ if len(src) <= 0 {+ return "", fmt.Errorf("Alias \"\" is an empty string")+ }
+
+ alias := filepath.Clean(src)
+ components := strings.Split(alias, helpers.FilePathSeparator)
+
+ if !w.allowRoot && alias == helpers.FilePathSeparator {+ 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)+ }
+
+ // Handle Windows file and directory naming restrictions
+ // See "Naming Files, Paths, and Namespaces" on MSDN
+ // https://msdn.microsoft.com/en-us/library/aa365247%28v=VS.85%29.aspx?f=255&MSPPError=-2147217396
+ msgs := []string{}+ reservedNames := []string{"CON", "PRN", "AUX", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"}+
+ if strings.ContainsAny(alias, ":*?\"<>|") {+ msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains invalid characters on Windows: : * ? \" < > |", originalAlias))+ }
+ for _, ch := range alias {+ if ch < ' ' {+ msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains ASCII control code (0x00 to 0x1F), invalid on Windows: : * ? \" < > |", originalAlias))+ continue
+ }
+ }
+ for _, comp := range components {+ if strings.HasSuffix(comp, " ") || strings.HasSuffix(comp, ".") {+ msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains component with a trailing space or period, problematic on Windows", originalAlias))+ }
+ for _, r := range reservedNames {+ if comp == r {+ msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains component with reserved name \"%s\" on Windows", originalAlias, r))+ }
+ }
+ }
+ if len(msgs) > 0 {+ if runtime.GOOS == "windows" {+ for _, m := range msgs {+ w.log.ERROR.Println(m)
+ }
+ return "", fmt.Errorf("Cannot create \"%s\": Windows filename restriction", originalAlias)+ }
+ for _, m := range msgs {+ w.log.WARN.Println(m)
+ }
+ }
+
+ // Add the final touch
+ alias = strings.TrimPrefix(alias, helpers.FilePathSeparator)
+ if strings.HasSuffix(alias, helpers.FilePathSeparator) {+ alias = alias + "index.html"
+ } else if !strings.HasSuffix(alias, ".html") {+ alias = alias + helpers.FilePathSeparator + "index.html"
+ }
+ if originalAlias != alias {+ w.log.INFO.Printf("Alias \"%s\" translated to \"%s\"\n", originalAlias, alias)+ }
+
+ return filepath.Join(w.publishDir, alias), nil
+}
+
+func extension(ext string) string {+ switch ext {+ case ".md", ".rst":
+ return ".html"
+ }
+
+ if ext != "" {+ return ext
+ }
+
+ return ".html"
+}
+
+func filename(f string) string {+ ext := filepath.Ext(f)
+ if ext == "" {+ return f
+ }
+
+ return f[:len(f)-len(ext)]
+}
+
+func (w siteWriter) writeDestPage(path string, reader io.Reader) (err error) {+ w.log.DEBUG.Println("creating page:", path)+ targetPath, err := w.targetPathPage(path)
+ if err != nil {+ return err
+ }
+
+ return w.publish(targetPath, reader)
+}
+
+func (w siteWriter) writeDestFile(path string, r io.Reader) (err error) {+ w.log.DEBUG.Println("creating file:", path)+ targetPath, err := w.targetPathFile(path)
+ if err != nil {+ return err
+ }
+ return w.publish(targetPath, r)
+}
+
+func (w siteWriter) publish(path string, r io.Reader) (err error) {+ return helpers.WriteToDisk(path, r, w.fs.Destination)
+}
--- /dev/null
+++ b/hugolib/site_writer_test.go
@@ -1,0 +1,146 @@
+// 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"
+ "runtime"
+ "testing"
+)
+
+func TestTargetPathHTMLRedirectAlias(t *testing.T) {+ w := siteWriter{log: newErrorLogger()}+
+ errIsNilForThisOS := runtime.GOOS != "windows"
+
+ tests := []struct {+ value string
+ expected string
+ errIsNil bool
+ }{+ {"", "", false},+ {"s", filepath.FromSlash("s/index.html"), true},+ {"/", "", false},+ {"alias 1", filepath.FromSlash("alias 1/index.html"), true},+ {"alias 2/", filepath.FromSlash("alias 2/index.html"), true},+ {"alias 3.html", "alias 3.html", true},+ {"alias4.html", "alias4.html", true},+ {"/alias 5.html", "alias 5.html", true},+ {"/трям.html", "трям.html", true},+ {"../../../../tmp/passwd", "", false},+ {"/foo/../../../../tmp/passwd", filepath.FromSlash("tmp/passwd/index.html"), true},+ {"foo/../../../../tmp/passwd", "", false},+ {"C:\\Windows", filepath.FromSlash("C:\\Windows/index.html"), errIsNilForThisOS},+ {"/trailing-space /", filepath.FromSlash("trailing-space /index.html"), errIsNilForThisOS},+ {"/trailing-period./", filepath.FromSlash("trailing-period./index.html"), errIsNilForThisOS},+ {"/tab\tseparated/", filepath.FromSlash("tab\tseparated/index.html"), errIsNilForThisOS},+ {"/chrome/?p=help&ctx=keyboard#topic=3227046", filepath.FromSlash("chrome/?p=help&ctx=keyboard#topic=3227046/index.html"), errIsNilForThisOS},+ {"/LPT1/Printer/", filepath.FromSlash("LPT1/Printer/index.html"), errIsNilForThisOS},+ }
+
+ for _, test := range tests {+ path, err := w.targetPathAlias(test.value)
+ if (err == nil) != test.errIsNil {+ t.Errorf("Expected err == nil => %t, got: %t. err: %s", test.errIsNil, err == nil, err)+ continue
+ }
+ if err == nil && path != test.expected {+ t.Errorf("Expected: \"%s\", got: \"%s\"", test.expected, path)+ }
+ }
+}
+
+func TestTargetPathPage(t *testing.T) {+ w := siteWriter{log: newErrorLogger()}+
+ tests := []struct {+ content string
+ expected string
+ }{+ {"/", "index.html"},+ {"index.html", "index.html"},+ {"bar/index.html", "bar/index.html"},+ {"foo", "foo/index.html"},+ {"foo.html", "foo/index.html"},+ {"foo.xhtml", "foo/index.xhtml"},+ {"section", "section/index.html"},+ {"section/", "section/index.html"},+ {"section/foo", "section/foo/index.html"},+ {"section/foo.html", "section/foo/index.html"},+ {"section/foo.rss", "section/foo/index.rss"},+ }
+
+ for _, test := range tests {+ dest, err := w.targetPathPage(filepath.FromSlash(test.content))
+ expected := filepath.FromSlash(test.expected)
+ if err != nil {+ t.Fatalf("Translate returned and unexpected err: %s", err)+ }
+
+ if dest != expected {+ t.Errorf("Translate expected return: %s, got: %s", expected, dest)+ }
+ }
+}
+
+func TestTargetPathPageBase(t *testing.T) {+ w := siteWriter{log: newErrorLogger()}+
+ tests := []struct {+ content string
+ expected string
+ }{+ {"/", "a/base/index.html"},+ }
+
+ for _, test := range tests {+
+ for _, pd := range []string{"a/base", "a/base/"} {+ w.publishDir = pd
+ dest, err := w.targetPathPage(test.content)
+ if err != nil {+ t.Fatalf("Translated returned and err: %s", err)+ }
+
+ if dest != filepath.FromSlash(test.expected) {+ t.Errorf("Translate expected: %s, got: %s", test.expected, dest)+ }
+ }
+ }
+}
+
+func TestTargetPathUglyURLs(t *testing.T) {+ w := siteWriter{log: newErrorLogger(), uglyURLs: true}+
+ tests := []struct {+ content string
+ expected string
+ }{+ {"foo.html", "foo.html"},+ {"/", "index.html"},+ {"section", "section.html"},+ {"index.html", "index.html"},+ }
+
+ for _, test := range tests {+ dest, err := w.targetPathPage(filepath.FromSlash(test.content))
+ if err != nil {+ t.Fatalf("Translate returned an unexpected err: %s", err)+ }
+
+ if dest != test.expected {+ t.Errorf("Translate expected return: %s, got: %s", test.expected, dest)+ }
+ }
+}
--- a/hugolib/testhelpers_test.go
+++ b/hugolib/testhelpers_test.go
@@ -151,6 +151,9 @@
return jww.NewNotepad(jww.LevelDebug, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime)
}
+func newErrorLogger() *jww.Notepad {+ return jww.NewNotepad(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime)
+}
func createWithTemplateFromNameValues(additionalTemplates ...string) func(templ tpl.Template) error { return func(templ tpl.Template) error {--- a/target/alias_test.go
+++ /dev/null
@@ -1,63 +1,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 target
-
-import (
- "path/filepath"
- "runtime"
- "testing"
-)
-
-func TestHTMLRedirectAlias(t *testing.T) {- var o Translator
- o = new(HTMLRedirectAlias)
-
- errIsNilForThisOS := runtime.GOOS != "windows"
-
- tests := []struct {- value string
- expected string
- errIsNil bool
- }{- {"", "", false},- {"s", filepath.FromSlash("s/index.html"), true},- {"/", "", false},- {"alias 1", filepath.FromSlash("alias 1/index.html"), true},- {"alias 2/", filepath.FromSlash("alias 2/index.html"), true},- {"alias 3.html", "alias 3.html", true},- {"alias4.html", "alias4.html", true},- {"/alias 5.html", "alias 5.html", true},- {"/трям.html", "трям.html", true},- {"../../../../tmp/passwd", "", false},- {"/foo/../../../../tmp/passwd", filepath.FromSlash("tmp/passwd/index.html"), true},- {"foo/../../../../tmp/passwd", "", false},- {"C:\\Windows", filepath.FromSlash("C:\\Windows/index.html"), errIsNilForThisOS},- {"/trailing-space /", filepath.FromSlash("trailing-space /index.html"), errIsNilForThisOS},- {"/trailing-period./", filepath.FromSlash("trailing-period./index.html"), errIsNilForThisOS},- {"/tab\tseparated/", filepath.FromSlash("tab\tseparated/index.html"), errIsNilForThisOS},- {"/chrome/?p=help&ctx=keyboard#topic=3227046", filepath.FromSlash("chrome/?p=help&ctx=keyboard#topic=3227046/index.html"), errIsNilForThisOS},- {"/LPT1/Printer/", filepath.FromSlash("LPT1/Printer/index.html"), errIsNilForThisOS},- }
-
- for _, test := range tests {- path, err := o.Translate(test.value)
- if (err == nil) != test.errIsNil {- t.Errorf("Expected err == nil => %t, got: %t. err: %s", test.errIsNil, err == nil, err)- continue
- }
- if err == nil && path != test.expected {- t.Errorf("Expected: \"%s\", got: \"%s\"", test.expected, path)- }
- }
-}
--- a/target/file.go
+++ /dev/null
@@ -1,68 +1,0 @@
-// Copyright 2016 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 target
-
-import (
- "io"
- "path/filepath"
-
- "github.com/spf13/hugo/helpers"
- "github.com/spf13/hugo/hugofs"
-)
-
-type Publisher interface {- Publish(string, io.Reader) error
-}
-
-type Translator interface {- Translate(string) (string, error)
-}
-
-// TODO(bep) consider other ways to solve this.
-type OptionalTranslator interface {- TranslateRelative(string) (string, error)
-}
-
-type Output interface {- Publisher
- Translator
-}
-
-type Filesystem struct {- PublishDir string
-
- Fs *hugofs.Fs
-}
-
-func (fs *Filesystem) Publish(path string, r io.Reader) (err error) {- translated, err := fs.Translate(path)
- if err != nil {- return
- }
-
- return helpers.WriteToDisk(translated, r, fs.Fs.Destination)
-}
-
-func (fs *Filesystem) Translate(src string) (dest string, err error) {- return filepath.Join(fs.PublishDir, filepath.FromSlash(src)), nil
-}
-
-func filename(f string) string {- ext := filepath.Ext(f)
- if ext == "" {- return f
- }
-
- return f[:len(f)-len(ext)]
-}
--- a/target/htmlredirect.go
+++ /dev/null
@@ -1,151 +1,0 @@
-// Copyright 2016 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 target
-
-import (
- "bytes"
- "fmt"
- "html/template"
- "path/filepath"
- "runtime"
- "strings"
-
- "github.com/spf13/hugo/helpers"
- "github.com/spf13/hugo/hugofs"
- jww "github.com/spf13/jwalterweatherman"
-)
-
-const alias = "<!DOCTYPE html><html><head><title>{{ .Permalink }}</title><link rel=\"canonical\" href=\"{{ .Permalink }}\"/><meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" /><meta http-equiv=\"refresh\" content=\"0; url={{ .Permalink }}\" /></head></html>"-const aliasXHtml = "<!DOCTYPE html><html xmlns=\"http://www.w3.org/1999/xhtml\"><head><title>{{ .Permalink }}</title><link rel=\"canonical\" href=\"{{ .Permalink }}\"/><meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" /><meta http-equiv=\"refresh\" content=\"0; url={{ .Permalink }}\" /></head></html>"-
-var defaultAliasTemplates *template.Template
-
-func init() {- defaultAliasTemplates = template.New("")- template.Must(defaultAliasTemplates.New("alias").Parse(alias))- template.Must(defaultAliasTemplates.New("alias-xhtml").Parse(aliasXHtml))-}
-
-type AliasPublisher interface {- Translator
- Publish(path string, permalink string, page interface{}) error-}
-
-type HTMLRedirectAlias struct {- PublishDir string
- Templates *template.Template
- AllowRoot bool // for the language redirects
-
- Fs *hugofs.Fs
-}
-
-func (h *HTMLRedirectAlias) Translate(alias string) (aliasPath string, err error) {- originalAlias := alias
- if len(alias) <= 0 {- return "", fmt.Errorf("Alias \"\" is an empty string")- }
-
- alias = filepath.Clean(alias)
- components := strings.Split(alias, helpers.FilePathSeparator)
-
- if !h.AllowRoot && alias == helpers.FilePathSeparator {- 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)- }
-
- // Handle Windows file and directory naming restrictions
- // See "Naming Files, Paths, and Namespaces" on MSDN
- // https://msdn.microsoft.com/en-us/library/aa365247%28v=VS.85%29.aspx?f=255&MSPPError=-2147217396
- msgs := []string{}- reservedNames := []string{"CON", "PRN", "AUX", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"}-
- if strings.ContainsAny(alias, ":*?\"<>|") {- msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains invalid characters on Windows: : * ? \" < > |", originalAlias))- }
- for _, ch := range alias {- if ch < ' ' {- msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains ASCII control code (0x00 to 0x1F), invalid on Windows: : * ? \" < > |", originalAlias))- continue
- }
- }
- for _, comp := range components {- if strings.HasSuffix(comp, " ") || strings.HasSuffix(comp, ".") {- msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains component with a trailing space or period, problematic on Windows", originalAlias))- }
- for _, r := range reservedNames {- if comp == r {- msgs = append(msgs, fmt.Sprintf("Alias \"%s\" contains component with reserved name \"%s\" on Windows", originalAlias, r))- }
- }
- }
- if len(msgs) > 0 {- if runtime.GOOS == "windows" {- for _, m := range msgs {- jww.ERROR.Println(m)
- }
- return "", fmt.Errorf("Cannot create \"%s\": Windows filename restriction", originalAlias)- }
- for _, m := range msgs {- jww.WARN.Println(m)
- }
- }
-
- // Add the final touch
- alias = strings.TrimPrefix(alias, helpers.FilePathSeparator)
- if strings.HasSuffix(alias, helpers.FilePathSeparator) {- alias = alias + "index.html"
- } else if !strings.HasSuffix(alias, ".html") {- alias = alias + helpers.FilePathSeparator + "index.html"
- }
- if originalAlias != alias {- jww.INFO.Printf("Alias \"%s\" translated to \"%s\"\n", originalAlias, alias)- }
-
- return filepath.Join(h.PublishDir, alias), nil
-}
-
-type AliasNode struct {- Permalink string
- Page interface{}-}
-
-func (h *HTMLRedirectAlias) Publish(path string, permalink string, page interface{}) (err error) {- if path, err = h.Translate(path); err != nil {- jww.ERROR.Printf("%s, skipping.", err)- return nil
- }
-
- t := "alias"
- if strings.HasSuffix(path, ".xhtml") {- t = "alias-xhtml"
- }
-
- template := defaultAliasTemplates
- if h.Templates != nil {- template = h.Templates
- t = "alias.html"
- }
-
- buffer := new(bytes.Buffer)
- err = template.ExecuteTemplate(buffer, t, &AliasNode{permalink, page})- if err != nil {- return
- }
-
- return helpers.WriteToDisk(path, buffer, h.Fs.Destination)
-}
--- a/target/memory.go
+++ /dev/null
@@ -1,37 +1,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 target
-
-import (
- "bytes"
- "io"
-)
-
-type InMemoryTarget struct {- Files map[string][]byte
-}
-
-func (t *InMemoryTarget) Publish(label string, reader io.Reader) (err error) {- if t.Files == nil {- t.Files = make(map[string][]byte)
- }
- bytes := new(bytes.Buffer)
- bytes.ReadFrom(reader)
- t.Files[label] = bytes.Bytes()
- return
-}
-
-func (t *InMemoryTarget) Translate(label string) (dest string, err error) {- return label, nil
-}
--- a/target/page.go
+++ /dev/null
@@ -1,103 +1,0 @@
-// Copyright 2016 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 target
-
-import (
- "html/template"
- "io"
- "path/filepath"
-
- "github.com/spf13/hugo/helpers"
- "github.com/spf13/hugo/hugofs"
-)
-
-type PagePublisher interface {- Translator
- Publish(string, template.HTML) error
-}
-
-type PagePub struct {- UglyURLs bool
- DefaultExtension string
- PublishDir string
-
- // LangDir will contain the subdir for the language, i.e. "en", "de" etc.
- // It will be empty if the site is rendered in root.
- LangDir string
-
- Fs *hugofs.Fs
-}
-
-func (pp *PagePub) Publish(path string, r io.Reader) (err error) {-
- translated, err := pp.Translate(path)
- if err != nil {- return
- }
-
- return helpers.WriteToDisk(translated, r, pp.Fs.Destination)
-}
-
-func (pp *PagePub) Translate(src string) (dest string, err error) {- dir, err := pp.TranslateRelative(src)
- if err != nil {- return dir, err
- }
- if pp.PublishDir != "" {- dir = filepath.Join(pp.PublishDir, dir)
- }
- return dir, nil
-}
-
-func (pp *PagePub) TranslateRelative(src string) (dest string, err error) {- if src == helpers.FilePathSeparator {- return "index.html", nil
- }
-
- dir, file := filepath.Split(src)
- isRoot := dir == ""
- ext := pp.extension(filepath.Ext(file))
- name := filename(file)
-
- // TODO(bep) Having all of this path logic here seems wrong, but I guess
- // we'll clean this up when we redo the output files.
- // This catches the home page in a language sub path. They should never
- // have any ugly URLs.
- if pp.LangDir != "" && dir == helpers.FilePathSeparator && name == pp.LangDir {- return filepath.Join(dir, name, "index"+ext), nil
- }
-
- if pp.UglyURLs || file == "index.html" || (isRoot && file == "404.html") {- return filepath.Join(dir, name+ext), nil
- }
-
- return filepath.Join(dir, name, "index"+ext), nil
-}
-
-func (pp *PagePub) extension(ext string) string {- switch ext {- case ".md", ".rst": // TODO make this list configurable. page.go has the list of markup types.
- return ".html"
- }
-
- if ext != "" {- return ext
- }
-
- if pp.DefaultExtension != "" {- return pp.DefaultExtension
- }
-
- return ".html"
-}
--- a/target/page_test.go
+++ /dev/null
@@ -1,113 +1,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 target
-
-import (
- "path/filepath"
- "testing"
-
- "github.com/spf13/hugo/hugofs"
- "github.com/spf13/viper"
-)
-
-func TestPageTranslator(t *testing.T) {- fs := hugofs.NewMem(viper.New())
-
- tests := []struct {- content string
- expected string
- }{- {"/", "index.html"},- {"index.html", "index.html"},- {"bar/index.html", "bar/index.html"},- {"foo", "foo/index.html"},- {"foo.html", "foo/index.html"},- {"foo.xhtml", "foo/index.xhtml"},- {"section", "section/index.html"},- {"section/", "section/index.html"},- {"section/foo", "section/foo/index.html"},- {"section/foo.html", "section/foo/index.html"},- {"section/foo.rss", "section/foo/index.rss"},- }
-
- for _, test := range tests {- f := &PagePub{Fs: fs}- dest, err := f.Translate(filepath.FromSlash(test.content))
- expected := filepath.FromSlash(test.expected)
- if err != nil {- t.Fatalf("Translate returned and unexpected err: %s", err)- }
-
- if dest != expected {- t.Errorf("Translate expected return: %s, got: %s", expected, dest)- }
- }
-}
-
-func TestPageTranslatorBase(t *testing.T) {- tests := []struct {- content string
- expected string
- }{- {"/", "a/base/index.html"},- }
-
- for _, test := range tests {- f := &PagePub{PublishDir: "a/base"}- fts := &PagePub{PublishDir: "a/base/"}-
- for _, fs := range []*PagePub{f, fts} {- dest, err := fs.Translate(test.content)
- if err != nil {- t.Fatalf("Translated returned and err: %s", err)- }
-
- if dest != filepath.FromSlash(test.expected) {- t.Errorf("Translate expected: %s, got: %s", test.expected, dest)- }
- }
- }
-}
-
-func TestTranslateUglyURLs(t *testing.T) {- tests := []struct {- content string
- expected string
- }{- {"foo.html", "foo.html"},- {"/", "index.html"},- {"section", "section.html"},- {"index.html", "index.html"},- }
-
- for _, test := range tests {- f := &PagePub{UglyURLs: true}- dest, err := f.Translate(filepath.FromSlash(test.content))
- if err != nil {- t.Fatalf("Translate returned an unexpected err: %s", err)- }
-
- if dest != test.expected {- t.Errorf("Translate expected return: %s, got: %s", test.expected, dest)- }
- }
-}
-
-func TestTranslateDefaultExtension(t *testing.T) {- f := &PagePub{DefaultExtension: ".foobar"}- dest, _ := f.Translate("baz")- if dest != filepath.FromSlash("baz/index.foobar") {- t.Errorf("Translate expected return: %s, got %s", "baz/index.foobar", dest)- }
-}
--
⑨