Skip to content

Commit

Permalink
hugolib: Refactor/-work the permalink/target path logic
Browse files Browse the repository at this point in the history
This is a pretty fundamental change in Hugo, but absolutely needed if we should have any hope of getting "multiple outputs" done.

This commit's goal is to say:

* Every file target path is created by `createTargetPath`, i.e. one function for all.
* That function takes every page and site parameter into account, to avoid fragile string parsing to uglify etc. later on.
* The path creation logic has full test coverage.
* All permalinks, paginator URLs etc. are then built on top of that same logic.

Fixes #1252
Fixes #2110
Closes #2374
Fixes #1885
Fixes #3102
Fixes #3179
Fixes #1641
Fixes #1989
  • Loading branch information
bep committed Mar 27, 2017
1 parent c8fff95 commit 6bf010f
Show file tree
Hide file tree
Showing 26 changed files with 915 additions and 403 deletions.
10 changes: 7 additions & 3 deletions helpers/pathspec.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (

// PathSpec holds methods that decides how paths in URLs and files in Hugo should look like.
type PathSpec struct {
BaseURL

disablePathToLower bool
removePathAccents bool
uglyURLs bool
Expand All @@ -32,8 +34,7 @@ type PathSpec struct {
// pagination path handling
paginatePath string

baseURL string
theme string
theme string

// Directories
themesDir string
Expand Down Expand Up @@ -61,6 +62,9 @@ func (p PathSpec) String() string {
// NewPathSpec creats a new PathSpec from the given filesystems and Language.
func NewPathSpec(fs *hugofs.Fs, cfg config.Provider) *PathSpec {

// TODO(bep) output error handling
baseURL, _ := newBaseURLFromString(cfg.GetString("baseURL"))

ps := &PathSpec{
fs: fs,
disablePathToLower: cfg.GetBool("disablePathToLower"),
Expand All @@ -71,7 +75,7 @@ func NewPathSpec(fs *hugofs.Fs, cfg config.Provider) *PathSpec {
defaultContentLanguageInSubdir: cfg.GetBool("defaultContentLanguageInSubdir"),
defaultContentLanguage: cfg.GetString("defaultContentLanguage"),
paginatePath: cfg.GetString("paginatePath"),
baseURL: cfg.GetString("baseURL"),
BaseURL: baseURL,
themesDir: cfg.GetString("themesDir"),
layoutDir: cfg.GetString("layoutDir"),
workingDir: cfg.GetString("workingDir"),
Expand Down
2 changes: 1 addition & 1 deletion helpers/pathspec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func TestNewPathSpecFromConfig(t *testing.T) {
require.Equal(t, "no", p.language.Lang)
require.Equal(t, "side", p.paginatePath)

require.Equal(t, "http://base.com", p.baseURL)
require.Equal(t, "http://base.com", p.BaseURL.String())
require.Equal(t, "thethemes", p.themesDir)
require.Equal(t, "thelayouts", p.layoutDir)
require.Equal(t, "thework", p.workingDir)
Expand Down
77 changes: 67 additions & 10 deletions helpers/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,39 @@ import (
"fmt"
"net/url"
"path"
"path/filepath"
"strings"

"github.com/PuerkitoBio/purell"
)

type BaseURL struct {
url *url.URL
urlStr string
}

func (b BaseURL) String() string {
return b.urlStr
}

func (b BaseURL) URL() *url.URL {
// create a copy as it will be modified.
c := *b.url
return &c
}

func newBaseURLFromString(b string) (BaseURL, error) {
var result BaseURL

base, err := url.Parse(b)
if err != nil {
return result, err
}

// TODO(bep) output consider saving original URL?
return BaseURL{url: base, urlStr: base.String()}, nil
}

type pathBridge struct {
}

Expand Down Expand Up @@ -101,10 +129,20 @@ func SanitizeURLKeepTrailingSlash(in string) string {
// uri: Vim (text editor)
// urlize: vim-text-editor
func (p *PathSpec) URLize(uri string) string {
sanitized := p.MakePathSanitized(uri)
return p.URLEscape(p.MakePathSanitized(uri))

}

// URLizeFilename creates an URL from a filename by esacaping unicode letters
// and turn any filepath separator into forward slashes.
func (p *PathSpec) URLizeFilename(filename string) string {
return p.URLEscape(filepath.ToSlash(filename))
}

// URLEscape escapes unicode letters.
func (p *PathSpec) URLEscape(uri string) string {
// escape unicode letters
parsedURI, err := url.Parse(sanitized)
parsedURI, err := url.Parse(uri)
if err != nil {
// if net/url can not parse URL it means Sanitize works incorrectly
panic(err)
Expand All @@ -118,6 +156,7 @@ func (p *PathSpec) URLize(uri string) string {
// base: http://spf13.com/
// path: post/how-i-blog
// result: http://spf13.com/post/how-i-blog
// TODO(bep) output check why this is still in use.
func MakePermalink(host, plink string) *url.URL {

base, err := url.Parse(host)
Expand Down Expand Up @@ -156,14 +195,13 @@ func (p *PathSpec) AbsURL(in string, addLanguage bool) string {
return in
}

baseURL := p.baseURL
var baseURL string
if strings.HasPrefix(in, "/") {
p, err := url.Parse(baseURL)
if err != nil {
panic(err)
}
p.Path = ""
baseURL = p.String()
u := p.BaseURL.URL()
u.Path = ""
baseURL = u.String()
} else {
baseURL = p.BaseURL.String()
}

if addLanguage {
Expand Down Expand Up @@ -218,7 +256,7 @@ func IsAbsURL(path string) bool {
// RelURL creates a URL relative to the BaseURL root.
// Note: The result URL will not include the context root if canonifyURLs is enabled.
func (p *PathSpec) RelURL(in string, addLanguage bool) string {
baseURL := p.baseURL
baseURL := p.BaseURL.String()
canonifyURLs := p.canonifyURLs
if (!strings.HasPrefix(in, baseURL) && strings.HasPrefix(in, "http")) || strings.HasPrefix(in, "//") {
return in
Expand Down Expand Up @@ -287,8 +325,27 @@ func AddContextRoot(baseURL, relativePath string) string {
return newPath
}

// PrependBasePath prepends any baseURL sub-folder to the given resource
// if canonifyURLs is disabled.
// If canonifyURLs is set, we will globally prepend the absURL with any sub-folder,
// so avoid doing anything here to avoid getting double paths.
func (p *PathSpec) PrependBasePath(rel string) string {
basePath := p.BaseURL.url.Path
if !p.canonifyURLs && basePath != "" && basePath != "/" {
rel = filepath.ToSlash(rel)
// Need to prepend any path from the baseURL
hadSlash := strings.HasSuffix(rel, "/")
rel = path.Join(basePath, rel)
if hadSlash {
rel += "/"
}
}
return rel
}

// URLizeAndPrep applies misc sanitation to the given URL to get it in line
// with the Hugo standard.
// TODO(bep) output check usage
func (p *PathSpec) URLizeAndPrep(in string) string {
return p.URLPrep(p.URLize(in))
}
Expand Down
6 changes: 3 additions & 3 deletions hugolib/embedded_shortcodes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import (
)

const (
baseURL = "http://foo/bar"
testBaseURL = "http://foo/bar"
)

func TestShortcodeCrossrefs(t *testing.T) {
Expand All @@ -46,7 +46,7 @@ func doTestShortcodeCrossrefs(t *testing.T, relative bool) {
cfg, fs = newTestCfg()
)

cfg.Set("baseURL", baseURL)
cfg.Set("baseURL", testBaseURL)

var refShortcode string
var expectedBase string
Expand All @@ -56,7 +56,7 @@ func doTestShortcodeCrossrefs(t *testing.T, relative bool) {
expectedBase = "/bar"
} else {
refShortcode = "ref"
expectedBase = baseURL
expectedBase = testBaseURL
}

path := filepath.FromSlash("blog/post.md")
Expand Down
5 changes: 0 additions & 5 deletions hugolib/hugo_sites.go
Original file line number Diff line number Diff line change
Expand Up @@ -548,11 +548,6 @@ func (s *Site) preparePagesForRender(cfg *BuildCfg) {
p.Content = helpers.BytesToHTML(workContentCopy)
}

// May have been set in front matter
if len(p.outputTypes) == 0 {
p.outputTypes = defaultOutputDefinitions.ForKind(p.Kind)
}

//analyze for raw stats
p.analyzePage()

Expand Down
6 changes: 6 additions & 0 deletions hugolib/hugo_sites_build.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,12 @@ func (h *HugoSites) assemble(config *BuildCfg) error {
}

for _, s := range h.Sites {
for _, p := range s.Pages {
// May have been set in front matter
if len(p.outputTypes) == 0 {
p.outputTypes = s.defaultOutputDefinitions.ForKind(p.Kind)
}
}
s.assembleMenus()
s.refreshPageCaches()
s.setupSitePages()
Expand Down
28 changes: 12 additions & 16 deletions hugolib/hugo_sites_build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,13 @@ func doTestMultiSitesMainLangInRoot(t *testing.T, defaultInSubDir bool) {
th.assertFileContent("public/en/sitemap.xml", "<loc>http://example.com/blog/en/</loc>")

// Check rss
th.assertFileContent("public/fr/index.xml", `<atom:link href="http://example.com/blog/fr/index.xml"`)
th.assertFileContent("public/en/index.xml", `<atom:link href="http://example.com/blog/en/index.xml"`)
th.assertFileContent("public/fr/sect/index.xml", `<atom:link href="http://example.com/blog/fr/sect/index.xml"`)
th.assertFileContent("public/en/sect/index.xml", `<atom:link href="http://example.com/blog/en/sect/index.xml"`)
th.assertFileContent("public/fr/plaques/frtag1/index.xml", `<atom:link href="http://example.com/blog/fr/plaques/frtag1/index.xml"`)
th.assertFileContent("public/en/tags/tag1/index.xml", `<atom:link href="http://example.com/blog/en/tags/tag1/index.xml"`)
// TODO(bep) output the Atom link must be cretated from the OutputFormats.RSS.Permalink
// th.assertFileContent("public/fr/index.xml", `<atom:link href="http://example.com/blog/fr/index.xml"`)
// th.assertFileContent("public/en/index.xml", `<atom:link href="http://example.com/blog/en/index.xml"`)
// th.assertFileContent("public/fr/sect/index.xml", `<atom:link href="http://example.com/blog/fr/sect/index.xml"`)
// th.assertFileContent("public/en/sect/index.xml", `<atom:link href="http://example.com/blog/en/sect/index.xml"`)
// th.assertFileContent("public/fr/plaques/frtag1/index.xml", `<atom:link href="http://example.com/blog/fr/plaques/frtag1/index.xml"`)
// th.assertFileContent("public/en/tags/tag1/index.xml", `<atom:link href="http://example.com/blog/en/tags/tag1/index.xml"`)

// Check paginators
th.assertFileContent("public/fr/page/1/index.html", `refresh" content="0; url=http://example.com/blog/fr/"`)
Expand Down Expand Up @@ -250,7 +251,7 @@ func doTestMultiSitesBuild(t *testing.T, configTemplate, configSuffix string) {
// 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, "http://example.com/blog/superbob/", permalink, "invalid doc3 permalink")

require.Equal(t, "/superbob", doc3.URL(), "invalid url, was specified on doc3")
th.assertFileContent("public/superbob/index.html", "doc3|Hello|en")
Expand All @@ -274,7 +275,7 @@ func doTestMultiSitesBuild(t *testing.T, configTemplate, configSuffix string) {

doc5 := enSite.AllPages[5]
permalink = doc5.Permalink()
require.Equal(t, "http://example.com/blog/fr/somewhere/else/doc5", permalink, "invalid 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")
Expand Down Expand Up @@ -594,14 +595,6 @@ func assertShouldNotBuild(t *testing.T, sites *HugoSites) {

require.Equal(t, p.shouldBuild(), p.Content != "", p.BaseFileName())

// TODO(bep) output
/*filename := filepath.Join("public", p.TargetPath())
if strings.HasSuffix(filename, ".html") {
// TODO(bep) the end result is correct, but it is weird that we cannot use targetPath directly here.
filename = strings.Replace(filename, ".html", "/index.html", 1)
}
require.Equal(t, p.shouldBuild(), destinationExists(sites.Fs, filename), filename)*/
}
}

Expand Down Expand Up @@ -825,6 +818,7 @@ disableRSS = false
rssURI = "index.xml"
paginate = 1
disablePathToLower = true
defaultContentLanguage = "{{ .DefaultContentLanguage }}"
defaultContentLanguageInSubdir = {{ .DefaultContentLanguageInSubdir }}
Expand Down Expand Up @@ -884,6 +878,7 @@ disableSitemap: false
disableRSS: false
rssURI: "index.xml"
disablePathToLower: true
paginate: 1
defaultContentLanguage: "{{ .DefaultContentLanguage }}"
defaultContentLanguageInSubdir: {{ .DefaultContentLanguageInSubdir }}
Expand Down Expand Up @@ -945,6 +940,7 @@ var multiSiteJSONConfigTemplate = `
"disableRSS": false,
"rssURI": "index.xml",
"paginate": 1,
"disablePathToLower": true,
"defaultContentLanguage": "{{ .DefaultContentLanguage }}",
"defaultContentLanguageInSubdir": true,
"permalinks": {
Expand Down
7 changes: 5 additions & 2 deletions hugolib/node_as_page_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,9 @@ func doTestNodesWithNoContentFile(t *testing.T, ugly bool) {
func TestNodesAsPageMultilingual(t *testing.T) {
t.Parallel()
for _, ugly := range []bool{false, true} {
doTestNodesAsPageMultilingual(t, ugly)
t.Run(fmt.Sprintf("ugly=%t", ugly), func(t *testing.T) {
doTestNodesAsPageMultilingual(t, ugly)
})
}
}

Expand Down Expand Up @@ -369,7 +371,8 @@ title = "Deutsche Hugo"
require.Len(t, deHome.Translations(), 2, deHome.Translations()[0].Language().Lang)
require.Equal(t, "en", deHome.Translations()[1].Language().Lang)
require.Equal(t, "nn", deHome.Translations()[0].Language().Lang)
require.Equal(t, expetedPermalink(ugly, "/de/"), deHome.Permalink())
// See issue #3179
require.Equal(t, expetedPermalink(false, "/de/"), deHome.Permalink())

enSect := sites.Sites[1].getPage("section", "sect1")
require.NotNil(t, enSect)
Expand Down
Loading

0 comments on commit 6bf010f

Please sign in to comment.